diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 99ff49e8da4..817abc52f95 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -7,7 +7,7 @@ class DocumentCaptureController < ApplicationController include StepIndicatorConcern before_action :confirm_not_rate_limited, except: [:update] - before_action :confirm_step_allowed + before_action :confirm_step_allowed, unless: -> { allow_direct_ipp? } before_action :override_csp_to_allow_acuant def show @@ -47,6 +47,7 @@ def extra_view_variables sp_name: decorated_sp_session.sp_name, failure_to_proof_url: return_to_sp_failure_to_proof_url(step: 'document_capture'), skip_doc_auth: idv_session.skip_doc_auth, + skip_doc_auth_from_handoff: idv_session.skip_doc_auth_from_handoff, opted_in_to_in_person_proofing: idv_session.opted_in_to_in_person_proofing, doc_auth_selfie_capture: decorated_sp_session.selfie_required?, }.merge( @@ -62,6 +63,7 @@ def self.step_info preconditions: ->(idv_session:, user:) { idv_session.flow_path == 'standard' && ( # mobile + idv_session.skip_doc_auth_from_handoff || idv_session.skip_hybrid_handoff || idv_session.skip_doc_auth || !idv_session.selfie_check_required || # desktop but selfie not required @@ -109,5 +111,25 @@ def handle_stored_result failure(I18n.t('doc_auth.errors.general.network_error'), extra) end end + + def allow_direct_ipp? + # not allowed when no step param and action:show(get request) + return false if params[:step].blank? || params[:action].to_s != 'show' || + idv_session.flow_path == 'hybrid' + # Only allow direct access to document capture if IPP available + return false unless IdentityConfig.store.in_person_doc_auth_button_enabled && + Idv::InPersonConfig.enabled_for_issuer?(decorated_sp_session.sp_issuer) + case params[:step] + when 'hybrid_handoff' + @previous_step_url = idv_hybrid_handoff_path + else + @previous_step_url = nil + end + # allow + idv_session.flow_path = 'standard' + idv_session.skip_doc_auth_from_handoff = true + idv_session.skip_hybrid_handoff = nil + true + end end end diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 829ff2abe7c..932183b9fe5 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -13,6 +13,9 @@ def show @upload_disabled = idv_session.selfie_check_required && !idv_session.desktop_selfie_test_mode_enabled? + @opt_in_ipp_enabled = IdentityConfig.store.in_person_doc_auth_button_enabled && + Idv::InPersonConfig.enabled_for_issuer?(decorated_sp_session.sp_issuer) + @selfie_required = idv_session.selfie_check_required analytics.idv_doc_auth_hybrid_handoff_visited(**analytics_arguments) @@ -22,6 +25,8 @@ def show true ) + # reset if we visit or come back + idv_session.skip_doc_auth_from_handoff = nil render :show, locals: extra_view_variables end @@ -42,9 +47,11 @@ def self.selected_remote(idv_session:) if IdentityConfig.store.in_person_proofing_opt_in_enabled && IdentityConfig.store.in_person_proofing_enabled && idv_session.service_provider&.in_person_proofing_enabled - idv_session.skip_doc_auth == false + idv_session.skip_doc_auth == false || + idv_session.skip_doc_auth_from_handoff == true else - idv_session.skip_doc_auth.nil? || idv_session.skip_doc_auth == false + idv_session.skip_doc_auth.nil? || idv_session.skip_doc_auth == false || + idv_session.skip_doc_auth_from_handoff == true end end diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx index 4c1f7885bb1..d01e438d5d0 100644 --- a/app/javascript/packages/document-capture/components/document-capture.tsx +++ b/app/javascript/packages/document-capture/components/document-capture.tsx @@ -37,7 +37,8 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { const { t } = useI18n(); const { flowPath } = useContext(UploadContext); const { trackSubmitEvent, trackVisitEvent } = useContext(AnalyticsContext); - const { inPersonFullAddressEntryEnabled, inPersonURL, skipDocAuth } = useContext(InPersonContext); + const { inPersonFullAddressEntryEnabled, inPersonURL, skipDocAuth, skipDocAuthFromHandoff } = + useContext(InPersonContext); const appName = getConfigValue('appName'); useDidUpdateEffect(onStepChange, [stepName]); @@ -137,12 +138,15 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { // If the user got here by opting-in to in-person proofing, when skipDocAuth === true, // then set steps to inPersonSteps - const steps: FormStep[] = skipDocAuth ? inPersonSteps : defaultSteps; + const steps: FormStep[] = skipDocAuth || skipDocAuthFromHandoff ? inPersonSteps : defaultSteps; - // If the user got here by opting-in to in-person proofing, when skipDocAuth === true, + // If the user got here by opting-in to in-person proofing, when skipDocAuth === true; + // or opting-in ipp from handoff page, and selfie is required, when skipDocAuthFromHandoff === true // then set stepIndicatorPath to VerifyFlowPath.IN_PERSON const stepIndicatorPath = - (stepName && ['location', 'prepare', 'switch_back'].includes(stepName)) || skipDocAuth + (stepName && ['location', 'prepare', 'switch_back'].includes(stepName)) || + skipDocAuth || + skipDocAuthFromHandoff ? VerifyFlowPath.IN_PERSON : VerifyFlowPath.DEFAULT; diff --git a/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx b/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx index 91ed67aad59..a8de5bbe374 100644 --- a/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx +++ b/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx @@ -20,11 +20,16 @@ function InPersonPrepareStep({ toPreviousStep }) { inPersonOutageMessageEnabled, inPersonOutageExpectedUpdateDate, skipDocAuth, + skipDocAuthFromHandoff, howToVerifyURL, + previousStepURL, } = useContext(InPersonContext); function goBack() { - if (skipDocAuth && howToVerifyURL) { + if (skipDocAuthFromHandoff && previousStepURL) { + // directly from handoff page + forceRedirect(previousStepURL); + } else if (skipDocAuth && howToVerifyURL) { forceRedirect(howToVerifyURL); } else { toPreviousStep(); diff --git a/app/javascript/packages/document-capture/context/in-person.ts b/app/javascript/packages/document-capture/context/in-person.ts index 90900fb8acf..5de622d6ccb 100644 --- a/app/javascript/packages/document-capture/context/in-person.ts +++ b/app/javascript/packages/document-capture/context/in-person.ts @@ -49,10 +49,21 @@ export interface InPersonContextProps { */ skipDocAuth?: boolean; + /** + * Flag set when user select IPP from handoff page when IPP is available + * and selfie is required + */ + skipDocAuthFromHandoff?: boolean; + /** * URL for Opt-in IPP, used when in_person_proofing_opt_in_enabled is enabled */ howToVerifyURL?: string; + + /** + * URL for going back to previous steps in Doc Auth, like handoff and howToVerify + */ + previousStepURL?: string; } const InPersonContext = createContext({ diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index 68e690dd167..d40211a206e 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -37,7 +37,9 @@ interface AppRootData { optedInToInPersonProofing: string; securityAndPrivacyHowItWorksUrl: string; skipDocAuth: string; + skipDocAuthFromHandoff: string; howToVerifyURL: string; + previousStepUrl: string; uiExitQuestionSectionEnabled: string; docAuthSelfieDesktopTestMode: string; } @@ -105,7 +107,9 @@ const { optedInToInPersonProofing, usStatesTerritories = '', skipDocAuth, + skipDocAuthFromHandoff, howToVerifyUrl, + previousStepUrl, uiExitQuestionSectionEnabled = '', docAuthSelfieDesktopTestMode, } = appRoot.dataset as DOMStringMap & AppRootData; @@ -131,7 +135,9 @@ const App = composeComponents( optedInToInPersonProofing: optedInToInPersonProofing === 'true', usStatesTerritories: parsedUsStatesTerritories, skipDocAuth: skipDocAuth === 'true', + skipDocAuthFromHandoff: skipDocAuthFromHandoff === 'true', howToVerifyURL: howToVerifyUrl, + previousStepURL: previousStepUrl, }, }, ], diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 9a9c7db6171..534a07ff45c 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -25,6 +25,7 @@ class Session selfie_check_performed selfie_check_required skip_doc_auth + skip_doc_auth_from_handoff skip_hybrid_handoff ssn threatmetrix_review_status diff --git a/app/views/idv/document_capture/show.html.erb b/app/views/idv/document_capture/show.html.erb index 4ef8ddd986b..67482ca0a14 100644 --- a/app/views/idv/document_capture/show.html.erb +++ b/app/views/idv/document_capture/show.html.erb @@ -9,5 +9,6 @@ acuant_version: acuant_version, opted_in_to_in_person_proofing: opted_in_to_in_person_proofing, skip_doc_auth: skip_doc_auth, + skip_doc_auth_from_handoff: skip_doc_auth_from_handoff, doc_auth_selfie_capture: doc_auth_selfie_capture, ) %> diff --git a/app/views/idv/hybrid_handoff/show.html.erb b/app/views/idv/hybrid_handoff/show.html.erb index a7164548357..8c0de7694bb 100644 --- a/app/views/idv/hybrid_handoff/show.html.erb +++ b/app/views/idv/hybrid_handoff/show.html.erb @@ -89,6 +89,16 @@ <%= f.submit t('forms.buttons.send_link') %> <% end %> + <% if @opt_in_ipp_enabled %> +
+

+ <%= t('doc_auth.info.hybrid_handoff_ipp_html') %>' +

+

+ <%= link_to t('in_person_proofing.headings.prepare'), idv_document_capture_path(step: :hybrid_handoff) %> +

+
+ <% end %> <% end %> diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index 02e22f02c8e..fb6e4cb1367 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -39,7 +39,9 @@ doc_auth_selfie_capture: FeatureManagement.idv_allow_selfie_check? && doc_auth_selfie_capture, doc_auth_selfie_desktop_test_mode: IdentityConfig.store.doc_auth_selfie_desktop_test_mode, skip_doc_auth: skip_doc_auth, + skip_doc_auth_from_handoff: skip_doc_auth_from_handoff, how_to_verify_url: idv_how_to_verify_url, + previous_step_url: @previous_step_url, ui_exit_question_section_enabled: IdentityConfig.store.doc_auth_exit_question_section_enabled, } %> <%= simple_form_for( diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index 119cb19b2da..5427f6bb3ed 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -208,6 +208,8 @@ en: at a participating Post Office how_to_verify_troubleshooting_options_header: Want to learn more about how to verify your identity? hybrid_handoff: We’ll collect information about you by reading your state‑issued ID. + hybrid_handoff_ipp_html: Don’t have a mobile phone? You can + verify your identity at a United States Post Office instead. image_loaded: Image loaded image_loading: Image loading image_updated: Image updated diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index 80dbecd6dd7..581f6de2bce 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -243,6 +243,9 @@ es: how_to_verify_troubleshooting_options_header: ¿Quiere saber más sobre cómo verificar su identidad? hybrid_handoff: Recopilaremos información sobre usted leyendo su documento de identidad expedido por el estado. + hybrid_handoff_ipp_html: ¿No tiene celular? Como alternativa, + puede verificar su identidad en una oficina de correos de Estados + Unidos. image_loaded: Imagen cargada image_loading: Cargando la imagen image_updated: Imagen actualizada diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index dd58bc92ec7..86a677a7ca1 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -251,6 +251,9 @@ fr: how_to_verify_troubleshooting_options_header: Vous voulez en savoir plus sur la façon de vérifier votre identité? hybrid_handoff: Nous recueillons des informations sur vous en lisant votre carte d’identité délivrée par l’État. + hybrid_handoff_ipp_html: Vous n’avez pas de téléphone + cellulaire? Vous pouvez vérifier votre identité dans un bureau + de poste américain. image_loaded: Image chargée image_loading: Chargement de l’image image_updated: Image mise à jour diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index dc1c7bec1ae..6ef47db3842 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -224,6 +224,8 @@ before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { true } allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true } + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled).and_return(true) end it 'renders show when flow path is standard' do @@ -249,6 +251,28 @@ expect(response).to redirect_to(idv_hybrid_handoff_url) end + + it 'renders show when accessed from handoff' do + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled).and_return(true) + get :show, params: { step: 'hybrid_handoff' } + expect(response).to render_template :show + expect(subject.idv_session.skip_doc_auth_from_handoff).to eq(true) + end + end + + context 'ipp disabled for sp' do + before do + allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).and_return(false) + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything).and_return(false) + allow(subject.decorated_sp_session).to receive(:selfie_required?).and_return(true) + end + it 'redirect back when accessed from handoff' do + subject.idv_session.skip_hybrid_handoff = nil + get :show, params: { step: 'hybrid_handoff' } + expect(response).to redirect_to(idv_hybrid_handoff_url) + expect(subject.idv_session.skip_doc_auth_from_handoff).to_not eq(true) + end end end diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 8efff18e1cd..41a2c3549a7 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -235,6 +235,7 @@ allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode). and_return(false) subject.idv_session.skip_doc_auth = true + subject.idv_session.skip_hybrid_handoff = true end it 'redirects to the how to verify page' do diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 797e7136fa0..93f6a4cda3a 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -569,6 +569,37 @@ expect(page).to have_current_path(idv_phone_url) end end + + context 'when ipp is enabled' do + let(:in_person_doc_auth_button_enabled) { true } + let(:sp_ipp_enabled) { true } + before do + allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled). + and_return(in_person_doc_auth_button_enabled) + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything). + and_return(sp_ipp_enabled) + end + describe 'when ipp is selected' do + it 'proceed to the next page and start ipp' do + perform_in_browser(:desktop) do + visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_hybrid_handoff_step + # we still have option to continue on handoff, since it's desktop no skip_hand_off + expect(page).to have_current_path(idv_hybrid_handoff_path) + expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie')) + click_on t('in_person_proofing.headings.prepare') + expect(page).to have_current_path( + idv_document_capture_path({ step: 'hybrid_handoff' }), + ) + expect_step_indicator_current_step( + t('step_indicator.flows.idv.find_a_post_office'), + ) + expect_doc_capture_page_header(t('in_person_proofing.headings.prepare')) + end + end + end + end end end end diff --git a/spec/features/idv/doc_auth/how_to_verify_spec.rb b/spec/features/idv/doc_auth/how_to_verify_spec.rb index ac85ea7e62f..71ce2ccbf2b 100644 --- a/spec/features/idv/doc_auth/how_to_verify_spec.rb +++ b/spec/features/idv/doc_auth/how_to_verify_spec.rb @@ -119,8 +119,10 @@ when the sp has opted into ipp' do context 'opt in false at start but true during navigation' do it 'should be bounced back from Hybrid Handoff to How to Verify' do + sleep(5) expect(page).to have_current_path(idv_hybrid_handoff_url) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { true } + sleep(5) page.refresh expect(page).to have_current_path(idv_how_to_verify_url) end diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index f7ade66a6f0..5eff9e44c0b 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -230,6 +230,40 @@ expect(mobile_form).to have_name(t('forms.buttons.send_link')) expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff_selfie')) end + context 'on a desktop choose ipp', js: true do + let(:in_person_doc_auth_button_enabled) { true } + let(:sp_ipp_enabled) { true } + before do + allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled). + and_return(in_person_doc_auth_button_enabled) + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything). + and_return(sp_ipp_enabled) + complete_doc_auth_steps_before_hybrid_handoff_step + end + + context 'when ipp is enabled' do + it 'proceeds to ipp if selected and can go back' do + expect(page).to have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html'))) + click_on t('in_person_proofing.headings.prepare') + expect(page).to have_current_path(idv_document_capture_path({ step: 'hybrid_handoff' })) + click_on t('forms.buttons.back') + expect(page).to have_current_path(idv_hybrid_handoff_path) + end + end + + context 'when ipp is disabled' do + let(:in_person_doc_auth_button_enabled) { false } + let(:sp_ipp_enabled) { false } + it 'has no ipp option can be selected' do + expect(page).to_not have_content( + strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html')), + ) + expect(page).to_not have_content( + t('in_person_proofing.headings.prepare'), + ) + end + end + end end describe 'when selfie is not required by sp' do diff --git a/spec/views/idv/hybrid_handoff/show.html.erb_spec.rb b/spec/views/idv/hybrid_handoff/show.html.erb_spec.rb index 8ee305e5127..53a6b590b34 100644 --- a/spec/views/idv/hybrid_handoff/show.html.erb_spec.rb +++ b/spec/views/idv/hybrid_handoff/show.html.erb_spec.rb @@ -34,6 +34,14 @@ expect(rendered).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff')) expect(rendered).to have_selector('h2', text: t('doc_auth.headings.upload_from_phone')) end + + it 'does not display IPP related content' do + expect(rendered).to_not have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html'))) + expect(rendered).to_not have_link( + t('in_person_proofing.headings.prepare'), + href: idv_document_capture_path(step: :hybrid_handoff), + ) + end end context 'when selfie is required' do before do @@ -48,5 +56,31 @@ it 'displays the expected headings from the "a" case' do expect(rendered).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff_selfie')) end + + describe 'when ipp is enabled' do + before do + @opt_in_ipp_enabled = true + end + it 'displays content and link for choose ipp' do + expect(rendered).to have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html'))) + expect(rendered).to have_link( + t('in_person_proofing.headings.prepare'), + href: idv_document_capture_path(step: :hybrid_handoff), + ) + end + end + + describe 'when ipp is not enabled' do + before do + @opt_in_ipp_enabled = false + end + it 'displays content and link for choose ipp' do + expect(rendered).to_not have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html'))) + expect(rendered).to_not have_link( + t('in_person_proofing.headings.prepare'), + href: idv_document_capture_path(step: :hybrid_handoff), + ) + end + end end end diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb index b8b43e93f2d..2073dc66d3a 100644 --- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb +++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb @@ -16,6 +16,7 @@ let(:acuant_version) { '1.3.3.7' } let(:skip_doc_auth) { false } + let(:skip_doc_auth_from_handoff) { false } let(:opted_in_to_in_person_proofing) { false } before do @@ -47,6 +48,7 @@ acuant_version: acuant_version, doc_auth_selfie_capture: selfie_capture_enabled, skip_doc_auth: skip_doc_auth, + skip_doc_auth_from_handoff: skip_doc_auth_from_handoff, opted_in_to_in_person_proofing: opted_in_to_in_person_proofing, } end @@ -105,6 +107,13 @@ ) end + it 'sends skip_doc_auth_from_handoff to in the frontend' do + render_partial + expect(rendered).to have_css( + "#document-capture-form[data-skip-doc-auth-from-handoff='false']", + ) + end + context 'when selfie FF enabled' do before do expect(FeatureManagement).to receive(:idv_allow_selfie_check?).at_least(:once).