) => {
+ const { target } = event;
+ input(target.value);
+ };
+
+ const onAddressChange = inputChangeHandler(setAddressInput);
+ const onCityChange = inputChangeHandler(setCityInput);
+ const onStateChange = inputChangeHandler(setStateInput);
+ const onZipCodeChange = inputChangeHandler(setZipCodeInput);
+
+ useEffect(() => {
+ spinnerButtonRef.current?.toggleSpinner(isLoading);
+ onLoadingLocations(isLoading);
+ }, [isLoading]);
+
+ useEffect(() => {
+ uspsError && onError(uspsError);
+ }, [uspsError]);
+
+ useDidUpdateEffect(() => {
+ onFoundLocations(locationQuery, locationResults);
+ }, [locationResults]);
+
+ const handleSearch = useCallback(
+ (event) => {
+ onError(null);
+ onSearch(event, addressInput, cityInput, stateInput, zipCodeInput);
+ },
+ [addressInput, cityInput, stateInput, zipCodeInput],
+ );
+
+ const { usStatesTerritories } = useContext(InPersonContext);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {usStatesTerritories.map(([name, abbr]) => (
+
+ ))}
+
+
+
+
+
+
+
+ {t('in_person_proofing.body.location.po_search.search_button')}
+
+
+ >
+ );
+}
+
+export default FullAddressSearch;
diff --git a/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.spec.tsx b/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.spec.tsx
new file mode 100644
index 00000000000..07b01579e9f
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.spec.tsx
@@ -0,0 +1,28 @@
+import { render } from '@testing-library/react';
+import { SWRConfig } from 'swr';
+import { ComponentType } from 'react';
+import InPersonLocationFullAddressEntryPostOfficeSearchStep from './in-person-location-full-address-entry-post-office-search-step';
+
+const DEFAULT_PROPS = {
+ toPreviousStep() {},
+ onChange() {},
+ value: {},
+ registerField() {},
+};
+
+describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => {
+ const wrapper: ComponentType = ({ children }) => (
+ new Map() }}>{children}
+ );
+
+ it('renders the step', () => {
+ const { getByRole } = render(
+ ,
+ {
+ wrapper,
+ },
+ );
+
+ expect(getByRole('heading', { name: 'in_person_proofing.headings.po_search.location' }));
+ });
+});
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
new file mode 100644
index 00000000000..e0506317236
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.tsx
@@ -0,0 +1,139 @@
+import { useState, useEffect, useCallback, useRef, useContext } from 'react';
+import { useI18n } from '@18f/identity-react-i18n';
+import { Alert, PageHeading } from '@18f/identity-components';
+import { request } from '@18f/identity-request';
+import { forceRedirect } from '@18f/identity-url';
+import {
+ transformKeys,
+ snakeCase,
+ LocationQuery,
+ LOCATIONS_URL,
+} from '@18f/identity-address-search';
+import FullAddressSearch from './in-person-full-address-search';
+import BackButton from './back-button';
+import AnalyticsContext from '../context/analytics';
+import InPersonLocations, { FormattedLocation } from './in-person-locations';
+import { InPersonContext } from '../context';
+import UploadContext from '../context/upload';
+
+function InPersonLocationFullAddressEntryPostOfficeSearchStep({
+ onChange,
+ toPreviousStep,
+ registerField,
+}) {
+ const { inPersonURL } = useContext(InPersonContext);
+ const { t } = useI18n();
+ const [inProgress, setInProgress] = useState(false);
+ const [isLoadingLocations, setLoadingLocations] = useState(false);
+ const [autoSubmit, setAutoSubmit] = useState(false);
+ const { trackEvent } = useContext(AnalyticsContext);
+ const [locationResults, setLocationResults] = useState(
+ null,
+ );
+ const [foundAddress, setFoundAddress] = useState(null);
+ const [apiError, setApiError] = useState(null);
+ const [disabledAddressSearch, setDisabledAddressSearch] = useState(false);
+ const { flowPath } = useContext(UploadContext);
+
+ const onFoundLocations = (
+ address: LocationQuery | null,
+ locations: FormattedLocation[] | null | undefined,
+ ) => {
+ setFoundAddress(address);
+ setLocationResults(locations);
+ };
+
+ // ref allows us to avoid a memory leak
+ const mountedRef = useRef(false);
+
+ useEffect(() => {
+ mountedRef.current = true;
+ return () => {
+ mountedRef.current = false;
+ };
+ }, []);
+
+ // useCallBack here prevents unnecessary rerenders due to changing function identity
+ const handleLocationSelect = useCallback(
+ async (e: any, id: number) => {
+ if (flowPath !== 'hybrid') {
+ e.preventDefault();
+ }
+ const selectedLocation = locationResults![id]!;
+ const { streetAddress, formattedCityStateZip } = selectedLocation;
+ const selectedLocationAddress = `${streetAddress}, ${formattedCityStateZip}`;
+ onChange({ selectedLocationAddress });
+ if (autoSubmit) {
+ setDisabledAddressSearch(true);
+ setTimeout(() => {
+ if (mountedRef.current) {
+ setDisabledAddressSearch(false);
+ }
+ }, 250);
+ return;
+ }
+ if (inProgress) {
+ return;
+ }
+ const selected = transformKeys(selectedLocation, snakeCase);
+ setInProgress(true);
+ try {
+ await request(LOCATIONS_URL, {
+ json: selected,
+ method: 'PUT',
+ });
+ // In try block set success of request. If the request is successful, fire remaining code?
+ if (mountedRef.current) {
+ setAutoSubmit(true);
+ setImmediate(() => {
+ e.target.disabled = false;
+ if (flowPath !== 'hybrid') {
+ trackEvent('IdV: location submitted', {
+ selected_location: selectedLocationAddress,
+ });
+ forceRedirect(inPersonURL!);
+ }
+ // allow process to be re-triggered in case submission did not work as expected
+ setAutoSubmit(false);
+ });
+ }
+ } catch {
+ setAutoSubmit(false);
+ } finally {
+ if (mountedRef.current) {
+ setInProgress(false);
+ }
+ }
+ },
+ [locationResults, inProgress],
+ );
+
+ return (
+ <>
+ {apiError && (
+
+ {t('idv.failure.exceptions.post_office_search_error')}
+
+ )}
+ {t('in_person_proofing.headings.po_search.location')}
+ {t('in_person_proofing.body.location.po_search.po_search_about')}
+
+ {locationResults && foundAddress && !isLoadingLocations && (
+
+ )}
+
+ >
+ );
+}
+
+export default InPersonLocationFullAddressEntryPostOfficeSearchStep;
diff --git a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx
index 1ecd45e168c..fef6d77cea7 100644
--- a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx
+++ b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx
@@ -56,7 +56,7 @@ const DEFAULT_PROPS = {
registerField() {},
};
-describe('InPersonPostOfficeSearchStep', () => {
+describe('InPersonLocationPostOfficeSearchStep', () => {
const wrapper: ComponentType = ({ children }) => (
new Map() }}>{children}
);
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 aa3d79760c1..f69d556b7f0 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
@@ -10,6 +10,7 @@ describe('InPersonOutageAlert', () => {
value={{
inPersonOutageExpectedUpdateDate: 'January 1, 2024',
inPersonOutageMessageEnabled: true,
+ inPersonFullAddressEntryEnabled: false,
}}
>
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 e1e2046f336..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
@@ -25,6 +25,8 @@ describe('InPersonPrepareStep', () => {
value={{
inPersonOutageMessageEnabled: true,
inPersonOutageExpectedUpdateDate: 'January 1, 2024',
+ inPersonFullAddressEntryEnabled: false,
+ usStatesTerritories: [],
}}
>
@@ -36,7 +38,13 @@ describe('InPersonPrepareStep', () => {
});
it('does not render a warning when the flag is disabled', () => {
const { queryByText } = render(
-
+
,
);
diff --git a/app/javascript/packages/document-capture/components/po-search-fallback-select.scss b/app/javascript/packages/document-capture/components/po-search-fallback-select.scss
new file mode 100644
index 00000000000..a3a27c2df8f
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/po-search-fallback-select.scss
@@ -0,0 +1,3 @@
+.usa-input--error.usa-input--error.usa-input--error {
+ border-color: #e21c3d;
+}
\ No newline at end of file
diff --git a/app/javascript/packages/document-capture/components/select-error.scss b/app/javascript/packages/document-capture/components/select-error.scss
new file mode 100644
index 00000000000..361d2549a72
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/select-error.scss
@@ -0,0 +1,3 @@
+.usa-input--error.usa-input--error.usa-input--error {
+ border-color: #e21c3d;
+}
diff --git a/app/javascript/packages/document-capture/context/failed-capture-attempts.tsx b/app/javascript/packages/document-capture/context/failed-capture-attempts.tsx
index 12d300bd0a5..b55935a7091 100644
--- a/app/javascript/packages/document-capture/context/failed-capture-attempts.tsx
+++ b/app/javascript/packages/document-capture/context/failed-capture-attempts.tsx
@@ -5,6 +5,7 @@ import useCounter from '../hooks/use-counter';
interface CaptureAttemptMetadata {
isAssessedAsGlare: boolean;
isAssessedAsBlurry: boolean;
+ isAssessedAsUnsupported: boolean;
}
interface FailedCaptureAttemptsContextInterface {
@@ -66,6 +67,7 @@ interface FailedCaptureAttemptsContextInterface {
const DEFAULT_LAST_ATTEMPT_METADATA: CaptureAttemptMetadata = {
isAssessedAsGlare: false,
isAssessedAsBlurry: false,
+ isAssessedAsUnsupported: false,
};
const FailedCaptureAttemptsContext = createContext({
diff --git a/app/javascript/packages/document-capture/context/in-person.ts b/app/javascript/packages/document-capture/context/in-person.ts
index 0e4e13eb1a5..818ada23e4c 100644
--- a/app/javascript/packages/document-capture/context/in-person.ts
+++ b/app/javascript/packages/document-capture/context/in-person.ts
@@ -15,10 +15,23 @@ export interface InPersonContextProps {
* Date communicated to users regarding expected update about their enrollment after an outage
*/
inPersonOutageExpectedUpdateDate?: string;
+
+ /**
+ * 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/packages/document-capture/styles.scss b/app/javascript/packages/document-capture/styles.scss
index 1bf22932693..7f5a3f89355 100644
--- a/app/javascript/packages/document-capture/styles.scss
+++ b/app/javascript/packages/document-capture/styles.scss
@@ -1,3 +1,4 @@
@import './components/acuant-capture';
@import './components/acuant-capture-canvas';
@import './components/location-collection-item';
+@import './components/select-error';
diff --git a/app/javascript/packages/form-steps/form-steps.tsx b/app/javascript/packages/form-steps/form-steps.tsx
index c1ac8ac689f..497b3e9ce56 100644
--- a/app/javascript/packages/form-steps/form-steps.tsx
+++ b/app/javascript/packages/form-steps/form-steps.tsx
@@ -31,7 +31,7 @@ interface FormStepRegisterFieldOptions {
export type RegisterFieldCallback = (
field: string,
options?: Partial,
-) => undefined | RefCallback;
+) => undefined | RefCallback;
export type OnErrorCallback = (error: Error, options?: { field?: string | null }) => void;
diff --git a/app/javascript/packages/validated-field/validated-field.spec.tsx b/app/javascript/packages/validated-field/validated-field.spec.tsx
index a292746490d..ad4ce5d1f68 100644
--- a/app/javascript/packages/validated-field/validated-field.spec.tsx
+++ b/app/javascript/packages/validated-field/validated-field.spec.tsx
@@ -162,6 +162,24 @@ describe('ValidatedField', () => {
expect(input.validationMessage).to.equal('oops');
});
+ it('validates using validate prop with select element', () => {
+ const validate = sinon.stub().throws(new Error('oops'));
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ const input = getByRole('combobox') as HTMLSelectElement;
+
+ expect(input.checkValidity()).to.be.false();
+ expect(input.validationMessage).to.equal('oops');
+ });
+
it('merges classNames', () => {
const { getByRole } = render(
diff --git a/app/javascript/packages/validated-field/validated-field.tsx b/app/javascript/packages/validated-field/validated-field.tsx
index d03bc9a6f77..497c1f976fe 100644
--- a/app/javascript/packages/validated-field/validated-field.tsx
+++ b/app/javascript/packages/validated-field/validated-field.tsx
@@ -69,13 +69,13 @@ export function getErrorMessages(inputType?: string) {
return messages;
}
-function ValidatedField(
+function ValidatedField(
{
validate = () => {},
messages,
children,
...inputProps
- }: ValidatedFieldProps & InputHTMLAttributes,
+ }: ValidatedFieldProps & InputHTMLAttributes,
forwardedRef,
) {
const fieldRef = useRef();
@@ -97,11 +97,19 @@ function ValidatedField(
nextError = nextError || (input.validity.customError && input.validationMessage) || '';
input.setCustomValidity(nextError);
- return !nextError && HTMLInputElement.prototype.checkValidity.call(input);
+ return (
+ !nextError &&
+ (input instanceof HTMLSelectElement
+ ? HTMLSelectElement.prototype.checkValidity.call(input)
+ : HTMLInputElement.prototype.checkValidity.call(input))
+ );
};
input.reportValidity = () => {
input.checkValidity();
+ if (input instanceof HTMLSelectElement) {
+ return HTMLSelectElement.prototype.reportValidity.call(input);
+ }
return HTMLInputElement.prototype.reportValidity.call(input);
};
}
@@ -109,8 +117,8 @@ function ValidatedField(
const errorId = `validated-field-error-${instanceId}`;
- const input: ReactHTMLElement = children
- ? (Children.only(children) as ReactHTMLElement)
+ const input: ReactHTMLElement = children
+ ? (Children.only(children) as ReactHTMLElement)
: createElement('input');
const inputClasses = ['validated-field__input', inputProps.className, input.props.className]
diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx
index 25d84ba0f64..d70821ccd6e 100644
--- a/app/javascript/packs/document-capture.tsx
+++ b/app/javascript/packs/document-capture.tsx
@@ -80,10 +80,17 @@ const {
cancelUrl: cancelURL,
idvInPersonUrl: inPersonURL,
securityAndPrivacyHowItWorksUrl: securityAndPrivacyHowItWorksURL,
+ 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 }],
@@ -94,6 +101,8 @@ const App = composeComponents(
inPersonURL,
inPersonOutageMessageEnabled: inPersonOutageMessageEnabled === 'true',
inPersonOutageExpectedUpdateDate,
+ inPersonFullAddressEntryEnabled: inPersonFullAddressEntryEnabled === 'true',
+ usStatesTerritories: parsedUsStatesTerritories,
},
},
],
@@ -138,7 +147,12 @@ const App = composeComponents(
maxSubmissionAttemptsBeforeNativeCamera: Number(maxSubmissionAttemptsBeforeNativeCamera),
},
],
- [DocumentCapture, { onStepChange: extendSession }],
+ [
+ DocumentCapture,
+ {
+ onStepChange: extendSession,
+ },
+ ],
);
render(, appRoot);
diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb
index 662c6ff5973..54a48fe0836 100644
--- a/app/views/idv/shared/_document_capture.html.erb
+++ b/app/views/idv/shared/_document_capture.html.erb
@@ -30,8 +30,10 @@
failure_to_proof_url: failure_to_proof_url,
idv_in_person_url: (IdentityConfig.store.in_person_doc_auth_button_enabled && Idv::InPersonConfig.enabled_for_issuer?(decorated_session.sp_issuer)) ? idv_in_person_url : nil,
security_and_privacy_how_it_works_url: MarketingSite.security_and_privacy_how_it_works_url,
+ 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/config/application.yml.default b/config/application.yml.default
index d4450e14df5..529d15b7716 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -67,9 +67,6 @@ broken_personal_key_window_start: '2021-07-29T00:00:00Z'
broken_personal_key_window_finish: '2021-09-22T00:00:00Z'
component_previews_enabled: false
country_phone_number_overrides: '{}'
-doc_auth_error_dpi_threshold: 290
-doc_auth_error_sharpness_threshold: 40
-doc_auth_error_glare_threshold: 40
database_pool_extra_connections_for_worker: 4
database_pool_idp: 5
database_socket: ''
@@ -153,6 +150,7 @@ in_person_enrollments_ready_job_wait_time_seconds: 20
in_person_results_delay_in_hours: 1
in_person_ssn_info_controller_enabled: false
in_person_completion_survey_url: 'https://login.gov'
+in_person_full_address_entry_enabled: false
in_person_outage_message_enabled: false
# in_person_outage_expected_update_date and in_person_outage_emailed_by_date below
# are strings in the format 'Month day, year'
@@ -287,7 +285,6 @@ requests_per_ip_period: 300
requests_per_ip_track_only_mode: false
reset_password_email_max_attempts: 20
reset_password_email_window_in_minutes: 60
-rewrite_oidc_request_prompt: true
risc_notifications_local_enabled: false
risc_notifications_active_job_enabled: false
risc_notifications_rate_limit_interval: 60
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index c91745ff7db..c5f00f01cba 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -52,6 +52,7 @@ en:
browser or system settings, reload this page, or upload a photo
instead.
failed: Camera failed to start, please try again.
+ card_type: Try again with your driver’s license or state ID card.
dpi:
failed_short: Image is too small or blurry, please try again.
top_msg: We couldn’t read your ID. Your image size may be too small, or your ID
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index c741fd81ca6..7c8caa16b9a 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -62,6 +62,8 @@ es:
compruebe la configuración de su navegador o sistema, recargue esta
página o suba una foto en su lugar.
failed: No se ha podido encender la cámara, por favor, inténtelo de nuevo.
+ card_type: Solo se aceptan licencias de conducir o documentos de identidad
+ estatales.
dpi:
failed_short: La imagen es demasiado pequeña o está borrosa, por favor inténtelo
de nuevo.
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index 24ff5d1f60a..cedab5fca79 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -67,6 +67,8 @@ fr:
Veuillez vérifier les paramètres de votre navigateur ou de votre
système, recharger cette page ou télécharger une photo à la place.
failed: L’appareil photo n’a pas réussi à démarrer, veuillez réessayer.
+ card_type: Réessayez avec votre permis de conduire ou carte d’identité délivrée
+ par l’État.
dpi:
failed_short: L’image est trop petite ou floue, veuillez réessayer.
top_msg: Nous n’avons pas pu lire votre pièce d’identité. La taille de votre
diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml
index 687a297745a..35f2574846d 100644
--- a/config/locales/in_person_proofing/en.yml
+++ b/config/locales/in_person_proofing/en.yml
@@ -41,8 +41,10 @@ en:
inline_error: Enter a valid address with city, state, and ZIP code
location_button: Select
po_search:
+ address_label: Address
address_search_hint: 'Example: 1234 N Example St., Allentown, PA 12345'
address_search_label: Enter an address to find a Post Office near you
+ city_label: City
is_searching_message: Searching for Post Office locations…
none_found: Sorry, there are no participating Post Offices within 50 miles of
%{address}.
@@ -57,6 +59,8 @@ en:
results_instructions: Select a Post Office location below, or search again using
a different address.
search_button: Search
+ state_label: State
+ zipcode_label: ZIP Code
retail_hours_heading: Retail Hours
retail_hours_sat: 'Sat:'
retail_hours_sun: 'Sun:'
diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml
index 2d1b9935bf1..d37e5cde735 100644
--- a/config/locales/in_person_proofing/es.yml
+++ b/config/locales/in_person_proofing/es.yml
@@ -44,9 +44,11 @@ es:
inline_error: Ingrese una dirección válida que incluya ciudad, estado y código postal
location_button: Seleccionar
po_search:
+ address_label: Dirección
address_search_hint: 'Ejemplo: 1234 N Example St., Allentown, PA 12345'
address_search_label: Introduzca una dirección para encontrar una Oficina de
Correos cercana a usted
+ city_label: Ciudad
is_searching_message: Buscando oficinas de correos…
none_found: Lo sentimos, no hay Oficinas de Correos participantes en un radio de
50 millas de la %{address}.
@@ -63,6 +65,8 @@ es:
results_instructions: Seleccione una ubicación de la Oficina de Correos a
continuación, o busque de nuevo utilizando una dirección diferente.
search_button: Buscar
+ state_label: Estado
+ zipcode_label: Código postal
retail_hours_heading: Horario de atención al público
retail_hours_sat: 'Sáb:'
retail_hours_sun: 'Dom:'
diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml
index e5bb6a14460..4e0468bd9f6 100644
--- a/config/locales/in_person_proofing/fr.yml
+++ b/config/locales/in_person_proofing/fr.yml
@@ -45,8 +45,10 @@ fr:
inline_error: Saisissez une adresse valide avec la ville, l’état et le code postal
location_button: Sélectionner
po_search:
+ address_label: Adresse
address_search_hint: 'Exemple: 1234 N Example St., Allentown, PA 12345'
address_search_label: Entrez une adresse pour trouver un bureau de poste près de chez vous.
+ city_label: Ville
is_searching_message: Recherche des emplacements de bureau de poste…
none_found: Désolé, il n’y a pas de bureaux de poste participants dans un rayon
de 50 miles de la ville %{address}
@@ -63,6 +65,8 @@ fr:
results_instructions: Sélectionnez un emplacement de bureau de poste ci-dessous,
ou effectuez une nouvelle recherche en utilisant une autre adresse.
search_button: Rechercher
+ state_label: État
+ zipcode_label: Code postal
retail_hours_heading: Heures de vente au détail
retail_hours_sat: 'Sam:'
retail_hours_sun: 'Dim:'
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 4f7ee430a3c..2b122bb88a3 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -243,6 +243,7 @@ def self.build_store(config_map)
config.add(:in_person_results_delay_in_hours, type: :integer)
config.add(:in_person_ssn_info_controller_enabled, type: :boolean)
config.add(:in_person_completion_survey_url, type: :string)
+ config.add(:in_person_full_address_entry_enabled, type: :boolean)
config.add(:in_person_outage_message_enabled, type: :boolean)
config.add(:in_person_outage_expected_update_date, type: :string)
config.add(:in_person_outage_emailed_by_date, type: :string)
@@ -390,7 +391,6 @@ def self.build_store(config_map)
config.add(:requests_per_ip_track_only_mode, type: :boolean)
config.add(:reset_password_email_max_attempts, type: :integer)
config.add(:reset_password_email_window_in_minutes, type: :integer)
- config.add(:rewrite_oidc_request_prompt, type: :boolean)
config.add(:risc_notifications_local_enabled, type: :boolean)
config.add(:risc_notifications_active_job_enabled, type: :boolean)
config.add(:risc_notifications_rate_limit_interval, type: :integer)
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 197aededbac..8c8bca397ff 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -476,13 +476,6 @@ def index
end
end
- context 'with a url that has prompt=login' do
- let(:sp_session_request_url) { '/authorize?prompt=login' }
- it 'changes it to prompt=select_account' do
- expect(url_with_updated_params).to eq('/authorize?prompt=select_account')
- end
- end
-
context 'when the locale has been changed' do
before { I18n.locale = :es }
let(:sp_session_request_url) { '/authorize' }
diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb
index 47b4f93f50b..efd31329765 100644
--- a/spec/features/idv/in_person_spec.rb
+++ b/spec/features/idv/in_person_spec.rb
@@ -930,4 +930,101 @@
end
end
end
+ context 'when manual address entry is enabled for post office search' do
+ let(:user) { user_with_2fa }
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_full_address_entry_enabled).and_return(true)
+ end
+
+ it 'allows the user to search by full address', allow_browser_log: true do
+ sign_in_and_2fa_user(user)
+ begin_in_person_proofing(user)
+ # prepare page
+ complete_prepare_step(user)
+
+ # location page
+ complete_full_address_location_step
+
+ # state ID page
+ complete_state_id_step(user)
+
+ # address page
+ complete_address_step(user)
+
+ # ssn page
+ select 'Reject', from: :mock_profiling_result
+ complete_ssn_step(user)
+
+ # verify page
+ expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
+ expect(page).to have_content(t('headings.verify'))
+ expect(page).to have_current_path(idv_in_person_verify_info_path)
+ expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME)
+ expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME)
+ expect(page).to have_text(InPersonHelper::GOOD_DOB_FORMATTED_EVENT)
+ expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER)
+ expect(page).to have_text(InPersonHelper::GOOD_ADDRESS1)
+ expect(page).to have_text(InPersonHelper::GOOD_CITY)
+ expect(page).to have_text(InPersonHelper::GOOD_ZIPCODE)
+ expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:state])
+ expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED)
+ complete_verify_step(user)
+
+ # phone page
+ expect_in_person_step_indicator_current_step(
+ t('step_indicator.flows.idv.verify_phone_or_address'),
+ )
+ expect(page).to have_content(t('titles.idv.phone'))
+ fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone)
+ click_idv_send_security_code
+ expect_in_person_step_indicator_current_step(
+ t('step_indicator.flows.idv.verify_phone_or_address'),
+ )
+
+ expect_in_person_step_indicator_current_step(
+ t('step_indicator.flows.idv.verify_phone_or_address'),
+ )
+ fill_in_code_with_last_phone_otp
+ click_submit_default
+
+ # password confirm page
+ expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account'))
+ expect(page).to have_content(t('idv.titles.session.review', app_name: APP_NAME))
+ complete_review_step(user)
+
+ # personal key page
+ expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account'))
+ expect(page).to have_content(t('titles.idv.personal_key'))
+ deadline = nil
+ freeze_time do
+ acknowledge_and_confirm_personal_key
+ deadline = (Time.zone.now +
+ IdentityConfig.store.in_person_enrollment_validity_in_days.days).
+ in_time_zone(Idv::InPerson::ReadyToVerifyPresenter::USPS_SERVER_TIMEZONE).
+ strftime(t('time.formats.event_date'))
+ end
+
+ # ready to verify page
+ expect_in_person_step_indicator_current_step(
+ t('step_indicator.flows.idv.go_to_the_post_office'),
+ )
+ expect(page).to be_axe_clean.according_to :section508, :"best-practice", :wcag21aa
+ enrollment_code = JSON.parse(
+ UspsInPersonProofing::Mock::Fixtures.request_enroll_response,
+ )['enrollmentCode']
+ expect(page).to have_content(t('in_person_proofing.headings.barcode').tr(' ', ' '))
+ expect(page).to have_content(Idv::InPerson::EnrollmentCodeFormatter.format(enrollment_code))
+ expect(page).to have_content(
+ t('in_person_proofing.body.barcode.deadline', deadline: deadline),
+ )
+ expect(page).to have_content('MILWAUKEE')
+ expect(page).to have_content('Sunday: Closed')
+
+ # signing in again before completing in-person proofing at a post office
+ Capybara.reset_session!
+ sign_in_live_with_2fa(user)
+ expect(page).to have_current_path(idv_in_person_ready_to_verify_path)
+ end
+ end
end
diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb
index 3149846a8d6..c55cb3619fe 100644
--- a/spec/features/openid_connect/openid_connect_spec.rb
+++ b/spec/features/openid_connect/openid_connect_spec.rb
@@ -24,12 +24,7 @@
expect(certs_response[:keys].find { |key| key[:kid] == kid }).to be
end
- context 'with client_secret_jwt and disabling rewrite_oidc_request_prompt' do
- before do
- allow(IdentityConfig.store).to receive(:rewrite_oidc_request_prompt).
- and_return(false)
- end
-
+ context 'with client_secret_jwt' do
it 'succeeds with prompt login and no prior session' do
oidc_end_client_secret_jwt(prompt: 'login')
end
diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx
index 96bf37f3063..320abd0c920 100644
--- a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx
@@ -547,6 +547,47 @@ describe('document-capture/components/acuant-capture', () => {
);
});
+ it('renders error message and logs metadata if capture succeeds but the document type identified is unsupported', async () => {
+ const trackEvent = sinon.spy();
+ const { getByText, findByText } = render(
+
+
+
+
+
+
+ ,
+ );
+
+ initialize({
+ start: sinon.stub().callsFake(async (callbacks) => {
+ await Promise.resolve();
+ callbacks.onCaptured();
+ await Promise.resolve();
+ callbacks.onCropped({
+ ...ACUANT_CAPTURE_SUCCESS_RESULT,
+ cardtype: 2,
+ });
+ }),
+ });
+
+ const button = getByText('doc_auth.buttons.take_picture');
+ fireEvent.click(button);
+
+ const error = await findByText('doc_auth.errors.card_type');
+
+ expect(trackEvent).to.have.been.calledWith(
+ 'IdV: test image added',
+ sinon.match({
+ documentType: 'passport',
+ isAssessedAsUnsupported: true,
+ assessment: 'unsupported',
+ }),
+ );
+
+ expect(error).to.be.ok();
+ });
+
it('renders error message if capture succeeds but photo glare exceeds threshold', async () => {
const trackEvent = sinon.spy();
const { getByText, findByText } = render(
@@ -585,6 +626,7 @@ describe('document-capture/components/acuant-capture', () => {
height: 1104,
sharpnessScoreThreshold: sinon.match.number,
glareScoreThreshold: 50,
+ isAssessedAsUnsupported: false,
isAssessedAsBlurry: false,
isAssessedAsGlare: true,
assessment: 'glare',
@@ -639,6 +681,7 @@ describe('document-capture/components/acuant-capture', () => {
height: 1104,
sharpnessScoreThreshold: 50,
glareScoreThreshold: sinon.match.number,
+ isAssessedAsUnsupported: false,
isAssessedAsBlurry: true,
isAssessedAsGlare: false,
assessment: 'blurry',
@@ -746,6 +789,7 @@ describe('document-capture/components/acuant-capture', () => {
height: 1104,
sharpnessScoreThreshold: 50,
glareScoreThreshold: sinon.match.number,
+ isAssessedAsUnsupported: false,
isAssessedAsBlurry: true,
isAssessedAsGlare: false,
assessment: 'blurry',
diff --git a/spec/javascript/packages/document-capture/components/capture-troubleshooting-spec.jsx b/spec/javascript/packages/document-capture/components/capture-troubleshooting-spec.jsx
index 35060c7313a..10e6e9b8419 100644
--- a/spec/javascript/packages/document-capture/components/capture-troubleshooting-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/capture-troubleshooting-spec.jsx
@@ -92,6 +92,7 @@ describe('document-capture/context/capture-troubleshooting', () => {
expect(trackEvent).to.have.been.calledWith('IdV: Capture troubleshooting shown', {
isAssessedAsGlare: false,
isAssessedAsBlurry: false,
+ isAssessedAsUnsupported: false,
});
const tryAgainButton = getByRole('button', { name: 'idv.failure.button.warning' });
diff --git a/spec/javascript/packages/document-capture/components/document-capture-spec.jsx b/spec/javascript/packages/document-capture/components/document-capture-spec.jsx
index 8f681bd89e7..c685a5dc62c 100644
--- a/spec/javascript/packages/document-capture/components/document-capture-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/document-capture-spec.jsx
@@ -91,6 +91,7 @@ describe('document-capture/components/document-capture', () => {
image: {
data: validUpload,
},
+ cardtype: 1,
});
});
diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
index e1d5adf102a..0a289f0386a 100644
--- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
+++ b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx
@@ -66,7 +66,7 @@ describe('document-capture/components/documents-step', () => {
);
initialize();
- const result = { sharpness: 100, image: { data: '' } };
+ const result = { sharpness: 100, image: { data: '' }, cardtype: 1 };
window.AcuantCameraUI.start.callsFake(({ onCropped }) => onCropped({ ...result, glare: 10 }));
await userEvent.click(getByLabelText('doc_auth.headings.document_capture_front'));
diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb
index 769dd513f8e..cb7b884f0d8 100644
--- a/spec/support/features/in_person_helper.rb
+++ b/spec/support/features/in_person_helper.rb
@@ -110,6 +110,37 @@ def complete_location_step(_user = nil)
end
end
+ def search_for_post_office_with_full_address
+ expect(page).to(have_content(t('in_person_proofing.headings.po_search.location')))
+ expect(page).to(have_content(t('in_person_proofing.body.location.po_search.po_search_about')))
+ expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.find_a_post_office'))
+ fill_in t('in_person_proofing.body.location.po_search.address_label'),
+ 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
+ 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'))
+ expect(page).to have_css('.location-collection-item')
+ end
+
+ def complete_full_address_location_step(_user = nil)
+ search_for_post_office_with_full_address
+ within first('.location-collection-item') do
+ click_spinner_button_and_wait t('in_person_proofing.body.location.location_button')
+ end
+
+ # pause for the location list to disappear
+ begin
+ expect(page).to have_no_css('.location-collection-item')
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError
+ # A StaleElementReferenceError means that the context the element
+ # was in has disappeared, which means the element is gone too.
+ end
+ end
+
def complete_prepare_step(_user = nil)
expect(page).to(have_content(t('in_person_proofing.headings.prepare')))
expect(page).to(have_content(t('in_person_proofing.body.prepare.verify_step_about')))