diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3ba83d81395..050f0f7b6b6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -366,6 +366,7 @@ review-app: - |- export IDP_ENV=$(cat <= 1.0.0) rake foundation_emails (2.2.1.0) - fugit (1.8.1) + fugit (1.9.0) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) geocoder (1.7.0) @@ -324,7 +324,7 @@ GEM ffi (~> 1.0) globalid (1.2.1) activesupport (>= 6.1) - good_job (3.19.4) + good_job (3.21.1) activejob (>= 6.0.0) activerecord (>= 6.0.0) concurrent-ruby (>= 1.0.2) @@ -355,7 +355,7 @@ GEM terminal-table (>= 1.5.1) ice_nine (0.11.2) io-console (0.6.0) - irb (1.8.3) + irb (1.9.1) rdoc reline (>= 0.3.8) jmespath (1.6.2) @@ -379,7 +379,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) lookbook (2.0.5) @@ -411,7 +411,7 @@ GEM minitest (5.20.0) msgpack (1.7.2) multiset (0.5.3) - mutex_m (0.1.2) + mutex_m (0.2.0) net-imap (0.4.2) date net-protocol @@ -468,12 +468,13 @@ GEM yard (~> 0.9.11) pry-rails (0.3.9) pry (>= 0.10.4) - psych (4.0.2) + psych (5.1.1.1) + stringio public_suffix (5.0.3) puma (5.6.7) nio4r (~> 2.0) raabro (1.4.0) - racc (1.7.2) + racc (1.7.3) rack (2.2.8) rack-attack (6.5.0) rack (>= 1.0, < 3) @@ -532,11 +533,11 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rdoc (6.5.0) + rdoc (6.6.0) psych (>= 4.0.0) redacted_struct (1.1.0) redcarpet (3.6.0) @@ -545,7 +546,7 @@ GEM redis-client (0.14.1) connection_pool regexp_parser (2.8.2) - reline (0.3.9) + reline (0.4.0) io-console (~> 0.5) request_store (1.5.1) rack (>= 1.4) @@ -657,6 +658,7 @@ GEM unf (~> 0.1.4) smart_properties (1.17.0) stringex (2.8.5) + stringio (3.0.9) strong_migrations (1.6.4) activerecord (>= 5.2) subprocess (1.5.5) @@ -665,7 +667,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) thor (1.3.0) thread_safe (0.3.6) - timeout (0.4.0) + timeout (0.4.1) tpm-key_attestation (0.11.0) bindata (~> 2.4) openssl (> 2.0, < 3.1) diff --git a/app/assets/stylesheets/components/_btn.scss b/app/assets/stylesheets/components/_btn.scss index 5ce81998618..1208f91f830 100644 --- a/app/assets/stylesheets/components/_btn.scss +++ b/app/assets/stylesheets/components/_btn.scss @@ -7,10 +7,12 @@ margin-right: 0; } +// Upstream: https://github.com/uswds/uswds/pull/5631 .usa-button--unstyled { // Temporary: To be backported to design system. Unstyled buttons should inherit the appearance // of a link. display: inline; + width: auto; } .usa-button:disabled.usa-button--active, diff --git a/app/components/webauthn_verify_button_component.html.erb b/app/components/webauthn_verify_button_component.html.erb index 6f5940c6c98..f8a96854bbb 100644 --- a/app/components/webauthn_verify_button_component.html.erb +++ b/app/components/webauthn_verify_button_component.html.erb @@ -26,4 +26,5 @@ <%= hidden_field_tag :signature, '' %> <%= hidden_field_tag :client_data_json, '' %> <%= hidden_field_tag :webauthn_error, '' %> + <%= hidden_field_tag :screen_lock_error, '' %> <% end %> diff --git a/app/controllers/concerns/idv/ab_test_analytics_concern.rb b/app/controllers/concerns/idv/ab_test_analytics_concern.rb index f99e767c9dd..0ab10dc0823 100644 --- a/app/controllers/concerns/idv/ab_test_analytics_concern.rb +++ b/app/controllers/concerns/idv/ab_test_analytics_concern.rb @@ -1,7 +1,6 @@ module Idv module AbTestAnalyticsConcern include AcuantConcern - include Idv::GettingStartedAbTestConcern include Idv::PhoneQuestionAbTestConcern def ab_test_analytics_buckets @@ -12,7 +11,6 @@ def ab_test_analytics_buckets end buckets.merge(acuant_sdk_ab_test_analytics_args). - merge(getting_started_ab_test_analytics_bucket). merge(phone_question_ab_test_analytics_bucket) end end diff --git a/app/controllers/concerns/idv/availability_concern.rb b/app/controllers/concerns/idv/availability_concern.rb new file mode 100644 index 00000000000..d9d70b4305d --- /dev/null +++ b/app/controllers/concerns/idv/availability_concern.rb @@ -0,0 +1,15 @@ +module Idv + module AvailabilityConcern + extend ActiveSupport::Concern + + included do + before_action :redirect_if_idv_unavailable + end + + def redirect_if_idv_unavailable + return if FeatureManagement.idv_available? + + redirect_to idv_unavailable_url + end + end +end diff --git a/app/controllers/concerns/idv/getting_started_ab_test_concern.rb b/app/controllers/concerns/idv/getting_started_ab_test_concern.rb deleted file mode 100644 index 97c191110a3..00000000000 --- a/app/controllers/concerns/idv/getting_started_ab_test_concern.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Idv - module GettingStartedAbTestConcern - def getting_started_ab_test_bucket - AbTests::IDV_GETTING_STARTED.bucket(getting_started_user.uuid) - end - - def getting_started_user - if defined?(document_capture_user) # hybrid flow - document_capture_user - else - current_user - end - end - - def maybe_redirect_for_getting_started_ab_test - return if getting_started_ab_test_bucket != :getting_started - - redirect_to idv_getting_started_url - end - - def getting_started_ab_test_analytics_bucket - { - getting_started_ab_test_bucket: - getting_started_ab_test_bucket, - } - end - end -end diff --git a/app/controllers/concerns/idv/step_indicator_concern.rb b/app/controllers/concerns/idv/step_indicator_concern.rb index 084b37032b6..0a37ce7947b 100644 --- a/app/controllers/concerns/idv/step_indicator_concern.rb +++ b/app/controllers/concerns/idv/step_indicator_concern.rb @@ -48,7 +48,7 @@ def gpo_address_verification? return false unless current_user return true if current_user.gpo_verification_pending_profile? - return idv_session.address_verification_mechanism == 'gpo' + return idv_session.verify_by_mail? end end end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 2421aafd1c4..fb2e23a0efa 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -315,14 +315,6 @@ def move_applicant_to_idv_session idv_session.applicant = pii idv_session.applicant[:ssn] = idv_session.ssn idv_session.applicant['uuid'] = current_user.uuid - delete_pii - end - - def delete_pii - idv_session.pii_from_doc = nil - if defined?(flow_session) # no longer defined for remote flow - flow_session.delete(:pii_from_user) - end end def add_proofing_costs(results) diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 656a5a5c49e..057de096794 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -3,7 +3,7 @@ module IdvSession included do before_action :redirect_unless_idv_session_user - before_action :redirect_if_sp_context_needed + before_action :redirect_unless_sp_requested_verification end def confirm_idv_needed @@ -53,11 +53,17 @@ def redirect_unless_idv_session_user redirect_to root_url if !idv_session_user end - def redirect_if_sp_context_needed - return if sp_from_sp_session.present? - return unless IdentityConfig.store.idv_sp_required + def redirect_unless_sp_requested_verification + return if !IdentityConfig.store.idv_sp_required return if idv_session_user.profiles.any? + ial_context = IalContext.new( + ial: sp_session_ial, + service_provider: sp_from_sp_session, + user: idv_session_user, + ) + return if ial_context.ial2_or_greater? + redirect_to account_url end diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index 0635ca4c020..c9396b0ff3f 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -9,14 +9,20 @@ module IdvStepConcern included do before_action :confirm_two_factor_authenticated before_action :confirm_idv_needed + before_action :confirm_letter_recently_enqueued before_action :confirm_no_pending_gpo_profile before_action :confirm_no_pending_in_person_enrollment before_action :handle_fraud before_action :check_for_mail_only_outage end + def confirm_letter_recently_enqueued + # idv session should be clear when user returns to enter code + return redirect_to idv_letter_enqueued_url if letter_recently_enqueued? + end + def confirm_no_pending_gpo_profile - redirect_to idv_verify_by_mail_enter_code_url if current_user&.gpo_verification_pending_profile? + redirect_to idv_verify_by_mail_enter_code_url if letter_not_recently_enqueued? end def confirm_no_pending_in_person_enrollment @@ -47,9 +53,6 @@ def flow_path def confirm_hybrid_handoff_needed if params[:redo] idv_session.redo_document_capture = true - elsif idv_session.document_capture_complete? - redirect_to idv_ssn_url - return end # If we previously skipped hybrid handoff, keep doing that. @@ -64,29 +67,6 @@ def confirm_hybrid_handoff_needed private - def confirm_document_capture_not_complete - return unless idv_session.document_capture_complete? - - redirect_to idv_ssn_url - end - - def confirm_ssn_step_complete - return if pii.present? && idv_session.ssn.present? - redirect_to prev_url - end - - def confirm_document_capture_complete - return if idv_session.pii_from_doc.present? - - if flow_path == 'standard' - redirect_to idv_document_capture_url - elsif flow_path == 'hybrid' - redirect_to idv_link_sent_url - else # no flow_path - redirect_to idv_hybrid_handoff_path - end - end - def confirm_verify_info_step_complete return if idv_session.verify_info_step_complete? @@ -103,7 +83,7 @@ def confirm_verify_info_step_needed end def confirm_address_step_complete - return if idv_session.address_step_complete? + return if idv_session.phone_or_address_step_complete? redirect_to idv_otp_verification_url end @@ -123,6 +103,16 @@ def extra_analytics_properties extra end + def letter_recently_enqueued? + current_user&.gpo_verification_pending_profile? && + idv_session.verify_by_mail? + end + + def letter_not_recently_enqueued? + current_user&.gpo_verification_pending_profile? && + !idv_session.address_verification_mechanism + end + def flow_policy @flow_policy ||= Idv::FlowPolicy.new(idv_session: idv_session, user: current_user) end @@ -135,10 +125,11 @@ def confirm_step_allowed def url_for_latest_step step_info = flow_policy.info_for_latest_step + url_for(controller: step_info.controller, action: step_info.action) end - def clear_invalid_steps! - flow_policy.undo_steps_from_controller!(controller: self.class) + def clear_future_steps! + flow_policy.undo_future_steps_from_controller!(controller: self.class) end end diff --git a/app/controllers/concerns/phone_confirmation.rb b/app/controllers/concerns/phone_confirmation.rb deleted file mode 100644 index 2b014db6cdc..00000000000 --- a/app/controllers/concerns/phone_confirmation.rb +++ /dev/null @@ -1,25 +0,0 @@ -module PhoneConfirmation - def prompt_to_confirm_phone(id:, phone:, selected_delivery_method: nil, - selected_default_number: nil, phone_type: nil) - - user_session[:unconfirmed_phone] = phone - user_session[:context] = 'confirmation' - user_session[:phone_type] = phone_type.to_s - - redirect_to otp_send_url( - otp_delivery_selection_form: { - otp_delivery_preference: otp_delivery_method(id, phone, selected_delivery_method), - otp_make_default_number: selected_default_number, - }, - ) - end - - private - - def otp_delivery_method(_id, phone, selected_delivery_method) - capabilities = PhoneNumberCapabilities.new(phone, phone_confirmed: false) - return :sms if capabilities.sms_only? - return selected_delivery_method if selected_delivery_method.present? - current_user.otp_delivery_preference - end -end diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index c1b426680f1..86cc4e84996 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -169,7 +169,7 @@ def attribute_asserter(principal) def decrypted_pii cacher = Pii::Cacher.new(current_user, user_session) - cacher.fetch + cacher.fetch(current_user&.active_profile&.id) end def build_asserted_attributes(principal) diff --git a/app/controllers/idv/address_controller.rb b/app/controllers/idv/address_controller.rb index 8efc1c4679c..0bcfcbf079e 100644 --- a/app/controllers/idv/address_controller.rb +++ b/app/controllers/idv/address_controller.rb @@ -1,9 +1,11 @@ module Idv class AddressController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern before_action :confirm_not_rate_limited_after_doc_auth - before_action :confirm_document_capture_complete + before_action :confirm_step_allowed + before_action :confirm_verify_info_step_needed def new analytics.idv_address_visit @@ -12,6 +14,7 @@ def new end def update + clear_future_steps! form_result = idv_form.submit(profile_params) analytics.idv_address_submitted(**form_result.to_h) capture_address_edited(form_result) @@ -22,6 +25,16 @@ def update end end + def self.step_info + Idv::StepInfo.new( + key: :address, + controller: controller_name, + next_steps: [:verify_info], + preconditions: ->(idv_session:, user:) { idv_session.document_capture_complete? }, + undo_step: ->(idv_session:, user:) {}, + ) + end + private def idv_form diff --git a/app/controllers/idv/agreement_controller.rb b/app/controllers/idv/agreement_controller.rb index c2dcc75e20d..a052c2f5395 100644 --- a/app/controllers/idv/agreement_controller.rb +++ b/app/controllers/idv/agreement_controller.rb @@ -1,11 +1,12 @@ module Idv class AgreementController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern before_action :confirm_not_rate_limited before_action :confirm_step_allowed - before_action :confirm_document_capture_not_complete + before_action :confirm_verify_info_step_needed def show analytics.idv_doc_auth_agreement_visited(**analytics_arguments) @@ -21,7 +22,7 @@ def show end def update - clear_invalid_steps! + clear_future_steps! skip_to_capture if params[:skip_hybrid_handoff] result = Idv::ConsentForm.new.submit(consent_form_params) @@ -49,7 +50,10 @@ def self.step_info controller: controller_name, next_steps: [:hybrid_handoff, :document_capture, :phone_question, :how_to_verify], preconditions: ->(idv_session:, user:) { idv_session.welcome_visited }, - undo_step: ->(idv_session:, user:) { idv_session.idv_consent_given = nil }, + undo_step: ->(idv_session:, user:) do + idv_session.idv_consent_given = nil + idv_session.skip_hybrid_handoff = nil + end, ) end diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index d5061a69468..8cdfbc671e0 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -1,6 +1,7 @@ module Idv module ByMail class EnterCodeController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include Idv::StepIndicatorConcern include FraudReviewConcern diff --git a/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb b/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb index da0f5d17c50..fdf614e5a68 100644 --- a/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_rate_limited_controller.rb @@ -1,6 +1,7 @@ module Idv module ByMail class EnterCodeRateLimitedController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include FraudReviewConcern diff --git a/app/controllers/idv/by_mail/letter_enqueued_controller.rb b/app/controllers/idv/by_mail/letter_enqueued_controller.rb index 44823646216..aef641aaa05 100644 --- a/app/controllers/idv/by_mail/letter_enqueued_controller.rb +++ b/app/controllers/idv/by_mail/letter_enqueued_controller.rb @@ -1,6 +1,7 @@ module Idv module ByMail class LetterEnqueuedController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include Idv::StepIndicatorConcern diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index 2bcfd919ff4..739e33b5a13 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -1,12 +1,11 @@ module Idv module ByMail class RequestLetterController < ApplicationController - include IdvSession + include Idv::AvailabilityConcern + include IdvStepConcern + skip_before_action :confirm_no_pending_gpo_profile include Idv::StepIndicatorConcern - include Idv::AbTestAnalyticsConcern - before_action :confirm_two_factor_authenticated - before_action :confirm_idv_needed before_action :confirm_user_completed_idv_profile_step before_action :confirm_mail_not_rate_limited before_action :confirm_profile_not_too_old diff --git a/app/controllers/idv/cancellations_controller.rb b/app/controllers/idv/cancellations_controller.rb index 3798119cf0d..8da8d988e77 100644 --- a/app/controllers/idv/cancellations_controller.rb +++ b/app/controllers/idv/cancellations_controller.rb @@ -1,5 +1,6 @@ module Idv class CancellationsController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include GoBackHelper diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb index 3510c2fc58f..4e69fb81765 100644 --- a/app/controllers/idv/capture_doc_status_controller.rb +++ b/app/controllers/idv/capture_doc_status_controller.rb @@ -1,5 +1,7 @@ module Idv class CaptureDocStatusController < ApplicationController + include Idv::AvailabilityConcern + before_action :confirm_two_factor_authenticated respond_to :json diff --git a/app/controllers/idv/confirm_start_over_controller.rb b/app/controllers/idv/confirm_start_over_controller.rb index d8c7ea88c2f..e28e4fb795d 100644 --- a/app/controllers/idv/confirm_start_over_controller.rb +++ b/app/controllers/idv/confirm_start_over_controller.rb @@ -1,5 +1,6 @@ module Idv class ConfirmStartOverController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include StepIndicatorConcern include GoBackHelper diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 24e2b51e70d..dff19236bd7 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -1,5 +1,6 @@ module Idv class DocumentCaptureController < ApplicationController + include Idv::AvailabilityConcern include AcuantConcern include DocumentCaptureConcern include IdvStepConcern @@ -8,8 +9,7 @@ class DocumentCaptureController < ApplicationController before_action :confirm_not_rate_limited, except: [:update] before_action :confirm_step_allowed - before_action :confirm_hybrid_handoff_complete - before_action :confirm_document_capture_needed + before_action :confirm_verify_info_step_needed before_action :override_csp_to_allow_acuant def show @@ -22,7 +22,7 @@ def show end def update - clear_invalid_steps! + clear_future_steps! idv_session.redo_document_capture = nil # done with this redo # Not used in standard flow, here for data consistency with hybrid flow. document_capture_session.confirm_ocr @@ -59,31 +59,19 @@ def self.step_info Idv::StepInfo.new( key: :document_capture, controller: controller_name, - next_steps: [:success], # [:ssn], + next_steps: [:ssn], # :ipp_state_id preconditions: ->(idv_session:, user:) { idv_session.flow_path == 'standard' }, undo_step: ->(idv_session:, user:) do idv_session.pii_from_doc = nil idv_session.invalidate_in_person_pii_from_user! + idv_session.had_barcode_attention_error = nil + idv_session.had_barcode_read_failure = nil end, ) end private - def confirm_hybrid_handoff_complete - return if idv_session.flow_path.present? - - redirect_to idv_hybrid_handoff_url - end - - def confirm_document_capture_needed - return if idv_session.redo_document_capture - - return if idv_session.pii_from_doc.blank? && !idv_session.verify_info_step_complete? - - redirect_to idv_ssn_url - end - def cancel_establishing_in_person_enrollments UspsInPersonProofing::EnrollmentHelper. cancel_stale_establishing_enrollments_for_user(current_user) diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index 438583895bf..8a334844627 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -1,11 +1,11 @@ module Idv class EnterPasswordController < ApplicationController + include Idv::AvailabilityConcern + before_action :personal_key_confirmed include IdvStepConcern include StepIndicatorConcern - include PhoneConfirmation - include FraudReviewConcern before_action :confirm_verify_info_step_complete before_action :confirm_address_step_complete @@ -20,14 +20,14 @@ def new Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer). call(:encrypt, :view, true) analytics.idv_enter_password_visited( - address_verification_method: address_verification_method, + address_verification_method: idv_session.address_verification_mechanism, **ab_test_analytics_buckets, ) @title = title @heading = heading - @verifying_by_mail = address_verification_method == 'gpo' + @verify_by_mail = idv_session.verify_by_mail? end def create @@ -38,7 +38,7 @@ def create user_session[:need_personal_key_confirmation] = true flash[:success] = - if gpo_user_flow? + if idv_session.verify_by_mail? t('idv.messages.gpo.letter_on_the_way') else t('idv.messages.confirm') @@ -72,18 +72,20 @@ def create end def step_indicator_step - return :secure_account unless address_verification_method == 'gpo' + return :secure_account unless idv_session.verify_by_mail? :get_a_letter end private def title - gpo_user_flow? ? t('titles.idv.enter_password_letter') : t('titles.idv.enter_password') + idv_session.verify_by_mail? ? + t('titles.idv.enter_password_letter') + : t('titles.idv.enter_password') end def heading - if gpo_user_flow? + if idv_session.verify_by_mail? t('idv.titles.session.enter_password_letter', app_name: APP_NAME) else t('idv.titles.session.enter_password', app_name: APP_NAME) @@ -112,14 +114,10 @@ def gpo_mail_service @gpo_mail_service ||= Idv::GpoMail.new(current_user) end - def address_verification_method - user_session.with_indifferent_access.dig('idv', 'address_verification_mechanism') - end - def init_profile idv_session.create_profile_from_applicant_with_password(password) - if idv_session.address_verification_mechanism == 'gpo' + if idv_session.verify_by_mail? current_user.send_email_to_all_addresses(:letter_reminder) analytics.idv_gpo_address_letter_enqueued( enqueued_at: Time.zone.now, @@ -165,17 +163,13 @@ def need_personal_key_confirmation? end def next_step - if gpo_user_flow? + if idv_session.verify_by_mail? idv_letter_enqueued_url else idv_personal_key_url end end - def gpo_user_flow? - idv_session.address_verification_mechanism == 'gpo' - end - def handle_request_enroll_exception(err) analytics.idv_in_person_usps_request_enroll_exception( context: context, diff --git a/app/controllers/idv/forgot_password_controller.rb b/app/controllers/idv/forgot_password_controller.rb index 938fb7bff83..9056f0eeb45 100644 --- a/app/controllers/idv/forgot_password_controller.rb +++ b/app/controllers/idv/forgot_password_controller.rb @@ -1,5 +1,6 @@ module Idv class ForgotPasswordController < ApplicationController + include Idv::AvailabilityConcern include IdvSession before_action :confirm_two_factor_authenticated diff --git a/app/controllers/idv/getting_started_controller.rb b/app/controllers/idv/getting_started_controller.rb deleted file mode 100644 index d6d52c2b89f..00000000000 --- a/app/controllers/idv/getting_started_controller.rb +++ /dev/null @@ -1,79 +0,0 @@ -module Idv - class GettingStartedController < ApplicationController - include IdvStepConcern - - before_action :confirm_not_rate_limited - before_action :confirm_document_capture_not_complete - - def show - analytics.idv_doc_auth_getting_started_visited(**analytics_arguments) - - # Register both Welcome and Agreement steps in DocAuthLog - Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). - call('welcome', :view, true) - Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). - call('agreement', :view, true) - - @sp_name = decorated_sp_session.sp_name || APP_NAME - @title = t('doc_auth.headings.getting_started', sp_name: @sp_name) - end - - def update - skip_to_capture if params[:skip_hybrid_handoff] - - result = Idv::ConsentForm.new.submit(consent_form_params) - - analytics.idv_doc_auth_getting_started_submitted( - **analytics_arguments.merge(result.to_h), - ) - - if result.success? - idv_session.idv_consent_given = true - - create_document_capture_session - cancel_previous_in_person_enrollments - - redirect_to idv_hybrid_handoff_url - else - redirect_to idv_getting_started_url - end - end - - private - - def analytics_arguments - { - step: 'getting_started', - analytics_id: 'Doc Auth', - skip_hybrid_handoff: idv_session.skip_hybrid_handoff, - irs_reproofing: irs_reproofing?, - }.merge(ab_test_analytics_buckets) - end - - def create_document_capture_session - document_capture_session = DocumentCaptureSession.create( - user_id: current_user.id, - issuer: sp_session[:issuer], - ) - idv_session.document_capture_session_uuid = document_capture_session.uuid - end - - def cancel_previous_in_person_enrollments - return unless IdentityConfig.store.in_person_proofing_enabled - UspsInPersonProofing::EnrollmentHelper. - cancel_stale_establishing_enrollments_for_user(current_user) - end - - def skip_to_capture - idv_session.flow_path = 'standard' - - # Store that we're skipping hybrid handoff so if the user - # tries to redo document capture they can skip it then too. - idv_session.skip_hybrid_handoff = true - end - - def consent_form_params - params.require(:doc_auth).permit(:idv_consent_given) - end - end -end diff --git a/app/controllers/idv/how_to_verify_controller.rb b/app/controllers/idv/how_to_verify_controller.rb index 9a952481393..0be8af23274 100644 --- a/app/controllers/idv/how_to_verify_controller.rb +++ b/app/controllers/idv/how_to_verify_controller.rb @@ -1,5 +1,6 @@ module Idv class HowToVerifyController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include RenderConditionConcern @@ -13,7 +14,7 @@ def show end def update - clear_invalid_steps! + clear_future_steps! result = Idv::HowToVerifyForm.new.submit(how_to_verify_form_params) analytics.idv_doc_auth_how_to_verify_submitted( @@ -38,7 +39,7 @@ def self.step_info controller: controller_name, next_steps: [:hybrid_handoff, :document_capture], preconditions: ->(idv_session:, user:) do - self.enabled? + self.enabled? && idv_session.idv_consent_given end, undo_step: ->(idv_session:, user:) {}, # clear any saved data ) diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index be83c31b68d..dc55e73043f 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -1,5 +1,6 @@ module Idv class HybridHandoffController < ApplicationController + include Idv::AvailabilityConcern include ActionView::Helpers::DateHelper include IdvStepConcern include StepIndicatorConcern @@ -24,7 +25,7 @@ def show end def update - clear_invalid_steps! + clear_future_steps! irs_attempts_api_tracker.idv_document_upload_method_selected( upload_method: params[:type], ) @@ -42,7 +43,10 @@ def self.step_info controller: controller_name, next_steps: [:link_sent, :document_capture], preconditions: ->(idv_session:, user:) { idv_session.idv_consent_given }, - undo_step: ->(idv_session:, user:) { idv_session.flow_path = nil }, + undo_step: ->(idv_session:, user:) do + idv_session.flow_path = nil + idv_session.phone_for_mobile_flow = nil + end, ) end diff --git a/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb b/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb index 13b2e88acf1..f66e5823cc5 100644 --- a/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb +++ b/app/controllers/idv/hybrid_mobile/capture_complete_controller.rb @@ -1,6 +1,7 @@ module Idv module HybridMobile class CaptureCompleteController < ApplicationController + include Idv::AvailabilityConcern include HybridMobileConcern before_action :check_valid_document_capture_session diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 88ead00ade9..35b5fb3a93b 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -1,6 +1,7 @@ module Idv module HybridMobile class DocumentCaptureController < ApplicationController + include Idv::AvailabilityConcern include DocumentCaptureConcern include HybridMobileConcern include PhoneQuestionAbTestConcern diff --git a/app/controllers/idv/hybrid_mobile/entry_controller.rb b/app/controllers/idv/hybrid_mobile/entry_controller.rb index 4eb7c478eec..d609dfa0c4d 100644 --- a/app/controllers/idv/hybrid_mobile/entry_controller.rb +++ b/app/controllers/idv/hybrid_mobile/entry_controller.rb @@ -3,6 +3,7 @@ module HybridMobile # Controller responsible for taking a `document-capture-session` UUID and configuring # the user's Session to work when they're forwarded on to document capture. class EntryController < ApplicationController + include Idv::AvailabilityConcern include HybridMobileConcern def show diff --git a/app/controllers/idv/image_uploads_controller.rb b/app/controllers/idv/image_uploads_controller.rb index 353743ac1ee..aa656b2cc55 100644 --- a/app/controllers/idv/image_uploads_controller.rb +++ b/app/controllers/idv/image_uploads_controller.rb @@ -20,6 +20,7 @@ def create def image_upload_form @image_upload_form ||= Idv::ApiImageUploadForm.new( params, + liveness_checking_enabled: liveness_checking_enabled?, service_provider: current_sp, analytics: analytics, uuid_prefix: current_sp&.app_id, @@ -31,5 +32,17 @@ def image_upload_form def store_encrypted_images? IdentityConfig.store.encrypted_document_storage_enabled end + + def liveness_checking_enabled? + IdentityConfig.store.doc_auth_selfie_capture[:enabled] + end + + def ial_context + @ial_context ||= IalContext.new( + ial: sp_session_ial, + service_provider: current_sp, + user: current_user, + ) + end end end diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb index f998dc8faea..3c8959262b5 100644 --- a/app/controllers/idv/in_person/address_controller.rb +++ b/app/controllers/idv/in_person/address_controller.rb @@ -1,6 +1,7 @@ module Idv module InPerson class AddressController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern before_action :render_404_if_in_person_residential_address_controller_enabled_not_set diff --git a/app/controllers/idv/in_person/ready_to_verify_controller.rb b/app/controllers/idv/in_person/ready_to_verify_controller.rb index b93a82ad26e..5c5fdf2ec89 100644 --- a/app/controllers/idv/in_person/ready_to_verify_controller.rb +++ b/app/controllers/idv/in_person/ready_to_verify_controller.rb @@ -1,6 +1,7 @@ module Idv module InPerson class ReadyToVerifyController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include RenderConditionConcern include StepIndicatorConcern diff --git a/app/controllers/idv/in_person/ssn_controller.rb b/app/controllers/idv/in_person/ssn_controller.rb index e97834264a1..e8e0c0ca684 100644 --- a/app/controllers/idv/in_person/ssn_controller.rb +++ b/app/controllers/idv/in_person/ssn_controller.rb @@ -1,6 +1,7 @@ module Idv module InPerson class SsnController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern include Steps::ThreatMetrixStepHelper diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb index f2181284127..f1b97b207a8 100644 --- a/app/controllers/idv/in_person/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/usps_locations_controller.rb @@ -3,6 +3,7 @@ module Idv module InPerson class UspsLocationsController < ApplicationController + include Idv::AvailabilityConcern include RenderConditionConcern include UspsInPersonProofing include EffectiveUser diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb index fe0f26320af..5ace3f505fe 100644 --- a/app/controllers/idv/in_person/verify_info_controller.rb +++ b/app/controllers/idv/in_person/verify_info_controller.rb @@ -1,6 +1,7 @@ module Idv module InPerson class VerifyInfoController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern include Steps::ThreatMetrixStepHelper @@ -69,6 +70,11 @@ def analytics_arguments }.merge(ab_test_analytics_buckets). merge(**extra_analytics_properties) end + + def confirm_ssn_step_complete + return if pii.present? && idv_session.ssn.present? + redirect_to prev_url + end end end end diff --git a/app/controllers/idv/in_person_controller.rb b/app/controllers/idv/in_person_controller.rb index b35a3de87f5..31de0e49eb8 100644 --- a/app/controllers/idv/in_person_controller.rb +++ b/app/controllers/idv/in_person_controller.rb @@ -1,5 +1,6 @@ module Idv class InPersonController < ApplicationController + include Idv::AvailabilityConcern include RenderConditionConcern check_or_render_not_found -> { InPersonConfig.enabled_for_issuer?(current_sp&.issuer) } diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index 2fa7f3cb78b..f32b78ff4ae 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -1,5 +1,6 @@ module Idv class LinkSentController < ApplicationController + include Idv::AvailabilityConcern include DocumentCaptureConcern include IdvStepConcern include StepIndicatorConcern @@ -7,8 +8,7 @@ class LinkSentController < ApplicationController before_action :confirm_not_rate_limited before_action :confirm_step_allowed - before_action :confirm_hybrid_handoff_complete - before_action :confirm_document_capture_needed + before_action :confirm_verify_info_step_needed def show analytics.idv_doc_auth_link_sent_visited(**analytics_arguments) @@ -20,7 +20,7 @@ def show end def update - clear_invalid_steps! + clear_future_steps! analytics.idv_doc_auth_link_sent_submitted(**analytics_arguments) return render_document_capture_cancelled if document_capture_session&.cancelled_at @@ -45,35 +45,19 @@ def self.step_info Idv::StepInfo.new( key: :link_sent, controller: controller_name, - next_steps: [:success], # [:ssn], + next_steps: [:ssn], preconditions: ->(idv_session:, user:) { idv_session.flow_path == 'hybrid' }, undo_step: ->(idv_session:, user:) do idv_session.pii_from_doc = nil idv_session.invalidate_in_person_pii_from_user! + idv_session.had_barcode_attention_error = nil + idv_session.had_barcode_read_failure = nil end, ) end private - def confirm_hybrid_handoff_complete - return if idv_session.flow_path == 'hybrid' - - if idv_session.flow_path == 'standard' - redirect_to idv_document_capture_url - else - redirect_to idv_hybrid_handoff_url - end - end - - def confirm_document_capture_needed - return if idv_session.redo_document_capture - - return if idv_session.pii_from_doc.blank? && !idv_session.verify_info_step_complete? - - redirect_to idv_ssn_url - end - def analytics_arguments { step: 'link_sent', diff --git a/app/controllers/idv/mail_only_warning_controller.rb b/app/controllers/idv/mail_only_warning_controller.rb index 706cdb20d2b..aad49565bdb 100644 --- a/app/controllers/idv/mail_only_warning_controller.rb +++ b/app/controllers/idv/mail_only_warning_controller.rb @@ -1,5 +1,6 @@ module Idv class MailOnlyWarningController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include StepIndicatorConcern diff --git a/app/controllers/idv/not_verified_controller.rb b/app/controllers/idv/not_verified_controller.rb index 7065a293856..31f3902b8ed 100644 --- a/app/controllers/idv/not_verified_controller.rb +++ b/app/controllers/idv/not_verified_controller.rb @@ -1,5 +1,7 @@ module Idv class NotVerifiedController < ApplicationController + include Idv::AvailabilityConcern + before_action :confirm_two_factor_authenticated def show diff --git a/app/controllers/idv/otp_verification_controller.rb b/app/controllers/idv/otp_verification_controller.rb index f5327b95876..9c2f238e048 100644 --- a/app/controllers/idv/otp_verification_controller.rb +++ b/app/controllers/idv/otp_verification_controller.rb @@ -1,5 +1,6 @@ module Idv class OtpVerificationController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include StepIndicatorConcern include PhoneOtpRateLimitable diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index df50057ee7c..5f751975008 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -1,5 +1,6 @@ module Idv class PersonalKeyController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include StepIndicatorConcern include SecureHeadersConcern @@ -12,7 +13,7 @@ class PersonalKeyController < ApplicationController def show analytics.idv_personal_key_visited( - address_verification_method: address_verification_method, + address_verification_method: idv_session.address_verification_mechanism, in_person_verification_pending: idv_session.profile&.in_person_verification_pending?, ) add_proofing_component @@ -24,7 +25,7 @@ def update user_session[:need_personal_key_confirmation] = false analytics.idv_personal_key_submitted( - address_verification_method: address_verification_method, + address_verification_method: idv_session.address_verification_mechanism, deactivation_reason: idv_session.profile&.deactivation_reason, in_person_verification_pending: idv_session.profile&.in_person_verification_pending?, fraud_review_pending: fraud_review_pending?, @@ -38,10 +39,6 @@ def update private - def address_verification_method - user_session.dig('idv', 'address_verification_mechanism') - end - def next_step if in_person_enrollment? idv_in_person_ready_to_verify_url diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index 811661fd035..e43c4f54ba1 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -1,5 +1,6 @@ module Idv class PhoneController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern include PhoneOtpRateLimitable diff --git a/app/controllers/idv/phone_errors_controller.rb b/app/controllers/idv/phone_errors_controller.rb index 1aa30734735..0ddadb5d828 100644 --- a/app/controllers/idv/phone_errors_controller.rb +++ b/app/controllers/idv/phone_errors_controller.rb @@ -1,5 +1,6 @@ module Idv class PhoneErrorsController < ApplicationController + include Idv::AvailabilityConcern include StepIndicatorConcern include IdvSession include Idv::AbTestAnalyticsConcern diff --git a/app/controllers/idv/phone_question_controller.rb b/app/controllers/idv/phone_question_controller.rb index 037b6b4faa7..e1c711b77f5 100644 --- a/app/controllers/idv/phone_question_controller.rb +++ b/app/controllers/idv/phone_question_controller.rb @@ -1,6 +1,7 @@ module Idv class PhoneQuestionController < ApplicationController include ActionView::Helpers::DateHelper + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern @@ -16,7 +17,7 @@ def show end def phone_with_camera - clear_invalid_steps! + clear_future_steps! idv_session.phone_with_camera = true analytics.idv_doc_auth_phone_question_submitted(**analytics_arguments) @@ -24,7 +25,7 @@ def phone_with_camera end def phone_without_camera - clear_invalid_steps! + clear_future_steps! idv_session.flow_path = 'standard' idv_session.phone_with_camera = false analytics.idv_doc_auth_phone_question_submitted(**analytics_arguments) diff --git a/app/controllers/idv/please_call_controller.rb b/app/controllers/idv/please_call_controller.rb index 019b75574db..677a384e0fe 100644 --- a/app/controllers/idv/please_call_controller.rb +++ b/app/controllers/idv/please_call_controller.rb @@ -1,5 +1,6 @@ module Idv class PleaseCallController < ApplicationController + include Idv::AvailabilityConcern include FraudReviewConcern before_action :confirm_two_factor_authenticated diff --git a/app/controllers/idv/resend_otp_controller.rb b/app/controllers/idv/resend_otp_controller.rb index f020b6fd49c..0b07250a210 100644 --- a/app/controllers/idv/resend_otp_controller.rb +++ b/app/controllers/idv/resend_otp_controller.rb @@ -1,5 +1,6 @@ module Idv class ResendOtpController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include PhoneOtpRateLimitable include PhoneOtpSendable diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb index 2dfdcf52d94..a3f0d613f32 100644 --- a/app/controllers/idv/session_errors_controller.rb +++ b/app/controllers/idv/session_errors_controller.rb @@ -1,5 +1,6 @@ module Idv class SessionErrorsController < ApplicationController + include Idv::AvailabilityConcern include IdvSession include StepIndicatorConcern diff --git a/app/controllers/idv/sessions_controller.rb b/app/controllers/idv/sessions_controller.rb index f967e623100..620e1024e84 100644 --- a/app/controllers/idv/sessions_controller.rb +++ b/app/controllers/idv/sessions_controller.rb @@ -1,5 +1,6 @@ module Idv class SessionsController < ApplicationController + include Idv::AvailabilityConcern include IdvSession before_action :confirm_two_factor_authenticated diff --git a/app/controllers/idv/ssn_controller.rb b/app/controllers/idv/ssn_controller.rb index 9e314571bad..891dd7a35a5 100644 --- a/app/controllers/idv/ssn_controller.rb +++ b/app/controllers/idv/ssn_controller.rb @@ -1,14 +1,14 @@ module Idv class SsnController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern include Steps::ThreatMetrixStepHelper include ThreatMetrixConcern before_action :confirm_not_rate_limited_after_doc_auth + before_action :confirm_step_allowed before_action :confirm_verify_info_step_needed - before_action :confirm_document_capture_complete - before_action :confirm_repeat_ssn, only: :show before_action :override_csp_for_threat_metrix attr_reader :ssn_presenter @@ -34,6 +34,7 @@ def show end def update + clear_future_steps! ssn_form = Idv::SsnFormatForm.new(idv_session.ssn) form_response = ssn_form.submit(params.require(:doc_auth).permit(:ssn)) @ssn_presenter = Idv::SsnPresenter.new( @@ -58,15 +59,21 @@ def update end end - private - - def confirm_repeat_ssn - return if !idv_session.ssn - return if request.referer == idv_verify_info_url - - redirect_to idv_verify_info_url + def self.step_info + Idv::StepInfo.new( + key: :ssn, + controller: controller_name, + next_steps: [:verify_info], + preconditions: ->(idv_session:, user:) { idv_session.document_capture_complete? }, + undo_step: ->(idv_session:, user:) do + idv_session.ssn = nil + idv_session.threatmetrix_session_id = nil + end, + ) end + private + def next_url if idv_session.pii_from_doc[:state] == 'PR' && !ssn_presenter.updating_ssn? idv_address_url diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb index b30db05794d..5eb287561c8 100644 --- a/app/controllers/idv/verify_info_controller.rb +++ b/app/controllers/idv/verify_info_controller.rb @@ -1,17 +1,19 @@ module Idv class VerifyInfoController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern include VerifyInfoConcern include Steps::ThreatMetrixStepHelper before_action :confirm_not_rate_limited_after_doc_auth, except: [:show] - before_action :confirm_ssn_step_complete + before_action :confirm_step_allowed before_action :confirm_verify_info_step_needed def show @step_indicator_steps = step_indicator_steps @ssn = idv_session.ssn + @pii = pii analytics.idv_doc_auth_verify_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). @@ -22,6 +24,7 @@ def show end def update + clear_future_steps! success = shared_update if success @@ -35,6 +38,21 @@ def update end end + def self.step_info + Idv::StepInfo.new( + key: :verify_info, + controller: controller_name, + next_steps: [:success], # [:phone], + preconditions: ->(idv_session:, user:) do + idv_session.ssn && idv_session.document_capture_complete? + end, + undo_step: ->(idv_session:, user:) do + idv_session.resolution_successful = nil + idv_session.address_edited = nil + end, + ) + end + private def flow_param; end diff --git a/app/controllers/idv/welcome_controller.rb b/app/controllers/idv/welcome_controller.rb index 022381f9be0..1af9eec8632 100644 --- a/app/controllers/idv/welcome_controller.rb +++ b/app/controllers/idv/welcome_controller.rb @@ -1,12 +1,11 @@ module Idv class WelcomeController < ApplicationController + include Idv::AvailabilityConcern include IdvStepConcern include StepIndicatorConcern - include GettingStartedAbTestConcern before_action :confirm_not_rate_limited - before_action :confirm_document_capture_not_complete - before_action :maybe_redirect_for_getting_started_ab_test + before_action :confirm_verify_info_step_needed def show analytics.idv_doc_auth_welcome_visited(**analytics_arguments) @@ -16,14 +15,10 @@ def show @sp_name = decorated_sp_session.sp_name || APP_NAME @title = t('doc_auth.headings.getting_started', sp_name: @sp_name) - - @ab_test_bucket = getting_started_ab_test_bucket - - render :show end def update - clear_invalid_steps! + clear_future_steps! analytics.idv_doc_auth_welcome_submitted(**analytics_arguments) create_document_capture_session @@ -40,7 +35,10 @@ def self.step_info controller: controller_name, next_steps: [:agreement], preconditions: ->(idv_session:, user:) { true }, - undo_step: ->(idv_session:, user:) { idv_session.welcome_visited = nil }, + undo_step: ->(idv_session:, user:) do + idv_session.welcome_visited = nil + idv_session.document_capture_session_uuid = nil + end, ) end diff --git a/app/controllers/sign_up/registrations_controller.rb b/app/controllers/sign_up/registrations_controller.rb index 25d47ac5998..588e8c989bc 100644 --- a/app/controllers/sign_up/registrations_controller.rb +++ b/app/controllers/sign_up/registrations_controller.rb @@ -1,6 +1,5 @@ module SignUp class RegistrationsController < ApplicationController - include PhoneConfirmation include ApplicationHelper # for ial2_requested? before_action :confirm_two_factor_authenticated, only: [:destroy_confirm] diff --git a/app/controllers/two_factor_authentication/sms_opt_in_controller.rb b/app/controllers/two_factor_authentication/sms_opt_in_controller.rb index 0709f3a8aa0..63c548dbe2f 100644 --- a/app/controllers/two_factor_authentication/sms_opt_in_controller.rb +++ b/app/controllers/two_factor_authentication/sms_opt_in_controller.rb @@ -3,8 +3,8 @@ class SmsOptInController < ApplicationController before_action :load_phone def new - @other_mfa_options_url = other_options_mfa_url @cancel_url = cancel_url + @presenter = TwoFactorAuthCode::SmsOptInPresenter.new analytics.sms_opt_in_visit( new_user: new_user?, @@ -28,8 +28,8 @@ def create @phone_number_opt_out.opt_in redirect_to otp_send_url(otp_delivery_selection_form: { otp_delivery_preference: :sms }) else - @other_mfa_options_url = other_options_mfa_url @cancel_url = cancel_url + @presenter = TwoFactorAuthCode::SmsOptInPresenter.new if !response.error # unsuccessful, but didn't throw an exception: already opted in last 30 days @@ -61,14 +61,6 @@ def load_phone render_not_found end - def other_options_mfa_url - if new_user? - authentication_methods_setup_path - elsif has_other_auth_methods? && !user_fully_authenticated? - login_two_factor_options_path - end - end - def cancel_url if user_fully_authenticated? account_path diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb index 28ba92782ba..39b1e0a42ec 100644 --- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb +++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb @@ -121,6 +121,7 @@ def form signature: params[:signature], credential_id: params[:credential_id], webauthn_error: params[:webauthn_error], + screen_lock_error: params[:screen_lock_error], ) end diff --git a/app/controllers/users/phone_setup_controller.rb b/app/controllers/users/phone_setup_controller.rb index 6fe2105e6f5..b125d8eebdd 100644 --- a/app/controllers/users/phone_setup_controller.rb +++ b/app/controllers/users/phone_setup_controller.rb @@ -2,7 +2,6 @@ module Users class PhoneSetupController < ApplicationController include TwoFactorAuthenticatableMethods include UserAuthenticator - include PhoneConfirmation include MfaSetupConcern include RecaptchaConcern include ReauthenticationRequiredConcern @@ -86,6 +85,28 @@ def handle_create_success(phone) end end + def prompt_to_confirm_phone(id:, phone:, selected_delivery_method: nil, + selected_default_number: nil, phone_type: nil) + + user_session[:unconfirmed_phone] = phone + user_session[:context] = 'confirmation' + user_session[:phone_type] = phone_type.to_s + + redirect_to otp_send_url( + otp_delivery_selection_form: { + otp_delivery_preference: otp_delivery_method(id, phone, selected_delivery_method), + otp_make_default_number: selected_default_number, + }, + ) + end + + def otp_delivery_method(_id, phone, selected_delivery_method) + capabilities = PhoneNumberCapabilities.new(phone, phone_confirmed: false) + return :sms if capabilities.sms_only? + return selected_delivery_method if selected_delivery_method.present? + current_user.otp_delivery_preference + end + def check_max_phone_numbers_per_account max_phones_count = IdentityConfig.store.max_phone_numbers_per_account return if current_user.phone_configurations.count < max_phones_count diff --git a/app/controllers/users/piv_cac_setup_controller.rb b/app/controllers/users/piv_cac_setup_controller.rb index a644c7e6649..aa3f9d9edfa 100644 --- a/app/controllers/users/piv_cac_setup_controller.rb +++ b/app/controllers/users/piv_cac_setup_controller.rb @@ -1,6 +1,5 @@ module Users class PivCacSetupController < ApplicationController - include PhoneConfirmation include ReauthenticationRequiredConcern before_action :confirm_two_factor_authenticated diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index b15b71c3af7..bd0c3ac1050 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -5,6 +5,7 @@ class ApiImageUploadForm validates_presence_of :front validates_presence_of :back + validates_presence_of :selfie, if: :liveness_checking_enabled validates_presence_of :document_capture_session validate :validate_images @@ -12,7 +13,8 @@ class ApiImageUploadForm validate :limit_if_rate_limited def initialize(params, service_provider:, analytics: nil, - uuid_prefix: nil, irs_attempts_api_tracker: nil, store_encrypted_images: false) + uuid_prefix: nil, irs_attempts_api_tracker: nil, store_encrypted_images: false, + liveness_checking_enabled: false) @params = params @service_provider = service_provider @analytics = analytics @@ -20,6 +22,7 @@ def initialize(params, service_provider:, analytics: nil, @uuid_prefix = uuid_prefix @irs_attempts_api_tracker = irs_attempts_api_tracker @store_encrypted_images = store_encrypted_images + @liveness_checking_enabled = liveness_checking_enabled end def submit @@ -51,7 +54,7 @@ def submit private attr_reader :params, :analytics, :service_provider, :form_response, :uuid_prefix, - :irs_attempts_api_tracker + :irs_attempts_api_tracker, :liveness_checking_enabled def increment_rate_limiter! return unless document_capture_session @@ -80,9 +83,11 @@ def post_images_to_client doc_auth_client.post_images( front_image: front_image_bytes, back_image: back_image_bytes, + selfie_image: liveness_checking_enabled ? selfie_image_bytes : nil, image_source: image_source, user_uuid: user_uuid, uuid_prefix: uuid_prefix, + liveness_checking_enabled: liveness_checking_enabled, ) end @@ -105,6 +110,10 @@ def back_image_bytes @back_image_bytes ||= back.read end + def selfie_image_bytes + @selfie_image_bytes ||= selfie.read + end + def validate_pii_from_doc(client_response) response = Idv::DocPiiForm.new( pii: client_response.pii_from_doc, @@ -146,7 +155,6 @@ def extra_attributes @extra_attributes[:front_image_fingerprint] = front_image_fingerprint @extra_attributes[:back_image_fingerprint] = back_image_fingerprint - @extra_attributes.merge!(getting_started_ab_test_analytics_bucket) @extra_attributes.merge!(phone_question_ab_test_analytics_bucket) @extra_attributes end @@ -201,6 +209,10 @@ def back as_readable(:back) end + def selfie + as_readable(:selfie) + end + def document_capture_session @document_capture_session ||= DocumentCaptureSession.find_by( uuid: document_capture_session_uuid, @@ -220,6 +232,12 @@ def validate_images type: :not_a_file ) end + if selfie.is_a? DataUrlImage::InvalidUrlFormatError + errors.add( + :selfie, t('doc_auth.errors.not_a_file'), + type: :not_a_file + ) + end end def validate_duplicate_images @@ -267,7 +285,6 @@ def doc_auth_client warn_notifier: proc do |attrs| analytics&.doc_auth_warning( **attrs. - merge(getting_started_ab_test_analytics_bucket). merge(phone_question_ab_test_analytics_bucket), ) end, @@ -305,7 +322,6 @@ def update_analytics(client_response:, vendor_request_time_in_ms:) vendor_request_time_in_ms: vendor_request_time_in_ms, ).except(:classification_info). merge(acuant_sdk_upgrade_ab_test_data). - merge(getting_started_ab_test_analytics_bucket). merge(phone_question_ab_test_analytics_bucket), ) end @@ -337,13 +353,6 @@ def acuant_sdk_upgrade_ab_test_data } end - def getting_started_ab_test_analytics_bucket - { - getting_started_ab_test_bucket: - AbTests::IDV_GETTING_STARTED.bucket(user_uuid), - } - end - def phone_question_ab_test_analytics_bucket { phone_question_ab_test_bucket: @@ -372,7 +381,8 @@ def image_metadata def add_costs(response) Db::AddDocumentVerificationAndSelfieCosts. new(user_id: user_id, - service_provider: service_provider). + service_provider: service_provider, + liveness_checking_enabled: liveness_checking_enabled). call(response) end diff --git a/app/forms/otp_verification_form.rb b/app/forms/otp_verification_form.rb index 8a5a5efd490..c4cd27b545d 100644 --- a/app/forms/otp_verification_form.rb +++ b/app/forms/otp_verification_form.rb @@ -61,11 +61,8 @@ def otp_expired? end def extra_analytics_attributes - multi_factor_auth_method_created_at = phone_configuration&.created_at&.strftime('%s%L') - { - multi_factor_auth_method: 'otp_code', - multi_factor_auth_method_created_at: multi_factor_auth_method_created_at, + multi_factor_auth_method_created_at: phone_configuration&.created_at&.strftime('%s%L'), } end end diff --git a/app/forms/webauthn_verification_form.rb b/app/forms/webauthn_verification_form.rb index 0ce31ead056..492df43286d 100644 --- a/app/forms/webauthn_verification_form.rb +++ b/app/forms/webauthn_verification_form.rb @@ -5,14 +5,16 @@ class WebauthnVerificationForm include ActionView::Helpers::TranslationHelper include Rails.application.routes.url_helpers + validates :screen_lock_error, + absence: { message: proc { |object| object.send(:screen_lock_error_message) } } validates :challenge, :authenticator_data, :client_data_json, :signature, :webauthn_configuration, - presence: { message: proc { |object| object.instance_eval { generic_error_message } } } + presence: { message: proc { |object| object.send(:generic_error_message) } } validates :webauthn_error, - absence: { message: proc { |object| object.instance_eval { generic_error_message } } } + absence: { message: proc { |object| object.send(:generic_error_message) } } validate :validate_assertion_response attr_reader :url_options, :platform_authenticator @@ -29,7 +31,8 @@ def initialize( client_data_json: nil, signature: nil, credential_id: nil, - webauthn_error: nil + webauthn_error: nil, + screen_lock_error: nil ) @user = user @platform_authenticator = platform_authenticator @@ -42,6 +45,7 @@ def initialize( @signature = signature @credential_id = credential_id @webauthn_error = webauthn_error + @screen_lock_error = screen_lock_error end def submit @@ -73,7 +77,8 @@ def self.domain_name :client_data_json, :signature, :credential_id, - :webauthn_error + :webauthn_error, + :screen_lock_error def validate_assertion_response return if webauthn_error.present? || webauthn_configuration.blank? || valid_assertion_response? @@ -127,13 +132,42 @@ def generic_error_message end end - def extra_analytics_attributes - auth_method = if webauthn_configuration&.platform_authenticator - 'webauthn_platform' - else - 'webauthn' - end + def screen_lock_error_message + if user_has_other_authentication_method? + t( + 'two_factor_authentication.webauthn_error.screen_lock_other_mfa_html', + link_html: link_to( + t('two_factor_authentication.webauthn_error.use_a_different_method'), + login_two_factor_options_path, + ), + ) + else + t( + 'two_factor_authentication.webauthn_error.screen_lock_no_other_mfa', + link_html: link_to( + t('two_factor_authentication.webauthn_error.use_a_different_method'), + login_two_factor_options_path, + ), + ) + end + end + def auth_method + if platform_authenticator? + 'webauthn_platform' + else + 'webauthn' + end + end + + def user_has_other_authentication_method? + MfaContext.new(user).two_factor_configurations.any? do |configuration| + !configuration.is_a?(WebauthnConfiguration) || + configuration.platform_authenticator? != platform_authenticator? + end + end + + def extra_analytics_attributes { multi_factor_auth_method: auth_method, webauthn_configuration_id: webauthn_configuration&.id, diff --git a/app/javascript/packages/build-sass/CHANGELOG.md b/app/javascript/packages/build-sass/CHANGELOG.md index 7cd6c81234f..f011da1f5a7 100644 --- a/app/javascript/packages/build-sass/CHANGELOG.md +++ b/app/javascript/packages/build-sass/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Breaking Changes + +- Requires Node.js v18 or newer + ## 2.0.0 ### Breaking Changes diff --git a/app/javascript/packages/build-sass/cli.js b/app/javascript/packages/build-sass/cli.js index c97ca735d60..34cc6ecb7b1 100755 --- a/app/javascript/packages/build-sass/cli.js +++ b/app/javascript/packages/build-sass/cli.js @@ -3,9 +3,9 @@ /* eslint-disable no-console */ import { mkdir } from 'node:fs/promises'; +import { parseArgs } from 'node:util'; import { watch } from 'chokidar'; import { fileURLToPath } from 'url'; -import { parseArgs } from '@pkgjs/parseargs'; // Note: Use native util.parseArgs after Node v18 import { buildFile } from './index.js'; import getDefaultLoadPaths from './get-default-load-paths.js'; import getErrorSassStackPaths from './get-error-sass-stack-paths.js'; @@ -26,9 +26,12 @@ const { values: flags, positionals: fileArgs } = parseArgs({ }, }); -const isWatching = flags.watch; -const outDir = flags['out-dir']; -const loadPaths = [...flags['load-path'], ...getDefaultLoadPaths()]; +const { watch: isWatching, 'out-dir': outDir, 'load-path': loadPaths = [] } = flags; +loadPaths.push(...getDefaultLoadPaths()); + +if (!outDir) { + throw new TypeError('Output directory must be provided using the `--out-dir` option.'); +} /** @type {BuildOptions & SyncSassOptions} */ const options = { outDir, loadPaths, optimize: isProduction }; diff --git a/app/javascript/packages/build-sass/package.json b/app/javascript/packages/build-sass/package.json index ca08fdd9e11..f70d1b41d28 100644 --- a/app/javascript/packages/build-sass/package.json +++ b/app/javascript/packages/build-sass/package.json @@ -4,6 +4,9 @@ "private": false, "description": "Stylesheet compilation utility with reasonable defaults and fast performance.", "type": "module", + "engines": { + "node": ">= 18" + }, "bin": { "build-sass": "./cli.js" }, @@ -25,7 +28,6 @@ "homepage": "https://github.com/18f/identity-idp", "dependencies": { "@aduth/is-dependency": "^1.0.0", - "@pkgjs/parseargs": "^0.11.0", "browserslist": "^4.22.1", "chokidar": "^3.5.3", "lightningcss": "^1.22.0", diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 21b36aae963..438a56b75a9 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -15,6 +15,8 @@ import { useDidUpdateEffect } from '@18f/identity-react-hooks'; import { useI18n } from '@18f/identity-react-i18n'; import { removeUnloadProtection } from '@18f/identity-url'; import AcuantCamera, { AcuantDocumentType } from './acuant-camera'; +import AcuantSelfieCamera from './acuant-selfie-camera'; +import AcuantSelfieCaptureCanvas from './acuant-selfie-capture-canvas'; import type { AcuantCaptureFailureError, AcuantSuccessResponse, @@ -335,6 +337,9 @@ function AcuantCapture( const [attempt, incrementAttempt] = useCounter(1); const [acuantFailureCookie, setAcuantFailureCookie, refreshAcuantFailureCookie] = useCookie('AcuantCameraHasFailed'); + // There's some pretty significant changes to this component when it's used for + // selfie capture vs document image capture. This controls those changes. + const selfieCapture = name === 'selfie'; const { failedCaptureAttempts, @@ -493,6 +498,30 @@ function AcuantCapture( } } + function onSelfieCaptureSuccess({ image }: { image: string }) { + onChangeAndResetError(image); + onResetFailedCaptureAttempts(); + setIsCapturingEnvironment(false); + } + + function onSelfieCaptureFailure() { + // Internally, Acuant sets a cookie to bail on guided capture if initialization had + // previously failed for any reason, including declined permission. Since the cookie + // never expires, and since we want to re-prompt even if the user had previously + // declined, unset the cookie value when failure occurs for permissions. + setAcuantFailureCookie(null); + onCameraAccessDeclined(); + + // Due to a bug with Safari on iOS we force the page to refresh on the third + // time a user denies permissions. + onFailedCameraPermissionAttempt(); + if (failedCameraPermissionAttempts > 2) { + removeUnloadProtection(); + window.location.reload(); + } + setIsCapturingEnvironment(false); + } + function onAcuantImageCaptureSuccess( nextCapture: AcuantSuccessResponse | LegacyAcuantSuccessResponse, ) { @@ -603,7 +632,7 @@ function AcuantCapture( return (
- {isCapturingEnvironment && ( + {isCapturingEnvironment && !selfieCapture && ( setHasStartedCropping(true)} onImageCaptureSuccess={onAcuantImageCaptureSuccess} @@ -620,6 +649,20 @@ function AcuantCapture( )} )} + {isCapturingEnvironment && selfieCapture && ( + setIsCapturingEnvironment(true)} + onImageCaptureClose={() => setIsCapturingEnvironment(false)} + > + setIsCapturingEnvironment(false)} + /> + + )} void; + +interface AcuantPassiveLivenessInterface { + /** + * Start capture + */ + start: AcuantPassiveLivenessStart; + /** + * End capture + */ + end: () => void; +} + +interface AcuantSelfieCameraContextProps { + /** + * Success callback + */ + onImageCaptureSuccess: ({ image }: { image: string }) => void; + /** + * Failure callback + */ + onImageCaptureFailure: any; + /** + * Capture open callback, tells the rest of the page + * when the fullscreen selfie capture page is open + */ + onImageCaptureOpen: () => void; + /** + * Capture close callback, tells the rest of the page + * when the fullscreen selfie capture page has been closed + */ + onImageCaptureClose: () => void; + /** + * React children node + */ + children: ReactNode; +} + +interface FaceCaptureCallback { + onDetectorInitialized: () => void; + onDetection: (text) => void; + onOpened: () => void; + onClosed: () => void; + onError: (error) => void; + onPhotoTaken: () => void; + onPhotoRetake: () => void; + onCaptured: (base64Image: Blob) => void; +} + +interface FaceDetectionStates { + FACE_NOT_FOUND: string; + TOO_MANY_FACES: string; + FACE_ANGLE_TOO_LARGE: string; + PROBABILITY_TOO_SMALL: string; + FACE_TOO_SMALL: string; + FACE_CLOSE_TO_BORDER: string; +} + +function AcuantSelfieCamera({ + onImageCaptureSuccess = () => {}, + onImageCaptureFailure = () => {}, + onImageCaptureOpen = () => {}, + onImageCaptureClose = () => {}, + children, +}: AcuantSelfieCameraContextProps) { + const { isReady, setIsActive } = useContext(AcuantContext); + + useEffect(() => { + const faceCaptureCallback: FaceCaptureCallback = { + onDetectorInitialized: () => { + // This callback is triggered when the face detector is ready. + // Until then, no actions are executed and the user sees only the camera stream. + // You can opt to display an alert before the callback is triggered. + }, + onDetection: () => { + // Triggered when the face does not pass the scan. The UI element + // should be updated here to provide guidence to the user + }, + onOpened: () => { + // Camera has opened + onImageCaptureOpen(); + }, + onClosed: () => { + // Camera has closed + onImageCaptureClose(); + }, + onError: (error) => { + // Error occurred. Camera permission not granted will + // manifest here with 1 as error code. Unexpected errors will have 2 as error code. + onImageCaptureFailure({ error }); + }, + onPhotoTaken: () => { + // The photo has been taken and it's showing a preview with a button to accept or retake the image. + }, + onPhotoRetake: () => { + // Triggered when retake button is tapped + }, + onCaptured: (base64Image) => { + // Triggered when accept button is tapped + onImageCaptureSuccess({ image: `data:image/jpeg;base64,${base64Image}` }); + }, + }; + + const faceDetectionStates = { + FACE_NOT_FOUND: 'FACE NOT FOUND', + TOO_MANY_FACES: 'TOO MANY FACES', + FACE_ANGLE_TOO_LARGE: 'FACE ANGLE TOO LARGE', + PROBABILITY_TOO_SMALL: 'PROBABILITY TOO SMALL', + FACE_TOO_SMALL: 'FACE TOO SMALL', + FACE_CLOSE_TO_BORDER: 'TOO CLOSE TO THE FRAME', + }; + const cleanupSelfieCamera = () => { + window.AcuantPassiveLiveness.end(); + setIsActive(false); + }; + + const startSelfieCamera = () => { + window.AcuantPassiveLiveness.start(faceCaptureCallback, faceDetectionStates); + setIsActive(true); + }; + + if (isReady) { + startSelfieCamera(); + } + // Cleanup when the AcuantSelfieCamera component is unmounted + return () => (isReady ? cleanupSelfieCamera() : undefined); + }, [isReady]); + + return <>{children}; +} + +export default AcuantSelfieCamera; diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx new file mode 100644 index 00000000000..6091567aabf --- /dev/null +++ b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx @@ -0,0 +1,36 @@ +import { useContext } from 'react'; +import { getAssetPath } from '@18f/identity-assets'; +import { FullScreen } from '@18f/identity-components'; +import AcuantContext from '../context/acuant'; + +function FullScreenLoadingSpinner({ fullScreenRef, onRequestClose, fullScreenLabel }) { + return ( + + + + ); +} + +function AcuantSelfieCaptureCanvas({ fullScreenRef, onRequestClose, fullScreenLabel }) { + const { isReady } = useContext(AcuantContext); + // The Acuant SDK script AcuantPassiveLiveness attaches to whatever element has + // this id. It then uses that element as the root for the full screen selfie capture + const acuantCaptureContainerId = 'acuant-face-capture-container'; + return isReady ? ( +
+ ) : ( + + ); +} + +export default AcuantSelfieCaptureCanvas; 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 cdf9c8ceddb..e0edf5410b2 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 @@ -28,12 +28,8 @@ interface DocumentCaptureReviewIssuesProps { hasDismissed: boolean; } -type DocumentSide = 'front' | 'back'; +type DocumentSide = 'front' | 'back' | 'selfie'; -/** - * Sides of the document to present as file input. - */ -const DOCUMENT_SIDES: DocumentSide[] = ['front', 'back']; function DocumentCaptureReviewIssues({ isFailedDocType, remainingAttempts = Infinity, @@ -47,7 +43,14 @@ function DocumentCaptureReviewIssues({ hasDismissed, }: DocumentCaptureReviewIssuesProps) { const { t } = useI18n(); - const { notReadySectionEnabled, exitQuestionSectionEnabled } = useContext(FeatureFlagContext); + const { notReadySectionEnabled, exitQuestionSectionEnabled, selfieCaptureEnabled } = + useContext(FeatureFlagContext); + + // Sides of document to present as file input. + const documentSides: DocumentSide[] = selfieCaptureEnabled + ? ['front', 'back', 'selfie'] + : ['front', 'back']; + return ( <> {t('doc_auth.headings.review_issues')} @@ -70,7 +73,7 @@ function DocumentCaptureReviewIssues({ ]} /> )} - {DOCUMENT_SIDES.map((side) => ( + {documentSides.map((side) => ( void} onChange Update values, @@ -50,9 +50,11 @@ function DocumentSideAcuantCapture({ ref={registerField(side, { isRequired: true })} /* i18n-tasks-use t('doc_auth.headings.document_capture_back') */ /* i18n-tasks-use t('doc_auth.headings.document_capture_front') */ + /* i18n-tasks-use t('doc_auth.headings.document_capture_selfie') */ label={t(`doc_auth.headings.document_capture_${side}`)} /* i18n-tasks-use t('doc_auth.headings.back') */ /* i18n-tasks-use t('doc_auth.headings.front') */ + /* i18n-tasks-use t('doc_auth.headings.selfie') */ bannerText={t(`doc_auth.headings.${side}`)} value={value} onChange={(nextValue, metadata) => { diff --git a/app/javascript/packages/document-capture/components/documents-step.jsx b/app/javascript/packages/document-capture/components/documents-step.jsx index 827d757fa6b..d8b9492a171 100644 --- a/app/javascript/packages/document-capture/components/documents-step.jsx +++ b/app/javascript/packages/document-capture/components/documents-step.jsx @@ -13,7 +13,7 @@ import { FeatureFlagContext } from '../context'; import DocumentCaptureAbandon from './document-capture-abandon'; /** - * @typedef {'front'|'back'} DocumentSide + * @typedef {'front'|'back'|'selfie'} DocumentSide */ /** @@ -25,13 +25,6 @@ import DocumentCaptureAbandon from './document-capture-abandon'; * @prop {string=} back_image_metadata Back image metadata. */ -/** - * Sides of document to present as file input. - * - * @type {DocumentSide[]} - */ -const DOCUMENT_SIDES = ['front', 'back']; - /** * @param {import('@18f/identity-form-steps').FormStepComponentProps} props Props object. */ @@ -46,7 +39,16 @@ function DocumentsStep({ const { isMobile } = useContext(DeviceContext); const { isLastStep } = useContext(FormStepsContext); const { flowPath } = useContext(UploadContext); - const { notReadySectionEnabled, exitQuestionSectionEnabled } = useContext(FeatureFlagContext); + const { notReadySectionEnabled, exitQuestionSectionEnabled, selfieCaptureEnabled } = + useContext(FeatureFlagContext); + + /** + * Sides of document to present as file input. + * + * @type {DocumentSide[]} + */ + const documentSides = selfieCaptureEnabled ? ['front', 'back', 'selfie'] : ['front', 'back']; + return ( <> {flowPath === 'hybrid' && } @@ -61,7 +63,7 @@ function DocumentsStep({ t('doc_auth.tips.document_capture_id_text3'), ].concat(!isMobile ? [t('doc_auth.tips.document_capture_id_text4')] : [])} /> - {DOCUMENT_SIDES.map((side) => ( + {documentSides.map((side) => ( { expect(getByText('in_person_proofing.body.prepare.verify_step_post_office')).to.exist(); expect(getByText('in_person_proofing.body.prepare.verify_step_enter_pii')).to.exist(); expect(getByText('in_person_proofing.body.prepare.verify_step_enter_phone')).to.exist(); - expect(getByText('in_person_proofing.body.prepare.verify_step_visit_post_office')).to.exist(); }); - it('renders about and additional information steps', () => { + it('renders about information', () => { const { getByText } = render(); expect(getByText('in_person_proofing.body.prepare.verify_step_about')).to.exist(); - expect(getByText('in_person_proofing.body.prepare.additional_information')).to.exist(); }); context('Outage message', () => { 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 312c180b70b..80d4ab28ebf 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 @@ -38,12 +38,7 @@ function InPersonPrepareStep({ toPreviousStep }) { heading={t('in_person_proofing.body.prepare.verify_step_enter_phone')} headingUnstyled /> - -

{t('in_person_proofing.body.prepare.additional_information')}

{inPersonURL && flowPath === 'standard' ? ( ) : ( diff --git a/app/javascript/packages/document-capture/context/feature-flag.tsx b/app/javascript/packages/document-capture/context/feature-flag.tsx index 0066169e98b..8e9ad79ef83 100644 --- a/app/javascript/packages/document-capture/context/feature-flag.tsx +++ b/app/javascript/packages/document-capture/context/feature-flag.tsx @@ -10,11 +10,16 @@ export interface FeatureFlagContextProps { * Specify whether to show exit optional questions on doc capture screen. */ exitQuestionSectionEnabled: boolean; + /** + * Specify whether to show the selfie capture on the doc capture screen. + */ + selfieCaptureEnabled: boolean; } const FeatureFlagContext = createContext({ notReadySectionEnabled: false, exitQuestionSectionEnabled: false, + selfieCaptureEnabled: false, }); FeatureFlagContext.displayName = 'FeatureFlagContext'; diff --git a/app/javascript/packages/document-capture/context/service-provider.tsx b/app/javascript/packages/document-capture/context/service-provider.tsx index 06f625d7a12..8c74c75ff2e 100644 --- a/app/javascript/packages/document-capture/context/service-provider.tsx +++ b/app/javascript/packages/document-capture/context/service-provider.tsx @@ -16,36 +16,25 @@ export interface ServiceProviderContextType { * specific location within the step. */ getFailureToProofURL: (location: string) => string; - /** - * Whether or not the selfie feature is currently on - */ - selfieCaptureEnabled?: Boolean; } const ServiceProviderContext = createContext({ name: null, failureToProofURL: '', getFailureToProofURL: () => '', - selfieCaptureEnabled: false, }); ServiceProviderContext.displayName = 'ServiceProviderContext'; interface ServiceProviderContextProviderProps { value: Omit; - selfieCaptureEnabled?: Boolean; children: ReactNode; } -function ServiceProviderContextProvider({ - value, - selfieCaptureEnabled, - children, -}: ServiceProviderContextProviderProps) { +function ServiceProviderContextProvider({ value, children }: ServiceProviderContextProviderProps) { const mergedValue = useMemo( () => ({ ...value, - selfieCaptureEnabled, getFailureToProofURL: (location: string) => addSearchParams(value.failureToProofURL, { location }), }), diff --git a/app/javascript/packages/phone-input/package.json b/app/javascript/packages/phone-input/package.json index f0c319b0b5e..93fa0c0df0f 100644 --- a/app/javascript/packages/phone-input/package.json +++ b/app/javascript/packages/phone-input/package.json @@ -4,6 +4,6 @@ "version": "1.0.0", "dependencies": { "intl-tel-input": "^17.0.19", - "libphonenumber-js": "^1.10.49" + "libphonenumber-js": "^1.10.51" } } diff --git a/app/javascript/packages/webauthn/is-expected-error.spec.ts b/app/javascript/packages/webauthn/is-expected-error.spec.ts index 5943091862b..58834e29d05 100644 --- a/app/javascript/packages/webauthn/is-expected-error.spec.ts +++ b/app/javascript/packages/webauthn/is-expected-error.spec.ts @@ -1,4 +1,5 @@ import isExpectedWebauthnError from './is-expected-error'; +import { SCREEN_LOCK_ERROR } from './is-user-verification-screen-lock-error'; describe('isExpectedWebauthnError', () => { it('returns false for any error other than DOMException', () => { @@ -21,4 +22,18 @@ describe('isExpectedWebauthnError', () => { expect(result).to.be.true(); }); + + it('returns false for a screen lock error', () => { + const error = new DOMException(SCREEN_LOCK_ERROR, 'NotSupportedError'); + const result = isExpectedWebauthnError(error); + + expect(result).to.be.false(); + }); + + it('returns true for a screen lock error specified to have occurred during verification', () => { + const error = new DOMException(SCREEN_LOCK_ERROR, 'NotSupportedError'); + const result = isExpectedWebauthnError(error, { isVerifying: true }); + + expect(result).to.be.true(); + }); }); diff --git a/app/javascript/packages/webauthn/is-expected-error.ts b/app/javascript/packages/webauthn/is-expected-error.ts index 04d1c88aca7..7ee1c48babc 100644 --- a/app/javascript/packages/webauthn/is-expected-error.ts +++ b/app/javascript/packages/webauthn/is-expected-error.ts @@ -1,3 +1,5 @@ +import isUserVerificationScreenLockError from './is-user-verification-screen-lock-error'; + /** * Set of expected DOM exceptions, which occur based on some user behavior that is not noteworthy: * @@ -13,7 +15,21 @@ const EXPECTED_DOM_EXCEPTIONS: Set = new Set([ 'InvalidStateError', ]); -const isExpectedWebauthnError = (error: Error): boolean => - error instanceof DOMException && EXPECTED_DOM_EXCEPTIONS.has(error.name); +interface IsExpectedErrorOptions { + /** + * Whether the error happened in the context of a verification ceremony. + */ + isVerifying: boolean; +} + +function isExpectedWebauthnError( + error: Error, + { isVerifying }: Partial = {}, +): boolean { + return ( + (error instanceof DOMException && EXPECTED_DOM_EXCEPTIONS.has(error.name)) || + (!!isVerifying && isUserVerificationScreenLockError(error)) + ); +} export default isExpectedWebauthnError; diff --git a/app/javascript/packages/webauthn/is-user-verification-screen-lock-error.spec.ts b/app/javascript/packages/webauthn/is-user-verification-screen-lock-error.spec.ts new file mode 100644 index 00000000000..1be38b87bdd --- /dev/null +++ b/app/javascript/packages/webauthn/is-user-verification-screen-lock-error.spec.ts @@ -0,0 +1,17 @@ +import isUserVerificationScreenLockError, { + SCREEN_LOCK_ERROR, +} from './is-user-verification-screen-lock-error'; + +describe('isUserVerificationScreenLockError', () => { + it('returns false for an error that is not a screen lock error', () => { + const error = new DOMException('', 'NotSupportedError'); + + expect(isUserVerificationScreenLockError(error)).to.be.false(); + }); + + it('returns true for an error that is a screen lock error', () => { + const error = new DOMException(SCREEN_LOCK_ERROR, 'NotSupportedError'); + + expect(isUserVerificationScreenLockError(error)).to.be.true(); + }); +}); diff --git a/app/javascript/packages/webauthn/is-user-verification-screen-lock-error.ts b/app/javascript/packages/webauthn/is-user-verification-screen-lock-error.ts new file mode 100644 index 00000000000..bc442b5cb62 --- /dev/null +++ b/app/javascript/packages/webauthn/is-user-verification-screen-lock-error.ts @@ -0,0 +1,10 @@ +/** + * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/credentialmanagement/credentials_container.cc;l=432;drc=6d16761b175fd105f879a4e1803547381e97402d + */ +export const SCREEN_LOCK_ERROR = + 'The specified `userVerification` requirement cannot be fulfilled by this device unless the device is secured with a screen lock.'; + +const isUserVerificationScreenLockError = (error: Error): boolean => + error.message === SCREEN_LOCK_ERROR; + +export default isUserVerificationScreenLockError; diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts index c9e6affc43c..5101427babd 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts @@ -3,6 +3,7 @@ import quibble from 'quibble'; import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import '@18f/identity-submit-button/submit-button-element'; +import { SCREEN_LOCK_ERROR } from './is-user-verification-screen-lock-error'; import type { WebauthnVerifyButtonDataset } from './webauthn-verify-button-element'; describe('WebauthnVerifyButtonElement', () => { @@ -41,6 +42,7 @@ describe('WebauthnVerifyButtonElement', () => { + `; @@ -109,6 +111,7 @@ describe('WebauthnVerifyButtonElement', () => { client_data_json: '', signature: '', webauthn_error: 'NotAllowedError', + screen_lock_error: '', }); expect(trackError).not.to.have.been.called(); }); @@ -132,6 +135,7 @@ describe('WebauthnVerifyButtonElement', () => { client_data_json: '', signature: '', webauthn_error: 'CustomError', + screen_lock_error: '', }); expect(trackError).to.have.been.calledWith(error); }); @@ -155,6 +159,27 @@ describe('WebauthnVerifyButtonElement', () => { client_data_json: 'json', signature: 'sig', webauthn_error: '', + screen_lock_error: '', }); }); + + it('submits with NotSupportedError resulting from userVerification requirement', async () => { + const { form } = createElement(); + + verifyWebauthnDevice.throws(new DOMException(SCREEN_LOCK_ERROR, 'NotSupportedError')); + + const button = screen.getByRole('button', { name: 'Authenticate' }); + await userEvent.click(button); + await expect(form.submit).to.eventually.be.called(); + + expect(Object.fromEntries(new window.FormData(form))).to.deep.equal({ + credential_id: '', + authenticator_data: '', + client_data_json: '', + signature: '', + webauthn_error: 'NotSupportedError', + screen_lock_error: 'true', + }); + expect(trackError).not.to.have.been.called(); + }); }); diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts index 21d8378df9c..d937c879f49 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts @@ -1,8 +1,9 @@ import { trackError } from '@18f/identity-analytics'; import type SubmitButtonElement from '@18f/identity-submit-button/submit-button-element'; import verifyWebauthnDevice from './verify-webauthn-device'; -import type { VerifyCredentialDescriptor } from './verify-webauthn-device'; import isExpectedWebauthnError from './is-expected-error'; +import isUserVerificationScreenLockError from './is-user-verification-screen-lock-error'; +import type { VerifyCredentialDescriptor } from './verify-webauthn-device'; export interface WebauthnVerifyButtonDataset extends DOMStringMap { credentials: string; @@ -58,10 +59,14 @@ class WebauthnVerifyButtonElement extends HTMLElement { this.setInputValue('client_data_json', result.clientDataJSON); this.setInputValue('signature', result.signature); } catch (error) { - if (!isExpectedWebauthnError(error)) { + if (!isExpectedWebauthnError(error, { isVerifying: true })) { trackError(error); } + if (isUserVerificationScreenLockError(error)) { + this.setInputValue('screen_lock_error', 'true'); + } + this.setInputValue('webauthn_error', error.name); } diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index e570edf2ccf..394fa8323b7 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -172,7 +172,6 @@ const App = composeComponents( ServiceProviderContextProvider, { value: getServiceProvider(), - selfieCaptureEnabled: getSelfieCaptureEnabled(), }, ], [ @@ -188,6 +187,7 @@ const App = composeComponents( value: { notReadySectionEnabled: String(uiNotReadySectionEnabled) === 'true', exitQuestionSectionEnabled: String(uiExitQuestionSectionEnabled) === 'true', + selfieCaptureEnabled: getSelfieCaptureEnabled(), }, }, ], diff --git a/app/jobs/gpo_expiration_job.rb b/app/jobs/gpo_expiration_job.rb index 5403d9008e7..dc57a0f97ab 100644 --- a/app/jobs/gpo_expiration_job.rb +++ b/app/jobs/gpo_expiration_job.rb @@ -1,11 +1,6 @@ class GpoExpirationJob < ApplicationJob queue_as :low - def initialize(analytics: nil, on_profile_expired: nil) - @analytics = analytics - @on_profile_expired = on_profile_expired - end - def perform( dry_run: false, limit: nil, @@ -28,11 +23,6 @@ def perform( end expire_profile(profile: profile) unless dry_run - - on_profile_expired&.call( - profile: profile, - gpo_verification_pending_at: gpo_verification_pending_at, - ) end end end @@ -47,8 +37,6 @@ def gpo_profiles_that_should_be_expired(as_of:, min_profile_age: nil) private - attr_reader :on_profile_expired - def expire_profile(profile:) gpo_verification_pending_at = profile.gpo_verification_pending_at diff --git a/app/policies/idv/flow_policy.rb b/app/policies/idv/flow_policy.rb index bbfd8ce9f48..01936ee9f61 100644 --- a/app/policies/idv/flow_policy.rb +++ b/app/policies/idv/flow_policy.rb @@ -18,11 +18,11 @@ def info_for_latest_step steps[latest_step] end - def undo_steps_from_controller!(controller:) + def undo_future_steps_from_controller!(controller:) controller_name = controller < ApplicationController ? controller.controller_name : controller key = controller_to_key(controller: controller_name) - undo_steps_from!(key: key) + undo_future_steps!(key: key) end private @@ -55,6 +55,9 @@ def steps hybrid_handoff: Idv::HybridHandoffController.step_info, link_sent: Idv::LinkSentController.step_info, document_capture: Idv::DocumentCaptureController.step_info, + ssn: Idv::SsnController.step_info, + verify_info: Idv::VerifyInfoController.step_info, + address: Idv::AddressController.step_info, } end @@ -72,6 +75,12 @@ def undo_steps_from!(key:) end end + def undo_future_steps!(key:) + steps[key].next_steps.each do |next_step| + undo_steps_from!(key: next_step) + end + end + def controller_to_key(controller:) steps.keys.each do |key| return key if steps[key].controller == controller diff --git a/app/presenters/two_factor_auth_code/sms_opt_in_presenter.rb b/app/presenters/two_factor_auth_code/sms_opt_in_presenter.rb new file mode 100644 index 00000000000..9e5d1753ccf --- /dev/null +++ b/app/presenters/two_factor_auth_code/sms_opt_in_presenter.rb @@ -0,0 +1,9 @@ +module TwoFactorAuthCode + class SmsOptInPresenter < GenericDeliveryPresenter + def initialize; end + + def redirect_location_step + :sms_opt_in + end + end +end diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index ed30b21e87f..da9b171d195 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -28,11 +28,19 @@ def title end def heading - t('two_factor_authentication.login_options_title') + if reauthentication_context? + t('two_factor_authentication.login_options_reauthentication_title') + else + t('two_factor_authentication.login_options_title') + end end def info - t('two_factor_authentication.login_intro') + if reauthentication_context? + t('two_factor_authentication.login_intro_reauthentication') + else + t('two_factor_authentication.login_intro') + end end def restricted_options_warning_text @@ -77,7 +85,7 @@ def account_reset_or_cancel_link end def cancel_link - if @reauthentication_context + if reauthentication_context? account_path else sign_out_path diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 8bf937e0877..97860f4c1b5 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -290,16 +290,14 @@ def contact_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra end # @param [String] message the warning - # @param [String] getting_started_ab_test_bucket Which initial IdV screen the user saw # @param [String] phone_question_ab_test_bucket Prompt user with phone question before doc auth # Logged when there is a non-user-facing error in the doc auth process, such as an unrecognized # field from a vendor - def doc_auth_warning(message: nil, getting_started_ab_test_bucket: nil, + def doc_auth_warning(message: nil, phone_question_ab_test_bucket: nil, **extra) track_event( 'Doc Auth Warning', message: message, - getting_started_ab_test_bucket: getting_started_ab_test_bucket, phone_question_ab_test_bucket: phone_question_ab_test_bucket, **extra, ) @@ -890,14 +888,6 @@ def idv_doc_auth_failed_image_resubmitted(side:, **extra) ) end - def idv_doc_auth_getting_started_submitted(**extra) - track_event('IdV: doc auth getting_started submitted', **extra) - end - - def idv_doc_auth_getting_started_visited(**extra) - track_event('IdV: doc auth getting_started visited', **extra) - end - def idv_doc_auth_how_to_verify_submitted(**extra) track_event(:idv_doc_auth_how_to_verify_submitted, **extra) end @@ -974,7 +964,6 @@ def idv_doc_auth_ssn_visited(**extra) # @param [String] flow_path # @param [String] front_image_fingerprint Fingerprint of front image data # @param [String] back_image_fingerprint Fingerprint of back image data - # @param [String] getting_started_ab_test_bucket Which initial IdV screen the user saw # @param [String] phone_question_ab_test_bucket Prompt user with phone question before doc auth # @param [String] phone_with_camera the result of the phone question a/b test # The document capture image uploaded was locally validated during the IDV process @@ -987,7 +976,6 @@ def idv_doc_auth_submitted_image_upload_form( user_id: nil, front_image_fingerprint: nil, back_image_fingerprint: nil, - getting_started_ab_test_bucket: nil, phone_question_ab_test_bucket: nil, phone_with_camera: nil, **extra @@ -1002,7 +990,6 @@ def idv_doc_auth_submitted_image_upload_form( flow_path: flow_path, front_image_fingerprint: front_image_fingerprint, back_image_fingerprint: back_image_fingerprint, - getting_started_ab_test_bucket: getting_started_ab_test_bucket, phone_question_ab_test_bucket: phone_question_ab_test_bucket, phone_with_camera: phone_with_camera, **extra, @@ -1024,7 +1011,6 @@ def idv_doc_auth_submitted_image_upload_form( # @param [Float] vendor_request_time_in_ms Time it took to upload images & get a response. # @param [String] front_image_fingerprint Fingerprint of front image data # @param [String] back_image_fingerprint Fingerprint of back image data - # @param [String] getting_started_ab_test_bucket Which initial IdV screen the user saw # @param [String] phone_question_ab_test_bucket Prompt user with phone question before doc auth # @param [String] phone_with_camera the result of the phone question a/b test # The document capture image was uploaded to vendor during the IDV process @@ -1043,7 +1029,6 @@ def idv_doc_auth_submitted_image_upload_vendor( vendor_request_time_in_ms: nil, front_image_fingerprint: nil, back_image_fingerprint: nil, - getting_started_ab_test_bucket: nil, phone_question_ab_test_bucket: nil, phone_with_camera: nil, **extra @@ -1065,7 +1050,6 @@ def idv_doc_auth_submitted_image_upload_vendor( vendor_request_time_in_ms: vendor_request_time_in_ms, front_image_fingerprint: front_image_fingerprint, back_image_fingerprint: back_image_fingerprint, - getting_started_ab_test_bucket: getting_started_ab_test_bucket, phone_question_ab_test_bucket: phone_question_ab_test_bucket, phone_with_camera: phone_with_camera, **extra, @@ -1080,7 +1064,6 @@ def idv_doc_auth_submitted_image_upload_vendor( # @param [String] flow_path # @param [String] front_image_fingerprint Fingerprint of front image data # @param [String] back_image_fingerprint Fingerprint of back image data - # @param [String] getting_started_ab_test_bucket Which initial IdV screen the user saw # @param [Hash] classification_info document image side information, issuing country and type etc # The PII that came back from the document capture vendor was validated def idv_doc_auth_submitted_pii_validation( @@ -1092,7 +1075,6 @@ def idv_doc_auth_submitted_pii_validation( user_id: nil, front_image_fingerprint: nil, back_image_fingerprint: nil, - getting_started_ab_test_bucket: nil, classification_info: {}, **extra ) @@ -1106,7 +1088,6 @@ def idv_doc_auth_submitted_pii_validation( flow_path: flow_path, front_image_fingerprint: front_image_fingerprint, back_image_fingerprint: back_image_fingerprint, - getting_started_ab_test_bucket: getting_started_ab_test_bucket, classification_info: classification_info, **extra, ) @@ -1129,11 +1110,7 @@ def idv_doc_auth_verify_visited(**extra) # @param [String] step_name # @param [Integer] remaining_attempts # The user was sent to a warning page during the IDV flow - def idv_doc_auth_warning_visited( - step_name:, - remaining_attempts:, - **extra - ) + def idv_doc_auth_warning_visited(step_name:, remaining_attempts:, **extra) track_event( 'IdV: doc auth warning visited', step_name: step_name, diff --git a/app/services/db/add_document_verification_and_selfie_costs.rb b/app/services/db/add_document_verification_and_selfie_costs.rb index 0d89768a255..96192917989 100644 --- a/app/services/db/add_document_verification_and_selfie_costs.rb +++ b/app/services/db/add_document_verification_and_selfie_costs.rb @@ -1,7 +1,8 @@ module Db class AddDocumentVerificationAndSelfieCosts - def initialize(user_id:, service_provider:) + def initialize(user_id:, service_provider:, liveness_checking_enabled:) @service_provider = service_provider + @liveness_checking_enabled = liveness_checking_enabled @user_id = user_id end @@ -13,7 +14,7 @@ def call(client_response) private - attr_reader :service_provider, :user_id + attr_reader :service_provider, :user_id, :liveness_checking_enabled def add_cost(token) Db::SpCost::AddSpCost.call(service_provider, 2, token) diff --git a/app/services/db/sp_cost/add_sp_cost.rb b/app/services/db/sp_cost/add_sp_cost.rb index 3e1a0dedb48..ea31d8d9b1f 100644 --- a/app/services/db/sp_cost/add_sp_cost.rb +++ b/app/services/db/sp_cost/add_sp_cost.rb @@ -7,6 +7,7 @@ class SpCostTypeError < StandardError; end aamva acuant_front_image acuant_back_image + acuant_selfie acuant_result lexis_nexis_resolution lexis_nexis_address diff --git a/app/services/doc_auth/acuant/acuant_client.rb b/app/services/doc_auth/acuant/acuant_client.rb index 7b29b7419d5..eccef4d89c0 100644 --- a/app/services/doc_auth/acuant/acuant_client.rb +++ b/app/services/doc_auth/acuant/acuant_client.rb @@ -42,7 +42,9 @@ def post_images( back_image:, image_source:, user_uuid: nil, - uuid_prefix: nil + uuid_prefix: nil, + selfie_image: nil, + liveness_checking_enabled: false ) document_response = create_document(image_source: image_source) return document_response unless document_response.success? diff --git a/app/services/doc_auth/error_generator.rb b/app/services/doc_auth/error_generator.rb index 7f1f521ce16..ded26d8915d 100644 --- a/app/services/doc_auth/error_generator.rb +++ b/app/services/doc_auth/error_generator.rb @@ -12,12 +12,14 @@ def initialize(config) ID = :id FRONT = :front BACK = :back + SELFIE = :selfie GENERAL = :general ERROR_KEYS = [ ID, FRONT, BACK, + SELFIE, GENERAL, ].to_set.freeze @@ -62,7 +64,9 @@ def generate_doc_auth_errors(response_info) image_metric_errors = get_image_metric_errors(response_info[:image_metrics]) return image_metric_errors.to_h unless image_metric_errors.empty? - alert_errors = get_error_messages(response_info) + liveness_enabled = response_info[:liveness_enabled] + alert_errors = get_error_messages(liveness_enabled, response_info) + alert_error_count += 1 if alert_errors.include?(SELFIE) error = '' side = nil @@ -171,7 +175,7 @@ def get_image_metric_errors(processed_image_metrics) error_result end - def get_error_messages(response_info) + def get_error_messages(liveness_enabled, response_info) errors = Hash.new { |hash, key| hash[key] = Set.new } if response_info[:doc_auth_result] != 'Passed' @@ -185,6 +189,11 @@ def get_error_messages(response_info) end end + portrait_match_results = response_info[:portrait_match_results] || {} + if liveness_enabled && portrait_match_results.dig(:FaceMatchResult) != 'Pass' + errors[SELFIE] << Errors::SELFIE_FAILURE + end + errors end @@ -215,8 +224,12 @@ def scan_for_unknown_alerts(response_info) unknown_fail_count end - def self.wrapped_general_error - { general: [Errors::GENERAL_ERROR], hints: true } + def self.general_error(_liveness_enabled) + Errors::GENERAL_ERROR + end + + def self.wrapped_general_error(liveness_enabled) + { general: [ErrorGenerator.general_error(liveness_enabled)], hints: true } end def supported_country_codes diff --git a/app/services/doc_auth/errors.rb b/app/services/doc_auth/errors.rb index fbc15666cab..cdd8e79bdc7 100644 --- a/app/services/doc_auth/errors.rb +++ b/app/services/doc_auth/errors.rb @@ -26,6 +26,7 @@ module Errors MULTIPLE_BACK_ID_FAILURES = 'multiple_back_id_failures' MULTIPLE_FRONT_ID_FAILURES = 'multiple_front_id_failures' REF_CONTROL_NUMBER_CHECK = 'ref_control_number_check' + SELFIE_FAILURE = 'selfie_failure' SEX_CHECK = 'sex_check' VISIBLE_COLOR_CHECK = 'visible_color_check' VISIBLE_PHOTO_CHECK = 'visible_photo_check' @@ -66,6 +67,7 @@ module Errors MULTIPLE_BACK_ID_FAILURES, MULTIPLE_FRONT_ID_FAILURES, REF_CONTROL_NUMBER_CHECK, + SELFIE_FAILURE, SEX_CHECK, VISIBLE_COLOR_CHECK, VISIBLE_PHOTO_CHECK, @@ -113,6 +115,8 @@ module Errors MULTIPLE_FRONT_ID_FAILURES => { long_msg: MULTIPLE_FRONT_ID_FAILURES, field_msg: FALLBACK_FIELD_LEVEL, hints: true }, MULTIPLE_BACK_ID_FAILURES => { long_msg: MULTIPLE_BACK_ID_FAILURES, field_msg: FALLBACK_FIELD_LEVEL, hints: true }, GENERAL_ERROR => { long_msg: GENERAL_ERROR, field_msg: FALLBACK_FIELD_LEVEL, hints: true }, + # Liveness, use general error for now + SELFIE_FAILURE => { long_msg: GENERAL_ERROR, field_msg: FALLBACK_FIELD_LEVEL, hints: false }, } # rubocop:enable Layout/LineLength end diff --git a/app/services/doc_auth/lexis_nexis/config.rb b/app/services/doc_auth/lexis_nexis/config.rb index 35572d96011..d17321ae892 100644 --- a/app/services/doc_auth/lexis_nexis/config.rb +++ b/app/services/doc_auth/lexis_nexis/config.rb @@ -7,6 +7,8 @@ module LexisNexis :trueid_account_id, :trueid_noliveness_cropping_workflow, :trueid_noliveness_nocropping_workflow, + :trueid_liveness_cropping_workflow, + :trueid_liveness_nocropping_workflow, :trueid_password, :trueid_username, :hmac_key_id, @@ -23,6 +25,8 @@ module LexisNexis :request_mode, :trueid_noliveness_cropping_workflow, :trueid_noliveness_nocropping_workflow, + :trueid_liveness_cropping_workflow, + :trueid_liveness_nocropping_workflow, :locale, :dpi_threshold, :sharpness_threshold, diff --git a/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb b/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb index e4253ca4caa..8c6a8438af8 100644 --- a/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb +++ b/app/services/doc_auth/lexis_nexis/lexis_nexis_client.rb @@ -15,9 +15,11 @@ def create_document def post_images( front_image:, back_image:, + selfie_image: nil, image_source: nil, user_uuid: nil, - uuid_prefix: nil + uuid_prefix: nil, + liveness_checking_enabled: false ) Requests::TrueIdRequest.new( config: config, @@ -25,7 +27,9 @@ def post_images( uuid_prefix: uuid_prefix, front_image: front_image, back_image: back_image, + selfie_image: selfie_image, image_source: image_source, + liveness_checking_required: liveness_checking_enabled, ).fetch end end diff --git a/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb b/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb index 5a20ec6bea7..01762f35d18 100644 --- a/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb +++ b/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb @@ -2,7 +2,7 @@ module DocAuth module LexisNexis module Requests class TrueIdRequest < DocAuth::LexisNexis::Request - attr_reader :front_image, :back_image + attr_reader :front_image, :back_image, :selfie_image, :liveness_checking_required def initialize( config:, @@ -10,12 +10,17 @@ def initialize( uuid_prefix:, front_image:, back_image:, - image_source: nil + selfie_image: nil, + image_source: nil, + liveness_checking_required: false ) super(config: config, user_uuid: user_uuid, uuid_prefix: uuid_prefix) @front_image = front_image @back_image = back_image + @selfie_image = selfie_image @image_source = image_source + # when set to required, be sure to pass in selfie_image + @liveness_checking_required = liveness_checking_required end private @@ -25,9 +30,10 @@ def body Document: { Front: encode(front_image), Back: encode(back_image), + Selfie: (encode(selfie_image) if include_liveness?), DocumentType: 'DriversLicense', }, - } + }.compact settings.merge(document).to_json end @@ -36,6 +42,7 @@ def handle_http_response(http_response) LexisNexis::Responses::TrueIdResponse.new( http_response, config, + liveness_checking_required, ) end @@ -57,9 +64,13 @@ def password def workflow if acuant_sdk_source? - config.trueid_noliveness_nocropping_workflow + include_liveness? ? + config.trueid_liveness_nocropping_workflow : + config.trueid_noliveness_nocropping_workflow else - config.trueid_noliveness_cropping_workflow + include_liveness? ? + config.trueid_liveness_cropping_workflow : + config.trueid_noliveness_cropping_workflow end end @@ -78,6 +89,10 @@ def metric_name def timeout IdentityConfig.store.lexisnexis_trueid_timeout end + + def include_liveness? + liveness_checking_required + end end end end diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index e9291a4cc77..a48f155fd22 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -44,10 +44,10 @@ class TrueIdResponse < DocAuth::Response }.freeze attr_reader :config, :http_response - def initialize(http_response, config) + def initialize(http_response, config, liveness_checking_enabled = false) @config = config @http_response = http_response - + @liveness_checking_enabled = liveness_checking_enabled super( success: successful_result?, errors: error_messages, @@ -74,7 +74,7 @@ def error_messages if true_id_product&.dig(:AUTHENTICATION_RESULT).present? ErrorGenerator.new(config).generate_doc_auth_errors(response_info) elsif true_id_product.present? - ErrorGenerator.wrapped_general_error + ErrorGenerator.wrapped_general_error(@liveness_checking_enabled) else { network: true } # return a generic technical difficulties error to user end diff --git a/app/services/doc_auth/mock/doc_auth_mock_client.rb b/app/services/doc_auth/mock/doc_auth_mock_client.rb index 141f8f986c7..a72aa774b2d 100644 --- a/app/services/doc_auth/mock/doc_auth_mock_client.rb +++ b/app/services/doc_auth/mock/doc_auth_mock_client.rb @@ -52,9 +52,11 @@ def post_back_image(image:, instance_id:) def post_images( front_image:, back_image:, + selfie_image: nil, image_source: nil, user_uuid: nil, - uuid_prefix: nil + uuid_prefix: nil, + liveness_checking_enabled: false ) return mocked_response_for_method(__method__) if method_mocked?(__method__) diff --git a/app/services/doc_auth_router.rb b/app/services/doc_auth_router.rb index 2545bbd859a..a1faba9cf0b 100644 --- a/app/services/doc_auth_router.rb +++ b/app/services/doc_auth_router.rb @@ -126,6 +126,7 @@ def translate_form_response!(response) def translate_doc_auth_errors!(response) error_keys = DocAuth::ErrorGenerator::ERROR_KEYS.dup + error_keys.delete(:selfie) if @client.is_a?(DocAuth::Acuant::AcuantClient) error_keys.each do |category| cat_errors = response.errors[category] @@ -178,6 +179,8 @@ def self.client(vendor_discriminator: nil, warn_notifier: nil, analytics: nil) trueid_account_id: IdentityConfig.store.lexisnexis_trueid_account_id, trueid_noliveness_cropping_workflow: IdentityConfig.store.lexisnexis_trueid_noliveness_cropping_workflow, trueid_noliveness_nocropping_workflow: IdentityConfig.store.lexisnexis_trueid_noliveness_nocropping_workflow, + trueid_liveness_cropping_workflow: IdentityConfig.store.lexisnexis_trueid_liveness_cropping_workflow, + trueid_liveness_nocropping_workflow: IdentityConfig.store.lexisnexis_trueid_liveness_nocropping_workflow, trueid_password: IdentityConfig.store.lexisnexis_trueid_password, trueid_username: IdentityConfig.store.lexisnexis_trueid_username, hmac_key_id: IdentityConfig.store.lexisnexis_trueid_hmac_key_id, @@ -204,11 +207,18 @@ def self.client(vendor_discriminator: nil, warn_notifier: nil, analytics: nil) def self.doc_auth_vendor(discriminator: nil, analytics: nil) case AbTests::DOC_AUTH_VENDOR.bucket(discriminator) when :alternate_vendor - IdentityConfig.store.doc_auth_vendor_randomize_alternate_vendor + vendor = IdentityConfig.store.doc_auth_vendor_randomize_alternate_vendor else analytics&.idv_doc_auth_randomizer_defaulted if discriminator.blank? - IdentityConfig.store.doc_auth_vendor + vendor = IdentityConfig.store.doc_auth_vendor end + + # if vendor is not set to mock and selfie enabled use lexisnexis + if IdentityConfig.store.doc_auth_selfie_capture[:enabled] && + vendor != Idp::Constants::Vendors::MOCK + vendor = Idp::Constants::Vendors::LEXIS_NEXIS + end + vendor end end diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index a25214ca436..a6a8c7b11d6 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -60,7 +60,7 @@ def create_profile_from_applicant_with_password(user_password) profile_maker = build_profile_maker(user_password) profile = profile_maker.save_profile( fraud_pending_reason: threatmetrix_fraud_pending_reason, - gpo_verification_needed: gpo_verification_needed?, + gpo_verification_needed: !phone_confirmed? || verify_by_mail?, in_person_verification_needed: current_user.has_in_person_enrollment?, ) @@ -91,8 +91,8 @@ def acknowledge_personal_key! session[:personal_key_acknowledged] = true end - def gpo_verification_needed? - !phone_confirmed? || address_verification_mechanism == 'gpo' + def verify_by_mail? + address_verification_mechanism == 'gpo' end def vendor_params @@ -163,16 +163,12 @@ def verify_info_step_complete? resolution_successful end - def address_step_complete? - if address_verification_mechanism == 'gpo' - true - else - phone_confirmed? - end + def phone_or_address_step_complete? + verify_by_mail? || phone_confirmed? end def address_mechanism_chosen? - vendor_phone_confirmation == true || address_verification_mechanism == 'gpo' + vendor_phone_confirmation == true || verify_by_mail? end def phone_confirmed? diff --git a/app/services/service_provider_seeder.rb b/app/services/service_provider_seeder.rb index 681a05e6a54..1fb1b0ce8ab 100644 --- a/app/services/service_provider_seeder.rb +++ b/app/services/service_provider_seeder.rb @@ -36,6 +36,28 @@ def run end end + def write_review_app_yaml(dashboard_url:) + hash = { + @rails_env.to_s => { + 'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:dashboard' => { + 'friendly_name' => 'Dashboard', + 'agency' => 'GSA', + 'agency_id' => 2, + 'logo' => '18f.svg', + 'certs' => ['identity_dashboard_cert'], + 'return_to_sp_url' => dashboard_url, + 'redirect_uris' => [ + "#{dashboard_url}/auth/logindotgov/callback", + dashboard_url, + ], + 'push_notification_url' => "#{dashboard_url}/api/security_events", + }, + }, + } + + File.write(Rails.root.join(@yaml_path, 'service_providers.yml'), hash.to_yaml) + end + private attr_reader :rails_env, :deploy_env diff --git a/app/views/idv/cancellations/new.html.erb b/app/views/idv/cancellations/new.html.erb index ac30b1bc550..492cc9ead83 100644 --- a/app/views/idv/cancellations/new.html.erb +++ b/app/views/idv/cancellations/new.html.erb @@ -34,7 +34,9 @@ method: :delete, big: true, wide: true, - ).with_content(t('idv.cancel.actions.start_over')) %> + form: { 'aria-label': t('idv.cancel.actions.start_over') }, + ).with_content(t('idv.cancel.actions.start_over')) + %>
<%= render ButtonComponent.new( action: ->(**tag_options, &block) do @@ -44,6 +46,7 @@ big: true, wide: true, outline: true, + form: { 'aria-label': t('idv.cancel.actions.keep_going') }, ).with_content(t('idv.cancel.actions.keep_going')) %>
@@ -65,7 +68,10 @@ big: true, wide: true, outline: true, - form: { data: { form_steps_wait: '' } }, + form: { + "aria-label": @presenter.exit_action_text, + data: { form_steps_wait: '' }, + }, ).with_content(@presenter.exit_action_text) %>
<% end %> diff --git a/app/views/idv/document_capture/show.html.erb b/app/views/idv/document_capture/show.html.erb index 7598c4f3611..9742a47df57 100644 --- a/app/views/idv/document_capture/show.html.erb +++ b/app/views/idv/document_capture/show.html.erb @@ -9,4 +9,4 @@ acuant_version: acuant_version, phone_question_ab_test_bucket: phone_question_ab_test_bucket, phone_with_camera: phone_with_camera, - ) %> \ No newline at end of file + ) %> diff --git a/app/views/idv/enter_password/new.html.erb b/app/views/idv/enter_password/new.html.erb index 935a7622023..673033b129a 100644 --- a/app/views/idv/enter_password/new.html.erb +++ b/app/views/idv/enter_password/new.html.erb @@ -34,7 +34,7 @@ <%= link_to(t('idv.forgot_password.link_text'), idv_forgot_password_url, class: 'margin-left-1') %> - <% if @verifying_by_mail %> + <% if @verify_by_mail %> <%= render AlertComponent.new( type: :warning, id: 'by-mail-password-warning', diff --git a/app/views/idv/getting_started/show.html.erb b/app/views/idv/getting_started/show.html.erb deleted file mode 100644 index fe9bec61bff..00000000000 --- a/app/views/idv/getting_started/show.html.erb +++ /dev/null @@ -1,79 +0,0 @@ -<% self.title = @title %> - -<%= render JavascriptRequiredComponent.new( - header: t('idv.getting_started.no_js_header'), - intro: t('idv.getting_started.no_js_intro', sp_name: @sp_name), - ) do %> - -<%= render PageHeadingComponent.new.with_content(@title) %> -

- <%= t( - 'doc_auth.info.getting_started_html', - sp_name: @sp_name, - link_html: new_tab_link_to( - t('doc_auth.info.getting_started_learn_more'), - help_center_redirect_path( - category: 'verify-your-identity', - article: 'how-to-verify-your-identity', - flow: :idv, - step: :getting_started, - location: 'intro_paragraph', - ), - ), - ) %> -

- -

<%= t('doc_auth.getting_started.instructions.getting_started') %>

- - <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet1')) do %> -

<%= t('doc_auth.getting_started.instructions.text1') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet2')) do %> -

<%= t('doc_auth.getting_started.instructions.text2') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet3')) do %> -

<%= t('doc_auth.getting_started.instructions.text3') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet4', app_name: APP_NAME)) do %> -

<%= t('doc_auth.getting_started.instructions.text4') %>

- <% end %> - <% end %> - -<%= simple_form_for( - :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' }, - ) do |f| %> - <%= render ClickObserverComponent.new(event_name: 'IdV: consent checkbox toggled') do %> - <%= render ValidatedFieldComponent.new( - form: f, - name: :idv_consent_given, - as: :boolean, - label: t('doc_auth.getting_started.instructions.consent', app_name: APP_NAME), - required: true, - ) %> - <% end %> -

- <%= new_tab_link_to( - t('doc_auth.getting_started.instructions.learn_more'), - policy_redirect_url(flow: :idv, step: :getting_started, location: :consent), - ) %> -

-
- <%= render( - SpinnerButtonComponent.new( - type: :submit, - big: true, - wide: true, - spin_on_click: false, - ).with_content(t('doc_auth.buttons.continue')), - ) %> -
-<% end %> - - <%= render 'shared/cancel', link: idv_cancel_path(step: 'getting_started') %> -<% end %> - -<%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/idv/welcome/_welcome_default.html.erb b/app/views/idv/welcome/_welcome_default.html.erb deleted file mode 100644 index f470abd531b..00000000000 --- a/app/views/idv/welcome/_welcome_default.html.erb +++ /dev/null @@ -1,106 +0,0 @@ -<% self.title = t('doc_auth.headings.welcome') %> - -<% content_for(:pre_flash_content) do %> - <%= render StepIndicatorComponent.new( - steps: Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS, - current_step: :getting_started, - locale_scope: 'idv', - class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', - ) %> -<% end %> - - <%= render JavascriptRequiredComponent.new( - header: t('idv.welcome.no_js_header'), - intro: t('idv.welcome.no_js_intro', sp_name: decorated_sp_session.sp_name || APP_NAME), - ) do %> - - <%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.welcome')) %> -

- <%= t('doc_auth.info.welcome', sp_name: decorated_sp_session.sp_name || APP_NAME) %> -

- -

<%= t('doc_auth.instructions.welcome') %>

- - <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> - <%= c.with_item(heading: t('doc_auth.instructions.bullet1')) do %> -

<%= t('doc_auth.instructions.text1') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.instructions.bullet2')) do %> -

<%= t('doc_auth.instructions.text2') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.instructions.bullet3')) do %> - - <%= new_tab_link_to( - t('idv.troubleshooting.options.learn_more_address_verification_options'), - help_center_redirect_path( - category: 'verify-your-identity', - article: 'phone-number', - flow: :idv, - step: :welcome, - location: 'you_will_need', - ), - ) %> - <% end %> - <% end %> - - <%= simple_form_for :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-y-5 js-consent-continue-form' } do |f| %> - <%= f.submit t('doc_auth.buttons.continue') %> - <% end %> - - <%= render( - 'shared/troubleshooting_options', - heading_tag: :h3, - heading: t('idv.troubleshooting.headings.missing_required_items'), - options: [ - { - url: help_center_redirect_path( - category: 'verify-your-identity', - article: 'accepted-state-issued-identification', - flow: :idv, - step: :welcome, - location: 'missing_items', - ), - text: t('idv.troubleshooting.options.supported_documents'), - new_tab: true, - }, - { - url: help_center_redirect_path( - category: 'verify-your-identity', - article: 'phone-number', - flow: :idv, - step: :welcome, - location: 'missing_items', - ), - text: t('idv.troubleshooting.options.learn_more_address_verification_options'), - new_tab: true, - }, - decorated_sp_session.sp_name && { - url: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'), - text: t('idv.troubleshooting.options.get_help_at_sp', sp_name: decorated_sp_session.sp_name), - new_tab: true, - }, - ].select(&:present?), - ) %> - -

<%= t('doc_auth.instructions.privacy') %>

-

- <%= t('doc_auth.info.privacy', app_name: APP_NAME) %> -

-

- <%= new_tab_link_to( - t('doc_auth.instructions.learn_more'), - policy_redirect_url(flow: :idv, step: :welcome, location: :footer), - ) %> -

- - <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %> -<% end %> - -<%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/idv/welcome/_welcome_new.html.erb b/app/views/idv/welcome/_welcome_new.html.erb deleted file mode 100644 index d575c1a8d3b..00000000000 --- a/app/views/idv/welcome/_welcome_new.html.erb +++ /dev/null @@ -1,65 +0,0 @@ -<% self.title = @title %> - -<%= render JavascriptRequiredComponent.new( - header: t('idv.getting_started.no_js_header'), - intro: t('idv.getting_started.no_js_intro', sp_name: @sp_name), - ) do %> - -<%= render PageHeadingComponent.new.with_content(@title) %> -

- <%= t( - 'doc_auth.info.getting_started_html', - sp_name: @sp_name, - link_html: new_tab_link_to( - t('doc_auth.info.getting_started_learn_more'), - help_center_redirect_path( - category: 'verify-your-identity', - article: 'how-to-verify-your-identity', - flow: :idv, - step: :welcome_new, - location: 'intro_paragraph', - ), - ), - ) %> -

- -

<%= t('doc_auth.getting_started.instructions.getting_started') %>

- - <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet1')) do %> -

<%= t('doc_auth.getting_started.instructions.text1') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet2')) do %> -

<%= t('doc_auth.getting_started.instructions.text2') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet3')) do %> -

<%= t('doc_auth.getting_started.instructions.text3') %>

- <% end %> - <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet4', app_name: APP_NAME)) do %> -

<%= t('doc_auth.getting_started.instructions.text4') %>

- <% end %> - <% end %> - -<%= simple_form_for( - :doc_auth, - url: url_for, - method: 'put', - html: { autocomplete: 'off', class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' }, - ) do |f| %> - -
- <%= render( - SpinnerButtonComponent.new( - type: :submit, - big: true, - wide: true, - spin_on_click: false, - ).with_content(t('doc_auth.buttons.continue')), - ) %> -
-<% end %> - - <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome_new') %> -<% end %> - -<%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/idv/welcome/show.html.erb b/app/views/idv/welcome/show.html.erb index 20bbd523398..f470abd531b 100644 --- a/app/views/idv/welcome/show.html.erb +++ b/app/views/idv/welcome/show.html.erb @@ -1,5 +1,106 @@ -<% if @ab_test_bucket == :welcome_new %> - <%= render 'welcome_new' %> -<% else %> - <%= render 'welcome_default' %> +<% self.title = t('doc_auth.headings.welcome') %> + +<% content_for(:pre_flash_content) do %> + <%= render StepIndicatorComponent.new( + steps: Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS, + current_step: :getting_started, + locale_scope: 'idv', + class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', + ) %> <% end %> + + <%= render JavascriptRequiredComponent.new( + header: t('idv.welcome.no_js_header'), + intro: t('idv.welcome.no_js_intro', sp_name: decorated_sp_session.sp_name || APP_NAME), + ) do %> + + <%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.welcome')) %> +

+ <%= t('doc_auth.info.welcome', sp_name: decorated_sp_session.sp_name || APP_NAME) %> +

+ +

<%= t('doc_auth.instructions.welcome') %>

+ + <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> + <%= c.with_item(heading: t('doc_auth.instructions.bullet1')) do %> +

<%= t('doc_auth.instructions.text1') %>

+ <% end %> + <%= c.with_item(heading: t('doc_auth.instructions.bullet2')) do %> +

<%= t('doc_auth.instructions.text2') %>

+ <% end %> + <%= c.with_item(heading: t('doc_auth.instructions.bullet3')) do %> + + <%= new_tab_link_to( + t('idv.troubleshooting.options.learn_more_address_verification_options'), + help_center_redirect_path( + category: 'verify-your-identity', + article: 'phone-number', + flow: :idv, + step: :welcome, + location: 'you_will_need', + ), + ) %> + <% end %> + <% end %> + + <%= simple_form_for :doc_auth, + url: url_for, + method: 'put', + html: { autocomplete: 'off', class: 'margin-y-5 js-consent-continue-form' } do |f| %> + <%= f.submit t('doc_auth.buttons.continue') %> + <% end %> + + <%= render( + 'shared/troubleshooting_options', + heading_tag: :h3, + heading: t('idv.troubleshooting.headings.missing_required_items'), + options: [ + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'accepted-state-issued-identification', + flow: :idv, + step: :welcome, + location: 'missing_items', + ), + text: t('idv.troubleshooting.options.supported_documents'), + new_tab: true, + }, + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'phone-number', + flow: :idv, + step: :welcome, + location: 'missing_items', + ), + text: t('idv.troubleshooting.options.learn_more_address_verification_options'), + new_tab: true, + }, + decorated_sp_session.sp_name && { + url: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'), + text: t('idv.troubleshooting.options.get_help_at_sp', sp_name: decorated_sp_session.sp_name), + new_tab: true, + }, + ].select(&:present?), + ) %> + +

<%= t('doc_auth.instructions.privacy') %>

+

+ <%= t('doc_auth.info.privacy', app_name: APP_NAME) %> +

+

+ <%= new_tab_link_to( + t('doc_auth.instructions.learn_more'), + policy_redirect_url(flow: :idv, step: :welcome, location: :footer), + ) %> +

+ + <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %> +<% end %> + +<%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/app/views/two_factor_authentication/sms_opt_in/error.html.erb b/app/views/two_factor_authentication/sms_opt_in/error.html.erb index 5c11889dc5d..bae936ffafd 100644 --- a/app/views/two_factor_authentication/sms_opt_in/error.html.erb +++ b/app/views/two_factor_authentication/sms_opt_in/error.html.erb @@ -13,22 +13,7 @@

<%= t('two_factor_authentication.opt_in.wait_30d_opt_in') %>

-<%= render( - 'shared/troubleshooting_options', - heading_tag: :h3, - heading: t('components.troubleshooting_options.default_heading'), - options: [ - @other_mfa_options_url && { - url: @other_mfa_options_url, - text: t('two_factor_authentication.login_options_link_text'), - }, - { - url: MarketingSite.contact_url, - text: t('links.contact_support', app_name: APP_NAME), - new_tab: true, - }, - ].select(&:present?), - ) %> +<%= render 'two_factor_authentication/troubleshooting_options', presenter: @presenter %> <%= render PageFooterComponent.new do %> <%= link_to cancel_link_text, @cancel_url %> diff --git a/app/views/two_factor_authentication/sms_opt_in/new.html.erb b/app/views/two_factor_authentication/sms_opt_in/new.html.erb index adf4bcf0f4d..b0336cbb4f8 100644 --- a/app/views/two_factor_authentication/sms_opt_in/new.html.erb +++ b/app/views/two_factor_authentication/sms_opt_in/new.html.erb @@ -20,12 +20,7 @@ class: 'usa-button usa-button--wide usa-button--big', form_class: 'margin-y-5' %> -<% if @other_mfa_options_url %> -

<%= t('two_factor_authentication.opt_in.cant_use_phone') %>

- - <%= link_to t('two_factor_authentication.login_options_link_text'), - @other_mfa_options_url %> -<% end %> +<%= render 'two_factor_authentication/troubleshooting_options', presenter: @presenter %> <%= render PageFooterComponent.new do %> <%= link_to cancel_link_text, @cancel_url %> diff --git a/config/application.yml.default b/config/application.yml.default index ce1b7a51314..66dfd7ea911 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -87,6 +87,7 @@ doc_auth_max_capture_attempts_before_native_camera: 3 doc_auth_max_submission_attempts_before_native_camera: 3 doc_auth_not_ready_section_enabled: false doc_auth_selfie_capture: '{"enabled":false}' +doc_auth_sdk_capture_orientation: '{"horizontal": 100, "vertical": 0}' doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]' doc_capture_request_valid_for_minutes: 15 email_from: no-reply@login.gov @@ -121,7 +122,6 @@ idv_acuant_sdk_version_default: '11.8.2' idv_acuant_sdk_version_alternate: '11.8.1' idv_acuant_sdk_upgrade_a_b_testing_enabled: false idv_acuant_sdk_upgrade_a_b_testing_percent: 50 -idv_getting_started_a_b_testing: '{"welcome_default":100, "welcome_new":0, "getting_started":0}' idv_phone_question_a_b_testing: '{"bypass_phone_question":100, "show_phone_question":0}' idv_send_link_attempt_window_in_minutes: 10 idv_send_link_max_attempts: 5 @@ -383,12 +383,12 @@ development: database_worker_jobs_host: '' database_worker_jobs_password: '' doc_auth_exit_question_section_enabled: true - doc_auth_selfie_capture: '{"enabled":false}' + doc_auth_not_ready_section_enabled: true + doc_auth_selfie_capture: '{"enabled": false}' doc_auth_vendor: 'mock' doc_auth_vendor_randomize: false doc_auth_vendor_randomize_percent: 0 doc_auth_vendor_randomize_alternate_vendor: '' - doc_auth_not_ready_section_enabled: true domain_name: localhost:3000 enable_rate_limiting: false hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c @@ -523,11 +523,11 @@ test: database_worker_jobs_host: '' database_worker_jobs_password: '' doc_auth_max_attempts: 4 + doc_auth_selfie_capture: '{"enabled":false}' doc_auth_vendor: 'mock' doc_auth_vendor_randomize: false doc_auth_vendor_randomize_percent: 0 doc_auth_vendor_randomize_alternate_vendor: '' - doc_auth_selfie_capture: '{"enabled":false}' doc_capture_polling_enabled: false domain_name: www.example.com hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c diff --git a/config/brakeman.ignore b/config/brakeman.ignore index a7bf5ee9ed6..07cc533c85e 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -3,13 +3,13 @@ { "warning_type": "Dynamic Render Path", "warning_code": 15, - "fingerprint": "0300e0665f4940ef0db57c7d483c5517e8b979314a36a15f699f192278339727", + "fingerprint": "406a2c5ea3d852268958d2db11c146841491b6e87cee9c583dd35f5f41898fb7", "check_name": "Render", "message": "Render path contains parameter value", "file": "app/views/idv/cancellations/new.html.erb", - "line": 41, + "line": 43, "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", - "code": "render(action => ButtonComponent.new(:action => (lambda do\n button_to(idv_cancel_path(:step => params[:step]), { **tag_options }, &block)\n end), :method => :put, :big => true, :wide => true, :outline => true).with_content(t(\"idv.cancel.actions.keep_going\")), {})", + "code": "render(action => ButtonComponent.new(:action => (lambda do\n button_to(idv_cancel_path(:step => params[:step]), { **tag_options }, &block)\n end), :method => :put, :big => true, :wide => true, :outline => true, :form => ({ :\"aria-label\" => t(\"idv.cancel.actions.keep_going\") })).with_content(t(\"idv.cancel.actions.keep_going\")), {})", "render_path": [ { "type": "controller", @@ -37,13 +37,13 @@ { "warning_type": "Dynamic Render Path", "warning_code": 15, - "fingerprint": "5bb8762cb8e92a80dabc5dbbe689746d93d00cb3caf4208917bb2f971307710b", + "fingerprint": "8b51f403181f74421f5681ada1096371e1f55fb03d0127db01b5e5da7dda3c51", "check_name": "Render", "message": "Render path contains parameter value", "file": "app/views/idv/cancellations/new.html.erb", "line": 32, "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", - "code": "render(action => ButtonComponent.new(:action => (lambda do\n button_to(idv_session_path(:step => params[:step]), { **tag_options }, &block)\n end), :method => :delete, :big => true, :wide => true).with_content(t(\"idv.cancel.actions.start_over\")), {})", + "code": "render(action => ButtonComponent.new(:action => (lambda do\n button_to(idv_session_path(:step => params[:step]), { **tag_options }, &block)\n end), :method => :delete, :big => true, :wide => true, :form => ({ :\"aria-label\" => t(\"idv.cancel.actions.start_over\") })).with_content(t(\"idv.cancel.actions.start_over\")), {})", "render_path": [ { "type": "controller", @@ -71,13 +71,13 @@ { "warning_type": "Dynamic Render Path", "warning_code": 15, - "fingerprint": "ffc1a9fa8c18bd803bf353cbaebf0c8ca890b71574cedc4efded0dda941a4719", + "fingerprint": "f7d01c6318e6ce369f9fe9bf59b6a3a323034b4b826e2a52a9a87b581d468598", "check_name": "Render", "message": "Render path contains parameter value", "file": "app/views/idv/cancellations/new.html.erb", - "line": 62, + "line": 65, "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", - "code": "render(action => SpinnerButtonComponent.new(:action => (lambda do\n button_to(idv_cancel_path(:step => params[:step], :location => \"cancel\"), { **tag_options }, &block)\n end), :method => :delete, :big => true, :wide => true, :outline => true, :form => ({ :data => ({ :form_steps_wait => \"\" }) })).with_content(CancellationsPresenter.new(:sp_name => decorated_sp_session.sp_name, :url_options => url_options).exit_action_text), {})", + "code": "render(action => SpinnerButtonComponent.new(:action => (lambda do\n button_to(idv_cancel_path(:step => params[:step], :location => \"cancel\"), { **tag_options }, &block)\n end), :method => :delete, :big => true, :wide => true, :outline => true, :form => ({ :\"aria-label\" => CancellationsPresenter.new(:sp_name => decorated_sp_session.sp_name, :url_options => url_options).exit_action_text, :data => ({ :form_steps_wait => \"\" }) })).with_content(CancellationsPresenter.new(:sp_name => decorated_sp_session.sp_name, :url_options => url_options).exit_action_text), {})", "render_path": [ { "type": "controller", @@ -103,6 +103,6 @@ "note": "" } ], - "updated": "2023-09-14 11:53:14 -0400", + "updated": "2023-11-02 09:34:28 -0400", "brakeman_version": "6.0.1" } diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 8ea40e45c0e..4c9d76dddc5 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -19,11 +19,6 @@ module AbTests }, ) - IDV_GETTING_STARTED = AbTestBucket.new( - experiment_name: 'Idv: Getting Started Experience', - buckets: IdentityConfig.store.idv_getting_started_a_b_testing, - ) - IDV_PHONE_QUESTION = AbTestBucket.new( experiment_name: 'Idv: Phone Question', buckets: IdentityConfig.store.idv_phone_question_a_b_testing, diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index 34d3f9e1c26..3ffaa4567c3 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -141,23 +141,6 @@ en: choose_file_html: Drag file here or choose from folder doc_success: We verified your information selected_file: Selected file - getting_started: - instructions: - bullet1: Add photos of your ID - bullet2: Enter your Social Security number - bullet3: Match to your phone number - bullet4: Re-enter your %{app_name} password - consent: By checking this box, you are letting %{app_name} ask for, use, keep, - and share your personal information. We will use it to verify your - identity. - getting_started: 'You’ll need to:' - learn_more: Learn more about our privacy and security measures - text1: Use your driver’s license or state ID card. Other forms of ID are not - accepted. - text2: You will not need your physical SSN card. - text3: Your phone number matches you to your personal information. After you - match, we’ll send you a code. - text4: Your password saves and encrypts your personal information. headings: address: Update your mailing address back: Back of your driver’s license or state ID @@ -169,6 +152,7 @@ en: document_capture: Add photos of your ID document_capture_back: Back of your ID document_capture_front: Front of your ID + document_capture_selfie: Selfie front: Front of your driver’s license or state ID getting_started: Let’s verify your identity for %{sp_name} how_to_verify: Choose how you want to verify your identity @@ -179,6 +163,7 @@ en: phone_question: Do you have a phone you can use to take photos of your ID? review_issues: Check your images and try again secure_account: Secure your account + selfie: Photo ssn: Enter your Social Security number ssn_update: Update your Social Security number switch_to_phone: Switch to your phone @@ -213,9 +198,6 @@ en: exit: with_sp: Exit %{app_name} and return to %{sp_name} without_sp: Exit identity verification and go to your account page - getting_started_html: '%{sp_name} needs to make sure you are you — not someone - pretending to be you. %{link_html}' - getting_started_learn_more: Learn more about what you need to verify your identity how_to_verify: You have the option to verify your identity online, or in person at a participating Post Office how_to_verify_troubleshooting_options_header: Want to learn more about how to verify your identity? diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index 387b0182421..c7081ed7614 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -171,23 +171,6 @@ es: carpeta doc_success: Verificamos sus datos selected_file: Archivo seleccionado - getting_started: - instructions: - bullet1: Incluir fotos de su identificación - bullet2: Introducir su número de Seguro Social - bullet3: Vincular su número de teléfono - bullet4: Volver a introducir su contraseña de %{app_name} - consent: Al marcar esta casilla, usted permite que %{app_name} solicite, - utilice, conserve y comparta su información personal. Los utilizamos - para verificar su identidad. - getting_started: 'Deberá:' - learn_more: Obtenga más información sobre nuestras medidas de privacidad y - seguridad - text1: Su documento de identidad no puede estar caducado. - text2: No necesitará la tarjeta con usted. - text3: Su número de teléfono se asocia a su información personal. Después de que - lo haya asociado, le enviaremos un código. - text4: Su contraseña guarda y encripta su información personal. headings: address: Actualice su dirección postal back: Reverso de su licencia de conducir o identificación estatal @@ -199,6 +182,7 @@ es: document_capture: Incluir fotos de su identificación document_capture_back: Parte trasera de su documento de identidad document_capture_front: Parte delantera de su documento de identidad + document_capture_selfie: Selfi front: Anverso de su licencia de conducir o identificación estatal getting_started: Vamos a verificar su identidad para %{sp_name} how_to_verify: Elija cómo quiere verificar su identidad @@ -210,6 +194,7 @@ es: de identidad? review_issues: Revise sus imágenes e inténtelo de nuevo secure_account: Asegure su cuenta + selfie: Foto ssn: Ingrese su número de Seguro Social ssn_update: Actualice su número de Seguro Social switch_to_phone: Cambiar al teléfono @@ -247,9 +232,6 @@ es: exit: with_sp: Salir de %{app_name} y volver a %{sp_name} without_sp: Salir de la verificación de identidad e ir a la página de su cuenta - getting_started_html: '%{sp_name} necesita asegurarse de que es usted realmente - y no alguien que se hace pasar por usted. %{link_html}' - getting_started_learn_more: Obtenga más información sobre lo que necesita para verificar su identidad how_to_verify: Tiene la opción de verificar su identidad en línea o en persona en una oficina de correos participante. how_to_verify_troubleshooting_options_header: ¿Quiere saber más sobre cómo verificar su identidad? diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index f7d38d07187..3fe949eb30e 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -179,23 +179,6 @@ fr: dossier doc_success: Nous avons vérifié vos informations selected_file: Fichier sélectionné - getting_started: - instructions: - bullet1: Ajoutez des photos de votre pièce d’identité - bullet2: Saisissez votre numéro de sécurité sociale - bullet3: Faire correspondre à votre numéro de téléphone - bullet4: Saisissez à nouveau votre mot de passe %{app_name} - consent: En cochant cette case, vous autorisez %{app_name} à demander, utiliser, - conserver et partager vos renseignements personnels. Nous les - utilisons pour vérifier votre identité. - getting_started: 'Vous aurez besoin de :' - learn_more: En savoir plus sur nos mesures de confidentialité et de sécurité - text1: Utilisez votre permis de conduire ou votre carte d’identité de l’État. - Les autres pièces d’identité ne sont pas acceptées. - text2: Vous n’aurez pas besoin de votre carte SSN physique. - text3: Votre numéro de téléphone correspond à vos informations personnelles. Une - fois la correspondance établie, nous vous enverrons un code. - text4: Votre mot de passe sauvegarde et crypte vos informations personnelles. headings: address: Mettre à jour votre adresse postale back: Verso de votre permis de conduire ou de votre carte d’identité de l’État @@ -207,6 +190,7 @@ fr: document_capture: Ajoutez des photos de votre pièce d’identité document_capture_back: Verso de votre carte d’identité document_capture_front: Recto de votre carte d’identité + document_capture_selfie: Égoportrait front: Recto de votre permis de conduire ou de votre carte d’identité de l’État getting_started: Vérifions votre identité pour %{sp_name} how_to_verify: Choisissez la manière dont vous souhaitez confirmer votre identité @@ -218,6 +202,7 @@ fr: photos de votre identifiant? review_issues: Vérifiez vos images et essayez à nouveau secure_account: Sécuriser votre compte + selfie: Photo ssn: Saisissez votre numéro de sécurité sociale ssn_update: Mettre à jour votre numéro de Sécurité Sociale switch_to_phone: Basculez vers votre téléphone @@ -254,9 +239,6 @@ fr: exit: with_sp: Quittez %{app_name} et retournez à %{sp_name} without_sp: Quittez la vérification d’identité et accédez à la page de votre compte - getting_started_html: '%{sp_name} doit s’assurer que c’est bien vous — et non - quelqu’un qui se fait passer pour vous. %{link_html}' - getting_started_learn_more: En savoir plus sur ce dont vous avez besoin pour vérifier votre identité how_to_verify: Vous avez la possibilité de confirmer votre identité en ligne ou en personne dans un bureau de poste participant. how_to_verify_troubleshooting_options_header: Vous voulez en savoir plus sur la façon de vérifier votre identité? diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 76d5eb50f56..8f43f13463e 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -172,10 +172,6 @@ en: ssn_label: Social Security number state: State zipcode: ZIP Code - getting_started: - no_js_header: You must enable JavaScript to verify your identity. - no_js_intro: '%{sp_name} needs you to verify your identity. You need to enable - JavaScript to continue this process.' gpo: alert_info: 'We sent a letter with your verification code to:' alert_rate_limit_warning_html: You can’t request more letters right now. Your diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index cafe35c1fa6..1e59b23e67c 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -181,10 +181,6 @@ es: ssn_label: Número de Seguro Social state: Estado zipcode: Código postal - getting_started: - no_js_header: Debe habilitar JavaScript para verificar su identidad. - no_js_intro: '%{sp_name} requiere que usted verifique su identidad. Debe - habilitar JavaScript para continuar con este proceso.' gpo: alert_info: 'Enviamos una carta con su código de verificación a:' alert_rate_limit_warning_html: No puede solicitar más cartas ahora mismo. Su diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index 07f5e764fde..b090d64f9b7 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -186,10 +186,6 @@ fr: ssn_label: Numéro de sécurité sociale state: État zipcode: Code postal - getting_started: - no_js_header: Vous devez activer JavaScript pour vérifier votre identité. - no_js_intro: '%{sp_name} a besoin de vous pour vérifier votre identité. Vous - devez activer JavaScript pour poursuivre ce processus.' gpo: alert_info: 'Nous avons envoyé une lettre avec votre code de vérification à:' alert_rate_limit_warning_html: Vous ne pouvez pas demander d’autres lettres pour diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml index a0044835ed7..3865e543db5 100644 --- a/config/locales/in_person_proofing/en.yml +++ b/config/locales/in_person_proofing/en.yml @@ -19,9 +19,8 @@ en: return_to_partner_link: sign out and return to %{sp_name} what_to_expect: What to expect at the Post Office cta: - button: Try at a Post Office - prompt_detail: You’ll start this process by entering your information online, - and continue to verify your identity in person at a participating Post + button: Try in person + prompt_detail: You may be able to verify your identity at a participating Post Office near you. expect: heading: What to expect after your visit @@ -64,24 +63,16 @@ en: retail_hours_weekday: 'Monday to Friday:' selection: 'This is the location you selected:' prepare: - additional_information: You’ll receive an email with the results within 24 hours - of your visit to the Post Office. If your identity verification was - successful, you’ll be able to sign in and share your verified - information with your agency. privacy_disclaimer: '%{app_name} is a secure, government website. We and the U.S. Postal Service use your data to verify your identity.' privacy_disclaimer_link: Learn more about privacy and security. privacy_disclaimer_questions: Questions? - verify_step_about: 'Complete the following steps to verify your identity at a - Post Office:' - verify_step_enter_phone: Enter your primary phone number or the number that you - use most often. If we’re able to verify your information, you’ll - receive a barcode. + verify_step_about: 'Complete the steps below to generate the barcode you’ll take + with you to the Post Office:' + verify_step_enter_phone: Enter your primary phone number or the number that you use most often. verify_step_enter_pii: Enter your name, date of birth, state‑issued ID number, address and Social Security number. verify_step_post_office: Find a participating Post Office near you. - verify_step_visit_post_office: Bring the barcode and your state-issued ID to a - participating Post Office. state_id: alert_message: 'Your state‑issued ID must not be expired. Accepted forms of ID are:' id_types: @@ -143,11 +134,11 @@ en: address: Enter your current residential address barcode: Show this barcode and your state‑issued ID at a Post Office to finish verifying your identity - cta: Try verifying your identity at a Post Office + cta: Try verifying your ID in person id_address: Address on your ID po_search: location: Find a participating Post Office - prepare: Verify your identity at a Post Office + prepare: Verify your identity in person state_id_milestone_2: Enter the information on your state‑issued ID switch_back: Switch back to your computer to prepare to verify your identity in person update_address: Update your current address diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml index fbd23201e29..6b10c4b730a 100644 --- a/config/locales/in_person_proofing/es.yml +++ b/config/locales/in_person_proofing/es.yml @@ -21,10 +21,9 @@ es: return_to_partner_link: cerrar sesión y regresar a %{sp_name} what_to_expect: Qué esperar en la oficina de correos cta: - button: Inténtelo en una oficina de correos - prompt_detail: Comenzará este proceso ingresando su información en línea, y - continuará verificando tu identidad en persona en una oficina de - correos participante cercana. + button: Inténtelo en persona + prompt_detail: Es posible que pueda verificar su identidad en una oficina postal + participante cercana. expect: heading: Qué esperar después de la visita info: Le enviaremos un correo electrónico para informarle si su verificación de @@ -69,10 +68,6 @@ es: retail_hours_weekday: 'De Lunes a Viernes:' selection: 'Esta es la oficina que seleccionó:' prepare: - additional_information: Recibirá un correo electrónico con los resultados dentro - de las 24 horas posteriores a su visita a la oficina de correos. Si su - verificación de identidad fue exitosa, podrá iniciar sesión y - compartir su información verificada con su agencia. privacy_disclaimer: '%{app_name} es un sitio web seguro del gobierno. Nosotros y el Servicio Postal de los Estados Unidos utilizamos sus datos para verificar su identidad.' @@ -81,14 +76,11 @@ es: verify_step_about: 'Complete los siguientes pasos para generar el código de barras que llevará a la oficina de correos:' verify_step_enter_phone: Introduzca su número de teléfono principal o el que use - con más frecuencia. Si podemos verificar su información, recibirá un - código de barras. + con más frecuencia. verify_step_enter_pii: Ingrese su nombre, fecha de nacimiento, número de identificación emitido por el estado, dirección y número de la Seguridad Social. verify_step_post_office: Encuentre una oficina de correos participantes cercana a usted. - verify_step_visit_post_office: Lleve el código de barras y su identificación - emitida por el estado a una oficina de correos participante. state_id: alert_message: 'Su identificación emitida por el estado no debe estar vencida. Se aceptan las siguientes formas de identificación:' @@ -155,11 +147,11 @@ es: address: Ingresa tu domicilio actual barcode: Muestre este código de barras y su documento nacional de identidad en una oficina de correos para terminar de verificar su identidad - cta: Intente verificar su identidad en una oficina de correos + cta: Intente verificar su ID en persona id_address: Domicilio que consta en su identificación po_search: location: Encuentre una oficina de correos participante - prepare: Verifique su identidad en una oficina de correos + prepare: Verifique su identidad en persona state_id_milestone_2: Ingrese la información de su identificación emitida por el estado switch_back: Vuelva a su computadora para prepararse para verificar su identidad en persona diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml index d73e40a2ef3..6b556c5b253 100644 --- a/config/locales/in_person_proofing/fr.yml +++ b/config/locales/in_person_proofing/fr.yml @@ -22,10 +22,9 @@ fr: return_to_partner_link: maintenant vous déconnecter et retourner à %{sp_name} what_to_expect: À quoi s’attendre au bureau de poste cta: - button: Essayez dans un bureau de poste - prompt_detail: Vous commencerez ce processus en saisissant vos informations en - ligne et continuerez à vérifier votre identité en personne dans un - bureau de poste participant près de chez vous. + button: Essayer en personne + prompt_detail: Vous pourrez peut-être vérifier votre identité dans un bureau de + poste participant près de chez vous. expect: heading: Que faire après votre visite info: Dans les 24 heures suivant votre visite au bureau de poste, nous vous @@ -69,26 +68,19 @@ fr: retail_hours_weekday: 'Lundi à Vendredi:' selection: 'Il s’agit du bureau que vous avez sélectionné:' prepare: - additional_information: Vous recevrez un e-mail avec les résultats dans les 24 - heures suivant votre visite au bureau de poste. Si votre vérification - d’identité est réussie, vous pourrez vous connecter et partager vos - informations vérifiées avec votre agence. privacy_disclaimer: '%{app_name} est un site gouvernemental sécurisé. Nous et le service postal américain utilisons vos données pour vérifier votre identité.' privacy_disclaimer_link: En savoir plus sur la confidentialité et la sécurité. privacy_disclaimer_questions: Vous avez des questions? - verify_step_about: 'Suivez les étapes suivantes pour vérifier votre identité - dans un bureau de poste:' + verify_step_about: 'Suivez les étapes ci-dessous pour générer le code-barres que + vous emporterez avec vous au bureau de poste:' verify_step_enter_phone: Entrez votre numéro de téléphone principal ou celui que - vous utilisez le plus souvent. Si nous sommes en mesure de vérifier - vos informations, vous recevrez un code-barres. - verify_step_enter_pii: Entrez votre nom, votre date de naissance, votre numéro - d’identification délivré par l’État, votre adresse et votre numéro de - sécurité sociale. + vous utilisez le plus souvent. + verify_step_enter_pii: Saisissez votre nom, votre date de naissance, votre + document d’identité délivré par l’État, votre adresse et votre numéro + de sécurité sociale. verify_step_post_office: Trouver un bureau de poste participant. - verify_step_visit_post_office: Apportez le code-barres et votre pièce d’identité - délivrée par l’État dans un bureau de poste participant. state_id: alert_message: 'Votre carte d’identité délivrée par l’État ne doit pas être périmée. Les pièces d’identité acceptées sont:' @@ -155,11 +147,11 @@ fr: address: Indiquez votre adresse résidentielle actuelle barcode: Présentez ce code-barres et votre pièce d’identité délivrée par l’État à un bureau de poste pour terminer la vérification de votre identité - cta: Essayez de vérifier votre identité dans un bureau de poste + cta: Essayez de vérifier votre identité en personne id_address: Adresse sur votre pièce d’identité po_search: location: Trouver un bureau de poste participant - prepare: Vérifiez votre identité dans un bureau de poste + prepare: Vérifiez votre identité en personne state_id_milestone_2: Saisissez les informations figurant sur votre carte d’identité délivrée par l’État switch_back: Retournez sur votre ordinateur pour vous préparer à vérifier votre diff --git a/config/locales/links/en.yml b/config/locales/links/en.yml index d0d19d56cfe..6ecd3b65c3c 100644 --- a/config/locales/links/en.yml +++ b/config/locales/links/en.yml @@ -9,7 +9,6 @@ en: cancel: Cancel cancel_account_creation: '‹ Cancel account creation' contact: Contact - contact_support: Contact %{app_name} Support continue_sign_in: Continue sign in create_account: Create an account exit_login: Exit %{app_name} diff --git a/config/locales/links/es.yml b/config/locales/links/es.yml index abfa3d0af66..320f7eda6ee 100644 --- a/config/locales/links/es.yml +++ b/config/locales/links/es.yml @@ -9,7 +9,6 @@ es: cancel: Cancelar cancel_account_creation: '‹ Cancelar la creación de cuenta' contact: Contactar - contact_support: Póngase en contacto con el soporte técnico de %{app_name} continue_sign_in: Continuar el inicio de sesión create_account: Crear cuenta exit_login: Salga de %{app_name} diff --git a/config/locales/links/fr.yml b/config/locales/links/fr.yml index 4311519a413..6ac0150fa43 100644 --- a/config/locales/links/fr.yml +++ b/config/locales/links/fr.yml @@ -9,7 +9,6 @@ fr: cancel: Annuler cancel_account_creation: '‹ Annuler la création du compte' contact: Contact - contact_support: Contactez %{app_name} le support continue_sign_in: Continuer la connexion create_account: Créer un compte exit_login: Quitter %{app_name} diff --git a/config/locales/two_factor_authentication/en.yml b/config/locales/two_factor_authentication/en.yml index bac52964f9d..d7cafce7f82 100644 --- a/config/locales/two_factor_authentication/en.yml +++ b/config/locales/two_factor_authentication/en.yml @@ -46,6 +46,9 @@ en: authentication method. learn_more: Learn more about authentication options login_intro: You set these up when you created your account. + login_intro_reauthentication: Before you can make changes to your account, we + need to make sure it’s really you by using one of your authentication + methods. login_options: auth_app: Authentication app auth_app_info: Use your authentication application to get a security code. @@ -66,6 +69,7 @@ en: webauthn_platform_info: Use your face or fingerprint to access your account without a one-time code. login_options_link_text: Choose another authentication method + login_options_reauthentication_title: Reauthentication required login_options_title: Select your authentication method max_backup_code_login_attempts_reached: For your security, your account is temporarily locked because you have entered the backup code incorrectly @@ -85,7 +89,6 @@ en: mobile_terms_of_service: Mobile terms of service no_auth_option: No authentication option could be found for you to sign in. opt_in: - cant_use_phone: Can’t use your phone? error_retry: Sorry, we are having trouble opting you in. Please try again. opted_out_html: You’ve opted out of receiving text messages at %{phone_number_html}. You can opt in and receive a security code again @@ -178,7 +181,14 @@ en: additional_methods_link: choose another authentication method connect_html: We were unable to connect the security key. Please try again or %{link_html}. + screen_lock_no_other_mfa: We couldn’t authenticate with face or touch unlock. + Try signing in on the device where you first set up face or touch + unlock. + screen_lock_other_mfa_html: We couldn’t authenticate with face or touch unlock. + %{link_html}, or try signing in on the device where you first set up + face or touch unlock. try_again: Face or touch unlock was unsuccessful. Please try again or %{link}. + use_a_different_method: Use a different authentication method webauthn_header_text: Connect your security key webauthn_platform_header_text: Use face or touch unlock webauthn_platform_use_key: Use face or touch unlock diff --git a/config/locales/two_factor_authentication/es.yml b/config/locales/two_factor_authentication/es.yml index c2a30013de9..6180bd85584 100644 --- a/config/locales/two_factor_authentication/es.yml +++ b/config/locales/two_factor_authentication/es.yml @@ -47,6 +47,9 @@ es: otro método de autenticación. learn_more: Más información sobre las opciones de autenticación. login_intro: Usted configuró esto cuando creó su cuenta. + login_intro_reauthentication: Antes de que pueda realizar cambios en su cuenta, + debemos confirmar su identidad mediante uno de sus métodos de + autenticación. login_options: auth_app: Aplicación de autenticación auth_app_info: Use su aplicación de autenticación para obtener el código de seguridad. @@ -71,6 +74,7 @@ es: webauthn_platform_info: Use la cara o la huella digital para acceder a su cuenta sin un código de un solo uso. login_options_link_text: Elige otra opción de seguridad + login_options_reauthentication_title: Se requiere reautenticación login_options_title: Seleccione su opción de seguridad max_backup_code_login_attempts_reached: Para su seguridad, su cuenta está bloqueada temporalmente porque ha ingresado el código de respaldo @@ -90,7 +94,6 @@ es: mobile_terms_of_service: Condiciones de servicio móvil no_auth_option: No se pudo encontrar ninguna opción de autenticación para iniciar sesión opt_in: - cant_use_phone: '¿No puede utilizar su teléfono?' error_retry: Lo sentimos, estamos teniendo problemas para aceptarlo. Por favor, inténtelo de nuevo. opted_out_html: Ha optado por no recibir mensajes de texto en el @@ -189,8 +192,16 @@ es: additional_methods_link: elija otro método de autenticación connect_html: No hemos podido conectar la clave de seguridad. Por favor, inténtelo de nuevo o %{link_html}. + screen_lock_no_other_mfa: No pudimos comprobar la autenticidad mediante + desbloqueo facial o táctil. Intente iniciar sesión en el dispositivo + donde configuró por primera vez el desbloqueo facial o táctil. + screen_lock_other_mfa_html: No pudimos comprobar la autenticidad mediante + desbloqueo facial o táctil. %{link_html} o intente iniciar sesión en el + dispositivo donde configuró por primera vez el desbloqueo facial o + táctil. try_again: El desbloqueo facial o táctil no fue exitoso. Por favor, inténtelo de nuevo o %{link}. + use_a_different_method: Utilice otro método de autenticación webauthn_header_text: Conecte su llave de seguridad webauthn_platform_header_text: Usar desbloqueo facial o táctil webauthn_platform_use_key: Usar desbloqueo facial o táctil diff --git a/config/locales/two_factor_authentication/fr.yml b/config/locales/two_factor_authentication/fr.yml index 52dee824d9f..d229f590c02 100644 --- a/config/locales/two_factor_authentication/fr.yml +++ b/config/locales/two_factor_authentication/fr.yml @@ -51,6 +51,9 @@ fr: choisissez une autre méthode d’authentification. learn_more: En savoir plus sur les options d’authentification login_intro: Vous les avez configurés lorsque vous avez crée votre compte. + login_intro_reauthentication: Avant que vous puissiez apporter des modifications + à votre compte, nous devons nous assurer qu’il s’agit bien de vous en + utilisant l’une de vos méthodes d’authentification. login_options: auth_app: Application d’authentification auth_app_info: Utilisez votre application d’authentification pour obtenir votre @@ -75,6 +78,7 @@ fr: webauthn_platform_info: Utilisez votre visage ou votre empreinte digitale pour accéder à votre compte sans code à usage unique. login_options_link_text: Choisissez une autre option de sécurité + login_options_reauthentication_title: Réauthentification requise login_options_title: Sélectionnez votre option de sécurité max_backup_code_login_attempts_reached: Pour votre sécurité, votre compte est temporairement verrouillé car vous avez saisi trop de fois le code de @@ -95,7 +99,6 @@ fr: mobile_terms_of_service: Conditions de service mobile no_auth_option: Aucune option d’authentification n’a été trouvée pour vous connecter opt_in: - cant_use_phone: Vous ne pouvez pas utiliser votre téléphone? error_retry: Désolé, nous avons des difficultés à vous connecter. Veuillez réessayer. opted_out_html: Vous avez choisi de ne plus recevoir de SMS à %{phone_number_html}. Vous pouvez vous inscrire et recevoir à nouveau un @@ -198,8 +201,17 @@ fr: additional_methods_link: choisir une autre méthode d’authentification connect_html: Nous n’avons pas pu connecter la clé de sécurité. Veuillez réessayer ou %{link_html}. + screen_lock_no_other_mfa: Nous n’avons pas pu nous authentifier avec le + déverrouillage facial ou tactile. Essayez de vous connecter sur + l’appareil sur lequel vous avez configuré le déverrouillage facial ou + tactile. + screen_lock_other_mfa_html: Nous n’avons pas pu nous authentifier avec le + déverrouillage facial ou tactile. %{link_html} ou essayez de vous + connecter sur l’appareil sur lequel vous avez configuré le + déverrouillage du visage ou du toucher. try_again: Le déverrouillage facial ou tactile n’a pas fonctionné. Veuillez réessayer ou %{link}. + use_a_different_method: Utilisez un autre moyen d’authentification webauthn_header_text: Connectez votre clé de sécurité webauthn_platform_header_text: Utilisez le déverrouillage facial ou tactile webauthn_platform_use_key: Utilisez le déverrouillage facial ou tactile diff --git a/config/routes.rb b/config/routes.rb index 2bc53a21d34..03558422efd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -307,11 +307,6 @@ get '/activated' => 'idv#activated' end scope '/verify', module: 'idv', as: 'idv' do - if !FeatureManagement.idv_available? - # IdV has been disabled. - match '/*path' => 'unavailable#show', via: %i[get post] - end - get '/mail_only_warning' => 'mail_only_warning#show' get '/personal_key' => 'personal_key#show' post '/personal_key' => 'personal_key#update' @@ -326,8 +321,6 @@ # This route is included in SMS messages sent to users who start the IdV hybrid flow. It # should be kept short, and should not include underscores ("_"). get '/documents' => 'hybrid_mobile/entry#show', as: :hybrid_mobile_entry - get '/getting_started' => 'getting_started#show' - put '/getting_started' => 'getting_started#update' get '/hybrid_mobile/document_capture' => 'hybrid_mobile/document_capture#show' put '/hybrid_mobile/document_capture' => 'hybrid_mobile/document_capture#update' get '/hybrid_mobile/capture_complete' => 'hybrid_mobile/capture_complete#show' diff --git a/db/seeds.rb b/db/seeds.rb index 3d2b700d8f7..1889793d403 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,5 +1,13 @@ # add config/service_providers.yml -ServiceProviderSeeder.new.run +if ENV['KUBERNETES_REVIEW_APP'] == 'true' && ENV['DASHBOARD_URL'].present? + dashboard_url = ENV['DASHBOARD_URL'] + + service_provider_seeder = ServiceProviderSeeder.new + service_provider_seeder.write_review_app_yaml(dashboard_url: dashboard_url) + service_provider_seeder.run +else + ServiceProviderSeeder.new.run +end # add config/agencies.yml AgencySeeder.new.run diff --git a/dockerfiles/idp_review_app.Dockerfile b/dockerfiles/idp_review_app.Dockerfile index 86a2b58ce75..4c7f5e87491 100644 --- a/dockerfiles/idp_review_app.Dockerfile +++ b/dockerfiles/idp_review_app.Dockerfile @@ -155,7 +155,7 @@ COPY --chown=app:app config/integrations.localdev.yml $RAILS_ROOT/config/integra COPY --chown=app:app config/partner_account_statuses.localdev.yml $RAILS_ROOT/config/partner_account_statuses.yml COPY --chown=app:app config/partner_accounts.localdev.yml $RAILS_ROOT/config/partner_accounts.yml COPY --chown=app:app certs.example $RAILS_ROOT/certs -RUN ./scripts/review_app_service_providers.rb > $RAILS_ROOT/config/service_providers.yml +COPY --chown=app:app config/service_providers.localdev.yml $RAILS_ROOT/config/service_providers.yaml # Expose the port the app runs on EXPOSE 3000 diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 85854adba04..0c3f8a7a5a4 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -186,6 +186,7 @@ def self.build_store(config_map) config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer) config.add(:doc_auth_s3_request_timeout, type: :integer) config.add(:doc_auth_selfie_capture, type: :json, options: { symbolize_names: true }) + config.add(:doc_auth_sdk_capture_orientation, type: :json, options: { symbolize_names: true }) config.add(:doc_auth_supported_country_codes, type: :json) config.add(:doc_auth_vendor, type: :string) config.add(:doc_auth_vendor_randomize, type: :boolean) @@ -231,7 +232,6 @@ def self.build_store(config_map) config.add(:idv_attempt_window_in_hours, type: :integer) config.add(:idv_available, type: :boolean) config.add(:idv_contact_phone_number, type: :string) - config.add(:idv_getting_started_a_b_testing, type: :json, options: { symbolize_names: true }) config.add(:idv_phone_question_a_b_testing, type: :json, options: { symbolize_names: true }) config.add(:idv_max_attempts, type: :integer) config.add(:idv_min_age_years, type: :integer) diff --git a/public/acuant/11.9.1/face_landmark_68_tiny_model-weights_manifest.json b/public/acuant/11.9.1/face_landmark_68_tiny_model-weights_manifest.json new file mode 100644 index 00000000000..83de57b3f22 --- /dev/null +++ b/public/acuant/11.9.1/face_landmark_68_tiny_model-weights_manifest.json @@ -0,0 +1,39 @@ +[ + { + "weights": + [ + {"name":"dense0/conv0/filters","shape":[3,3,3,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008194216092427571,"min":-0.9423348506291708}}, + {"name":"dense0/conv0/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006839508168837603,"min":-0.8412595047670252}}, + {"name":"dense0/conv1/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009194007106855804,"min":-1.2779669878529567}}, + {"name":"dense0/conv1/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0036026100317637128,"min":-0.3170296827952067}}, + {"name":"dense0/conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.000740380117706224,"min":-0.06367269012273527}}, + {"name":"dense0/conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":1,"min":0}}, + {"name":"dense0/conv2/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":1,"min":0}}, + {"name":"dense0/conv2/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0037702228508743585,"min":-0.6220867703942692}}, + {"name":"dense1/conv0/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0033707996209462483,"min":-0.421349952618281}}, + {"name":"dense1/conv0/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.014611541991140328,"min":-1.8556658328748217}}, + {"name":"dense1/conv0/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002832523046755323,"min":-0.30307996600281956}}, + {"name":"dense1/conv1/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006593170586754294,"min":-0.6329443763284123}}, + {"name":"dense1/conv1/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.012215249211180444,"min":-1.6001976466646382}}, + {"name":"dense1/conv1/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002384825547536214,"min":-0.3028728445370992}}, + {"name":"dense1/conv2/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005859645441466687,"min":-0.7617539073906693}}, + {"name":"dense1/conv2/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013121426806730382,"min":-1.7845140457153321}}, + {"name":"dense1/conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0032247188044529336,"min":-0.46435950784122243}}, + {"name":"dense2/conv0/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002659512618008782,"min":-0.32977956463308894}}, + {"name":"dense2/conv0/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015499923743453681,"min":-1.9839902391620712}}, + {"name":"dense2/conv0/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0032450980999890497,"min":-0.522460794098237}}, + {"name":"dense2/conv1/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005911862382701799,"min":-0.792189559282041}}, + {"name":"dense2/conv1/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021025861478319356,"min":-2.2077154552235325}}, + {"name":"dense2/conv1/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00349616945958605,"min":-0.46149436866535865}}, + {"name":"dense2/conv2/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008104994250278847,"min":-1.013124281284856}}, + {"name":"dense2/conv2/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.029337059282789044,"min":-3.5791212325002633}}, + {"name":"dense2/conv2/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0038808938334969913,"min":-0.4230174278511721}}, + {"name":"fc/weights","shape":[128,136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.014016061670639936,"min":-1.8921683255363912}}, + {"name":"fc/bias","shape":[136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0029505149698724935,"min":0.088760145008564}} + ], + "paths": + [ + "face_landmark_68_tiny_model.bin" + ] + } +] \ No newline at end of file diff --git a/public/acuant/11.9.1/face_landmark_68_tiny_model.bin b/public/acuant/11.9.1/face_landmark_68_tiny_model.bin new file mode 100644 index 00000000000..f04a9d5ecd2 Binary files /dev/null and b/public/acuant/11.9.1/face_landmark_68_tiny_model.bin differ diff --git a/public/acuant/11.9.1/tiny_face_detector_model-shard1 b/public/acuant/11.9.1/tiny_face_detector_model-shard1 new file mode 100644 index 00000000000..a3f113a5422 Binary files /dev/null and b/public/acuant/11.9.1/tiny_face_detector_model-shard1 differ diff --git a/public/acuant/11.9.1/tiny_face_detector_model-weights_manifest.json b/public/acuant/11.9.1/tiny_face_detector_model-weights_manifest.json new file mode 100644 index 00000000000..131d9a51e6c --- /dev/null +++ b/public/acuant/11.9.1/tiny_face_detector_model-weights_manifest.json @@ -0,0 +1 @@ +[{"weights":[{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}],"paths":["tiny_face_detector_model-shard1"]}] diff --git a/scripts/review_app_service_providers.rb b/scripts/review_app_service_providers.rb deleted file mode 100755 index 4111102f1a5..00000000000 --- a/scripts/review_app_service_providers.rb +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env ruby - -require 'yaml' - -dashboard_url = "https://#{ENV.fetch('CI_ENVIRONMENT_SLUG')}-review-app-dashboard.review-app.identitysandbox.gov" - -hash = { - 'production' => { - 'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:dashboard' => { - 'friendly_name' => 'Dashboard', - 'agency' => 'GSA', - 'agency_id' => 2, - 'logo' => '18f.svg', - 'certs' => ['identity_dashboard_cert'], - 'return_to_sp_url' => dashboard_url, - 'redirect_uris' => [ - "#{dashboard_url}/auth/logindotgov/callback", - dashboard_url, - ], - 'push_notification_url' => "#{dashboard_url}/api/security_events", - }, - }, -} - -puts hash.to_yaml diff --git a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb index 001d92543b9..219325b40e6 100644 --- a/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb +++ b/spec/controllers/concerns/idv/ab_test_analytics_concern_spec.rb @@ -12,15 +12,12 @@ end let(:acuant_sdk_args) { { as_bucket: :as_value } } - let(:getting_started_args) { { gs_bucket: :gs_value } } let(:phone_question_args) { { pq_bucket: :pq_value } } before do allow(subject).to receive(:current_user).and_return(user) expect(subject).to receive(:acuant_sdk_ab_test_analytics_args). and_return(acuant_sdk_args) - expect(subject).to receive(:getting_started_ab_test_analytics_bucket). - and_return(getting_started_args) expect(subject).to receive(:phone_question_ab_test_analytics_bucket). and_return(phone_question_args) end @@ -34,10 +31,6 @@ expect(controller.ab_test_analytics_buckets).to include(acuant_sdk_args) end - it 'includes getting_started_ab_test_analytics_bucket' do - expect(controller.ab_test_analytics_buckets).to include(getting_started_args) - end - it 'includes skip_hybrid_handoff' do idv_session.skip_hybrid_handoff = :shh_value expect(controller.ab_test_analytics_buckets).to include({ skip_hybrid_handoff: :shh_value }) diff --git a/spec/controllers/concerns/idv/getting_started_ab_test_concern_spec.rb b/spec/controllers/concerns/idv/getting_started_ab_test_concern_spec.rb deleted file mode 100644 index fb4eb300f2a..00000000000 --- a/spec/controllers/concerns/idv/getting_started_ab_test_concern_spec.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'rails_helper' - -RSpec.describe Idv::GettingStartedAbTestConcern do - let(:user) { create(:user, :fully_registered, email: 'old_email@example.com') } - - controller(ApplicationController) do - include Idv::GettingStartedAbTestConcern - - before_action :maybe_redirect_for_getting_started_ab_test - - def index - render plain: 'Hello' - end - end - - describe '#getting_started_ab_test_bucket' do - before do - allow(controller).to receive(:current_user).and_return(user) - allow(AbTests::IDV_GETTING_STARTED).to receive(:bucket) do |discriminator| - case discriminator - when user.uuid - :getting_started - else :welcome_default - end - end - end - - it 'returns the bucket based on user id' do - expect(controller.getting_started_ab_test_bucket).to eq(:getting_started) - end - - context 'with a different user' do - before do - user2 = create(:user, :fully_registered, email: 'new_email@example.com') - allow(controller).to receive(:current_user).and_return(user2) - end - it 'returns the bucket based on user id' do - expect(controller.getting_started_ab_test_bucket).to eq(:welcome_default) - end - end - end - - describe '#getting_started_user' do - let(:document_capture_user) { create(:user) } - let(:current_user) { create(:user) } - before do - allow(controller).to receive(:current_user).and_return(current_user) - end - - context 'when document_capture_user is defined (hybrid flow)' do - before do - allow(controller).to receive(:document_capture_user).and_return(document_capture_user) - end - - it 'uses the document_capture_user to choose a bucket' do - expect(controller.getting_started_user).to eq(document_capture_user) - end - end - - context 'when falling back to current_user' do - it 'falls back to current_user when document_capture_user undefined' do - expect(controller.getting_started_user).to eq(current_user) - end - end - end - - context '#maybe_redirect_for_getting_started_ab_test' do - before do - sign_in(user) - end - - context 'A/B test specifies getting started page' do - before do - allow(controller).to receive(:getting_started_ab_test_bucket). - and_return(:getting_started) - end - - it 'redirects to idv_getting_started_url' do - get :index - - expect(response).to redirect_to(idv_getting_started_url) - end - end - - context 'A/B test specifies welcome page' do - before do - allow(controller).to receive(:getting_started_ab_test_bucket). - and_return(:welcome_default) - end - - it 'does not redirect users away from welcome page' do - get :index - - expect(response.body).to eq('Hello') - expect(response.status).to eq(200) - end - end - - context 'A/B test specifies some other value' do - before do - allow(controller).to receive(:getting_started_ab_test_bucket). - and_return(:something_else) - end - - it 'does not redirect users away from welcome page' do - get :index - - expect(response.body).to eq('Hello') - expect(response.status).to eq(200) - end - end - end -end diff --git a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb index 65c472d744e..071532addc3 100644 --- a/spec/controllers/concerns/idv/step_indicator_concern_spec.rb +++ b/spec/controllers/concerns/idv/step_indicator_concern_spec.rb @@ -14,9 +14,7 @@ describe '#step_indicator_steps' do def force_gpo idv_session = instance_double(Idv::Session) - allow(idv_session).to receive(:method_missing). - with(:address_verification_mechanism). - and_return('gpo') + allow(idv_session).to receive(:method_missing).with(:verify_by_mail?).and_return(true) allow(controller).to receive(:idv_session).and_return(idv_session) end diff --git a/spec/controllers/concerns/idv_step_concern_spec.rb b/spec/controllers/concerns/idv_step_concern_spec.rb index 86e7a90ef47..baa096d9d88 100644 --- a/spec/controllers/concerns/idv_step_concern_spec.rb +++ b/spec/controllers/concerns/idv_step_concern_spec.rb @@ -64,9 +64,9 @@ def show idv_session.pii_from_doc = { first_name: 'Susan' } end - it 'redirects to ssn screen' do + it 'allows the back button and stays on page' do get :show - expect(response).to redirect_to(idv_ssn_url) + expect(response).to have_http_status(200) end context 'and redo specified' do @@ -206,9 +206,9 @@ def show end end - describe '#confirm_document_capture_not_complete' do + describe '#confirm_verify_info_step_complete' do controller(idv_step_controller_class) do - before_action :confirm_document_capture_not_complete + before_action :confirm_verify_info_step_complete end before(:each) do @@ -218,10 +218,9 @@ def show end end - context 'the user has not completed document capture' do + context 'the user has completed the verify info step' do it 'does not redirect and renders the view' do - idv_session.pii_from_doc = nil - idv_session.resolution_successful = nil + idv_session.resolution_successful = true get :show @@ -230,91 +229,69 @@ def show end end - context 'the user has completed remote document capture but not verify_info' do - it 'redirects to the ssn step' do - idv_session.pii_from_doc = { first_name: 'Susan' } - idv_session.resolution_successful = false + context 'the user has not completed the verify info step' do + it 'redirects to the remote verify info step' do + idv_session.resolution_successful = nil get :show - expect(response).to redirect_to(idv_ssn_url) + expect(response).to redirect_to(idv_verify_info_url) end end - context 'the user has completed in person document capture but not verify_info' do - it 'redirects to the ssn step' do - subject.user_session['idv/in_person'] = {} - subject.user_session['idv/in_person'][:pii_from_user] = { first_name: 'Susan' } - idv_session.resolution_successful = false - - get :show - - expect(response).to redirect_to(idv_ssn_url) + context 'the user has not completed the verify info step with an in-person enrollment' do + let(:selected_location_details) do + JSON.parse(UspsInPersonProofing::Mock::Fixtures.enrollment_selected_location_details) end - end - context 'the user has completed document capture and verify_info' do - it 'redirects to the ssn step' do - idv_session.pii_from_doc = nil - idv_session.resolution_successful = true + it 'redirects to the in-person verify info step' do + idv_session.resolution_successful = nil + + InPersonEnrollment.find_or_create_by( + user: user, + ).update!( + selected_location_details: selected_location_details, + ) get :show - expect(response).to redirect_to(idv_ssn_url) + expect(response).to redirect_to(idv_in_person_verify_info_url) end end end - describe '#confirm_verify_info_step_complete' do + describe '#confirm_letter_recently_enqueued' do controller(idv_step_controller_class) do - before_action :confirm_verify_info_step_complete + before_action :confirm_letter_recently_enqueued end before(:each) do sign_in(user) + allow(subject).to receive(:current_user).and_return(user) routes.draw do get 'show' => 'anonymous#show' end end - context 'the user has completed the verify info step' do - it 'does not redirect and renders the view' do - idv_session.resolution_successful = true - - get :show - - expect(response.body).to eq('Hello') - expect(response.status).to eq(200) - end - end - - context 'the user has not completed the verify info step' do - it 'redirects to the remote verify info step' do - idv_session.resolution_successful = nil - + context 'letter was not recently enqueued' do + it 'does not redirect' do get :show - expect(response).to redirect_to(idv_verify_info_url) + expect(response.body).to eq 'Hello' + expect(response).to_not redirect_to idv_letter_enqueued_url + expect(response.status).to eq 200 end end - context 'the user has not completed the verify info step with an in-person enrollment' do - let(:selected_location_details) do - JSON.parse(UspsInPersonProofing::Mock::Fixtures.enrollment_selected_location_details) - end - - it 'redirects to the in-person verify info step' do - idv_session.resolution_successful = nil + context 'letter was recently enqueued' do + let(:user) { create(:user, :with_pending_gpo_profile, :fully_registered) } - InPersonEnrollment.find_or_create_by( - user: user, - ).update!( - selected_location_details: selected_location_details, - ) + it 'redirects to letter enqueued page' do + idv_session.address_verification_mechanism = 'gpo' get :show - expect(response).to redirect_to(idv_in_person_verify_info_url) + expect(response).to redirect_to idv_letter_enqueued_url end end end diff --git a/spec/controllers/idv/address_controller_spec.rb b/spec/controllers/idv/address_controller_spec.rb index 5f0e805c728..0b9f0a94e24 100644 --- a/spec/controllers/idv/address_controller_spec.rb +++ b/spec/controllers/idv/address_controller_spec.rb @@ -9,18 +9,35 @@ stub_sign_in(user) stub_analytics stub_idv_steps_before_verify_step(user) + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true subject.idv_session.flow_path = 'standard' subject.idv_session.pii_from_doc = pii_from_doc end - describe '#new' do - before do - get :new + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::AddressController.step_info).to be_valid end + end + describe '#new' do it 'logs an analytics event' do + get :new expect(@analytics).to have_logged_event('IdV: address visited') end + + context 'verify_info already submitted' do + before do + subject.idv_session.resolution_successful = true + end + + it 'redirects to enter_password' do + get :new + + expect(response).to redirect_to(idv_enter_password_url) + end + end end describe '#update' do @@ -63,6 +80,12 @@ ) end + it 'invalidates future steps' do + expect(subject).to receive(:clear_future_steps!) + + put :update, params: params + end + it 'logs an analytics event' do put :update, params: params expect(@analytics).to have_logged_event( diff --git a/spec/controllers/idv/agreement_controller_spec.rb b/spec/controllers/idv/agreement_controller_spec.rb index ff12f6194e9..36a95181335 100644 --- a/spec/controllers/idv/agreement_controller_spec.rb +++ b/spec/controllers/idv/agreement_controller_spec.rb @@ -79,22 +79,25 @@ context 'agreement already visited' do it 'does not redirect to hybrid_handoff' do - allow(subject.idv_session).to receive(:idv_consent_given).and_return(true) + subject.idv_session.idv_consent_given = true get :show expect(response).to render_template('idv/agreement/show') end - end - - context 'and document capture already completed' do - before do - subject.idv_session.pii_from_doc = { first_name: 'Susan' } - end - it 'redirects to ssn step' do - get :show - expect(response).to redirect_to(idv_ssn_url) + context 'and verify info already completed' do + before do + subject.idv_session.flow_path = 'standard' + subject.idv_session.pii_from_doc = { first_name: 'Susan' } + subject.idv_session.ssn = '123-45-6789' + subject.idv_session.resolution_successful = true + end + + it 'redirects to enter password step' do + get :show + expect(response).to redirect_to(idv_enter_password_url) + end end end end @@ -125,7 +128,7 @@ end it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) put :update, params: params end diff --git a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb index f25cfd85661..2e0a7c38980 100644 --- a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb +++ b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb @@ -25,7 +25,7 @@ end it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end diff --git a/spec/controllers/idv/cancellations_controller_spec.rb b/spec/controllers/idv/cancellations_controller_spec.rb index 939a2fc136d..0266bc134f6 100644 --- a/spec/controllers/idv/cancellations_controller_spec.rb +++ b/spec/controllers/idv/cancellations_controller_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Idv::CancellationsController do describe 'before_actions' do it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index bf4e90620ed..56acad2f715 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -47,13 +47,6 @@ :check_for_mail_only_outage, ) end - - it 'checks that hybrid_handoff is complete' do - expect(subject).to have_actions( - :before, - :confirm_hybrid_handoff_complete, - ) - end end describe '#show' do @@ -119,12 +112,18 @@ end end - context 'with pii in idv_session' do - it 'redirects to ssn step' do + context 'verify info step is complete' do + it 'redirects to enter password step' do + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true + subject.idv_session.flow_path = 'standard' subject.idv_session.pii_from_doc = Idp::Constants::MOCK_IDV_APPLICANT + subject.idv_session.ssn = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] + subject.idv_session.resolution_successful = true + get :show - expect(response).to redirect_to(idv_ssn_url) + expect(response).to redirect_to(idv_enter_password_url) end end @@ -166,7 +165,7 @@ let(:result) { { success: true, errors: {} } } it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) put :update end diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index 26b9dd51a80..25687b3fca7 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -31,7 +31,9 @@ before do stub_analytics - allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + stub_sign_in(user) + stub_attempts_tracker + allow(@irs_attempts_api_tracker).to receive(:track_event) allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end @@ -46,44 +48,11 @@ end it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) - end - end - - describe '#confirm_idv_steps_complete' do - controller do - before_action :confirm_idv_steps_complete - - def show - render plain: 'Hello' - end - end - - before(:each) do - stub_sign_in(user) - routes.draw do - get 'show' => 'idv/enter_password#show' - end - end - - context 'user has missed address step' do - before do - idv_session.vendor_phone_confirmation = false - end - - it 'redirects to address step' do - get :show - - expect(response).to redirect_to idv_otp_verification_url - end + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end describe '#confirm_current_password' do - let(:applicant) do - Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(phone_confirmed_at: Time.zone.now) - end - controller do before_action :confirm_current_password @@ -92,13 +61,10 @@ def show end end - before(:each) do - stub_sign_in(user) - stub_attempts_tracker + before do routes.draw do post 'show' => 'idv/enter_password#show' end - allow(subject).to receive(:confirm_idv_steps_complete).and_return(true) allow(subject).to receive(:idv_session).and_return(idv_session) allow(@irs_attempts_api_tracker).to receive(:track_event) end @@ -140,11 +106,6 @@ def show end describe '#new' do - before do - stub_sign_in(user) - allow(subject).to receive(:confirm_idv_applicant_created).and_return(true) - end - context 'user has completed all steps' do before do idv_session @@ -232,16 +193,7 @@ def show end describe '#create' do - before do - stub_sign_in(user) - allow(subject).to receive(:confirm_idv_applicant_created).and_return(true) - end - context 'user fails to supply correct password' do - let(:applicant) do - Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(phone_confirmed_at: Time.zone.now) - end - before do idv_session end @@ -268,8 +220,6 @@ def show context 'user has completed all steps' do before do idv_session - stub_attempts_tracker - allow(@irs_attempts_api_tracker).to receive(:track_event) end it 'redirects to personal key path' do @@ -359,18 +309,16 @@ def show let!(:enrollment) do create(:in_person_enrollment, :establishing, user: user, profile: nil) end - let(:stub_usps_response) do - stub_request_enroll - end let(:applicant) do Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID_WITH_PHONE end before do stub_request_token - stub_usps_response + stub_request_enroll ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) end it 'redirects to personal key path' do @@ -429,7 +377,7 @@ def show end context 'when there is a 4xx error' do - let(:stub_usps_response) do + before do stub_request_enroll_bad_request_response end @@ -459,7 +407,7 @@ def show end context 'when there is 5xx error' do - let(:stub_usps_response) do + before do stub_request_enroll_internal_server_error_response end @@ -509,7 +457,7 @@ def show end context 'when the USPS response is not a hash' do - let(:stub_usps_response) do + before do stub_request_enroll_non_hash_response end @@ -529,7 +477,7 @@ def show end context 'when the USPS response is missing an enrollment code' do - let(:stub_usps_response) do + before do stub_request_enroll_invalid_response end @@ -597,9 +545,6 @@ def show allow(IdentityConfig.store).to receive(:proofing_device_profiling). and_return(proofing_device_profiling_state) idv_session.threatmetrix_review_status = review_status - end - - before(:each) do stub_request_token end diff --git a/spec/controllers/idv/forgot_password_controller_spec.rb b/spec/controllers/idv/forgot_password_controller_spec.rb index 2dc4f523e0b..90547f3f9a8 100644 --- a/spec/controllers/idv/forgot_password_controller_spec.rb +++ b/spec/controllers/idv/forgot_password_controller_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Idv::ForgotPasswordController do describe 'before_actions' do it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end diff --git a/spec/controllers/idv/getting_started_controller_spec.rb b/spec/controllers/idv/getting_started_controller_spec.rb deleted file mode 100644 index 4f80bab9013..00000000000 --- a/spec/controllers/idv/getting_started_controller_spec.rb +++ /dev/null @@ -1,175 +0,0 @@ -require 'rails_helper' - -RSpec.describe Idv::GettingStartedController do - let(:user) { create(:user) } - - let(:ab_test_args) do - { sample_bucket1: :sample_value1, sample_bucket2: :sample_value2 } - end - - before do - stub_sign_in(user) - stub_analytics - allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) - end - - describe 'before_actions' do - it 'includes authentication before_action' do - expect(subject).to have_actions( - :before, - :confirm_two_factor_authenticated, - ) - end - - it 'includes outage before_action' do - expect(subject).to have_actions( - :before, - :check_for_mail_only_outage, - ) - end - end - - describe '#show' do - let(:analytics_name) { 'IdV: doc auth getting_started visited' } - let(:analytics_args) do - { - step: 'getting_started', - analytics_id: 'Doc Auth', - skip_hybrid_handoff: nil, - irs_reproofing: false, - }.merge(ab_test_args) - end - - it 'renders the show template' do - get :show - - expect(response).to render_template :show - end - - it 'sends analytics_visited event' do - get :show - - expect(@analytics).to have_logged_event(analytics_name, analytics_args) - end - - it 'updates DocAuthLog welcome_view_count' do - doc_auth_log = DocAuthLog.create(user_id: user.id) - - expect { get :show }.to( - change { doc_auth_log.reload.welcome_view_count }.from(0).to(1), - ) - end - - it 'updates DocAuthLog agreement_view_count' do - doc_auth_log = DocAuthLog.create(user_id: user.id) - - expect { get :show }.to( - change { doc_auth_log.reload.agreement_view_count }.from(0).to(1), - ) - end - - context 'document capture already completed' do - before do - subject.idv_session.pii_from_doc = { first_name: 'Susan' } - end - - it 'redirects to ssn step' do - get :show - expect(response).to redirect_to(idv_ssn_url) - end - end - - it 'redirects to please call page if fraud review is pending' do - profile = create(:profile, :fraud_review_pending) - - stub_sign_in(profile.user) - - get :show - - expect(response).to redirect_to(idv_please_call_url) - end - end - - describe '#update' do - let(:analytics_name) { 'IdV: doc auth getting_started submitted' } - - let(:analytics_args) do - { - success: true, - errors: {}, - step: 'getting_started', - analytics_id: 'Doc Auth', - skip_hybrid_handoff: nil, - irs_reproofing: false, - }.merge(ab_test_args) - end - - let(:skip_hybrid_handoff) { nil } - - let(:params) do - { - doc_auth: { - idv_consent_given: 1, - }, - skip_hybrid_handoff: skip_hybrid_handoff, - }.compact - end - - it 'sends analytics_submitted event with consent given' do - put :update, params: params - - expect(@analytics).to have_logged_event(analytics_name, analytics_args) - end - - it 'creates a document capture session' do - expect { put :update, params: params }. - to change { subject.idv_session.document_capture_session_uuid }.from(nil) - end - - context 'with previous establishing in-person enrollments' do - let!(:enrollment) { create(:in_person_enrollment, :establishing, user: user, profile: nil) } - - before do - allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) - end - - it 'cancels all previous establishing enrollments' do - put :update, params: params - - expect(enrollment.reload.status).to eq(InPersonEnrollment::STATUS_CANCELLED) - expect(user.establishing_in_person_enrollment).to be_blank - end - end - - it 'does not set flow_path' do - expect do - put :update, params: params - end.not_to change { - subject.idv_session.flow_path - }.from(nil) - end - - it 'redirects to hybrid handoff' do - put :update, params: params - expect(response).to redirect_to(idv_hybrid_handoff_url) - end - - context 'skip_hybrid_handoff present in params' do - let(:skip_hybrid_handoff) { '' } - it 'sets flow_path to standard' do - expect do - put :update, params: params - end.to change { - subject.idv_session.flow_path - }.from(nil).to('standard').and change { - subject.idv_session.skip_hybrid_handoff - }.from(nil).to(true) - end - - it 'redirects to hybrid handoff' do - put :update, params: params - expect(response).to redirect_to(idv_hybrid_handoff_url) - end - end - end -end diff --git a/spec/controllers/idv/how_to_verify_controller_spec.rb b/spec/controllers/idv/how_to_verify_controller_spec.rb index 97d69a41a7a..ba3ca5b3ac1 100644 --- a/spec/controllers/idv/how_to_verify_controller_spec.rb +++ b/spec/controllers/idv/how_to_verify_controller_spec.rb @@ -9,6 +9,7 @@ stub_sign_in(user) stub_analytics subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true end describe '#step_info' do @@ -32,11 +33,23 @@ expect(response).to render_template :show end + + context 'agreement step not completed' do + before do + subject.idv_session.idv_consent_given = nil + end + + it 'redirects to agreement path' do + get :show + + expect(response).to redirect_to idv_agreement_path + end + end end describe '#update' do it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) put :update end diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 6b9e0e10e38..83b28831bc3 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -221,7 +221,7 @@ let(:document_capture_session_uuid) { '09228b6d-dd39-4925-bf82-b69104095517' } it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) put :update, params: params end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 7f1efc5684a..7548033b067 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -5,7 +5,7 @@ let(:document_filename_regex) { /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}\.[a-z]+$/ } let(:base64_regex) { /^[a-z0-9+\/]+=*$/i } - + let(:selfie_img) { nil } describe '#create' do subject(:action) do post :create, params: params @@ -19,10 +19,11 @@ front: DocAuthImageFixtures.document_front_image_multipart, front_image_metadata: '{"glare":99.99}', back: DocAuthImageFixtures.document_back_image_multipart, + selfie: (selfie_img unless selfie_img.nil?), back_image_metadata: '{"glare":99.99}', document_capture_session_uuid: document_capture_session.uuid, flow_path: flow_path, - } + }.compact end let(:json) { JSON.parse(response.body, symbolize_names: true) } @@ -132,7 +133,6 @@ flow_path: 'standard', front_image_fingerprint: nil, back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -269,7 +269,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -372,7 +371,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -400,7 +398,6 @@ vendor_request_time_in_ms: a_kind_of(Float), front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, doc_type_supported: boolean, @@ -418,7 +415,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, classification_info: a_kind_of(Hash), @@ -556,7 +552,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -584,7 +579,6 @@ vendor_request_time_in_ms: a_kind_of(Float), front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, doc_type_supported: boolean, @@ -607,7 +601,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, classification_info: hash_including( @@ -654,7 +647,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -682,7 +674,6 @@ vendor_request_time_in_ms: a_kind_of(Float), front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, doc_type_supported: boolean, @@ -705,7 +696,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, classification_info: hash_including( @@ -752,7 +742,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -780,7 +769,6 @@ vendor_request_time_in_ms: a_kind_of(Float), front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, doc_type_supported: boolean, @@ -803,7 +791,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, classification_info: hash_including(:Front, :Back), @@ -870,7 +857,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -900,7 +886,6 @@ vendor_request_time_in_ms: a_kind_of(Float), front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, doc_type_supported: boolean, @@ -944,7 +929,6 @@ flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -976,7 +960,6 @@ vendor_request_time_in_ms: a_kind_of(Float), front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, doc_type_supported: boolean, @@ -1005,6 +988,20 @@ ] end end + + context 'when liveness checking enabled' do + before do + allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture). + and_return({ enabled: true }) + end + let(:selfie_img) { DocAuthImageFixtures.selfie_image_multipart } + it 'returns a successful response' do + action + expect(response.status).to eq(200) + expect(json[:success]).to eq(true) + expect(document_capture_session.reload.load_result.success?).to eq(true) + end + end end def expect_funnel_update_counts(user, count) diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index 44640a8a33f..1ed9c71067f 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -9,6 +9,8 @@ before do stub_sign_in(user) + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true subject.idv_session.flow_path = 'hybrid' stub_analytics stub_attempts_tracker @@ -37,10 +39,10 @@ ) end - it 'checks that hybrid_handoff is complete' do + it 'checks that step is allowed' do expect(subject).to have_actions( :before, - :confirm_hybrid_handoff_complete, + :confirm_step_allowed, ) end end @@ -76,17 +78,13 @@ ) end - context '#confirm_hybrid_handoff_complete' do - context 'no flow_path' do - it 'redirects to idv_hybrid_handoff_url' do - subject.idv_session.welcome_visited = true - subject.idv_session.idv_consent_given = true - subject.idv_session.flow_path = nil + context 'no flow_path in idv_session' do + it 'redirects to idv_hybrid_handoff_url' do + subject.idv_session.flow_path = nil - get :show + get :show - expect(response).to redirect_to(idv_hybrid_handoff_url) - end + expect(response).to redirect_to(idv_hybrid_handoff_url) end context 'flow_path is standard' do @@ -100,14 +98,14 @@ expect(response).to redirect_to(idv_document_capture_url) end end - end - context 'with pii in idv_session' do - it 'redirects to ssn step' do - subject.idv_session.pii_from_doc = Idp::Constants::MOCK_IDV_APPLICANT - get :show + context 'with pii in idv_session' do + it 'allows the back button and does not redirect' do + subject.idv_session.pii_from_doc = Idp::Constants::MOCK_IDV_APPLICANT + get :show - expect(response).to redirect_to(idv_ssn_url) + expect(response).to render_template :show + end end end end @@ -124,7 +122,7 @@ end it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) put :update end diff --git a/spec/controllers/idv/otp_verification_controller_spec.rb b/spec/controllers/idv/otp_verification_controller_spec.rb index 3c542b13646..3ca3199494d 100644 --- a/spec/controllers/idv/otp_verification_controller_spec.rb +++ b/spec/controllers/idv/otp_verification_controller_spec.rb @@ -36,7 +36,7 @@ describe 'before_actions' do it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index fd7281fe08c..df57c4f0398 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -51,7 +51,7 @@ def stub_idv_session end it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end describe '#confirm_profile_has_been_created' do diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb index 2c0d6a88298..cd65c39a93a 100644 --- a/spec/controllers/idv/phone_controller_spec.rb +++ b/spec/controllers/idv/phone_controller_spec.rb @@ -30,7 +30,7 @@ describe 'before_actions' do it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end @@ -85,8 +85,6 @@ before do subject.idv_session.applicant = nil subject.idv_session.resolution_successful = nil - - allow(controller).to receive(:confirm_idv_applicant_created).and_call_original end it 'redirects to the verify step' do diff --git a/spec/controllers/idv/phone_errors_controller_spec.rb b/spec/controllers/idv/phone_errors_controller_spec.rb index 6f08b18147c..eb9a367e6d6 100644 --- a/spec/controllers/idv/phone_errors_controller_spec.rb +++ b/spec/controllers/idv/phone_errors_controller_spec.rb @@ -21,7 +21,7 @@ shared_examples_for 'an idv phone errors controller action' do describe 'before_actions' do it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end diff --git a/spec/controllers/idv/phone_question_controller_spec.rb b/spec/controllers/idv/phone_question_controller_spec.rb index 32f25be8b42..d259a45d52f 100644 --- a/spec/controllers/idv/phone_question_controller_spec.rb +++ b/spec/controllers/idv/phone_question_controller_spec.rb @@ -145,7 +145,7 @@ let(:analytics_name) { :idv_doc_auth_phone_question_submitted } it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) get :phone_with_camera end @@ -173,7 +173,7 @@ let(:analytics_name) { :idv_doc_auth_phone_question_submitted } it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) get :phone_without_camera end diff --git a/spec/controllers/idv/resend_otp_controller_spec.rb b/spec/controllers/idv/resend_otp_controller_spec.rb index 2162252da92..4d0ccced7ef 100644 --- a/spec/controllers/idv/resend_otp_controller_spec.rb +++ b/spec/controllers/idv/resend_otp_controller_spec.rb @@ -26,7 +26,7 @@ describe 'before_actions' do it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end diff --git a/spec/controllers/idv/session_errors_controller_spec.rb b/spec/controllers/idv/session_errors_controller_spec.rb index b3e8d7f2044..e7fac844696 100644 --- a/spec/controllers/idv/session_errors_controller_spec.rb +++ b/spec/controllers/idv/session_errors_controller_spec.rb @@ -135,7 +135,7 @@ describe 'before_actions' do it 'includes before_actions from IdvSession' do - expect(subject).to have_actions(:before, :redirect_if_sp_context_needed) + expect(subject).to have_actions(:before, :redirect_unless_sp_requested_verification) end end diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 60a251e7f80..3adea38c86e 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -19,6 +19,12 @@ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::SsnController.step_info).to be_valid + end + end + describe 'before_actions' do it 'includes authentication before_action' do expect(subject).to have_actions( @@ -34,13 +40,6 @@ ) end - it 'checks that the previous step is complete' do - expect(subject).to have_actions( - :before, - :confirm_document_capture_complete, - ) - end - it 'overrides CSPs for ThreatMetrix' do expect(subject).to have_actions( :before, @@ -90,27 +89,14 @@ end context 'with an ssn in idv_session' do - let(:referer) { idv_document_capture_url } before do subject.idv_session.ssn = ssn - request.env['HTTP_REFERER'] = referer - end - - context 'referer is not verify_info' do - it 'redirects to verify_info' do - get :show - - expect(response).to redirect_to(idv_verify_info_url) - end end - context 'referer is verify_info' do - let(:referer) { idv_verify_info_url } - it 'does not redirect' do - get :show + it 'does not redirect and allows the back button' do + get :show - expect(response).to render_template 'idv/shared/ssn' - end + expect(response).to render_template 'idv/shared/ssn' end end @@ -176,6 +162,12 @@ end end + it 'invalidates future steps' do + expect(subject).to receive(:clear_future_steps!) + + put :update, params: params + end + it 'logs attempts api event' do expect(@irs_attempts_api_tracker).to receive(:idv_ssn_submitted).with( ssn: ssn, @@ -226,6 +218,8 @@ context 'when pii_from_doc is not present' do before do + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true subject.idv_session.flow_path = 'standard' subject.idv_session.pii_from_doc = nil end diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 4bcaa3dae30..a19c8b4c427 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -19,12 +19,20 @@ stub_analytics stub_attempts_tracker stub_idv_steps_before_verify_step(user) + subject.idv_session.welcome_visited = true + subject.idv_session.idv_consent_given = true subject.idv_session.flow_path = 'standard' subject.idv_session.pii_from_doc = Idp::Constants::MOCK_IDV_APPLICANT.dup subject.idv_session.ssn = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args) end + describe '#step_info' do + it 'returns a valid StepInfo object' do + expect(Idv::VerifyInfoController.step_info).to be_valid + end + end + describe 'before_actions' do it 'includes authentication before_action' do expect(subject).to have_actions( @@ -39,13 +47,6 @@ :check_for_mail_only_outage, ) end - - it 'confirms ssn step complete' do - expect(subject).to have_actions( - :before, - :confirm_ssn_step_complete, - ) - end end describe '#show' do @@ -365,6 +366,12 @@ end describe '#update' do + it 'invalidates future steps' do + expect(subject).to receive(:clear_future_steps!) + + put :update + end + it 'logs the correct analytics event' do put :update diff --git a/spec/controllers/idv/welcome_controller_spec.rb b/spec/controllers/idv/welcome_controller_spec.rb index c3d25a8128b..d86dae82f0c 100644 --- a/spec/controllers/idv/welcome_controller_spec.rb +++ b/spec/controllers/idv/welcome_controller_spec.rb @@ -33,13 +33,6 @@ :check_for_mail_only_outage, ) end - - it 'includes getting started ab test before_action' do - expect(subject).to have_actions( - :before, - :maybe_redirect_for_getting_started_ab_test, - ) - end end describe '#show' do @@ -81,14 +74,17 @@ expect(response).to render_template('idv/welcome/show') end - context 'and document capture already completed' do + context 'and verify info already completed' do before do + subject.idv_session.flow_path = 'standard' subject.idv_session.pii_from_doc = { first_name: 'Susan' } + subject.idv_session.ssn = '123-45-6789' + subject.idv_session.resolution_successful = true end - it 'redirects to ssn step' do + it 'redirects to enter password step' do get :show - expect(response).to redirect_to(idv_ssn_url) + expect(response).to redirect_to(idv_enter_password_url) end end end @@ -102,24 +98,6 @@ expect(response).to redirect_to(idv_please_call_url) end - - context 'getting_started_ab_test_bucket values' do - render_views - - it 'renders the welcome_new template for :welcome_new' do - allow(controller).to receive(:getting_started_ab_test_bucket).and_return(:welcome_new) - - get :show - expect(response).to render_template(partial: '_welcome_new') - end - - it 'it renders the welcome_default template for :welcome_default' do - allow(controller).to receive(:getting_started_ab_test_bucket).and_return(:welcome_default) - - get :show - expect(response).to render_template(partial: '_welcome_default') - end - end end describe '#update' do @@ -140,7 +118,7 @@ end it 'invalidates future steps' do - expect(subject).to receive(:clear_invalid_steps!) + expect(subject).to receive(:clear_future_steps!) put :update end diff --git a/spec/controllers/idv_controller_spec.rb b/spec/controllers/idv_controller_spec.rb index 4ade439f2bb..7593d4ffd65 100644 --- a/spec/controllers/idv_controller_spec.rb +++ b/spec/controllers/idv_controller_spec.rb @@ -167,30 +167,71 @@ expect(response).to redirect_to idv_welcome_path end - context 'no SP context' do + describe 'SP for IdV requirement' do + let(:current_sp) { create(:service_provider) } + let(:ial) { 2 } let(:user) { build(:user, password: ControllerHelper::VALID_PASSWORD) } before do stub_sign_in(user) - session[:sp] = {} + if current_sp.present? + session[:sp] = { issuer: current_sp.issuer, ial: ial } + else + session[:sp] = {} + end allow(IdentityConfig.store).to receive(:idv_sp_required).and_return(idv_sp_required) end - context 'sp required' do - let(:idv_sp_required) { true } + context 'without an SP context' do + let(:current_sp) { nil } - it 'redirects back to the account page' do - get :index + context 'when an SP is required' do + let(:idv_sp_required) { true } - expect(response).to redirect_to account_url + it 'redirects back to the account page' do + get :index + expect(response).to redirect_to account_url + end + + it 'begins the proofing process if the user has a profile' do + create(:profile, :verified, user: user) + get :index + expect(response).to redirect_to idv_welcome_url + end end - context 'user has an existing profile' do - let(:user) do - profile = create(:profile) - profile.user + context 'no SP required' do + let(:idv_sp_required) { false } + + it 'begins the identity proofing process' do + get :index + + expect(response).to redirect_to idv_welcome_url + end + end + end + + context 'with an SP context that does not require IdV' do + let(:ial) { 1 } + + context 'when an SP is required' do + let(:idv_sp_required) { true } + + it 'redirects back to the account page' do + get :index + expect(response).to redirect_to account_url end + it 'begins the proofing process if the user has a profile' do + create(:profile, :verified, user: user) + get :index + expect(response).to redirect_to idv_welcome_url + end + end + + context 'no SP required' do + let(:idv_sp_required) { false } + it 'begins the identity proofing process' do get :index @@ -199,13 +240,26 @@ end end - context 'sp not required' do - let(:idv_sp_required) { false } + context 'with an SP context that requires IdV' do + let(:ial) { 2 } + + context 'when an SP is required' do + let(:idv_sp_required) { true } + + it 'begins the identity proofing process' do + get :index + expect(response).to redirect_to idv_welcome_url + end + end + + context 'no SP required' do + let(:idv_sp_required) { false } - it 'begins the identity proofing process' do - get :index + it 'begins the identity proofing process' do + get :index - expect(response).to redirect_to idv_welcome_url + expect(response).to redirect_to idv_welcome_url + end end end end diff --git a/spec/controllers/two_factor_authentication/sms_opt_in_controller_spec.rb b/spec/controllers/two_factor_authentication/sms_opt_in_controller_spec.rb index 6b93bd08245..3f95b22769b 100644 --- a/spec/controllers/two_factor_authentication/sms_opt_in_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/sms_opt_in_controller_spec.rb @@ -22,6 +22,7 @@ action expect(assigns[:phone_configuration]).to eq(user.phone_configurations.first) + expect(assigns[:presenter]).to be_kind_of(TwoFactorAuthCode::GenericDeliveryPresenter) expect(@analytics).to have_logged_event( 'SMS Opt-In: Visited', @@ -31,16 +32,6 @@ ) end - context 'when the user has other auth methods' do - let(:user) { create(:user, :with_phone, :with_authentication_app) } - - it 'has an other mfa options url' do - action - - expect(assigns[:other_mfa_options_url]).to eq(login_two_factor_options_path) - end - end - context 'when the user is signing in through an SP' do let(:sp_name) { 'An Example SP' } @@ -171,6 +162,7 @@ expect(response).to render_template(:new) expect(flash[:error]).to be_present + expect(assigns[:presenter]).to be_kind_of(TwoFactorAuthCode::GenericDeliveryPresenter) expect(@analytics).to have_logged_event( 'SMS Opt-In: Submitted', diff --git a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb index 2431abb0291..8f1de36b8a6 100644 --- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb @@ -291,7 +291,7 @@ multi_factor_auth_method_created_at: second_webauthn_platform_configuration.created_at.strftime('%s%L'), webauthn_configuration_id: nil, - frontend_error: 'NotAllowedError', + frontend_error: webauthn_error, ) patch :confirm, params: params diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 744fa2bbc33..620e5dbdb99 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -39,28 +39,28 @@ { 'IdV: intro visited' => {}, 'IdV: doc auth welcome visited' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth welcome submitted' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth agreement visited' => { - step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: consent checkbox toggled' => { checked: true, }, 'IdV: doc auth agreement submitted' => { - success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth hybrid handoff visited' => { - step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth hybrid handoff submitted' => { - success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { - flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil @@ -69,36 +69,36 @@ width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'IdV: doc auth image upload form submitted' => { - success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {}, phone_with_camera: nil + success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {}, phone_with_camera: nil }, 'IdV: doc auth document_capture submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth ssn visited' => { - flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify visited' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify submitted' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { - acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation form' => { - success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation vendor' => { @@ -117,15 +117,15 @@ proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_visited => { - address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_submitted => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { @@ -147,28 +147,28 @@ { 'IdV: intro visited' => {}, 'IdV: doc auth welcome visited' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth welcome submitted' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth agreement visited' => { - step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: consent checkbox toggled' => { checked: true, }, 'IdV: doc auth agreement submitted' => { - success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth hybrid handoff visited' => { - step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth hybrid handoff submitted' => { - success: true, errors: hash_including(message: nil), destination: :link_sent, flow_path: 'hybrid', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, telephony_response: hash_including(errors: {}, message_id: 'fake-message-id', request_id: 'fake-message-request-id', success: true) + success: true, errors: hash_including(message: nil), destination: :link_sent, flow_path: 'hybrid', step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, telephony_response: hash_including(errors: {}, message_id: 'fake-message-id', request_id: 'fake-message-request-id', success: true) }, 'IdV: doc auth document_capture visited' => { - flow_path: 'hybrid', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'hybrid', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'hybrid', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil, phone_with_camera: nil @@ -177,36 +177,36 @@ width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'hybrid', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil, phone_with_camera: nil }, 'IdV: doc auth image upload form submitted' => { - success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'hybrid', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'hybrid', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'hybrid', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {}, phone_with_camera: nil + success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'hybrid', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {}, phone_with_camera: nil }, 'IdV: doc auth document_capture submitted' => { - success: true, errors: {}, flow_path: 'hybrid', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'hybrid', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn visited' => { - flow_path: 'hybrid', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'hybrid', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn submitted' => { - success: true, errors: {}, flow_path: 'hybrid', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'hybrid', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify visited' => { - flow_path: 'hybrid', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'hybrid', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify submitted' => { - flow_path: 'hybrid', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'hybrid', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'hybrid', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'hybrid', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { - acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation form' => { - success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation vendor' => { @@ -225,15 +225,15 @@ proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_visited => { - address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_submitted => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { @@ -255,25 +255,25 @@ { 'IdV: intro visited' => {}, 'IdV: doc auth welcome visited' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth welcome submitted' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth agreement visited' => { - step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth agreement submitted' => { - success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth hybrid handoff visited' => { - step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth hybrid handoff submitted' => { - success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { - flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil @@ -282,55 +282,55 @@ width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'IdV: doc auth image upload form submitted' => { - success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth image upload vendor pii validation' => { - success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {}, phone_with_camera: nil + success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), phone_question_ab_test_bucket: :bypass_phone_question, classification_info: {}, phone_with_camera: nil }, 'IdV: doc auth document_capture submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn visited' => { - flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth ssn submitted' => { - success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + success: true, errors: {}, flow_path: 'standard', step: 'ssn', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify visited' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify submitted' => { - flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false + flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { - acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: USPS address letter requested' => { - resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: request letter visited' => { letter_already_sent: false, }, :idv_enter_password_visited => { - address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: USPS address letter enqueued' => { - enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, + enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, :idv_enter_password_submitted => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: final resolution' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: letter enqueued visited' => { @@ -342,25 +342,25 @@ let(:in_person_path_events) do { 'IdV: doc auth welcome visited' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth welcome submitted' => { - step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil + step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil }, 'IdV: doc auth agreement visited' => { - step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth agreement submitted' => { - success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, step: 'agreement', analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth hybrid handoff visited' => { - step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + step: 'hybrid_handoff', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth hybrid handoff submitted' => { - success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + success: true, errors: {}, destination: :document_capture, flow_path: 'standard', redo_document_capture: nil, step: 'hybrid_handoff', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { - flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false + flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil @@ -369,7 +369,7 @@ width: 284, height: 38, mimeType: 'image/png', source: 'upload', size: 3694, attempt: 1, flow_path: 'standard', acuant_sdk_upgrade_a_b_testing_enabled: 'false', use_alternate_sdk: anything, acuant_version: anything, acuantCaptureMode: nil, fingerprint: anything, failedImageResubmission: boolean, phone_question_ab_test_bucket: 'bypass_phone_question', phone_with_camera: nil, documentType: nil, dpi: nil, glare: nil, glareScoreThreshold: nil, isAssessedAsBlurry: nil, isAssessedAsGlare: nil, isAssessedAsUnsupported: nil, moire: nil, sharpness: nil, sharpnessScoreThreshold: nil, assessment: nil }, 'IdV: doc auth image upload form submitted' => { - success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil + success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil }, 'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: true, doc_auth_result: 'Attention', phone_with_camera: nil), 'IdV: verify in person troubleshooting option clicked' => { @@ -400,23 +400,23 @@ success: true, step: 'address', flow_path: 'standard', step_count: 1, analytics_id: 'In Person Proofing', irs_reproofing: false, errors: {}, same_address_as_id: false }, 'IdV: doc auth ssn visited' => { - analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false + analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', irs_reproofing: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false }, 'IdV: doc auth ssn submitted' => { - analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false + analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', irs_reproofing: false, errors: {}, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, same_address_as_id: false }, 'IdV: doc auth verify visited' => { - analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil + analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil }, 'IdV: doc auth verify submitted' => { - analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil + analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', irs_reproofing: false, same_address_as_id: false, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, irs_reproofing: false, same_address_as_id: false, skip_hybrid_handoff: nil, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: 'aaa-bbb-ccc', success: true, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone confirmation form' => { - success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', proofing_components: { document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation vendor' => { @@ -435,15 +435,15 @@ proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_visited => { - acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, address_verification_method: 'phone', + acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, address_verification_method: 'phone', proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_submitted => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { - success: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, + success: true, acuant_sdk_upgrade_ab_test_bucket: :default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { diff --git a/spec/features/idv/cancel_spec.rb b/spec/features/idv/cancel_spec.rb index 2763a5798ea..bdddde878f4 100644 --- a/spec/features/idv/cancel_spec.rb +++ b/spec/features/idv/cancel_spec.rb @@ -28,7 +28,13 @@ hash_including(step: 'agreement'), ) - click_on t('idv.cancel.actions.keep_going') + expect(page).to have_unique_form_landmark_labels + + expect(page).to have_button(t('idv.cancel.actions.start_over')) + expect(page).to have_button(t('idv.cancel.actions.account_page')) + expect(page).to have_button(t('idv.cancel.actions.keep_going')) + + click_on(t('idv.cancel.actions.keep_going')) expect(current_path).to eq(original_path) expect(fake_analytics).to have_logged_event( @@ -47,6 +53,12 @@ hash_including(step: 'agreement'), ) + expect(page).to have_unique_form_landmark_labels + + expect(page).to have_button(t('idv.cancel.actions.start_over')) + expect(page).to have_button(t('idv.cancel.actions.account_page')) + expect(page).to have_button(t('idv.cancel.actions.keep_going')) + click_on t('idv.cancel.actions.start_over') expect(current_path).to eq(idv_welcome_path) @@ -66,6 +78,12 @@ hash_including(step: 'agreement'), ) + expect(page).to have_unique_form_landmark_labels + + expect(page).to have_button(t('idv.cancel.actions.start_over')) + expect(page).to have_button(t('idv.cancel.actions.account_page')) + expect(page).to have_button(t('idv.cancel.actions.keep_going')) + click_spinner_button_and_wait t('idv.cancel.actions.account_page') expect(current_path).to eq(account_path) @@ -96,6 +114,12 @@ step: 'ssn', ) + expect(page).to have_unique_form_landmark_labels + + expect(page).to have_button(t('idv.cancel.actions.start_over')) + expect(page).to have_button(t('idv.cancel.actions.account_page')) + expect(page).to have_button(t('idv.cancel.actions.keep_going')) + click_on t('idv.cancel.actions.keep_going') expect(fake_analytics).to have_logged_event( @@ -144,6 +168,12 @@ hash_including(step: 'agreement'), ) + expect(page).to have_button(t('idv.cancel.actions.start_over')) + expect(page).to have_button(t('idv.cancel.actions.exit', app_name: APP_NAME)) + expect(page).to have_button(t('idv.cancel.actions.keep_going')) + + expect(page).to have_unique_form_landmark_labels + click_spinner_button_and_wait t('idv.cancel.actions.exit', app_name: APP_NAME) expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 6224a75df38..1f58acd3191 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -182,7 +182,6 @@ expect_costing_for_document expect(DocAuthLog.find_by(user_id: user.id).state).to eq('MT') - visit(idv_document_capture_url) expect(page).to have_current_path(idv_ssn_url) fill_out_ssn_form_ok click_idv_continue diff --git a/spec/features/idv/doc_auth/getting_started_spec.rb b/spec/features/idv/doc_auth/getting_started_spec.rb deleted file mode 100644 index e4f2918a450..00000000000 --- a/spec/features/idv/doc_auth/getting_started_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'rails_helper' - -RSpec.feature 'getting started step' do - include IdvHelper - include DocAuthHelper - - let(:fake_analytics) { FakeAnalytics.new } - let(:sp_name) { 'Test SP' } - - before do - allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) - allow_any_instance_of(ServiceProviderSession).to receive(:sp_name).and_return(sp_name) - stub_const('AbTests::IDV_GETTING_STARTED', FakeAbTestBucket.new) - AbTests::IDV_GETTING_STARTED.assign_all(:getting_started) - - visit_idp_from_sp_with_ial2(:oidc) - sign_in_and_2fa_user - complete_doc_auth_steps_before_welcome_step - end - - it 'displays expected content with javascript enabled', :js do - expect(page).to have_current_path(idv_getting_started_path) - - # Try to continue with unchecked checkbox - click_continue - expect(page).to have_current_path(idv_getting_started_path) - expect(page).to have_content(t('forms.validation.required_checkbox')) - - complete_getting_started_step - expect(page).to have_current_path(idv_hybrid_handoff_path) - end - - it 'logs "intro_paragraph" learn more link click' do - click_on t('doc_auth.info.getting_started_learn_more') - - expect(fake_analytics).to have_logged_event( - 'External Redirect', - step: 'getting_started', - location: 'intro_paragraph', - flow: 'idv', - redirect_url: MarketingSite.help_center_article_url( - category: 'verify-your-identity', - article: 'how-to-verify-your-identity', - ), - ) - end - - context 'skipping hybrid_handoff step', :js, driver: :headless_chrome_mobile do - before do - complete_getting_started_step - end - - it 'progresses to document capture' do - expect(page).to have_current_path(idv_document_capture_url) - end - end - - def complete_getting_started_step - complete_agreement_step # it does the right thing - end -end diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb index e35031ce7dd..83f14f5b618 100644 --- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb @@ -54,10 +54,13 @@ DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images + expect(current_path).to eq(idv_ssn_path) + expect(page).to have_css('[role="status"]') # We verified your ID + complete_ssn_step + expect(current_path).to eq(idv_verify_info_path) check t('forms.ssn.show') expect(page).to have_content(DocAuthHelper::GOOD_SSN) - expect(page).to have_css('[role="status"]') # We verified your ID end it 'document capture cannot be reached after submitting verify info step' do @@ -79,6 +82,7 @@ expect(page).to have_current_path(idv_document_capture_path) DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images + complete_ssn_step complete_verify_step expect(page).to have_current_path(idv_phone_path) @@ -142,10 +146,13 @@ DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images + expect(current_path).to eq(idv_ssn_path) + expect(page).to have_css('[role="status"]') # We verified your ID + complete_ssn_step + expect(current_path).to eq(idv_verify_info_path) check t('forms.ssn.show') expect(page).to have_content(DocAuthHelper::GOOD_SSN) - expect(page).to have_css('[role="status"]') # We verified your ID end end end diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index 182f6544011..7b1f9a706be 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -350,8 +350,7 @@ context 'async missing' do it 'allows resubmitting form' do - sign_in_and_2fa_user(user) - complete_doc_auth_steps_before_verify_step + complete_ssn_step allow(DocumentCaptureSession).to receive(:find_by). and_return(nil) @@ -386,8 +385,7 @@ context 'async timed out' do it 'allows resubmitting form' do - sign_in_and_2fa_user(user) - complete_doc_auth_steps_before_verify_step + complete_ssn_step allow(DocumentCaptureSession).to receive(:find_by). and_return(nil) diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index 65d0751d1f6..619b590320d 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -16,16 +16,13 @@ complete_welcome_step validate_agreement_page - try_to_go_back_from_agreement try_to_skip_ahead_from_agreement complete_agreement_step validate_hybrid_handoff_page - try_to_go_back_from_hybrid_handoff try_to_skip_ahead_from_hybrid_handoff complete_hybrid_handoff_step # upload photos - try_to_go_back_from_document_capture validate_document_capture_page complete_document_capture_step validate_document_capture_submit(user) @@ -33,7 +30,6 @@ validate_ssn_page complete_ssn_step - try_to_go_back_from_verify_info validate_verify_info_page complete_verify_step validate_verify_info_submit(user) @@ -56,6 +52,40 @@ validate_return_to_sp end + scenario 'Unsupervised proofing back button' do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user + + complete_welcome_step + + test_go_back_from_agreement + complete_agreement_step + + test_go_back_from_hybrid_handoff + complete_hybrid_handoff_step # upload photos + + test_go_back_from_document_capture + complete_document_capture_step + + test_go_back_from_ssn_page + complete_ssn_step + + test_go_back_from_verify_info + complete_verify_step + + validate_phone_page + visit_by_mail_and_return + complete_otp_verification_page(user) + + complete_enter_password_step(user) + + acknowledge_and_confirm_personal_key + + click_agree_and_continue + + validate_return_to_sp + end + context 'with an sp that allows in person proofing' do before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) @@ -76,8 +106,9 @@ complete_enter_password_step(user) - validate_come_back_later_page - complete_come_back_later + try_to_go_back_from_letter_enqueued + validate_letter_enqueued_page + complete_letter_enqueued validate_return_to_sp visit sign_out_url @@ -285,7 +316,7 @@ def validate_enter_password_submit(user) expect(GpoConfirmation.count).to eq(0) end - def validate_come_back_later_page + def validate_letter_enqueued_page expect(page).to have_current_path(idv_letter_enqueued_path) expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) expect(page).to have_content(t('idv.titles.come_back_later')) @@ -356,7 +387,7 @@ def visit_by_mail_and_return expect(page).to have_current_path(idv_phone_path) end - def try_to_go_back_from_agreement + def test_go_back_from_agreement go_back expect(current_path).to eq(idv_welcome_path) complete_welcome_step @@ -367,7 +398,7 @@ def try_to_go_back_from_agreement ) end - def try_to_go_back_from_hybrid_handoff + def test_go_back_from_hybrid_handoff go_back expect(current_path).to eql(idv_agreement_path) expect(page).to have_checked_field( @@ -385,24 +416,45 @@ def try_to_go_back_from_hybrid_handoff complete_agreement_step end - def try_to_go_back_from_document_capture - visit(idv_agreement_path) + def test_go_back_from_document_capture + go_back + go_back expect(page).to have_current_path(idv_agreement_path) expect(page).to have_checked_field( t('doc_auth.instructions.consent', app_name: APP_NAME), visible: :all, ) - visit(idv_hybrid_handoff_url) + go_forward expect(page).to have_current_path(idv_hybrid_handoff_path) - visit(idv_document_capture_url) + go_forward + expect(page).to have_content(t('doc_auth.headings.front')) + expect(page).to have_content(t('doc_auth.headings.back')) end - def try_to_go_back_from_verify_info - visit(idv_document_capture_url) + def test_go_back_from_ssn_page + go_back + expect(page).to have_current_path(idv_document_capture_path) + go_forward + end + + def test_go_back_from_verify_info + go_back + go_back + expect(page).to have_current_path(idv_document_capture_path) + go_back + go_back + go_back + expect(page).to have_current_path(idv_welcome_path) + visit(idv_verify_info_path) expect(page).to have_current_path(idv_verify_info_path) + end + + def try_to_go_back_from_letter_enqueued + go_back + expect(page).to have_current_path(idv_letter_enqueued_path) visit(idv_welcome_path) - expect(page).to have_current_path(idv_verify_info_path) + expect(page).to have_current_path(idv_letter_enqueued_path) end def same_phone?(phone1, phone2) diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb index 5f5db03ac0b..4e0a16f59db 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb @@ -54,7 +54,6 @@ visit idv_hybrid_mobile_document_capture_url # Confirm that jumping to Welcome page does not cause errors - # This was added for the GettingStarted A/B Test visit idv_welcome_url expect(page).to have_current_path(root_url) visit idv_hybrid_mobile_document_capture_url @@ -241,7 +240,9 @@ end perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_verify_info_path, wait: 10) + expect(page).to have_current_path(idv_ssn_path, wait: 10) + complete_ssn_step + expect(page).to have_current_path(idv_verify_info_path) # verify orig pii no longer displayed expect(page).not_to have_text('DAVID') @@ -297,7 +298,9 @@ end perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_verify_info_path, wait: 10) + expect(page).to have_current_path(idv_ssn_path, wait: 10) + complete_ssn_step + expect(page).to have_current_path(idv_verify_info_path) # verify orig pii no longer displayed expect(page).not_to have_text('DAVID') diff --git a/spec/features/idv/outage_spec.rb b/spec/features/idv/outage_spec.rb index 1428ee58407..791ae9545e2 100644 --- a/spec/features/idv/outage_spec.rb +++ b/spec/features/idv/outage_spec.rb @@ -62,31 +62,6 @@ def sign_in_with_idv_required(user:, sms_or_totp: :sms) allow(IdentityConfig.store).to receive(key). and_return(send(key)) end - - # Configuration / vendor status changes can effect Rails routing tables. - # Force routes to be reloaded when we've modified configuration. - Rails.application.reload_routes! - end - - after do - # Don't leave stale routes sitting around! - # - Reset all the feature flags that could cause route changes - # - Reload routes to reset the environment for any specs that run next - - vendors.each do |service| - vendor_status_key = "vendor_status_#{service}".to_sym - allow(IdentityConfig.store).to receive(vendor_status_key).and_call_original - end - - config_flags.each do |key| - allow(IdentityConfig.store).to receive(key).and_call_original - end - - # Let e.g. frontend analytics requests to /api/logger settle before we reload routes - # to avoid flakiness in CI. - page.server.wait_for_pending_requests if page&.server - - Rails.application.reload_routes! end context 'vendor_status_lexisnexis_phone_finder set to full_outage' do diff --git a/spec/features/idv/steps/request_letter_step_spec.rb b/spec/features/idv/steps/request_letter_step_spec.rb index 7d6585aaf1b..b6cf6938eb2 100644 --- a/spec/features/idv/steps/request_letter_step_spec.rb +++ b/spec/features/idv/steps/request_letter_step_spec.rb @@ -87,13 +87,16 @@ # Confirm that user cannot visit other IdV pages while unverified visit idv_agreement_path - expect(page).to have_current_path(idv_verify_by_mail_enter_code_path) + expect(page).to have_current_path(idv_letter_enqueued_path) visit idv_ssn_url - expect(page).to have_current_path(idv_verify_by_mail_enter_code_path) + expect(page).to have_current_path(idv_letter_enqueued_path) visit idv_verify_info_url - expect(page).to have_current_path(idv_verify_by_mail_enter_code_path) + expect(page).to have_current_path(idv_letter_enqueued_path) # complete verification: end to end gpo test + sign_out + sign_in_live_with_2fa(user) + complete_gpo_verification(user) expect(user.identity_verified?).to be(true) expect(page).to_not have_content(t('account.index.verification.reactivate_button')) @@ -158,7 +161,7 @@ def confirm_rate_limited context 'GPO verified user has reset their password and needs to re-verify with GPO again', :js do let(:user) { user_verified_with_gpo } - it 'shows the user a GPO index screen asking to send a letter' do + it 'shows the user the request letter page' do visit_idp_from_ial2_oidc_sp trigger_reset_password_and_click_email_link(user.email) reset_password_and_sign_back_in(user) diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index d3c2dcc25d1..4f297b4941a 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -86,6 +86,32 @@ expect(page).to have_current_path(account_path) end + + scenario 'allows a user to recreate their account after account reset' do + sign_in_before_2fa(user) + email = user.confirmed_email_addresses.first.email + + expect(page).to have_content(t('two_factor_authentication.opt_in.title')) + + click_link t('two_factor_authentication.login_options_link_text') + click_link t('two_factor_authentication.account_reset.link') + click_link t('account_reset.request.yes_continue') + click_button t('account_reset.request.yes_continue') + + reset_email + + travel_to (IdentityConfig.store.account_reset_wait_period_days + 1).days.from_now do + AccountReset::GrantRequestsAndSendEmails.new.perform(Time.zone.today) + open_last_email + click_email_link_matching(/delete_account\?token/) + click_button t('account_reset.request.yes_continue') + click_link t('account_reset.confirm_delete_account.link_text') + sign_up_with(email) + open_last_email + click_email_link_matching(/confirmation_token/) + expect(page).to have_content(t('devise.confirmations.confirmed')) + end + end end context 'with international phone that does not support voice delivery' do diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index b92875d83b5..295da29ea8b 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -10,17 +10,21 @@ front_image_metadata: front_image_metadata, back: back_image, back_image_metadata: back_image_metadata, + selfie: selfie_image, document_capture_session_uuid: document_capture_session_uuid, ), service_provider: build(:service_provider, issuer: 'test_issuer'), analytics: fake_analytics, irs_attempts_api_tracker: irs_attempts_api_tracker, store_encrypted_images: store_encrypted_images, + liveness_checking_enabled: liveness_checking_enabled, ) end let(:front_image) { DocAuthImageFixtures.document_front_image_multipart } let(:back_image) { DocAuthImageFixtures.document_back_image_multipart } + let(:selfie_image) { nil } + let(:liveness_checking_enabled) { false } let(:front_image_metadata) do { width: 40, height: 40, mimeType: 'image/png', source: 'upload' }.to_json end @@ -73,6 +77,19 @@ expect(form.errors[:limit]).to eq([I18n.t('errors.doc_auth.rate_limited_heading')]) end end + + context 'when liveness check is enabled' do + let(:liveness_checking_enabled) { true } + it 'is not valid without selfie' do + expect(form.valid?).to eq(false) + end + context 'with valid selfie' do + let(:selfie_image) { DocAuthImageFixtures.selfie_image_multipart } + it 'is valid' do + expect(form.valid?).to eq(true) + end + end + end end describe '#submit' do @@ -107,7 +124,6 @@ flow_path: anything, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -145,7 +161,6 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -209,7 +224,6 @@ flow_path: anything, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, ) @@ -344,7 +358,6 @@ flow_path: anything, front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), - getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, phone_with_camera: nil, side: 'both', diff --git a/spec/forms/otp_verification_form_spec.rb b/spec/forms/otp_verification_form_spec.rb index 32af977d9ff..3f8ea236b1d 100644 --- a/spec/forms/otp_verification_form_spec.rb +++ b/spec/forms/otp_verification_form_spec.rb @@ -25,7 +25,6 @@ it 'returns a successful response' do expect(result.to_h).to eq( success: true, - multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), ) end @@ -47,7 +46,6 @@ error_details: { code: { blank: true, wrong_length: true }, }, - multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), ) end @@ -69,7 +67,6 @@ error_details: { code: { user_otp_missing: true }, }, - multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), ) end @@ -91,7 +88,6 @@ error_details: { code: { wrong_length: true, incorrect: true }, }, - multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), ) end @@ -113,7 +109,6 @@ error_details: { code: { pattern_mismatch: true, incorrect: true }, }, - multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), ) end @@ -138,7 +133,6 @@ error_details: { code: { user_otp_expired: true }, }, - multi_factor_auth_method: 'otp_code', multi_factor_auth_method_created_at: phone_configuration.created_at.strftime('%s%L'), ) end diff --git a/spec/forms/webauthn_verification_form_spec.rb b/spec/forms/webauthn_verification_form_spec.rb index efd4cd2ca1e..8c78ab827a9 100644 --- a/spec/forms/webauthn_verification_form_spec.rb +++ b/spec/forms/webauthn_verification_form_spec.rb @@ -7,6 +7,7 @@ let(:user) { create(:user) } let(:challenge) { webauthn_challenge } let(:webauthn_error) { nil } + let(:screen_lock_error) { nil } let(:platform_authenticator) { false } let(:client_data_json) { verification_client_data_json } let!(:webauthn_configuration) do @@ -32,6 +33,7 @@ signature: signature, credential_id: credential_id, webauthn_error: webauthn_error, + screen_lock_error:, ) end @@ -166,6 +168,112 @@ end end + context 'when a screen lock error is present' do + let(:screen_lock_error) { 'true' } + + context 'user does not have another authentication method available' do + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + screen_lock_error: { present: true }, + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + + it 'provides error message not suggesting other method' do + expect(result.first_error_message).to eq t( + 'two_factor_authentication.webauthn_error.screen_lock_no_other_mfa', + link_html: link_to( + t('two_factor_authentication.webauthn_error.use_a_different_method'), + login_two_factor_options_path, + ), + ) + end + end + + context 'user has another WebAuthn method available' do + context 'the other MFA method is WebAuthn of the same attachment' do + let(:platform_authenticator) { false } + let(:user) { create(:user, :with_webauthn) } + + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + screen_lock_error: { present: true }, + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + + it 'provides error message not suggesting other method' do + expect(result.first_error_message).to eq t( + 'two_factor_authentication.webauthn_error.screen_lock_no_other_mfa', + link_html: link_to( + t('two_factor_authentication.webauthn_error.use_a_different_method'), + login_two_factor_options_path, + ), + ) + end + end + + context 'the other MFA method is WebAuthn of a different attachment' do + let(:platform_authenticator) { false } + let(:user) { create(:user, :with_webauthn_platform) } + + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + screen_lock_error: { present: true }, + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + + it 'provides error message suggesting other method' do + expect(result.first_error_message).to eq t( + 'two_factor_authentication.webauthn_error.screen_lock_other_mfa_html', + link_html: link_to( + t('two_factor_authentication.webauthn_error.use_a_different_method'), + login_two_factor_options_path, + ), + ) + end + end + + context 'the other MFA method is not a WebAuthn method' do + let(:user) { create(:user, :with_phone) } + + it 'returns unsuccessful result' do + expect(result.to_h).to eq( + success: false, + error_details: { + screen_lock_error: { present: true }, + }, + multi_factor_auth_method: 'webauthn', + webauthn_configuration_id: webauthn_configuration.id, + ) + end + + it 'provides error message suggesting other method' do + expect(result.first_error_message).to eq t( + 'two_factor_authentication.webauthn_error.screen_lock_other_mfa_html', + link_html: link_to( + t('two_factor_authentication.webauthn_error.use_a_different_method'), + login_two_factor_options_path, + ), + ) + end + end + end + end + context 'when origin is invalid' do before do allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:6666') diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 80cd731ff49..d11c55a67f1 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -18,6 +18,7 @@ class BaseTask { key: 'datetime.dotiw.minutes.one' }, # "minute is minute" in French and English { key: 'datetime.dotiw.minutes.other' }, # "minute is minute" in French and English { key: 'doc_auth.headings.photo', locales: %i[fr] }, # "Photo" is "Photo" in French + { key: 'doc_auth.headings.selfie', locales: %i[fr] }, # "Photo" is "Photo" in French { key: /^i18n\.locale\./ }, # Show locale options translated as that language { key: /^i18n\.transliterate\./ }, # Approximate non-ASCII characters in ASCII { key: 'links.contact', locales: %i[fr] }, # "Contact" is "Contact" in French 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 87febba9db3..0a140ae24be 100644 --- a/spec/javascript/packages/document-capture/components/document-capture-spec.jsx +++ b/spec/javascript/packages/document-capture/components/document-capture-spec.jsx @@ -44,6 +44,14 @@ describe('document-capture/components/document-capture', () => { window.location.hash = originalHash; }); + it('does not render the selfie capture by default', () => { + const { queryByText } = render(); + + const selfie = queryByText('doc_auth.headings.document_capture_selfie'); + + expect(selfie).not.to.exist(); + }); + it('renders the form steps', () => { const { getByText } = render(); @@ -75,7 +83,7 @@ describe('document-capture/components/document-capture', () => { }); it('progresses through steps to completion', async () => { - const { getByLabelText, getByText, getAllByText, findAllByText } = render( + const { getByLabelText, getByText, getAllByText, findAllByText, queryByText } = render( @@ -120,6 +128,10 @@ describe('document-capture/components/document-capture', () => { await userEvent.click(getByLabelText('doc_auth.headings.document_capture_back')); + // Ensure the selfie field does not appear + const selfie = queryByText('doc_auth.headings.document_capture_selfie'); + expect(selfie).not.to.exist(); + // Continue only once all errors have been removed. await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); submitButton = getByText('forms.buttons.submit.default'); diff --git a/spec/javascript/packages/document-capture/context/feature-flag-spec.jsx b/spec/javascript/packages/document-capture/context/feature-flag-spec.jsx new file mode 100644 index 00000000000..c1f43c67e50 --- /dev/null +++ b/spec/javascript/packages/document-capture/context/feature-flag-spec.jsx @@ -0,0 +1,18 @@ +import { useContext } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import FeatureFlagContext from '@18f/identity-document-capture/context/feature-flag'; + +describe('document-capture/context/feature-flag', () => { + it('has expected default properties', () => { + const { result } = renderHook(() => useContext(FeatureFlagContext)); + + expect(result.current).to.have.keys([ + 'notReadySectionEnabled', + 'exitQuestionSectionEnabled', + 'selfieCaptureEnabled', + ]); + expect(result.current.notReadySectionEnabled).to.be.a('boolean'); + expect(result.current.exitQuestionSectionEnabled).to.be.a('boolean'); + expect(result.current.selfieCaptureEnabled).to.be.a('boolean'); + }); +}); diff --git a/spec/javascript/packages/document-capture/context/service-provider-spec.jsx b/spec/javascript/packages/document-capture/context/service-provider-spec.jsx index b1279b5e22a..1c22ab6bf56 100644 --- a/spec/javascript/packages/document-capture/context/service-provider-spec.jsx +++ b/spec/javascript/packages/document-capture/context/service-provider-spec.jsx @@ -8,16 +8,10 @@ describe('document-capture/context/service-provider', () => { it('has expected default properties', () => { const { result } = renderHook(() => useContext(ServiceProviderContext)); - expect(result.current).to.have.keys([ - 'name', - 'failureToProofURL', - 'getFailureToProofURL', - 'selfieCaptureEnabled', - ]); + expect(result.current).to.have.keys(['name', 'failureToProofURL', 'getFailureToProofURL']); expect(result.current.name).to.be.null(); expect(result.current.failureToProofURL).to.be.a('string'); expect(result.current.getFailureToProofURL).to.be.a('function'); - expect(result.current.selfieCaptureEnabled).to.be.a('boolean'); }); describe('Provider', () => { @@ -32,13 +26,5 @@ describe('document-capture/context/service-provider', () => { expect(failureToProofURL).to.equal('http://example.com/?a=1&location=location+name'); }); - it('provides selfieCaptureEnabled', () => { - const { result } = renderHook(() => useContext(ServiceProviderContext), { - wrapper: ({ children }) => {children}, - }); - - const { selfieCaptureEnabled } = result.current; - expect(selfieCaptureEnabled).to.equal(true); - }); }); }); diff --git a/spec/jobs/gpo_expiration_job_spec.rb b/spec/jobs/gpo_expiration_job_spec.rb index c1e841eb2dd..a9e4aa86631 100644 --- a/spec/jobs/gpo_expiration_job_spec.rb +++ b/spec/jobs/gpo_expiration_job_spec.rb @@ -45,6 +45,7 @@ allow(IdentityConfig.store).to receive(:usps_confirmation_max_days).and_return( usps_confirmation_max_days, ) + allow(subject).to receive(:analytics).and_return(analytics) end describe '#gpo_profiles_that_should_be_expired' do @@ -113,22 +114,6 @@ ) end - context 'when a callback is provided' do - it 'calls it for expired profiles' do - profile = users[:user_with_one_expired_gpo_profile].reload.gpo_verification_pending_profile - gpo_verification_pending_at = profile.gpo_verification_pending_at - - on_profile_expired = spy - expect(on_profile_expired).to receive(:call).with( - profile: profile, - gpo_verification_pending_at: gpo_verification_pending_at, - ) - - job = described_class.new(analytics: analytics, on_profile_expired: on_profile_expired) - job.perform - end - end - context('when dry_run is specified') do it 'does not write changes' do job.perform(dry_run: true) diff --git a/spec/policies/idv/flow_policy_spec.rb b/spec/policies/idv/flow_policy_spec.rb index 6c0c20f41f1..346086edc43 100644 --- a/spec/policies/idv/flow_policy_spec.rb +++ b/spec/policies/idv/flow_policy_spec.rb @@ -24,20 +24,48 @@ end end - context '#undo_steps_from_controller!' do - context 'user is on document_capture step' do + context '#undo_future_steps_from_controller!' do + context 'user is on verify_info step' do before do idv_session.welcome_visited = true + idv_session.document_capture_session_uuid = SecureRandom.uuid + idv_session.idv_consent_given = true + idv_session.skip_hybrid_handoff = true + idv_session.flow_path = 'standard' + idv_session.phone_for_mobile_flow = '201-555-1212' + + idv_session.pii_from_doc = Idp::Constants::MOCK_IDV_APPLICANT + idv_session.had_barcode_read_failure = true + idv_session.had_barcode_attention_error = true + + idv_session.ssn = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] + idv_session.threatmetrix_session_id = SecureRandom.uuid + + idv_session.address_edited = true end - it 'user goes back and submits welcome' do - subject.undo_steps_from_controller!(controller: Idv::WelcomeController) + it 'clears future steps when user goes back and submits welcome' do + subject.undo_future_steps_from_controller!(controller: Idv::WelcomeController) + + expect(idv_session.welcome_visited).not_to be_nil + expect(idv_session.document_capture_session_uuid).not_to be_nil - expect(idv_session.welcome_visited).to be_nil expect(idv_session.idv_consent_given).to be_nil + expect(idv_session.skip_hybrid_handoff).to be_nil + expect(idv_session.flow_path).to be_nil + expect(idv_session.phone_for_mobile_flow).to be_nil + + expect(idv_session.pii_from_doc).to be_nil + expect(idv_session.had_barcode_read_failure).to be_nil + expect(idv_session.had_barcode_attention_error).to be_nil + + expect(idv_session.ssn).to be_nil + expect(idv_session.threatmetrix_session_id).to be_nil + + expect(idv_session.address_edited).to be_nil end end end @@ -80,7 +108,7 @@ idv_session.flow_path = 'standard' expect(subject.info_for_latest_step.key).to eq(:document_capture) expect(subject.controller_allowed?(controller: Idv::DocumentCaptureController)).to be - # expect(subject.controller_allowed?(controller: Idv::SsnController)).not_to be + expect(subject.controller_allowed?(controller: Idv::SsnController)).not_to be end end @@ -91,7 +119,43 @@ idv_session.flow_path = 'hybrid' expect(subject.info_for_latest_step.key).to eq(:link_sent) expect(subject.controller_allowed?(controller: Idv::LinkSentController)).to be - # expect(subject.controller_allowed?(controller: Idv::SsnController)).not_to be + expect(subject.controller_allowed?(controller: Idv::SsnController)).not_to be + end + end + + context 'preconditions for ssn are present' do + before do + idv_session.welcome_visited = true + idv_session.idv_consent_given = true + idv_session.flow_path = 'standard' + idv_session.pii_from_doc = { pii: 'value' } + end + + it 'returns ssn for standard flow' do + expect(subject.info_for_latest_step.key).to eq(:ssn) + expect(subject.controller_allowed?(controller: Idv::SsnController)).to be + expect(subject.controller_allowed?(controller: Idv::VerifyInfoController)).not_to be + end + + it 'returns ssn for hybrid flow' do + idv_session.flow_path = 'hybrid' + expect(subject.info_for_latest_step.key).to eq(:ssn) + expect(subject.controller_allowed?(controller: Idv::SsnController)).to be + expect(subject.controller_allowed?(controller: Idv::VerifyInfoController)).not_to be + end + end + + context 'preconditions for verify_info are present' do + it 'returns verify_info' do + idv_session.welcome_visited = true + idv_session.idv_consent_given = true + idv_session.flow_path = 'standard' + idv_session.pii_from_doc = { pii: 'value' } + idv_session.ssn = '666666666' + + expect(subject.info_for_latest_step.key).to eq(:verify_info) + expect(subject.controller_allowed?(controller: Idv::VerifyInfoController)).to be + # expect(subject.controller_allowed?(controller: Idv::PhoneController)).not_to be end end end diff --git a/spec/presenters/two_factor_auth_code/sms_opt_in_presenter_spec.rb b/spec/presenters/two_factor_auth_code/sms_opt_in_presenter_spec.rb new file mode 100644 index 00000000000..f97d81d327c --- /dev/null +++ b/spec/presenters/two_factor_auth_code/sms_opt_in_presenter_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +RSpec.describe TwoFactorAuthCode::SmsOptInPresenter do + subject(:presenter) { TwoFactorAuthCode::SmsOptInPresenter.new } + + describe '#redirect_location_step' do + subject(:redirect_location_step) { presenter.redirect_location_step } + + it { expect(redirect_location_step).to be_present } + end +end diff --git a/spec/presenters/two_factor_login_options_presenter_spec.rb b/spec/presenters/two_factor_login_options_presenter_spec.rb index 1386d500dd9..79be918a647 100644 --- a/spec/presenters/two_factor_login_options_presenter_spec.rb +++ b/spec/presenters/two_factor_login_options_presenter_spec.rb @@ -26,9 +26,32 @@ t('two_factor_authentication.login_options_title') end - it 'supplies a heading' do - expect(presenter.heading).to eq \ - t('two_factor_authentication.login_options_title') + describe '#heading' do + subject { presenter.heading } + + context 'default user session context' do + it { should eq t('two_factor_authentication.login_options_title') } + end + + context 'reauthentication user session context' do + let(:reauthentication_context) { true } + + it { should eq t('two_factor_authentication.login_options_reauthentication_title') } + end + end + + describe '#info' do + subject { presenter.info } + + context 'default user session context' do + it { should eq t('two_factor_authentication.login_intro') } + end + + context 'reauthentication user session context' do + let(:reauthentication_context) { true } + + it { should eq t('two_factor_authentication.login_intro_reauthentication') } + end end it 'supplies a cancel link when the token is valid' do diff --git a/spec/services/db/add_document_verification_and_selfie_costs_spec.rb b/spec/services/db/add_document_verification_and_selfie_costs_spec.rb index 273c028e323..e8f28a94c29 100644 --- a/spec/services/db/add_document_verification_and_selfie_costs_spec.rb +++ b/spec/services/db/add_document_verification_and_selfie_costs_spec.rb @@ -3,6 +3,7 @@ RSpec.describe Db::AddDocumentVerificationAndSelfieCosts do let(:user_id) { 1 } let(:service_provider) { build(:service_provider, issuer: 'foo') } + let(:liveness_checking_enabled) { true } let(:billed_response) do DocAuth::Response.new( success: true, @@ -30,15 +31,19 @@ described_class.new( user_id: user_id, service_provider: service_provider, + liveness_checking_enabled: liveness_checking_enabled, ) end - it 'has costing for front, back, and result when billed' do - subject.call(billed_response) + context 'when livness check is disabled' do + let(:liveness_checking_enabled) { false } + it 'has costing for front, back and result when billed' do + subject.call(billed_response) - expect(costing_for(:acuant_front_image)).to be_present - expect(costing_for(:acuant_back_image)).to be_present - expect(costing_for(:acuant_result)).to be_present + expect(costing_for(:acuant_front_image)).to be_present + expect(costing_for(:acuant_back_image)).to be_present + expect(costing_for(:acuant_result)).to be_present + end end it 'has costing for front, back, but not result when not billed' do diff --git a/spec/services/doc_auth/error_generator_spec.rb b/spec/services/doc_auth/error_generator_spec.rb index 2d78483712f..84f1ff11e39 100644 --- a/spec/services/doc_auth/error_generator_spec.rb +++ b/spec/services/doc_auth/error_generator_spec.rb @@ -36,6 +36,14 @@ IssuerType: 'Country' } end + let(:liveness_enabled) { nil } + let(:face_match_result) { 'Pass' } + let(:portrait_match_results) do + { + FaceMatchResult: face_match_result, + } + end + def build_error_info( doc_result: nil, passed: [], @@ -46,7 +54,7 @@ def build_error_info( { conversation_id: 31000406181234, reference: 'Reference1', - liveness_enabled: false, + liveness_enabled: liveness_enabled, vendor: 'Test', transaction_reason_code: 'testing', doc_auth_result: doc_result, @@ -55,7 +63,7 @@ def build_error_info( failed: failed, }, alert_failure_count: failed&.count.to_i, - portrait_match_results: { FaceMatchResult: 'Pass' }, + portrait_match_results: portrait_match_results, image_metrics: image_metrics, classification_info: classification_info, } @@ -500,6 +508,49 @@ def build_error_info( end end + context 'The correct errors are delivered for selfie with metric error' do + let(:metrics) do + { + front: { + 'HorizontalResolution' => 300, + 'VerticalResolution' => 300, + 'SharpnessMetric' => 50, + 'GlareMetric' => 50, + }, + back: { + 'HorizontalResolution' => 300, + 'VerticalResolution' => 300, + 'SharpnessMetric' => 50, + 'GlareMetric' => 50, + }, + } + end + context 'when liveness is enabled' do + let(:liveness_enabled) { true } + context 'when liveness check passed' do + let(:face_match_result) { 'Pass' } + it 'DocAuthResult is Passed with no other error' do + error_info = build_error_info(doc_result: 'Passed', image_metrics: metrics) + + # this is an edge case, the generate_doc_auth_errors function should no be + # called when everything is successful + expect(warn_notifier).to receive(:call). + with(hash_including(:response_info, :message)).once + described_class.new(config).generate_doc_auth_errors(error_info) + end + end + + context 'when liveness check failed' do + let(:face_match_result) { 'Failure' } + it 'DocAuthResult is failed with selfie error' do + error_info = build_error_info(doc_result: 'Failed', image_metrics: metrics) + errors = described_class.new(config).generate_doc_auth_errors(error_info) + expect(errors.keys).to contain_exactly(:general, :selfie, :hints) + end + end + end + end + context 'with both doc type error and image metric error' do let(:metrics) do { diff --git a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb index 7ad5597fe51..e2b235bb97e 100644 --- a/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/requests/true_id_request_spec.rb @@ -14,8 +14,12 @@ base_url: base_url, trueid_noliveness_cropping_workflow: 'test_workflow_cropping', trueid_noliveness_nocropping_workflow: 'test_workflow', + trueid_liveness_cropping_workflow: 'test_workflow_liveness_cropping', + trueid_liveness_nocropping_workflow: 'test_workflow_liveness', ) end + let(:selfie_image) { DocAuthImageFixtures.selfie_image } + let(:liveness_checking_required) { false } let(:subject) do described_class.new( config: config, @@ -24,34 +28,90 @@ image_source: image_source, user_uuid: applicant[:uuid], uuid_prefix: applicant[:uuid_prefix], + selfie_image: selfie_image, + liveness_checking_required: liveness_checking_required, ) end shared_examples 'a successful request' do it 'uploads the image and returns a successful result' do - request_stub = stub_request(:post, full_url).to_return(body: response_body, status: 201) + include_liveness = liveness_checking_required && !selfie_image.nil? + request_stub_liveness = stub_request(:post, full_url).with do |request| + JSON.parse(request.body, symbolize_names: true)[:Document][:Selfie].present? + end.to_return(body: response_body(include_liveness), status: 201) + request_stub = stub_request(:post, full_url).with do |request| + !JSON.parse(request.body, symbolize_names: true)[:Document][:Selfie].present? + end.to_return(body: response_body(include_liveness), status: 201) response = subject.fetch expect(response.success?).to eq(true) expect(response.errors).to eq({}) expect(response.exception).to be_nil - expect(request_stub).to have_been_requested + if include_liveness + expect(request_stub_liveness).to have_been_requested + else + expect(request_stub).to have_been_requested + end + end + + context 'fails document authentication' do + it 'fails response with errors' do + include_liveness = liveness_checking_required && !selfie_image.nil? + request_stub_liveness = stub_request(:post, full_url).with do |request| + JSON.parse(request.body, symbolize_names: true)[:Document][:Selfie].present? + end.to_return(body: response_body_with_doc_auth_errors(include_liveness), status: 201) + request_stub = stub_request(:post, full_url).with do |request| + !JSON.parse(request.body, symbolize_names: true)[:Document][:Selfie].present? + end.to_return(body: response_body_with_doc_auth_errors(include_liveness), status: 201) + + response = subject.fetch + + expect(response.success?).to eq(false) + expect(response.errors.keys).to contain_exactly(:general, :front, :back, :hints) + expect(response.errors[:general]).to contain_exactly(DocAuth::Errors::GENERAL_ERROR) + expect(response.errors[:front]).to contain_exactly(DocAuth::Errors::FALLBACK_FIELD_LEVEL) + expect(response.errors[:back]).to contain_exactly(DocAuth::Errors::FALLBACK_FIELD_LEVEL) + expect(response.errors[:hints]).to eq(true) + expect(response.exception).to be_nil + if include_liveness + expect(request_stub_liveness).to have_been_requested + else + expect(request_stub).to have_been_requested + end + end end end - context 'with acuant image source' do - let(:workflow) { 'test_workflow' } - let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } + context 'with liveness_checking_enabled as false' do + let(:liveness_checking_required) { false } + context 'with acuant image source' do + let(:workflow) { 'test_workflow' } + let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } + it_behaves_like 'a successful request' + end + context 'with unknown image source' do + let(:workflow) { 'test_workflow_cropping' } + let(:image_source) { DocAuth::ImageSources::UNKNOWN } - it_behaves_like 'a successful request' + it_behaves_like 'a successful request' + end end - context 'with unknown image source' do - let(:workflow) { 'test_workflow_cropping' } - let(:image_source) { DocAuth::ImageSources::UNKNOWN } + context 'with liveness_checking_enabled as true' do + let(:liveness_checking_required) { true } + context 'with acuant image source' do + let(:workflow) { 'test_workflow_liveness' } + let(:image_source) { DocAuth::ImageSources::ACUANT_SDK } - it_behaves_like 'a successful request' + it_behaves_like 'a successful request' + end + context 'with unknown image source' do + let(:workflow) { 'test_workflow_liveness_cropping' } + let(:image_source) { DocAuth::ImageSources::UNKNOWN } + + it_behaves_like 'a successful request' + end end context 'with non 200 http status code' do @@ -70,7 +130,7 @@ end end -def response_body +def response_body(include_liveness) { Status: { TransactionStatus: 'passed', @@ -89,6 +149,49 @@ def response_body }, ], }, + *( + if include_liveness + [ + Group: 'PORTRAIT_MATCH_RESULT', + Name: 'FaceMatchResult', + Values: [{ Value: 'Success' }], + ] + end + ), + ], + }, + ], + }.to_json +end + +def response_body_with_doc_auth_errors(include_liveness) + { + Status: { + TransactionStatus: 'passed', + }, + Products: [ + { + ProductType: 'TrueID', + ProductStatus: 'pass', + ParameterDetails: [ + { + Group: 'AUTHENTICATION_RESULT', + Name: 'DocAuthResult', + Values: [ + { + Value: 'Failed', + }, + ], + }, + *( + if include_liveness + [ + Group: 'PORTRAIT_MATCH_RESULT', + Name: 'FaceMatchResult', + Values: [{ Value: 'Success' }], + ] + end + ), ], }, ], diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index c95940c1a0e..3c39172fcb8 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -6,6 +6,7 @@ instance_double(Faraday::Response, status: 200, body: success_response_body) end let(:failure_body_no_liveness) { LexisNexisFixtures.true_id_response_failure_no_liveness } + let(:failure_body_with_liveness) { LexisNexisFixtures.true_id_response_failure_with_liveness } let(:failure_body_with_all_failures) do LexisNexisFixtures.true_id_response_failure_with_all_failures end @@ -21,6 +22,9 @@ let(:failure_response_no_liveness) do instance_double(Faraday::Response, status: 200, body: failure_body_no_liveness) end + let(:failure_response_with_liveness) do + instance_double(Faraday::Response, status: 200, body: failure_body_with_liveness) + end let(:failure_response_tampering) do instance_double(Faraday::Response, status: 200, body: failure_body_tampering) end @@ -315,6 +319,11 @@ def get_decision_product(resp) to match(a_hash_including(visible_pattern: { no_side: 'Failed' })) end + it 'returns Failed for liveness failure' do + output = described_class.new(failure_response_with_liveness, config).to_h + expect(output[:success]).to eq(false) + end + it 'produces expected hash output' do output = described_class.new(failure_response_with_all_failures, config).to_h diff --git a/spec/services/doc_auth_router_spec.rb b/spec/services/doc_auth_router_spec.rb index bbfcaec3c8c..116a33f619d 100644 --- a/spec/services/doc_auth_router_spec.rb +++ b/spec/services/doc_auth_router_spec.rb @@ -81,6 +81,32 @@ def reload_ab_test_initializer! expect(result).to eq(doc_auth_vendor) end + + context 'when selfie is enabled' do + before do + allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture). + and_return({ enabled: true }) + end + context 'when vendor is not set to mock' do + it 'chose lexisnexis' do + result = DocAuthRouter.doc_auth_vendor( + discriminator: discriminator, + analytics: analytics, + ) + expect(result).to eq(Idp::Constants::Vendors::LEXIS_NEXIS) + end + end + context 'when vendor is set to mock' do + let(:doc_auth_vendor) { Idp::Constants::Vendors::MOCK } + it 'stays with the mock' do + result = DocAuthRouter.doc_auth_vendor( + discriminator: discriminator, + analytics: analytics, + ) + expect(result).to eq(Idp::Constants::Vendors::MOCK) + end + end + end end context 'with a discriminator that hashes inside the test group' do @@ -95,6 +121,25 @@ def reload_ab_test_initializer! to eq(doc_auth_vendor_randomize_alternate_vendor) end + context 'with selfie enabled' do + before do + allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture). + and_return({ enabled: true }) + end + it 'is the lexisnexis vendor' do + expect(DocAuthRouter.doc_auth_vendor(discriminator: discriminator)). + to eq(Idp::Constants::Vendors::LEXIS_NEXIS) + end + + context 'when alternate is set to mock' do + let(:doc_auth_vendor_randomize_alternate_vendor) { Idp::Constants::Vendors::MOCK } + it 'stays with the mock vendor' do + expect(DocAuthRouter.doc_auth_vendor(discriminator: discriminator)). + to eq(Idp::Constants::Vendors::MOCK) + end + end + end + context 'with randomize false' do let(:doc_auth_vendor_randomize) { false } diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb index 7dfa42ab378..b4d283f3b5d 100644 --- a/spec/support/controller_helper.rb +++ b/spec/support/controller_helper.rb @@ -68,7 +68,6 @@ def stub_verify_steps_one_and_two(user) ssn: '666-12-1234', }.with_indifferent_access idv_session.resolution_successful = true - allow(subject).to receive(:confirm_idv_applicant_created).and_return(true) allow(subject).to receive(:idv_session).and_return(idv_session) allow(subject).to receive(:user_session).and_return(user_session) end diff --git a/spec/support/doc_auth_image_fixtures.rb b/spec/support/doc_auth_image_fixtures.rb index a1fe793ae37..4063e925fe8 100644 --- a/spec/support/doc_auth_image_fixtures.rb +++ b/spec/support/doc_auth_image_fixtures.rb @@ -19,6 +19,18 @@ def self.document_back_image_multipart Rack::Test::UploadedFile.new(fixture_path('id-back.jpg'), 'image/jpeg') end + def self.selfie_image + load_image_data('selfie.jpg') + end + + def self.selfie_image_multipart + Rack::Test::UploadedFile.new(fixture_path('selfie.jpg'), 'image/jpeg') + end + + def self.selfie_image_data_uri + "data:image/jpeg;base64,#{Base64.strict_encode64(selfie_image)}" + end + def self.error_yaml_multipart path = File.join( File.dirname(__FILE__), diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 7b1ada73532..cbd50b5c1b0 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -175,7 +175,7 @@ def complete_gpo_verification(user) click_button t('idv.gpo.form.submit') end - def complete_come_back_later + def complete_letter_enqueued # Exit Login.gov and return to SP click_on t('idv.cancel.actions.exit', app_name: APP_NAME) end diff --git a/spec/support/matchers/accessibility.rb b/spec/support/matchers/accessibility.rb index 4a3759ad109..42e7be70ff8 100644 --- a/spec/support/matchers/accessibility.rb +++ b/spec/support/matchers/accessibility.rb @@ -140,6 +140,36 @@ def descriptors(element) end end +RSpec::Matchers.define :have_unique_form_landmark_labels do + # Could maybe be extended to other landmarks by modifying this function + def landmarks(page) + page.all(:css, 'form').reject { |element| element[:'aria-hidden'] == 'true' } + end + + match do |page| + labels = landmarks(page).map { |element| AccessibleName.new(page:).computed_name(element) } + + labels.one? || labels.compact.uniq.length == labels.length + end + + failure_message do |page| + message_parts = [] + labels_already_seen = [] + + landmarks(page).each do |landmark| + label = AccessibleName.new(page:).computed_name(landmark) + if !label + message_parts << "expected to find aria labeling in #{landmark.native.to_html}" + elsif labels_already_seen.include?(label) + message_parts << "label '#{label}' applies to more than one landmark" + end + labels_already_seen << label + end + + message_parts.join("\n") + end +end + class AccessibleName attr_reader :page diff --git a/spec/support/matchers/aria_view_matchers.rb b/spec/support/matchers/aria_view_matchers.rb new file mode 100644 index 00000000000..23505f914a8 --- /dev/null +++ b/spec/support/matchers/aria_view_matchers.rb @@ -0,0 +1,89 @@ +# Very minimal start on accessibility matchers for view specs. +# Defines one matcher, specific to buttons generated with Rails' +# `button_to` helper +# +# Matcher: `have_button_to_with_accessibility` +# Use: +# +# expect(rendered).to have_button_to_with_accessibility('expected button text', +# 'expected button action') +# or +# +# expect(rendered).to have_button_to_with_accessibility('expected button text', +# 'expected button action', +# 'expected aria label') +# +# In the first case, the expected aria-label attribute value will be +# the expected button text. + +class HaveButtonToWithAccessibilityMatcher + attr_reader :expected_button_text, :expected_action, :expected_aria_label, :actual + + def initialize(expected_button_text, + expected_action, + expected_aria_label) + @expected_button_text = expected_button_text + @expected_aria_label = expected_aria_label + @expected_action = expected_action + end + + def match(actual_text) + @actual = Capybara.string(actual_text.strip) + + button && enclosing_form && aria_label_ok? && action_ok? + end + + def failure_message(_actual_text) + return "expected to find button with text '#{expected_button_text}'" unless button + return 'expected button to be in a (Rails) "button_to" form tag' unless enclosing_form + + messages = [] + messages += aria_label_failure_message unless aria_label_ok? + messages += action_failure_message unless action_ok? + messages.join("\n") + end + + private + + def button + actual.find_button(expected_button_text) + rescue Capybara::ElementNotFound + return nil + end + + def enclosing_form + button&.ancestor('form.button_to') + rescue Capybara::ElementNotFound + return nil + end + + def aria_label_ok? + enclosing_form && enclosing_form['aria-label'] == expected_aria_label + end + + def aria_label_failure_message + "expected enclosing form to have an 'aria-label' attribute with value '#{expected_aria_label}'" + end + + def action_ok? + enclosing_form && enclosing_form['action'] == expected_action + end + + def action_failure_message + "expected enclosing form to have an 'action' attribute with value '#{expected_action}'" + end +end + +RSpec::Matchers.define :have_button_to_with_accessibility do |expected_button_text, + expected_action, + expected_aria_label = + expected_button_text| + matcher = HaveButtonToWithAccessibilityMatcher.new( + expected_button_text, + expected_action, + expected_aria_label, + ) + + match { |actual_text| matcher.match(actual_text) } + failure_message { |actual_text| matcher.failure_message(actual_text) } +end diff --git a/spec/views/idv/cancellations/new.html.erb_spec.rb b/spec/views/idv/cancellations/new.html.erb_spec.rb index 6a4369bb2fd..e207a4c379d 100644 --- a/spec/views/idv/cancellations/new.html.erb_spec.rb +++ b/spec/views/idv/cancellations/new.html.erb_spec.rb @@ -14,22 +14,32 @@ render end - it 'renders action to start over' do - expect(rendered).to have_button(t('idv.cancel.actions.start_over')) + it 'renders an action to keep going, with the correct aria attributes' do + expect(rendered).to have_button_to_with_accessibility( + t('idv.cancel.actions.keep_going'), + idv_cancel_path(step: params[:step]), + ) end - it 'renders action to keep going' do - expect(rendered).to have_text(t('idv.cancel.actions.keep_going')) + it 'renders action to start over, with the correct aria attributes' do + expect(rendered).to have_button_to_with_accessibility( + t('idv.cancel.actions.start_over'), + idv_session_path(step: params[:step]), + ) end - it 'renders action to exit and go to account page' do + it 'renders action to exit and go to account page, with the correct aria attributes' do expect(rendered).to have_content(t('idv.cancel.headings.exit.without_sp')) t( 'idv.cancel.description.exit.without_sp', app_name: APP_NAME, account_page_text: t('idv.cancel.description.account_page'), ).each { |expected_p| expect(rendered).to have_content(expected_p) } - expect(rendered).to have_button(t('idv.cancel.actions.account_page')) + + expect(rendered).to have_button_to_with_accessibility( + t('idv.cancel.actions.account_page'), + idv_cancel_path(step: params[:step], location: 'cancel'), + ) end context 'with hybrid flow' do diff --git a/spec/views/idv/getting_started/show.html.erb_spec.rb b/spec/views/idv/getting_started/show.html.erb_spec.rb deleted file mode 100644 index 722bc59513e..00000000000 --- a/spec/views/idv/getting_started/show.html.erb_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'idv/getting_started/show' do - let(:user_fully_authenticated) { true } - let(:sp_name) { nil } - let(:user) { create(:user) } - - before do - @decorated_sp_session = instance_double(ServiceProviderSession) - @sp_name = 'Login.gov' - @title = t('doc_auth.headings.getting_started', sp_name: @sp_name) - allow(@decorated_sp_session).to receive(:sp_name).and_return(sp_name) - allow(view).to receive(:decorated_sp_session).and_return(@decorated_sp_session) - allow(view).to receive(:user_fully_authenticated?).and_return(user_fully_authenticated) - allow(view).to receive(:user_signing_up?).and_return(false) - allow(view).to receive(:url_for).and_wrap_original do |method, *args, &block| - method.call(*args, &block) - rescue - '' - end - render - end - - context 'in doc auth with an authenticated user' do - before do - assign(:current_user, user) - - render - end - - it 'renders a link to return to the SP' do - expect(rendered).to have_link(t('links.cancel')) - end - end - - it 'includes code to track clicks on the consent checkbox' do - selector = [ - 'lg-click-observer[event-name="IdV: consent checkbox toggled"]', - '[name="doc_auth[idv_consent_given]"]', - ].join ' ' - - expect(rendered).to have_css(selector) - end - - it 'renders a link to help center article' do - expect(rendered).to have_link( - t('doc_auth.info.getting_started_learn_more'), - href: help_center_redirect_path( - category: 'verify-your-identity', - article: 'how-to-verify-your-identity', - flow: :idv, - step: :getting_started, - location: 'intro_paragraph', - ), - ) - end - - it 'renders a link to the privacy & security page' do - expect(rendered).to have_link( - t('doc_auth.getting_started.instructions.learn_more'), - href: policy_redirect_url(flow: :idv, step: :getting_started, location: :consent), - ) - end -end diff --git a/spec/views/idv/welcome/show.html.erb_spec.rb b/spec/views/idv/welcome/show.html.erb_spec.rb index e8824449022..c974f132235 100644 --- a/spec/views/idv/welcome/show.html.erb_spec.rb +++ b/spec/views/idv/welcome/show.html.erb_spec.rb @@ -68,66 +68,4 @@ href: policy_redirect_url(flow: :idv, step: :welcome, location: :footer), ) end - - context 'A/B test specifies welcome_new template' do - before do - @ab_test_bucket = :welcome_new - @sp_name = 'Login.gov' - @title = t('doc_auth.headings.getting_started', sp_name: @sp_name) - end - - it 'renders the welcome_new template' do - render - - expect(rendered).to have_content(@title) - expect(rendered).to have_content(t('doc_auth.getting_started.instructions.getting_started')) - expect(rendered).to have_link( - t('doc_auth.info.getting_started_learn_more'), - href: help_center_redirect_path( - category: 'verify-your-identity', - article: 'how-to-verify-your-identity', - flow: :idv, - step: :welcome_new, - location: 'intro_paragraph', - ), - ) - expect(rendered).not_to have_link( - t('doc_auth.instructions.learn_more'), - href: policy_redirect_url(flow: :idv, step: :welcome, location: :footer), - ) - end - end - - context 'A/B test specifies welcome_default template' do - before do - @ab_test_bucket = :welcome_default - end - - it 'renders the welcome_default template' do - render - - expect(rendered).to have_content(t('doc_auth.headings.welcome')) - expect(rendered).to have_content(t('doc_auth.instructions.welcome')) - expect(rendered).to have_link( - t('doc_auth.instructions.learn_more'), - href: policy_redirect_url(flow: :idv, step: :welcome, location: :footer), - ) - end - end - - context 'A/B test unspecified' do - before do - @ab_test_bucket = nil - end - it 'renders the welcome_default template' do - render - - expect(rendered).to have_content(t('doc_auth.headings.welcome')) - expect(rendered).to have_content(t('doc_auth.instructions.welcome')) - expect(rendered).to have_link( - t('doc_auth.instructions.learn_more'), - href: policy_redirect_url(flow: :idv, step: :welcome, location: :footer), - ) - end - end end diff --git a/spec/views/two_factor_authentication/options/index.html.erb_spec.rb b/spec/views/two_factor_authentication/options/index.html.erb_spec.rb index ee879276862..46e29eb14d9 100644 --- a/spec/views/two_factor_authentication/options/index.html.erb_spec.rb +++ b/spec/views/two_factor_authentication/options/index.html.erb_spec.rb @@ -4,6 +4,7 @@ let(:user) { User.new } let(:phishing_resistant_required) { false } let(:piv_cac_required) { false } + let(:reauthentication_context) { false } before do allow(view).to receive(:user_session).and_return({}) @@ -12,7 +13,7 @@ @presenter = TwoFactorLoginOptionsPresenter.new( user: user, view: view, - reauthentication_context: false, + reauthentication_context: reauthentication_context, service_provider: nil, phishing_resistant_required: phishing_resistant_required, piv_cac_required: piv_cac_required, @@ -35,6 +36,13 @@ t('two_factor_authentication.login_options_title') end + it 'has a localized intro text' do + render + + expect(rendered).to have_content \ + t('two_factor_authentication.login_intro') + end + it 'has a cancel link' do render @@ -97,4 +105,22 @@ ) end end + + context 'with context reauthentication' do + let(:reauthentication_context) { true } + + it 'has a localized heading' do + render + + expect(rendered).to have_content \ + t('two_factor_authentication.login_options_reauthentication_title') + end + + it 'has a localized intro text' do + render + + expect(rendered).to have_content \ + t('two_factor_authentication.login_intro_reauthentication') + end + end end diff --git a/spec/views/two_factor_authentication/sms_opt_in/error.html.erb_spec.rb b/spec/views/two_factor_authentication/sms_opt_in/error.html.erb_spec.rb index 037c19f741f..152394cf534 100644 --- a/spec/views/two_factor_authentication/sms_opt_in/error.html.erb_spec.rb +++ b/spec/views/two_factor_authentication/sms_opt_in/error.html.erb_spec.rb @@ -2,12 +2,12 @@ RSpec.describe 'two_factor_authentication/sms_opt_in/error.html.erb' do let(:phone_configuration) { build(:phone_configuration, phone: '1 888-867-5309') } - let(:other_mfa_options_url) { nil } + let(:presenter) { TwoFactorAuthCode::SmsOptInPresenter.new } let(:cancel_url) { '/account' } before do assign(:phone_configuration, phone_configuration) - assign(:other_mfa_options_url, other_mfa_options_url) + assign(:presenter, presenter) assign(:cancel_url, cancel_url) allow(view).to receive(:user_signing_up?).and_return(false) end @@ -18,39 +18,21 @@ expect(rendered).to have_content('(***) ***-5309') end - context 'troubleshooting links' do - it 'links to the contact form in a new window' do - render - - expect(rendered).to have_link( - t('links.contact_support', app_name: APP_NAME), - href: MarketingSite.contact_url, - class: 'usa-link--external', - ) - end - - context 'without an other_mfa_options_url' do - let(:other_mfa_options_url) { nil } - - it 'omits the other auth methods section' do - render - - expect(rendered).to_not have_content(t('two_factor_authentication.opt_in.cant_use_phone')) - expect(rendered).to_not have_content(t('two_factor_authentication.login_options_link_text')) - end - end - - context 'with an other_mfa_options_url' do - let(:other_mfa_options_url) { '/other' } - - it 'links to other options' do - render + it 'renders troubleshooting options' do + render - expect(rendered).to have_link( - t('two_factor_authentication.login_options_link_text'), - href: other_mfa_options_url, - ) - end - end + expect(rendered).to have_link( + t('two_factor_authentication.login_options_link_text'), + href: login_two_factor_options_path, + ) + expect(rendered).to have_link( + t('two_factor_authentication.learn_more'), + href: help_center_redirect_path( + category: 'get-started', + article: 'authentication-options', + flow: :two_factor_authentication, + step: :sms_opt_in, + ), + ) end end diff --git a/spec/views/two_factor_authentication/sms_opt_in/new.html.erb_spec.rb b/spec/views/two_factor_authentication/sms_opt_in/new.html.erb_spec.rb index b184da3b3b6..41f122904dd 100644 --- a/spec/views/two_factor_authentication/sms_opt_in/new.html.erb_spec.rb +++ b/spec/views/two_factor_authentication/sms_opt_in/new.html.erb_spec.rb @@ -4,13 +4,13 @@ let(:phone) { '1-888-867-5309' } let(:phone_configuration) { build(:phone_configuration, phone: phone) } let(:phone_number_opt_out) { PhoneNumberOptOut.create_or_find_with_phone(phone) } - let(:other_mfa_options_url) { nil } + let(:presenter) { TwoFactorAuthCode::SmsOptInPresenter.new } let(:cancel_url) { '/account' } before do assign(:phone_configuration, phone_configuration) assign(:phone_number_opt_out, phone_number_opt_out) - assign(:other_mfa_options_url, other_mfa_options_url) + assign(:presenter, presenter) assign(:cancel_url, cancel_url) allow(view).to receive(:user_signing_up?).and_return(false) end @@ -21,30 +21,21 @@ expect(rendered).to have_content('(***) ***-5309') end - context 'other authentication methods' do - context 'without an other_mfa_options_url' do - let(:other_mfa_options_url) { nil } - - it 'omits the other auth methods section' do - render - - expect(rendered).to_not have_content(t('two_factor_authentication.opt_in.cant_use_phone')) - expect(rendered).to_not have_content(t('two_factor_authentication.login_options_link_text')) - end - end - - context 'with an other_mfa_options_url' do - let(:other_mfa_options_url) { '/other' } - - it 'links to other options' do - render + it 'renders troubleshooting options' do + render - expect(rendered).to have_content(t('two_factor_authentication.opt_in.cant_use_phone')) - expect(rendered).to have_link( - t('two_factor_authentication.login_options_link_text'), - href: other_mfa_options_url, - ) - end - end + expect(rendered).to have_link( + t('two_factor_authentication.login_options_link_text'), + href: login_two_factor_options_path, + ) + expect(rendered).to have_link( + t('two_factor_authentication.learn_more'), + href: help_center_redirect_path( + category: 'get-started', + article: 'authentication-options', + flow: :two_factor_authentication, + step: :sms_opt_in, + ), + ) end end diff --git a/yarn.lock b/yarn.lock index a77727f7c3e..c5e866905e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1216,11 +1216,6 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - "@pkgr/utils@^2.3.1": version "2.4.2" resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.2.tgz#9e638bbe9a6a6f165580dc943f138fd3309a2cbc" @@ -4570,10 +4565,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.10.49: - version "1.10.49" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz#c871661c62452348d228c96425f75ddf7e10f05a" - integrity sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ== +libphonenumber-js@^1.10.51: + version "1.10.51" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.51.tgz#a3b8c15db2721c3e5f7fe6759e2a524712b578e6" + integrity sha512-vY2I+rQwrDQzoPds0JeTEpeWzbUJgqoV0O4v31PauHBb/e+1KCXKylHcDnBMgJZ9fH9mErsEbROJY3Z3JtqEmg== lightningcss-darwin-arm64@1.22.0: version "1.22.0"