diff --git a/app/controllers/idv/cancellations_controller.rb b/app/controllers/idv/cancellations_controller.rb index 1f48f3ce8e1..3798119cf0d 100644 --- a/app/controllers/idv/cancellations_controller.rb +++ b/app/controllers/idv/cancellations_controller.rb @@ -33,6 +33,16 @@ def destroy end end + def exit + analytics.idv_cancellation_confirmed(step: params[:step]) + cancel_session + if hybrid_session? + render :destroy + else + redirect_to cancelled_redirect_path + end + end + private def barcode_step? diff --git a/app/javascript/packages/components/checkbox.spec.tsx b/app/javascript/packages/components/checkbox.spec.tsx new file mode 100644 index 00000000000..23b430ddfce --- /dev/null +++ b/app/javascript/packages/components/checkbox.spec.tsx @@ -0,0 +1,49 @@ +import { render } from '@testing-library/react'; +import Checkbox from './checkbox'; + +describe('Checkbox', () => { + it('renders given checkbox', () => { + const { getByRole, getByText } = render( + , + ); + + const checkbox = getByRole('checkbox'); + expect(checkbox.classList.contains('usa-checkbox__input')).to.be.true(); + expect(checkbox.classList.contains('usa-button__input-title')).to.be.false(); + expect(checkbox.id).to.eq('checkbox1'); + + const label = getByText('A checkbox'); + expect(label).to.be.ok(); + expect(label.classList.contains('usa-checkbox__label')).to.be.true(); + expect(label.getAttribute('for')).eq('checkbox1'); + + const labelDescription = getByText('A checkbox for testing'); + expect(labelDescription).to.be.ok(); + expect(labelDescription.classList.contains('usa-checkbox__label-description')).to.be.true(); + }); + + context('with isTitle', () => { + it('renders with correct style', () => { + const { getByRole } = render( + , + ); + const checkbox = getByRole('checkbox'); + expect(checkbox.classList.contains('usa-button__input-title')).to.be.true(); + }); + }); + + context('with hint', () => { + it('renders hint', () => { + const { getByText } = render( + , + ); + const hint = getByText('Please check this box'); + expect(hint).to.be.ok(); + }); + }); +}); diff --git a/app/javascript/packages/components/checkbox.tsx b/app/javascript/packages/components/checkbox.tsx new file mode 100644 index 00000000000..286087bb90f --- /dev/null +++ b/app/javascript/packages/components/checkbox.tsx @@ -0,0 +1,74 @@ +import type { InputHTMLAttributes } from 'react'; +import { useInstanceId } from '@18f/identity-react-hooks'; + +export interface CheckboxProps extends InputHTMLAttributes { + /** + * Whether checkbox is considered title, a box around it, with optional description for the label + */ + isTitle?: boolean; + /** + * Whether is focused, a focus box around the checkbox + */ + isFocus?: boolean; + /** + * Optional id for the element + */ + id?: string; + /** + * Optional additional class names. + */ + className?: string; + /** + * Label text for the checkbox + */ + label: string; + /** + * Optional description for the label, used with isTitle + */ + labelDescription?: string; + /** + * Muted explainer text sitting below the label. + */ + hint?: string; +} + +function Checkbox({ + id, + isTitle, + isFocus, + className, + label, + labelDescription, + hint, + ...inputProps +}: CheckboxProps) { + const instanceId = useInstanceId(); + const inputId = id ?? `check-input-${instanceId}`; + const hintId = id ?? `check-input-hint-${instanceId}`; + const classes = [ + 'usa-checkbox__input', + isTitle && 'usa-button__input-title', + isFocus && 'usa-focus', + className, + ] + .filter(Boolean) + .join(' '); + + return ( +
+ + + {hint && ( +
+ {hint} +
+ )} +
+ ); +} +export default Checkbox; diff --git a/app/javascript/packages/components/field-set-spec.tsx b/app/javascript/packages/components/field-set-spec.tsx new file mode 100644 index 00000000000..a57ef4dfee0 --- /dev/null +++ b/app/javascript/packages/components/field-set-spec.tsx @@ -0,0 +1,30 @@ +import { render } from '@testing-library/react'; +import FieldSet from './field-set'; + +describe('FieldSet', () => { + it('renders given fieldset', () => { + const { getByRole, getByText } = render( +
+

Inner text

+
, + ); + const fieldSet = getByRole('group'); + expect(fieldSet).to.be.ok(); + expect(fieldSet.classList.contains('usa-fieldset')).to.be.true(); + + const child = getByText('Inner text'); + expect(child).to.be.ok(); + }); + context('with legend', () => { + it('renders legend', () => { + const { getByText } = render( +
+

Inner text

+
, + ); + const legend = getByText('Legend text'); + expect(legend).to.be.ok(); + expect(legend.classList.contains('usa-legend')).to.be.true(); + }); + }); +}); diff --git a/app/javascript/packages/components/field-set.tsx b/app/javascript/packages/components/field-set.tsx new file mode 100644 index 00000000000..08e0a2b46d1 --- /dev/null +++ b/app/javascript/packages/components/field-set.tsx @@ -0,0 +1,21 @@ +import { FieldsetHTMLAttributes, ReactNode } from 'react'; + +export interface FieldSetProps extends FieldsetHTMLAttributes { + /** + * Footer contents. + */ + children: ReactNode; + + legend?: string; +} + +function FieldSet({ legend, children }: FieldSetProps) { + return ( +
+ {legend && {legend}} + {children} +
+ ); +} + +export default FieldSet; diff --git a/app/javascript/packages/components/index.ts b/app/javascript/packages/components/index.ts index c3b5dac43ee..5888c91f13f 100644 --- a/app/javascript/packages/components/index.ts +++ b/app/javascript/packages/components/index.ts @@ -21,8 +21,11 @@ export { default as StatusPage } from './status-page'; export { default as Tag } from './tag'; export { default as TextInput } from './text-input'; export { default as TroubleshootingOptions } from './troubleshooting-options'; +export { default as Checkbox } from './checkbox'; +export { default as FieldSet } from './field-set'; export type { ButtonProps } from './button'; export type { FullScreenRefHandle } from './full-screen'; export type { LinkProps } from './link'; export type { TextInputProps } from './text-input'; +export type { CheckboxProps } from './checkbox'; diff --git a/app/javascript/packages/document-capture/components/document-capture-abandon.tsx b/app/javascript/packages/document-capture/components/document-capture-abandon.tsx new file mode 100644 index 00000000000..e381fbddaf3 --- /dev/null +++ b/app/javascript/packages/document-capture/components/document-capture-abandon.tsx @@ -0,0 +1,135 @@ +import { Tag, Checkbox, FieldSet, Button, Link } from '@18f/identity-components'; +import { useI18n } from '@18f/identity-react-i18n'; +import { useContext, useState } from 'react'; +import FlowContext from '@18f/identity-verify-flow/context/flow-context'; +import formatHTML from '@18f/identity-react-i18n/format-html'; +import { addSearchParams, forceRedirect, Navigate } from '@18f/identity-url'; +import { getConfigValue } from '@18f/identity-config'; +import AnalyticsContext from '../context/analytics'; +import { ServiceProviderContext } from '../context'; + +function formatContentHtml({ msg, url }) { + return formatHTML(msg, { + a: ({ children }) => ( + + {children} + + ), + strong: ({ children }) => {children}, + }); +} + +export interface DocumentCaptureAbandonProps { + navigate?: Navigate; +} + +function DocumentCaptureAbandon({ navigate }: DocumentCaptureAbandonProps) { + const { t } = useI18n(); + const { trackEvent } = useContext(AnalyticsContext); + const { currentStep, exitURL, cancelURL } = useContext(FlowContext); + const { name: spName } = useContext(ServiceProviderContext); + const appName = getConfigValue('appName'); + const header =

{t('doc_auth.exit_survey.header')}

; + + const content = ( +

+ {formatContentHtml({ + msg: spName?.trim() + ? t('doc_auth.exit_survey.content_html', { + sp_name: spName, + app_name: appName, + }) + : t('doc_auth.exit_survey.content_nosp_html', { + app_name: appName, + }), + url: addSearchParams(spName?.trim() ? exitURL : cancelURL, { + step: currentStep, + location: 'optional_question', + }), + })} +

+ ); + const optionalTag = ( + + {t('doc_auth.exit_survey.optional.tag', { app_name: appName })} + + ); + const optionalText = ( +

+ {t('doc_auth.exit_survey.optional.content', { app_name: appName })} +

+ ); + + const idTypeLabels = [ + t('doc_auth.exit_survey.optional.id_types.us_passport'), + t('doc_auth.exit_survey.optional.id_types.resident_card'), + t('doc_auth.exit_survey.optional.id_types.military_id'), + t('doc_auth.exit_survey.optional.id_types.tribal_id'), + t('doc_auth.exit_survey.optional.id_types.voter_registration_card'), + t('doc_auth.exit_survey.optional.id_types.other'), + ]; + + const allIdTypeOptions = [ + { name: 'us_passport', checked: false }, + { name: 'resident_card', checked: false }, + { name: 'military_id', checked: false }, + { name: 'tribal_id', checked: false }, + { name: 'voter_registration_card', checked: false }, + { name: 'other', checked: false }, + ]; + + const [idTypeOptions, setIdTypeOptions] = useState(allIdTypeOptions); + + const updateCheckStatus = (index: number) => { + setIdTypeOptions( + idTypeOptions.map((idOption, currentIndex) => + currentIndex === index ? { ...idOption, checked: !idOption.checked } : { ...idOption }, + ), + ); + }; + + const checkboxes = ( + <> + {idTypeOptions.map((idType, idx) => ( + updateCheckStatus(idx)} + /> + ))} + + ); + + const handleExit = () => { + trackEvent('IdV: exit optional questions', { ids: idTypeOptions }); + forceRedirect( + addSearchParams(spName ? exitURL : cancelURL, { + step: currentStep, + location: 'optional_question', + }), + navigate, + ); + }; + + return ( + <> + {header} + {content} +
+ {optionalTag} + {optionalText} +
{checkboxes}
+ +
+ {t('idv.legal_statement.information_collection')} +
+
+ + ); +} + +export default DocumentCaptureAbandon; diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx index 44dc1a798eb..dc81220c050 100644 --- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx @@ -10,6 +10,7 @@ import { useI18n } from '@18f/identity-react-i18n'; import UnknownError from './unknown-error'; import TipList from './tip-list'; import DocumentSideAcuantCapture from './document-side-acuant-capture'; +import DocumentCaptureAbandon from './document-capture-abandon'; interface DocumentCaptureReviewIssuesProps { isFailedDocType: boolean; @@ -78,6 +79,7 @@ function DocumentCaptureReviewIssues({ /> ))} + ); diff --git a/app/javascript/packages/document-capture/components/documents-step.jsx b/app/javascript/packages/document-capture/components/documents-step.jsx index bc88c754cf7..bad438701a6 100644 --- a/app/javascript/packages/document-capture/components/documents-step.jsx +++ b/app/javascript/packages/document-capture/components/documents-step.jsx @@ -8,6 +8,7 @@ import DocumentSideAcuantCapture from './document-side-acuant-capture'; import DeviceContext from '../context/device'; import UploadContext from '../context/upload'; import TipList from './tip-list'; +import DocumentCaptureAbandon from './document-capture-abandon'; /** * @typedef {'front'|'back'} DocumentSide @@ -70,6 +71,8 @@ function DocumentsStep({ /> ))} {isLastStep ? : } + + ); diff --git a/app/javascript/packages/document-capture/components/optional-questions.scss b/app/javascript/packages/document-capture/components/optional-questions.scss new file mode 100644 index 00000000000..77cfd283403 --- /dev/null +++ b/app/javascript/packages/document-capture/components/optional-questions.scss @@ -0,0 +1,23 @@ +@use 'uswds-core' as *; + +.document-capture-optional-questions { + margin-top: 1.5em; + .usa-fieldset { + margin-top: 0; + .usa-legend { + text-transform: none; + margin-top: 0; + font-size: 1rem; + line-height: 1.4; + display: block; + border-bottom: none; + padding-bottom: 0; + padding-top: 0; + } + .usa-checkbox { + .usa-checkbox__label { + display: block; //collapsing margin + } + } + } +} diff --git a/app/javascript/packages/document-capture/styles.scss b/app/javascript/packages/document-capture/styles.scss index 7f5a3f89355..ef6e8d0ea8e 100644 --- a/app/javascript/packages/document-capture/styles.scss +++ b/app/javascript/packages/document-capture/styles.scss @@ -2,3 +2,4 @@ @import './components/acuant-capture-canvas'; @import './components/location-collection-item'; @import './components/select-error'; +@import './components/optional-questions'; diff --git a/app/javascript/packages/verify-flow/cancel.spec.tsx b/app/javascript/packages/verify-flow/cancel.spec.tsx index 33492d2883e..2529a0c5359 100644 --- a/app/javascript/packages/verify-flow/cancel.spec.tsx +++ b/app/javascript/packages/verify-flow/cancel.spec.tsx @@ -15,6 +15,7 @@ describe('Cancel', () => { diff --git a/app/javascript/packages/verify-flow/context/flow-context.tsx b/app/javascript/packages/verify-flow/context/flow-context.tsx index 1579c06e97d..fe465c47284 100644 --- a/app/javascript/packages/verify-flow/context/flow-context.tsx +++ b/app/javascript/packages/verify-flow/context/flow-context.tsx @@ -6,6 +6,11 @@ export interface FlowContextValue { */ cancelURL: string; + /** + * URL to exit session without confirmation + */ + exitURL: string; + /** * Current step name. */ @@ -14,6 +19,7 @@ export interface FlowContextValue { const FlowContext = createContext({ cancelURL: '', + exitURL: '', currentStep: '', }); diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index 3448e037f5b..12bf557e1a2 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -30,6 +30,7 @@ interface AppRootData { acuantVersion: string; flowPath: FlowPath; cancelUrl: string; + exitUrl: string; idvInPersonUrl?: string; securityAndPrivacyHowItWorksUrl: string; } @@ -76,6 +77,7 @@ const { acuantVersion, flowPath, cancelUrl: cancelURL, + exitUrl: exitURL, idvInPersonUrl: inPersonURL, securityAndPrivacyHowItWorksUrl: securityAndPrivacyHowItWorksURL, inPersonFullAddressEntryEnabled, @@ -134,6 +136,7 @@ const App = composeComponents( { value: { cancelURL, + exitURL, currentStep: 'document_capture', }, }, diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index fb737b2b7de..56abfe5670b 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -26,6 +26,7 @@ sp_name: sp_name, flow_path: flow_path, cancel_url: idv_cancel_path(step: :document_capture), + exit_url: idv_exit_path, 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_sp_session.sp_issuer)) ? idv_in_person_url : nil, security_and_privacy_how_it_works_url: MarketingSite.security_and_privacy_how_it_works_url, diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index b5f01497c83..2acc0b4711a 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -105,6 +105,30 @@ en: top_msg_plural: We couldn’t read your ID. Your photos may be too blurry or dark. Try taking new pictures in a bright area. upload_error: Sorry, something went wrong on our end. + exit_survey: + content_html: If you do not have a driver’s license or state ID card, + you cannot continue with %{app_name}. Please exit + %{app_name} and contact %{sp_name} to find out what you can do. + content_nosp_html: If you do not have a driver’s license or state ID + card, you cannot continue with %{app_name}. Cancel verifying + your identity with %{app_name} and you can restart the process when + you’re ready. + header: Don’t have a driver’s license or state ID? + optional: + button: Submit and exit %{app_name} + content: Help us add more identity documents to %{app_name}. Which types of + identity documents do you have instead? + id_types: + military_id: Military ID card (this includes a Department of Defense + Identification Card, a Veteran Health Identification Card, or a + Veteran ID Card) + other: Something not listed + resident_card: U.S. Green Card (also referred to as a Permanent Resident Card) + tribal_id: Tribal ID card + us_passport: U.S. Passport + voter_registration_card: Voter registration card + legend: Optional. Select any of the documents you have. + tag: Optional forms: captured_image: Captured Image change_file: Change file diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index 96d3c2dad44..0c837758153 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -133,6 +133,32 @@ es: estén demasiado borrosas u oscuras. Intente tomar nuevas fotos en un área iluminada. upload_error: Lo siento, algo salió mal por nuestra parte. + exit_survey: + content_html: Si no tiene una licencia de conducir o identificación + estatal, no puede continuar en %{app_name}. Por favor salga + de %{app_name} y contacte a %{sp_name} para averiguar qué puede + hacer. + content_nosp_html: Si no tiene una licencia de conducir o identificación + estatal, no puede continuar en %{app_name}. Cancele la + verificación de identidad con %{app_name} y podrá reiniciar el + proceso cuando esté listo. + header: No cuenta con una licencia de conducir o identificación estatal? + optional: + button: Enviar y salir de %{app_name} + content: Ayúdenos a agregar más identificaciones oficiales a %{app_name}. ¿Qué + identificaciones tiene como alternativa? + id_types: + military_id: Credencial militar (puede ser una credencial del Departamento de + Defensa, una credencial de Salud de los Veteranos o una credencial + de veterano) + other: Una que no aparece en la lista + resident_card: Tarjeta Verde de Estados Unidos (también conocida como Tarjeta de + Residente Permanente) + tribal_id: Credencial tribal + us_passport: Pasaporte estadounidense + voter_registration_card: Credencial para votar + legend: Opcional. Seleccione todas las identificaciones que tenga. + tag: Opcional forms: captured_image: Imagen capturada change_file: Cambiar archivo diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index 3ec6678a271..9ed3aa1b518 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -139,6 +139,33 @@ fr: peut-être trop floues ou trop sombres. Essayez de prendre de nouvelles photos dans un endroit lumineux. upload_error: Désolé, quelque chose a mal tourné de notre côté. + exit_survey: + content_html: Si vous n’avez pas de permis de conduire ou de carte + d’identité de l’État, vous ne pouvez pas continuer à utiliser + %{app_name}. Veuillez quitter %{app_name} et contacter + %{sp_name} pour savoir ce que vous pouvez faire. + content_nosp_html: Si vous n’avez pas de permis de conduire ou de carte + d’identité de l’État, vous ne pouvez pas continuer à utiliser + %{app_name}. Annulez la vérification de votre identité avec + %{app_name} et vous pourrez redémarrer le processus lorsque vous + serez prêt. + header: N’avez-vous pas de permis de conduire ou de carte d’identité de l’État? + optional: + button: Soumettre et quitter %{app_name} + content: Aidez-nous à ajouter plus de documents d’identité à %{app_name}. Quels + types de documents d’identité avez-vous à la place? + id_types: + military_id: Carte d’identité militaire (y compris la carte d’identité du + ministère de la défense, la carte d’identité médicale des anciens + combattants ou la carte d’identité des anciens combattants) + other: Quelque chose qui ne figure pas dans la liste + resident_card: Carte Verte américaine (également appelée Carte de Résident + Permanent) + tribal_id: Carte d’identité tribale + us_passport: Passeport américain + voter_registration_card: Carte d’électeur + legend: Facultatif. Sélectionnez l’un des documents que vous possédez. + tag: Facultatif forms: captured_image: Image capturée change_file: Changer de fichier @@ -279,6 +306,7 @@ fr: lettre à votre adresse personnelle. Cela prend 5 à 10 jours.' welcome: 'Vous aurez besoin de votre:' + phone_question: do_not_have: Je n’ai pas de téléphone tips: diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index d42d783daba..cd766cfb952 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -209,6 +209,19 @@ en: wrong_address: Not the right address? images: come_back_later: Letter with a check mark + legal_statement: + information_collection: >- + This information collection meets the requirements of 44 U.S.C. § 3507, + as amended by section 2 of the Paperwork Reduction Act of 1995. You do + not need to answer these questions unless we display a valid Office of + Management and Budget (OMB) control number. The OMB control number for + this collection is 3090-0325. We estimate that it will take 1 minute to + read the instructions, gather the facts, and answer the questions. Send + only comments relating to our time estimate, including suggestions for + reducing this burden, or any other aspects of this collection of + information to: General Services Administration, Regulatory Secretariat + Division (MVCB), ATTN: Lois Mandell/IC 3090-0325, 1800 F Street, NW, + Washington, DC 20405. messages: activated_html: Your identity has been verified. If you need to change your verified information, please %{link_html}. diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index d1362c482a5..48f712aab43 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -218,6 +218,21 @@ es: wrong_address: ¿La dirección no es correcta? images: come_back_later: Carta con una marca de verificación + legal_statement: + information_collection: >- + Cette collecte d’informations répond aux exigences de l’article 3507 du + 44 U.S.C., tel que modifié par l’article 2 de la loi de 1995 sur la + réduction des tâches administratives. Vous n’avez pas besoin de répondre + à ces questions, sauf si nous affichons un numéro de contrôle valide de + l’Office of Management and Budget (OMB). Le numéro de contrôle de l’OMB + pour cette collecte est 3090-0325. Calculamos que tomará un minuto leer + las instrucciones, recopilar los datos y responder las preguntas. Envíe + solo comentarios relacionados con nuestro tiempo estimado, incluidas + sugerencias para reducir esta molestia, o cualquier otro aspecto + relacionado con esta recopilación de información a: Administración de + Servicios Generales, División de la Secretaría Reguladora (MVCB), a la + atención de Lois Mandell/IC 3090-0325, 1800 F Street, NW, Washington, D. + C. 20405. messages: activated_html: Su identidad ha sido verificada. Si necesita cambiar la información verificada, por favor, %{link_html}. diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index b9e1483000f..ed607f371db 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -229,6 +229,20 @@ fr: wrong_address: Pas la bonne adresse? images: come_back_later: Lettre avec un crochet + legal_statement: + information_collection: >- + Esta recopilación de información cumple con los requisitos del título 44 + del U.S.C., § 3507, modificado por la sección 2 de la Ley de Reducción + de Trámites de 1995. No es necesario que responda a estas preguntas, a + menos que le mostremos un número de control válido de la Oficina de + Administración y Presupuesto (OMB). El número de control de la OMB para + esta recopilación es 3090-0325. Nous estimons qu’il faut une minute pour + lire les instructions, rassembler les preuves et répondre aux questions. + N’envoyez que des commentaires relatifs à notre estimation du temps, y + compris des suggestions pour réduire cette charge, ou tout autre aspect + de cette collecte d’informations à : General Services Administration, + Regulatory Secretariat Division (MVCB), ATTN : Lois Mandell/IC + 3090-0325, 1800 F Street, NW, Washington, DC 20405. messages: activated_html: Votre identité a été vérifiée. Si vous souhaitez modifier votre information vérifiée, veuillez %{link_html}. diff --git a/config/routes.rb b/config/routes.rb index 7430fdda8cd..2f8df3af0df 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -369,6 +369,7 @@ get '/cancel/' => 'cancellations#new', as: :cancel put '/cancel' => 'cancellations#update' delete '/cancel' => 'cancellations#destroy' + get '/exit' => 'cancellations#exit', as: :exit get '/address' => 'address#new' post '/address' => 'address#update' get '/capture_doc' => 'hybrid_mobile/entry#show' diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index eedb3fef6bc..cad9cfa9349 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -134,6 +134,20 @@ expect(DocAuthLog.find_by(user_id: user.id).state).to be_nil end + + it 'return to sp when click on exit link', :js do + click_sp_exit_link(sp_name: sp_name) + expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') + end + + it 'logs event and return to sp when click on submit and exit button', :js do + click_submit_exit_button + expect(fake_analytics).to have_logged_event( + 'Frontend: IdV: exit optional questions', + hash_including('ids'), + ) + expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') + end end context 'standard mobile flow' do @@ -162,6 +176,16 @@ expect(page).to have_current_path(idv_phone_url) end end + + it 'return to sp when click on exit link', :js do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_document_capture_step + click_sp_exit_link(sp_name: sp_name) + expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') + end + end end def expect_costing_for_document diff --git a/spec/javascript/packages/document-capture/components/document-capture-abandon-spec.tsx b/spec/javascript/packages/document-capture/components/document-capture-abandon-spec.tsx new file mode 100644 index 00000000000..1235587fefa --- /dev/null +++ b/spec/javascript/packages/document-capture/components/document-capture-abandon-spec.tsx @@ -0,0 +1,199 @@ +import sinon from 'sinon'; + +import { FlowContext } from '@18f/identity-verify-flow'; +import DocumentCaptureAbandon from '@18f/identity-document-capture/components/document-capture-abandon'; +import { I18nContext } from '@18f/identity-react-i18n'; +import { I18n } from '@18f/identity-i18n'; +import userEvent from '@testing-library/user-event'; +import type { Navigate } from '@18f/identity-url'; +import { + AnalyticsContextProvider, + ServiceProviderContextProvider, +} from '@18f/identity-document-capture/context'; +import { expect } from 'chai'; +import { render } from '../../../support/document-capture'; + +describe('DocumentCaptureAbandon', () => { + beforeEach(() => { + const config = document.createElement('script'); + config.id = 'test-config'; + config.type = 'application/json'; + config.setAttribute('data-config', ''); + config.textContent = JSON.stringify({ appName: 'Login.gov' }); + document.body.append(config); + }); + const trackEvent = sinon.spy(); + const navigateSpy: Navigate = sinon.spy(); + context('with service provider', () => { + const spName = 'testSP'; + it('renders, track event and redirect', async () => { + const { getByRole, getByText } = render( + + '', + }} + > + + exit %{app_name} and contact %{sp_name} to find out what you can do.', + 'doc_auth.exit_survey.optional.button': 'Submit and exit %{app_name}', + }, + }) + } + > + + + + + , + ); + // header + expect(getByRole('heading', { name: 'header text', level: 2 })).to.be.ok(); + + // content and exit link + const exitLink = getByRole('link', { name: 'exit Login.gov and contact testSP' }); + expect(exitLink).to.be.ok(); + expect(exitLink.getAttribute('href')).to.contain( + '/exit?step=document_capture&location=optional_question', + ); + + expect(getByText('doc_auth.exit_survey.optional.tag')).to.be.ok(); + // legend + expect(getByText('doc_auth.exit_survey.optional.legend')).to.be.ok(); + // checkboxes + expect( + getByRole('checkbox', { name: 'doc_auth.exit_survey.optional.id_types.us_passport' }), + ).to.be.ok(); + expect( + getByRole('checkbox', { name: 'doc_auth.exit_survey.optional.id_types.resident_card' }), + ).to.be.ok(); + const militaryId = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.military_id', + }); + expect(militaryId).to.be.ok(); + expect( + getByRole('checkbox', { name: 'doc_auth.exit_survey.optional.id_types.tribal_id' }), + ).to.be.ok(); + expect( + getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.voter_registration_card', + }), + ).to.be.ok(); + const otherId = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.other', + }); + expect(otherId).to.be.ok(); + + // legal statement + expect(getByText('idv.legal_statement.information_collection')).to.be.ok(); + + // exit button + const exitButton = getByRole('button', { name: 'Submit and exit Login.gov' }); + expect(exitButton).to.be.ok(); + expect(exitButton.classList.contains('usa-button--outline')).to.be.true(); + + await userEvent.click(otherId); + await userEvent.click(militaryId); + await userEvent.click(exitButton); + expect(navigateSpy).to.be.called.calledWithMatch( + /exit\?step=document_capture&location=optional_question/, + ); + expect(trackEvent).to.be.calledWithMatch(/IdV: exit optional questions/, { + ids: [ + { name: 'us_passport', checked: false }, + { name: 'resident_card', checked: false }, + { name: 'military_id', checked: true }, + { name: 'tribal_id', checked: false }, + { name: 'voter_registration_card', checked: false }, + { name: 'other', checked: true }, + ], + }); + }); + }); + + context('without service provider', () => { + it('renders, track event and redirect', async () => { + const { getByRole, getByText } = render( + + '' }} + > + + Cancel verifying your identity with %{app_name} and you can restart the process when you’re ready.', + 'doc_auth.exit_survey.optional.button': 'Submit and exit %{app_name}', + }, + }) + } + > + + + + + , + ); + + expect( + getByRole('link', { name: 'Cancel verifying your identity with Login.gov' }).getAttribute( + 'href', + ), + ).to.contain('/cancel?step=document_capture&location=optional_question'); + + expect(getByText('doc_auth.exit_survey.optional.tag')).to.be.ok(); + + const usPassport = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.us_passport', + }); + expect(usPassport).to.be.ok(); + const otherId = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.other', + }); + expect(otherId).to.be.ok(); + + const exitButton = getByRole('button', { name: 'Submit and exit Login.gov' }); + expect(exitButton).to.be.ok(); + + await userEvent.click(otherId); + await userEvent.click(usPassport); + await userEvent.click(exitButton); + expect(navigateSpy).to.be.calledWithMatch( + /cancel\?step=document_capture&location=optional_question/, + ); + expect(trackEvent).to.be.calledWithMatch(/IdV: exit optional questions/, { + ids: [ + { name: 'us_passport', checked: true }, + { name: 'resident_card', checked: false }, + { name: 'military_id', checked: false }, + { name: 'tribal_id', checked: false }, + { name: 'voter_registration_card', checked: false }, + { name: 'other', checked: true }, + ], + }); + }); + }); +}); diff --git a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx b/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx index 28244c1787c..d640c75a01c 100644 --- a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx +++ b/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx @@ -67,9 +67,8 @@ describe('DocumentCaptureReviewIssues', () => { const backCapture = getByLabelText('doc_auth.headings.document_capture_back'); expect(backCapture).to.be.ok(); expect(getByText('back side error')).to.be.ok(); - const submitButton = getByRole('button'); - expect(submitButton).to.be.ok(); - expect(within(submitButton).getByText('forms.buttons.submit.default')).to.be.ok(); + expect(getByRole('button', { name: 'forms.buttons.submit.default' })).to.be.ok(); + expect(getByRole('button', { name: 'doc_auth.exit_survey.optional.button' })).to.be.ok(); }); it('renders for a doc type failure', () => { @@ -115,9 +114,8 @@ describe('DocumentCaptureReviewIssues', () => { const backCapture = getByLabelText('doc_auth.headings.document_capture_back'); expect(backCapture).to.be.ok(); expect(getByText('back side doc type error')).to.be.ok(); - const submitButton = getByRole('button'); - expect(submitButton).to.be.ok(); - expect(within(submitButton).getByText('forms.buttons.submit.default')).to.be.ok(); + expect(getByRole('button', { name: 'forms.buttons.submit.default' })).to.be.ok(); + expect(getByRole('button', { name: 'doc_auth.exit_survey.optional.button' })).to.be.ok(); }); }); }); 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 d160d95c5b4..ecff722a4d4 100644 --- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx @@ -82,4 +82,16 @@ describe('document-capture/components/documents-step', () => { expect(queryByText(notExpectedText)).to.not.exist(); }); + + it('renders optional question part', () => { + const { getByRole, getByText } = render( + + + + + , + ); + expect(getByRole('heading', { name: 'doc_auth.exit_survey.header', level: 2 })).to.be.ok(); + expect(getByText('doc_auth.exit_survey.optional.button')).to.be.ok(); + }); }); diff --git a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx index fac01242337..d24b71cfd1a 100644 --- a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx @@ -343,6 +343,14 @@ describe('document-capture/components/review-issues-step', () => { expect(getByLabelText('doc_auth.headings.document_capture_back')).to.be.ok(); }); + it('renders optional questions', async () => { + const { getByText, getByRole } = render(); + + await userEvent.click(getByRole('button', { name: 'idv.failure.button.warning' })); + expect(getByRole('heading', { name: 'doc_auth.exit_survey.header', level: 2 })).to.be.ok(); + expect(getByText('doc_auth.exit_survey.optional.button')).to.be.ok(); + }); + context('service provider context', () => { context('ial2', () => { it('renders with front and back inputs', async () => { diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index ca5dcdcf67b..df666b4862a 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -53,4 +53,12 @@ def api_image_submission_test_credential_part def click_try_again click_spinner_button_and_wait t('idv.failure.button.warning') end + + def click_sp_exit_link(sp_name: 'Test SP') + click_on "exit Login.gov and contact #{sp_name}" + end + + def click_submit_exit_button + click_on 'Submit and exit Login.gov' + end end