diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index dff19236bd7..c57d4f55329 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -49,6 +49,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'), phone_with_camera: idv_session.phone_with_camera, + skip_doc_auth: idv_session.skip_doc_auth, }.merge( acuant_sdk_upgrade_a_b_testing_variables, phone_question_ab_test_analytics_bucket, diff --git a/app/controllers/idv/how_to_verify_controller.rb b/app/controllers/idv/how_to_verify_controller.rb index 0be8af23274..cab3caa2a62 100644 --- a/app/controllers/idv/how_to_verify_controller.rb +++ b/app/controllers/idv/how_to_verify_controller.rb @@ -5,12 +5,19 @@ class HowToVerifyController < ApplicationController include RenderConditionConcern before_action :confirm_step_allowed + before_action :confirm_verify_info_step_needed check_or_render_not_found -> { self.class.enabled? } def show + @selection = if idv_session.skip_doc_auth == false + Idv::HowToVerifyForm::REMOTE + elsif idv_session.skip_doc_auth == true + Idv::HowToVerifyForm::IPP + end + analytics.idv_doc_auth_how_to_verify_visited(**analytics_arguments) - @idv_how_to_verify_form = Idv::HowToVerifyForm.new + @idv_how_to_verify_form = Idv::HowToVerifyForm.new(selection: @selection) end def update @@ -23,10 +30,14 @@ def update if result.success? if how_to_verify_form_params['selection'] == Idv::HowToVerifyForm::REMOTE + idv_session.skip_doc_auth = false redirect_to idv_hybrid_handoff_url else + idv_session.flow_path = 'standard' + idv_session.skip_doc_auth = true redirect_to idv_document_capture_url end + else flash[:error] = result.first_error_message redirect_to idv_how_to_verify_url @@ -41,7 +52,7 @@ def self.step_info preconditions: ->(idv_session:, user:) do self.enabled? && idv_session.idv_consent_given end, - undo_step: ->(idv_session:, user:) {}, # clear any saved data + undo_step: ->(idv_session:, user:) { idv_session.skip_doc_auth = nil }, ) end diff --git a/app/forms/idv/how_to_verify_form.rb b/app/forms/idv/how_to_verify_form.rb index b2b2066fc46..f23329917b1 100644 --- a/app/forms/idv/how_to_verify_form.rb +++ b/app/forms/idv/how_to_verify_form.rb @@ -1,42 +1,23 @@ -# frozen_string_literal: true - module Idv class HowToVerifyForm include ActiveModel::Model - ATTRIBUTES = [:selection].freeze - REMOTE = 'remote' - IPP = 'ipp' - attr_accessor :selection + REMOTE = 'remote'.freeze + IPP = 'ipp'.freeze + + attr_reader :selection - validates :selection, inclusion: { - in: [REMOTE, IPP], - } - validates :selection, presence: { - message: proc { I18n.t('errors.doc_auth.how_to_verify_form') }, - } + validates :selection, + presence: { message: proc { I18n.t('errors.doc_auth.how_to_verify_form') } } def initialize(selection: nil) @selection = selection end def submit(params) - consume_params(params) + @selection = params[:selection] FormResponse.new(success: valid?, errors: errors) end - - private - - def consume_params(params) - params.each do |key, value| - raise_invalid_how_to_verify_parameter_error(key) unless ATTRIBUTES.include?(key.to_sym) - send("#{key}=", value) - end - end - - def raise_invalid_how_to_verify_parameter_error(key) - raise ArgumentError, "#{key} is an invalid how_to_verify attribute" - end 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 76318da6a2c..c23629f82d5 100644 --- a/app/javascript/packages/document-capture/components/document-capture.tsx +++ b/app/javascript/packages/document-capture/components/document-capture.tsx @@ -37,7 +37,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { const { t } = useI18n(); const { flowPath, phoneWithCamera } = useContext(UploadContext); const { trackSubmitEvent, trackVisitEvent } = useContext(AnalyticsContext); - const { inPersonFullAddressEntryEnabled, inPersonURL } = useContext(InPersonContext); + const { inPersonFullAddressEntryEnabled, inPersonURL, skipDocAuth } = useContext(InPersonContext); const appName = getConfigValue('appName'); useDidUpdateEffect(onStepChange, [stepName]); @@ -105,7 +105,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { }, ].filter(Boolean) as FormStep[]); - const steps: FormStep[] = submissionError + const defaultSteps: FormStep[] = submissionError ? ( [ { @@ -133,8 +133,14 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { }, ].filter(Boolean) as FormStep[]); + // 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; + + // If the user got here by opting-in to in-person proofing, when skipDocAuth === true, + // then set stepIndicatorPath to VerifyFlowPath.IN_PERSON const stepIndicatorPath = - stepName && ['location', 'prepare', 'switch_back'].includes(stepName) + (stepName && ['location', 'prepare', 'switch_back'].includes(stepName)) || skipDocAuth ? VerifyFlowPath.IN_PERSON : VerifyFlowPath.DEFAULT; @@ -173,6 +179,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { onStepSubmit={trackSubmitEvent} autoFocus={!!submissionError} titleFormat={`%{step} - ${appName}`} + initialStep={skipDocAuth ? steps[0].name : undefined} /> )} 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 80d4ab28ebf..91ed67aad59 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 @@ -3,6 +3,7 @@ import { Link, PageHeading, ProcessList, ProcessListItem } from '@18f/identity-c import { getConfigValue } from '@18f/identity-config'; import { useI18n } from '@18f/identity-react-i18n'; import { FormStepsButton } from '@18f/identity-form-steps'; +import { forceRedirect } from '@18f/identity-url'; import UploadContext from '../context/upload'; import MarketingSiteContext from '../context/marketing-site'; import BackButton from './back-button'; @@ -14,8 +15,21 @@ function InPersonPrepareStep({ toPreviousStep }) { const { t } = useI18n(); const { flowPath } = useContext(UploadContext); const { securityAndPrivacyHowItWorksURL } = useContext(MarketingSiteContext); - const { inPersonURL, inPersonOutageMessageEnabled, inPersonOutageExpectedUpdateDate } = - useContext(InPersonContext); + const { + inPersonURL, + inPersonOutageMessageEnabled, + inPersonOutageExpectedUpdateDate, + skipDocAuth, + howToVerifyURL, + } = useContext(InPersonContext); + + function goBack() { + if (skipDocAuth && howToVerifyURL) { + forceRedirect(howToVerifyURL); + } else { + toPreviousStep(); + } + } return ( <> @@ -58,7 +72,7 @@ function InPersonPrepareStep({ toPreviousStep }) { )}

- + ); } diff --git a/app/javascript/packages/document-capture/context/in-person.ts b/app/javascript/packages/document-capture/context/in-person.ts index 3fcbb5ac2da..4aed409f00f 100644 --- a/app/javascript/packages/document-capture/context/in-person.ts +++ b/app/javascript/packages/document-capture/context/in-person.ts @@ -36,6 +36,18 @@ export interface InPersonContextProps { * Each item is [Long name, abbreviation], e.g. ['Ohio', 'OH'] */ usStatesTerritories: Array<[string, string]>; + + /** + * When skipDocAuth is true and in_person_proofing_opt_in_enabled is true, + * users are directed to the beginning of the IPP flow. This is set to true when + * they choose Opt-in IPP on the new How To Verify page + */ + skipDocAuth?: boolean; + + /** + * URL for Opt-in IPP, used when in_person_proofing_opt_in_enabled is enabled + */ + howToVerifyURL?: string; } const InPersonContext = createContext({ diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index 394fa8323b7..b543888bfbe 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -34,6 +34,8 @@ interface AppRootData { exitUrl: string; idvInPersonUrl?: string; securityAndPrivacyHowItWorksUrl: string; + skipDocAuth: string; + howToVerifyURL: string; uiNotReadySectionEnabled: string; uiExitQuestionSectionEnabled: string; } @@ -103,6 +105,8 @@ const { inPersonOutageExpectedUpdateDate, usStatesTerritories = '', phoneWithCamera = '', + skipDocAuth, + howToVerifyUrl, uiNotReadySectionEnabled = '', uiExitQuestionSectionEnabled = '', } = appRoot.dataset as DOMStringMap & AppRootData; @@ -126,6 +130,8 @@ const App = composeComponents( inPersonOutageExpectedUpdateDate, inPersonFullAddressEntryEnabled: inPersonFullAddressEntryEnabled === 'true', usStatesTerritories: parsedUsStatesTerritories, + skipDocAuth: skipDocAuth === 'true', + howToVerifyURL: howToVerifyUrl, }, }, ], diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index a6a8c7b11d6..40ecafa562e 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -22,6 +22,7 @@ class Session profile_id redo_document_capture resolution_successful + skip_doc_auth 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 9742a47df57..52959f33676 100644 --- a/app/views/idv/document_capture/show.html.erb +++ b/app/views/idv/document_capture/show.html.erb @@ -9,4 +9,5 @@ acuant_version: acuant_version, phone_question_ab_test_bucket: phone_question_ab_test_bucket, phone_with_camera: phone_with_camera, + skip_doc_auth: skip_doc_auth, ) %> diff --git a/app/views/idv/hybrid_mobile/document_capture/show.html.erb b/app/views/idv/hybrid_mobile/document_capture/show.html.erb index 6a20c1c62b7..b72d905f689 100644 --- a/app/views/idv/hybrid_mobile/document_capture/show.html.erb +++ b/app/views/idv/hybrid_mobile/document_capture/show.html.erb @@ -9,4 +9,5 @@ acuant_version: acuant_version, phone_question_ab_test_bucket: phone_question_ab_test_bucket, phone_with_camera: phone_with_camera, + skip_doc_auth: false, ) %> \ No newline at end of file diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index e7ec2c49c33..509c1e0c10f 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -38,6 +38,8 @@ in_person_outage_expected_update_date: IdentityConfig.store.in_person_outage_expected_update_date, us_states_territories: us_states_territories, doc_auth_selfie_capture: IdentityConfig.store.doc_auth_selfie_capture, + skip_doc_auth: skip_doc_auth, + how_to_verify_url: idv_how_to_verify_url, ui_not_ready_section_enabled: IdentityConfig.store.doc_auth_not_ready_section_enabled, ui_exit_question_section_enabled: IdentityConfig.store.doc_auth_exit_question_section_enabled, } %> diff --git a/spec/controllers/idv/how_to_verify_controller_spec.rb b/spec/controllers/idv/how_to_verify_controller_spec.rb index ba3ca5b3ac1..d52d7268a53 100644 --- a/spec/controllers/idv/how_to_verify_controller_spec.rb +++ b/spec/controllers/idv/how_to_verify_controller_spec.rb @@ -31,6 +31,7 @@ it 'renders the show template' do get :show + expect(subject.idv_session.skip_doc_auth).to be_nil expect(response).to render_template :show end @@ -48,10 +49,46 @@ end describe '#update' do + let(:params) do + { + idv_how_to_verify_form: { selection: selection }, + } + end + let(:selection) { 'remote' } + it 'invalidates future steps' do expect(subject).to receive(:clear_future_steps!) put :update end + + context 'remote' do + it 'sets skip doc auth on idv session to false and redirects to hybrid handoff' do + put :update, params: params + + expect(subject.idv_session.skip_doc_auth).to be false + expect(response).to redirect_to(idv_hybrid_handoff_url) + end + end + + context 'ipp' do + let(:selection) { 'ipp' } + + it 'sets skip doc auth on idv session to true and redirects to document capture' do + put :update, params: params + + expect(subject.idv_session.skip_doc_auth).to be true + expect(response).to redirect_to(idv_document_capture_url) + end + end + + context 'undo/back' do + it 'sets skip_doc_auth to nil and does not redirect' do + put :update, params: { undo_step: true } + + expect(subject.idv_session.skip_doc_auth).to be_nil + expect(response).to redirect_to(idv_how_to_verify_url) + 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 5052197cbe6..e6ef8c4701c 100644 --- a/spec/features/idv/doc_auth/how_to_verify_spec.rb +++ b/spec/features/idv/doc_auth/how_to_verify_spec.rb @@ -4,30 +4,31 @@ include IdvHelper include DocAuthHelper - context 'opt-in ipp is turned on' do - let(:enabled) { true } + let(:enabled) { true } - before do - allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { enabled } + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled) { enabled } - sign_in_and_2fa_user - complete_doc_auth_steps_before_agreement_step - complete_agreement_step - end + sign_in_and_2fa_user + complete_doc_auth_steps_before_agreement_step + complete_agreement_step + end - context 'opt-in ipp is turned off' do - let(:enabled) { false } + context 'opt-in ipp is turned off' do + let(:enabled) { false } - it 'skips when disabled' do - expect(page).to have_current_path(idv_hybrid_handoff_url) - end + it 'skips when disabled' do + expect(page).to have_current_path(idv_hybrid_handoff_url) end + end + context 'opt-in ipp is turned on' do it 'displays expected content and requires a choice' do expect(page).to have_current_path(idv_how_to_verify_path) # Try to continue without an option click_continue + expect(page).to have_current_path(idv_how_to_verify_path) expect(page).to have_content(t('errors.doc_auth.how_to_verify_form')) diff --git a/spec/forms/idv/how_to_verify_form_spec.rb b/spec/forms/idv/how_to_verify_form_spec.rb index c0439a9a4c6..7e4e5e50a83 100644 --- a/spec/forms/idv/how_to_verify_form_spec.rb +++ b/spec/forms/idv/how_to_verify_form_spec.rb @@ -13,22 +13,6 @@ expect(result.errors).to be_empty end end - - context 'when the form is invalid' do - it 'returns an unsuccessful form response' do - result = subject.submit(selection: 'peanut butter') - - expect(result).to be_kind_of(FormResponse) - expect(result.success?).to eq(false) - end - end - - context 'when the form has invalid attributes' do - it 'raises an error' do - expect { subject.submit(selection: Idv::HowToVerifyForm::REMOTE, foo: 1) }. - to raise_error(ArgumentError, 'foo is an invalid how_to_verify attribute') - end - end end describe 'presence validations' do 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 5d55d47f462..9eabd393c24 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(:doc_auth_selfie_capture) { { enabled: false } } let(:phone_with_camera) { false } let(:acuant_version) { '1.3.3.7' } + let(:skip_doc_auth) { false } before do decorated_sp_session = instance_double( @@ -47,6 +48,7 @@ phone_question_ab_test_bucket: phone_question_ab_test_bucket, doc_auth_selfie_capture: doc_auth_selfie_capture, phone_with_camera: phone_with_camera, + skip_doc_auth: skip_doc_auth, } end