diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index e02aa9b69b2..8e4334708fc 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -14,7 +14,7 @@ def handle_stored_result(user: current_user, store_in_session: true) successful_response else extra = { stored_result_present: stored_result.present? } - failure(I18n.t('doc_auth.errors.general.network_error'), extra) + failure(nil, extra) end end @@ -33,13 +33,25 @@ def successful_response end # copied from Flow::Failure module - def failure(message, extra = nil) - flash[:error] = message - form_response_params = { success: false, errors: { message: message } } + def failure(message = nil, extra = nil) + form_response_params = { success: false } + form_response_params[:errors] = make_error_hash(message) form_response_params[:extra] = extra unless extra.nil? FormResponse.new(**form_response_params) end + def make_error_hash(message) + Rails.logger.info("make_error_hash: stored_result: #{stored_result.inspect}") + + error_hash = { message: message || I18n.t('doc_auth.errors.general.network_error') } + + if stored_result&.errors&.has_key?(:socure) + error_hash[:socure] = stored_result.errors[:socure] + end + + error_hash + end + def extract_pii_from_doc(user, store_in_session: false) if defined?(idv_session) # hybrid mobile does not have idv_session idv_session.had_barcode_read_failure = stored_result.attention_with_barcode? diff --git a/app/controllers/concerns/idv/socure_errors_concern.rb b/app/controllers/concerns/idv/socure_errors_concern.rb new file mode 100644 index 00000000000..a26d165b4a6 --- /dev/null +++ b/app/controllers/concerns/idv/socure_errors_concern.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Idv + module SocureErrorsConcern + def errors + @presenter = socure_errors_presenter(handle_stored_result) + end + + def goto_in_person + InPersonEnrollment.find_or_initialize_by( + user: document_capture_session.user, + status: :establishing, + sponsor_id: IdentityConfig.store.usps_ipp_sponsor_id, + ).save! + + redirect_to idv_in_person_url + end + + def remaining_attempts + RateLimiter.new( + user: document_capture_session.user, + rate_limit_type: :idv_doc_auth, + ).remaining_count + end + end +end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 59249f6d9f2..1b8cf732cb4 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -12,7 +12,8 @@ class DocumentCaptureController < ApplicationController before_action :confirm_step_allowed, unless: -> { allow_direct_ipp? } before_action :override_csp_to_allow_acuant before_action :set_usps_form_presenter - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, false) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, false) }, + only: :show def show analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 023ce16fa46..5893501d39b 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -11,7 +11,8 @@ class DocumentCaptureController < ApplicationController before_action :override_csp_to_allow_acuant before_action :confirm_document_capture_needed, only: :show before_action :set_usps_form_presenter - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, true) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, true) }, + only: :show def show analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index 243c4e5d598..2e6a620d405 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -8,10 +8,12 @@ class DocumentCaptureController < ApplicationController include DocumentCaptureConcern include Idv::HybridMobile::HybridMobileConcern include RenderConditionConcern + include SocureErrorsConcern check_or_render_not_found -> { IdentityConfig.store.socure_docv_enabled } before_action :check_valid_document_capture_session, except: [:update] - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, true) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, true) }, + only: :show def show Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]). @@ -63,12 +65,21 @@ def update if result.success? redirect_to idv_hybrid_mobile_capture_complete_url else - redirect_to idv_hybrid_mobile_socure_document_capture_url + redirect_to idv_hybrid_mobile_socure_document_capture_errors_url end end private + def socure_errors_presenter(result) + SocureErrorPresenter.new( + error_code: result.errors[:socure][:reason_codes]&.first, + remaining_attempts: remaining_attempts, + sp_name: decorated_sp_session&.sp_name || APP_NAME, + hybrid_mobile: true, + ) + end + def wait_for_result? return false if stored_result.present? diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb index 1f764f64856..47025b0ff90 100644 --- a/app/controllers/idv/socure/document_capture_controller.rb +++ b/app/controllers/idv/socure/document_capture_controller.rb @@ -7,11 +7,13 @@ class DocumentCaptureController < ApplicationController include IdvStepConcern include DocumentCaptureConcern include RenderConditionConcern + include SocureErrorsConcern check_or_render_not_found -> { IdentityConfig.store.socure_docv_enabled } before_action :confirm_not_rate_limited before_action :confirm_step_allowed - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, false) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, false) }, + only: :show # reconsider and maybe remove these when implementing the real # update handler @@ -79,7 +81,7 @@ def update if result.success? redirect_to idv_ssn_url else - redirect_to idv_socure_document_capture_url + redirect_to idv_socure_document_capture_errors_url end end @@ -107,6 +109,15 @@ def self.step_info private + def socure_errors_presenter(result) + SocureErrorPresenter.new( + error_code: result.errors[:socure][:reason_codes]&.first, + remaining_attempts: remaining_attempts, + sp_name: decorated_sp_session&.sp_name || APP_NAME, + hybrid_mobile: false, + ) + end + def wait_for_result? return false if stored_result.present? diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb index 3b117e56ded..7caa58febf5 100644 --- a/app/models/document_capture_session.rb +++ b/app/models/document_capture_session.rb @@ -21,6 +21,8 @@ def store_result_from_response(doc_auth_response) session_result.attention_with_barcode = doc_auth_response.attention_with_barcode? session_result.doc_auth_success = doc_auth_response.doc_auth_success? session_result.selfie_status = doc_auth_response.selfie_status + session_result.errors = doc_auth_response.errors + EncryptedRedisStructStorage.store( session_result, expires_in: IdentityConfig.store.doc_capture_request_valid_for_minutes.minutes.in_seconds, diff --git a/app/presenters/socure_error_presenter.rb b/app/presenters/socure_error_presenter.rb new file mode 100644 index 00000000000..e138e1db327 --- /dev/null +++ b/app/presenters/socure_error_presenter.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +class SocureErrorPresenter + include Rails.application.routes.url_helpers + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TranslationHelper + include LinkHelper + + attr_reader :url_options + + def initialize(error_code:, remaining_attempts:, sp_name:, hybrid_mobile:) + @error_code = error_code + @remaining_attempts = remaining_attempts + @sp_name = sp_name + @hybrid_mobile = hybrid_mobile + @url_options = {} + end + + def heading + heading_string_for(error_code) + end + + def body_text + error_string_for(error_code) + end + + def rate_limit_text + if remaining_attempts == 1 + t('doc_auth.rate_limit_warning.singular_html') + else + t('doc_auth.rate_limit_warning.plural_html', remaining_attempts: remaining_attempts) + end + end + + def action + url = hybrid_mobile ? idv_hybrid_mobile_socure_document_capture_path + : idv_socure_document_capture_path + { + text: I18n.t('idv.failure.button.warning'), + url: url, + } + end + + def secondary_action_heading + I18n.t('in_person_proofing.headings.cta') + end + + def secondary_action_text + I18n.t('in_person_proofing.body.cta.prompt_detail') + end + + def secondary_action + url = hybrid_mobile ? idv_hybrid_mobile_socure_document_capture_goto_in_person_path + : idv_socure_document_capture_goto_in_person_path + + { + text: I18n.t('in_person_proofing.body.cta.button'), + url: url, + } + end + + def troubleshooting_heading + I18n.t('components.troubleshooting_options.ipp_heading') + end + + def options + [ + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'how-to-add-images-of-your-state-issued-id', + ), + text: I18n.t('idv.troubleshooting.options.doc_capture_tips'), + isExternal: true, + }, + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'accepted-identification-documents', + ), + text: I18n.t('idv.troubleshooting.options.supported_documents'), + isExternal: true, + }, + { + url: return_to_sp_failure_to_proof_url(step: 'document_capture'), + text: t( + 'idv.failure.verify.fail_link_html', + sp_name: sp_name, + ), + isExternal: true, + }, + ] + end + + def step_indicator_steps + Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS + end + + private + + attr_reader :error_code, :remaining_attempts, :sp_name, :hybrid_mobile + + SOCURE_ERROR_MAP = { + 'I848' => 'unreadable_id', + 'I854' => 'unreadable_id', + 'R810' => 'unreadable_id', + 'R820' => 'unreadable_id', + 'R822' => 'unreadable_id', + 'R823' => 'unreadable_id', + 'R824' => 'unreadable_id', + 'R825' => 'unreadable_id', + 'R826' => 'unreadable_id', + 'R831' => 'unreadable_id', + 'R833' => 'unreadable_id', + 'R838' => 'unreadable_id', + 'R859' => 'unreadable_id', + 'R861' => 'unreadable_id', + 'R863' => 'unreadable_id', + + 'I849' => 'unaccepted_id_type', + 'R853' => 'unaccepted_id_type', + 'R862' => 'unaccepted_id_type', + + 'R827' => 'expired_id', + + 'I808' => 'low_resolution', + + 'R845' => 'underage', + + 'I856' => 'id_not_found', + 'R819' => 'id_not_found', + }.freeze + + def remapped_error(error_code) + SOCURE_ERROR_MAP[error_code] || 'unreadable_id' + end + + def heading_string_for(error_code) + # i18n-tasks-use t('doc_auth.headers.unreadable_id') + # i18n-tasks-use t('doc_auth.headers.unaccepted_id_type') + # i18n-tasks-use t('doc_auth.headers.expired_id') + # i18n-tasks-use t('doc_auth.headers.low_resolution') + # i18n-tasks-use t('doc_auth.headers.underage') + # i18n-tasks-use t('doc_auth.headers.id_not_found') + I18n.t("doc_auth.headers.#{remapped_error(error_code)}") + end + + def error_string_for(error_code) + if remapped_error(error_code) == 'underage' # special handling because it says 'Login.gov' + I18n.t('doc_auth.errors.underage', app_name: APP_NAME) + else + # i18n-tasks-use t('doc_auth.errors.unreadable_id') + # i18n-tasks-use t('doc_auth.errors.unaccepted_id_type') + # i18n-tasks-use t('doc_auth.errors.expired_id') + # i18n-tasks-use t('doc_auth.errors.low_resolution') + # i18n-tasks-use t('doc_auth.errors.id_not_found') + I18n.t("doc_auth.errors.#{remapped_error(error_code)}") + end + end +end diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index aaf4413da27..41609231e3a 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -34,6 +34,7 @@ class DocvResultResponse < DocAuth::Response def initialize(http_response:, biometric_comparison_required: false) @http_response = http_response + @biometric_comparison_required = biometric_comparison_required @pii_from_doc = read_pii @@ -73,7 +74,7 @@ def error_messages return {} if successful_result? { - reason_codes: get_data(DATA_PATHS[:reason_codes]), + socure: { reason_codes: get_data(DATA_PATHS[:reason_codes]) }, } end diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb index 31ddc9b5e0b..dc45a26a2c2 100644 --- a/app/services/document_capture_session_result.rb +++ b/app/services/document_capture_session_result.rb @@ -10,11 +10,13 @@ :failed_back_image_fingerprints, :failed_selfie_image_fingerprints, :captured_at, - :doc_auth_success, :selfie_status, + :doc_auth_success, + :selfie_status, + :errors, keyword_init: true, allowed_members: [:id, :success, :attention_with_barcode, :failed_front_image_fingerprints, :failed_back_image_fingerprints, :failed_selfie_image_fingerprints, - :captured_at, :doc_auth_success, :selfie_status] + :captured_at, :doc_auth_success, :selfie_status, :errors], ) do include DocAuth::SelfieConcern diff --git a/app/views/idv/hybrid_mobile/socure/document_capture/errors.html.erb b/app/views/idv/hybrid_mobile/socure/document_capture/errors.html.erb new file mode 100644 index 00000000000..a7210f8471f --- /dev/null +++ b/app/views/idv/hybrid_mobile/socure/document_capture/errors.html.erb @@ -0,0 +1,17 @@ +<%= render( + 'idv/shared/error', + type: :warning, + heading: @presenter.heading, + action: @presenter.action, + secondary_action: @presenter.secondary_action, + current_step: :verify_id, + troubleshooting_heading: @presenter.troubleshooting_heading, + secondary_action_heading: @presenter.secondary_action_heading, + secondary_action_text: @presenter.secondary_action_text, + options: @presenter.options, + step_indicator_steps: @presenter.step_indicator_steps, + ) do +%> +

<%= @presenter.body_text %>

+

<%= @presenter.rate_limit_text %>

+<% end %> diff --git a/app/views/idv/session_errors/state_id_warning.html.erb b/app/views/idv/session_errors/state_id_warning.html.erb index 8534c683d64..183b28a7f8e 100644 --- a/app/views/idv/session_errors/state_id_warning.html.erb +++ b/app/views/idv/session_errors/state_id_warning.html.erb @@ -8,7 +8,7 @@ text: t('idv.warning.state_id.try_again_button'), url: @try_again_path, }, - action_secondary: { + secondary_action: { text: t('idv.warning.state_id.cancel_button', app_name: APP_NAME), url: return_to_sp_failure_to_proof_url(step: 'verify_info', location: 'state_id_warning'), }, diff --git a/app/views/idv/shared/_error.html.erb b/app/views/idv/shared/_error.html.erb index f2cff69cb3d..f9668ab2344 100644 --- a/app/views/idv/shared/_error.html.erb +++ b/app/views/idv/shared/_error.html.erb @@ -5,17 +5,24 @@ locals: * heading: Primary page heading text. Also used as page title if title is not given. * title: Optional custom page title, defaulting to the heading. * action: Optional hash of `text`, `url`, optional `method` of a primary action link. -* action_secondary: Optional hash of `text`, `url`, optional `method` of a secondary action link. +* secondary_action: Optional hash of `text`, `url`, optional `method` of a secondary action link. * current_step: Optionally identify the current step of the IdV flow. If provided, the step indicator will be rendered. * options: Array of troubleshooting options. %> <% if local_assigns.fetch(:type, :error) == :error icon_name = :error - troubleshooting_heading = t('idv.troubleshooting.headings.need_assistance') -else - icon_name = :warning - troubleshooting_heading = t('components.troubleshooting_options.default_heading') -end %> + else + icon_name = :warning + end + + troubleshooting_heading = local_assigns.fetch(:troubleshooting_heading) do + if local_assigns.fetch(:type, :error) == :error + troubleshooting_heading = t('idv.troubleshooting.headings.need_assistance') + else + troubleshooting_heading = t('components.troubleshooting_options.default_heading') + end + end + %> <% self.title = local_assigns.fetch(:title, heading) %> @@ -46,13 +53,23 @@ end %> ) %> - <% if local_assigns[:action_secondary] %> + <% if local_assigns[:secondary_action] %> +
+ + <% if local_assigns[:secondary_action_heading] %> +

<%= local_assigns[:secondary_action_heading] %>

+ <% end %> + + <% if local_assigns[:secondary_action_text] %> +

<%= local_assigns[:secondary_action_text] %>

+ <% end %> +
<%= button_or_link_to( - action_secondary[:text], - action_secondary[:url], + secondary_action[:text], + secondary_action[:url], class: 'usa-button usa-button--big usa-button--wide usa-button--outline', - method: action_secondary[:method], + method: secondary_action[:method], ) %>
<% end %> diff --git a/app/views/idv/socure/document_capture/errors.html.erb b/app/views/idv/socure/document_capture/errors.html.erb new file mode 100644 index 00000000000..741e7852045 --- /dev/null +++ b/app/views/idv/socure/document_capture/errors.html.erb @@ -0,0 +1,17 @@ +<%= render( + 'idv/shared/error', + type: :warning, + heading: @presenter.heading, + action: @presenter.action, + secondary_action: @presenter.secondary_action, + current_step: :verify_id, + troubleshooting_heading: @presenter.troubleshooting_heading, + secondary_action_heading: @presenter.secondary_action_heading, + secondary_action_text: @presenter.secondary_action_text, + options: @presenter.options, + step_indicator_steps: @presenter.step_indicator_steps, + ) do +%> +

<%= @presenter.body_text %>

+

<%= @presenter.rate_limit_text %>

+<% end %> diff --git a/app/views/idv/welcome/show.html.erb b/app/views/idv/welcome/show.html.erb index 39653251a16..6663f75943b 100644 --- a/app/views/idv/welcome/show.html.erb +++ b/app/views/idv/welcome/show.html.erb @@ -36,18 +36,18 @@ method: 'put', html: { 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( - 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') %> <% end %> <%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/config/locales/en.yml b/config/locales/en.yml index f528ad32963..5c2c3b285a1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -537,6 +537,7 @@ doc_auth.errors.document_capture_canceled: You have canceled uploading photos of doc_auth.errors.dpi.failed_short: Image is too small or blurry, please try again. doc_auth.errors.dpi.top_msg: We couldn’t read your ID. Your image size may be too small, or your ID is too small or blurry in the photo. Make sure your ID is large within the image frame and try taking a new picture. doc_auth.errors.dpi.top_msg_plural: We couldn’t read your ID. Your image sizes may be too small, or your ID is too small or blurry in the photos. Make sure your ID is large within the image frame and try taking new pictures. +doc_auth.errors.expired_id: You cannot use an expired ID card for identity verification. If your ID card is not expired, make sure everything on your ID is in focus and that the card fills the frame. doc_auth.errors.file_type.invalid: This file type is not accepted, please choose a JPG or PNG file. doc_auth.errors.general.fallback_field_level: Please add a new image doc_auth.errors.general.multiple_back_id_failures: We couldn’t verify the back of your ID. Try taking a new picture. @@ -555,6 +556,8 @@ doc_auth.errors.http.image_size.failed_short: Image file is not supported, pleas doc_auth.errors.http.image_size.top_msg: Your image size is too large or too small. Please add images of your ID that are about 2025 x 1275 pixels. doc_auth.errors.http.pixel_depth.failed_short: Image file is not supported, please try again. doc_auth.errors.http.pixel_depth.top_msg: The pixel depth of your image file is not supported. Please take new photos of your ID and try again. Supported image pixel depth is 24-bit RGB. +doc_auth.errors.id_not_found: Make sure you are taking a picture of your physical ID and not of a photo or screenshot. +doc_auth.errors.low_resolution: We couldn’t verify your ID because your device could not take a clear enough photo. If this error keeps occurring, try using another device or verify your ID at a local Post Office. doc_auth.errors.not_a_file: The selection was not a valid file. doc_auth.errors.phone_step_incomplete: You must go to your phone and upload photos of your ID before continuing. We sent you a link with instructions. doc_auth.errors.pii.birth_date_min_age: Your birthday does not meet the minimum age requirement. @@ -567,11 +570,20 @@ doc_auth.errors.send_link_limited: You tried too many times, please try again in doc_auth.errors.sharpness.failed_short: Image is blurry, please try again. doc_auth.errors.sharpness.top_msg: We couldn’t read your ID. Your photo may be too blurry or dark. Try taking a new picture in a bright area. doc_auth.errors.sharpness.top_msg_plural: We couldn’t read your ID. Your photos may be too blurry or dark. Try taking new pictures in a bright area. +doc_auth.errors.unaccepted_id_type: We do not accept passports, military IDs, paper IDs or temporary IDs. You may only use a driver’s license or an ID issued by a U.S. state or territory. +doc_auth.errors.underage: You must be at least 13 years old to use %{app_name}. If you are 13 or older, try again and make sure your date of birth is in focus. +doc_auth.errors.unreadable_id: Take your photos in a well-lit area without shadows or glare. Make sure that everything on your ID is in focus and that the card fills the frame. doc_auth.errors.upload_error: Sorry, something went wrong on our end. doc_auth.forms.change_file: Change file doc_auth.forms.choose_file_html: Drag file here or choose from folder doc_auth.forms.doc_success: We verified your information doc_auth.forms.selected_file: Selected file +doc_auth.headers.expired_id: Your ID may have expired +doc_auth.headers.id_not_found: We couldn’t find your ID. +doc_auth.headers.low_resolution: Your device’s camera may not be supported. +doc_auth.headers.unaccepted_id_type: Use a driver’s license or a state ID +doc_auth.headers.underage: Age requirement not met +doc_auth.headers.unreadable_id: We could not read your ID doc_auth.headings.address: Update your mailing address doc_auth.headings.back: Back of your driver’s license or state ID doc_auth.headings.capture_complete: We verified your ID @@ -685,6 +697,8 @@ doc_auth.instructions.text1: Other forms of ID are not accepted. We’ll check t doc_auth.instructions.text2: You will not need your physical SSN card. doc_auth.instructions.text3: We match your phone number with your personal information and send a one-time code to your phone. doc_auth.instructions.text4: Your password saves and encrypts your personal information. +doc_auth.rate_limit_warning.plural_html: For security reasons, you have %{remaining_attempts} attempts remaining. +doc_auth.rate_limit_warning.singular_html: For security reasons, you have 1 attempt remaining. doc_auth.tips.document_capture_hint: Must be a JPG or PNG doc_auth.tips.document_capture_id_text1: Use a flat and dark surface doc_auth.tips.document_capture_id_text2: Take photos in a well-lit place diff --git a/config/locales/es.yml b/config/locales/es.yml index 3640e2aa666..c7ee38961c2 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -548,6 +548,7 @@ doc_auth.errors.document_capture_canceled: Ha cancelado la carga de fotos de su doc_auth.errors.dpi.failed_short: La imagen es demasiado pequeña o está borrosa; inténtelo de nuevo. doc_auth.errors.dpi.top_msg: No pudimos leer su identificación. Es posible que el tamaño de su imagen o de su identificación sea demasiado pequeño o que la foto esté borrosa. Asegúrese de que su identificación se vea más grande dentro del marco de la imagen e intente tomar una nueva foto. doc_auth.errors.dpi.top_msg_plural: No pudimos leer su identificación. Es posible que el tamaño de sus imágenes o de su identificación sea demasiado pequeño o que las fotos estén borrosas. Asegúrese de que su identificación se vea grande dentro del marco de la imagen e intente tomar nuevas fotos. +doc_auth.errors.expired_id: No puede usar una tarjeta de identificación vencida para verificar su identidad. Si su tarjeta de identificación no está vencida, revise que la información en esta se vea nítida y que la tarjeta llene el marco. doc_auth.errors.file_type.invalid: No se acepta este tipo de archivo; elija un archivo JPG o PNG. doc_auth.errors.general.fallback_field_level: Agregue una imagen nueva doc_auth.errors.general.multiple_back_id_failures: No pudimos verificar el reverso de su identificación. Intente tomar una nueva foto. @@ -566,6 +567,8 @@ doc_auth.errors.http.image_size.failed_short: El archivo de la imagen no es comp doc_auth.errors.http.image_size.top_msg: El tamaño de la imagen es demasiado grande o demasiado pequeño. Añada imágenes de su identificación de unos 2025 x 1275 píxeles. doc_auth.errors.http.pixel_depth.failed_short: El archivo de la imagen no es compatible; inténtelo de nuevo. doc_auth.errors.http.pixel_depth.top_msg: La profundidad de píxel de su archivo de imagen no es compatible. Tome nuevas fotos de su identificación e inténtelo de nuevo. La profundidad de píxel de la imagen admitida es RGB de 24 bits. +doc_auth.errors.id_not_found: Verifique que está tomando una foto de su identificación física y no de una fotografía o captura de pantalla. +doc_auth.errors.low_resolution: No pudimos verificar su identificación porque su dispositivo no tomó una fotografía bastante clara. Si se sigue presentando este error, use otro dispositivo o verifique su identidad en una oficina de correos de su localidad. doc_auth.errors.not_a_file: La selección no era un archivo válido. doc_auth.errors.phone_step_incomplete: Debe ir a su teléfono y cargar fotos de su identificación antes de continuar. Le enviamos un vínculo con instrucciones. doc_auth.errors.pii.birth_date_min_age: Su fecha de nacimiento no cumple con el requisito de edad mínima. @@ -578,11 +581,20 @@ doc_auth.errors.send_link_limited: Lo intentó demasiadas veces; vuelva a intent doc_auth.errors.sharpness.failed_short: La imagen está borrosa; inténtelo de nuevo. doc_auth.errors.sharpness.top_msg: No pudimos leer su identificación. Es posible que su foto esté demasiado borrosa u oscura. Intente tomar una nueva foto en un lugar iluminado. doc_auth.errors.sharpness.top_msg_plural: No pudimos leer su identificación. Es posible que sus fotos estén demasiado borrosas u oscuras. Intente tomar nuevas fotos en un lugar iluminado. +doc_auth.errors.unaccepted_id_type: No aceptamos pasaportes, identificaciones militares, ni identificaciones impresas o temporales. Solo puede usar una licencia de conducir o una identificación emitida por un estado o territorio de los EE. UU. +doc_auth.errors.underage: Debe tener al menos 13 años para usar %{app_name}. Si tiene 13 años o más, vuelva a intentarlo y verifique que su fecha de nacimiento se vea nítida. +doc_auth.errors.unreadable_id: Tome sus fotografías en un lugar bien iluminado sin sombras ni reflejos. Revise que la información en su identificación se vea nítida y que la tarjeta llene el marco. doc_auth.errors.upload_error: Lo sentimos, algo no funcionó bien. doc_auth.forms.change_file: Cambiar archivo doc_auth.forms.choose_file_html: Arrastrar el archivo aquí o seleccionarlo de la carpeta doc_auth.forms.doc_success: Verificamos su información doc_auth.forms.selected_file: Archivo seleccionado +doc_auth.headers.expired_id: Su identificación puede estar vencida +doc_auth.headers.id_not_found: No pudimos encontrar su identificación +doc_auth.headers.low_resolution: Es posible que la cámara de su dispositivo no sea compatible +doc_auth.headers.unaccepted_id_type: Use una licencia de conducir o una identificación estatal +doc_auth.headers.underage: No se cumplió con el requisito de edad +doc_auth.headers.unreadable_id: No pudimos leer su identificación doc_auth.headings.address: Actualice su dirección postal doc_auth.headings.back: Reverso de su licencia de conducir o identificación estatal doc_auth.headings.capture_complete: Verificamos su identificación @@ -696,6 +708,8 @@ doc_auth.instructions.text1: No se aceptan otras formas de identificación. Revi doc_auth.instructions.text2: No necesita la tarjeta física del Seguro Social. doc_auth.instructions.text3: Revisamos que su número de teléfono coincida con su información personal y enviamos un código de un solo uso a su teléfono. doc_auth.instructions.text4: Su contraseña guarda y cifra su información personal. +doc_auth.rate_limit_warning.plural_html: Por motivos de seguridad, le quedan %{remaining_attempts} intentos. +doc_auth.rate_limit_warning.singular_html: Por motivos de seguridad, le queda un intento. doc_auth.tips.document_capture_hint: Debe ser un archivo JPG o PNG doc_auth.tips.document_capture_id_text1: Use una superficie plana y de color oscuro. doc_auth.tips.document_capture_id_text2: Tome fotos en un lugar bien iluminado diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 72ae134d8f5..4b2af9f2435 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -537,6 +537,7 @@ doc_auth.errors.document_capture_canceled: Vous avez annulé le téléchargement doc_auth.errors.dpi.failed_short: Image trop petite ou floue, veuillez réessayer. doc_auth.errors.dpi.top_msg: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que votre image soit de trop petite taille ou que votre pièce d’identité soit trop petite ou floue sur la photo. Assurez-vous que votre pièce d’identité remplisse le cadre de l’image, puis essayez de prendre une nouvelle photo. doc_auth.errors.dpi.top_msg_plural: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que votre image soit de trop petite taille ou que votre pièce d’identité soit trop petite ou floue sur les photos. Assurez-vous que votre pièce d’identité remplisse le cadre de l’image, puis essayez de prendre une nouvelle photo. +doc_auth.errors.expired_id: Il n’est pas possible d’utiliser une pièce d’identité périmée pour confirmer votre identité. Si votre pièce d’identité est toujours valable, veillez à ce que tous les renseignements y figurant soient nets et que l’ensemble de la carte remplisse le cadre. doc_auth.errors.file_type.invalid: Ce type de fichier n’est pas accepté ; veuillez choisir un fichier JPG ou PNG. doc_auth.errors.general.fallback_field_level: Veuillez ajouter une nouvelle image doc_auth.errors.general.multiple_back_id_failures: Nous n’avons pas pu vérifier le verso de votre pièce d’identité. Essayez de prendre une nouvelle photo. @@ -555,6 +556,8 @@ doc_auth.errors.http.image_size.failed_short: Le fichier image n’est pas pris doc_auth.errors.http.image_size.top_msg: La taille de votre image est trop grande ou trop petite. Veuillez ajouter des images de votre pièce d’identité d’environ 2 025 x 1 275 pixels. doc_auth.errors.http.pixel_depth.failed_short: Le fichier image n’est pas pris en charge, veuillez réessayer. doc_auth.errors.http.pixel_depth.top_msg: La profondeur de pixel de votre fichier image n’est pas prise en charge. Veuillez prendre de nouvelles photos de votre pièce d’identité et réessayer. La profondeur de pixel de l’image prise en charge est de 24 bits RGB. +doc_auth.errors.id_not_found: Veillez à prendre une photo de votre pièce d’identité physique et non d’une photo ou d’une capture d’écran. +doc_auth.errors.low_resolution: Nous n’avons pas pu vérifier votre identité car votre appareil n’a pas pris une photo suffisamment claire. Si cette erreur persiste, utilisez un autre appareil ou confirmez votre identité dans un bureau de poste local. doc_auth.errors.not_a_file: La sélection n’était pas un fichier valide. doc_auth.errors.phone_step_incomplete: Vous devez aller sur votre téléphone et télécharger des photos de votre pièce d’identité avant de continuer. Nous vous avons envoyé un lien avec des instructions. doc_auth.errors.pii.birth_date_min_age: Votre anniversaire ne correspond pas à l’âge minimum requis. @@ -567,11 +570,20 @@ doc_auth.errors.send_link_limited: Vous avez essayé trop de fois, veuillez rée doc_auth.errors.sharpness.failed_short: L’image est floue, veuillez réessayer. doc_auth.errors.sharpness.top_msg: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que votre photo soit trop floue ou trop sombre. Essayez de prendre une nouvelle photo dans un endroit bien éclairé. doc_auth.errors.sharpness.top_msg_plural: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que vos photos soient trop floues ou trop sombres. Essayez de prendre de nouvelles photos dans un endroit bien éclairé. +doc_auth.errors.unaccepted_id_type: Nous n’acceptons pas de passeports, de cartes d’identité militaires, de pièces d’identité papier ou provisoires. Vous pouvez uniquement utiliser un permis de conduire ou une pièce d’identité délivré par un État ou un territoire des États-Unis. +doc_auth.errors.underage: Vous devez être âgé de 13 ans au moins pour utiliser %{app_name}. Si vous avez 13 ans ou plus, réessayez et assurez-vous que votre date de naissance est bien visible. +doc_auth.errors.unreadable_id: Prenez vos photos dans un endroit bien éclairé sans ombre ou reflet. Veillez à ce que tous les renseignements figurant sur votre pièce d’identité soient nets et que l’ensemble de la pièce soit visible à l’intérieur du cadre. doc_auth.errors.upload_error: Désolé, il y a eu un problème de notre côté. doc_auth.forms.change_file: Changer de fichier doc_auth.forms.choose_file_html: Faites glisser le fichier ici ou choisissez dans un dossier doc_auth.forms.doc_success: Nous avons vérifié vos informations doc_auth.forms.selected_file: Fichier sélectionné +doc_auth.headers.expired_id: Votre pièce d’identité est peut-être périmée +doc_auth.headers.id_not_found: Nous n’avons pas trouvé votre pièce d’identité +doc_auth.headers.low_resolution: Il se peut que la caméra de votre appareil ne soit pas prise en charge +doc_auth.headers.unaccepted_id_type: Utiliser un permis de conduire ou une carte d’identité d’un État +doc_auth.headers.underage: Condition d’âge non remplie +doc_auth.headers.unreadable_id: Nous n’avons pas pu lire votre pièce d’identité doc_auth.headings.address: Mettre à jour votre adresse postale doc_auth.headings.back: Verso de votre permis de conduire ou de votre carte d’identité d’un État doc_auth.headings.capture_complete: Nous avons vérifié votre pièce d’identité @@ -685,6 +697,8 @@ doc_auth.instructions.text1: Les autres pièces d’identité ne sont pas accept doc_auth.instructions.text2: Vous n’aurez pas besoin de votre carte de sécurité sociale papier. doc_auth.instructions.text3: Nous comparons votre numéro de téléphone à vos informations personnelles et vous envoyons un code à usage unique sur votre téléphone. doc_auth.instructions.text4: Votre mot de passe s’enregistre et chiffre vos informations personnelles. +doc_auth.rate_limit_warning.plural_html: Pour des raisons de sécurité, il vous reste %{remaining_attempts} tentatives. +doc_auth.rate_limit_warning.singular_html: Pour des raisons de sécurité, il vous reste une tentative. doc_auth.tips.document_capture_hint: Doit être au format JPG ou PNG doc_auth.tips.document_capture_id_text1: Utilisez une surface plane et foncée doc_auth.tips.document_capture_id_text2: Prenez vos photos dans un endroit bien éclairé diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 62cd87be425..90ee4f2ac8c 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -548,6 +548,7 @@ doc_auth.errors.document_capture_canceled: 你已取消了用手机上传身份 doc_auth.errors.dpi.failed_short: 图像太小或模糊,请再试一次。 doc_auth.errors.dpi.top_msg: 我们无法辨认你的身份证件。你的图像尺寸可能太小,或者照片中你的身份证件太小或太模糊。确保你的身份证件在图框中比较大,然后试着重拍一下。 doc_auth.errors.dpi.top_msg_plural: 我们无法读取你的身份证件。你的图像尺寸可能太小,或者照片中你的身份证件太小或模糊。确保你的身份证件在图片框中很大,然后试着拍一张新照片。 +doc_auth.errors.expired_id: 你不能使用过期的身份证件进行身份验证。如果你的身份证件未过期,请确保你的身份证件上的所有内容对焦,而且身份卡填满画面。 doc_auth.errors.file_type.invalid: 这一文件类型我们不接受。请选择一个 JPG 或 PNG 文件。 doc_auth.errors.general.fallback_field_level: 请添加一个新图像 doc_auth.errors.general.multiple_back_id_failures: 我们无法验证你身份证件的背面。尝试重拍一张。 @@ -566,6 +567,8 @@ doc_auth.errors.http.image_size.failed_short: 系统不支持图像文件,请 doc_auth.errors.http.image_size.top_msg: 你的图像尺寸太大或太小。请添加你身份证件的图像,其像素应约为 2025 x 1275。 doc_auth.errors.http.pixel_depth.failed_short: 系统不支持图像文件,请再试一次。 doc_auth.errors.http.pixel_depth.top_msg: 你图像文件的像素深度系统不支持。请重拍你的身份证件并再试一次。系统支持的图像像素深度为 24位 RGB。 +doc_auth.errors.id_not_found: 确保你拍摄的是你的实体身份证件,而不是照片或屏幕截图。 +doc_auth.errors.low_resolution: 我们无法验证你的身份,因为你设备拍摄的照片不够清晰。如果这个错误持续发生,请使用其他设备或在当地邮局验证你的身份。 doc_auth.errors.not_a_file: 你选择的不是一个正确的文件。 doc_auth.errors.phone_step_incomplete: 在继续之前你必须使用手机上传你身份证件的图片。我们已给你发了带有说明的链接。 doc_auth.errors.pii.birth_date_min_age: 你的生日不满足最低年龄要求。 @@ -578,11 +581,20 @@ doc_auth.errors.send_link_limited: 你尝试了太多次。请在 %{timeout}后 doc_auth.errors.sharpness.failed_short: 图像模糊,请再试一次。 doc_auth.errors.sharpness.top_msg: 我们无法读取你的身份证件。你的照片可能太模糊或太暗。尝试在明亮的地方重拍一张。 doc_auth.errors.sharpness.top_msg_plural: 我们无法读取你的身份证件。你的照片可能太模糊或太暗。尝试在明亮的地方重拍一张。 +doc_auth.errors.unaccepted_id_type: 我们不接受护照、军人身份证件、纸质身份证件或临时身份证件。你只能使用驾驶执照或美国州或领地颁发的身份证件。 +doc_auth.errors.underage: 你必须年满 13 岁才能使用 %{app_name}。如果你年满 13 岁,请重试并确保你的出生日期对焦。 +doc_auth.errors.unreadable_id: 请在光线充足而且没有阴影或眩光的地方拍照。确保你身份证件上的所有内容都清晰对焦,并且身份卡填满画面。 doc_auth.errors.upload_error: 抱歉,我们这边出错了。 doc_auth.forms.change_file: 更改文件 doc_auth.forms.choose_file_html: 将文件拖到此处或者从文件夹中选择。 doc_auth.forms.doc_success: 我们验证了你的信息 doc_auth.forms.selected_file: 被选文件 +doc_auth.headers.expired_id: 你的身份证件可能已过期 +doc_auth.headers.id_not_found: 我们找不到你的身份证件 +doc_auth.headers.low_resolution: 你设备的相机可能不受支持 +doc_auth.headers.unaccepted_id_type: 使用驾驶执照或州颁发的身份证件 +doc_auth.headers.underage: 不符合年龄规定 +doc_auth.headers.unreadable_id: 我们无法读取你的身份证件 doc_auth.headings.address: 更新你的邮政地址 doc_auth.headings.back: 驾照或州政府颁发身份证件的背面。 doc_auth.headings.capture_complete: 我们验证了你的身份证件 @@ -696,6 +708,8 @@ doc_auth.instructions.text1: 其他形式的身份证件不被接受。我们要 doc_auth.instructions.text2: 不需要你社会保障卡实体。 doc_auth.instructions.text3: 我们把你的电话号码与个人信息匹配,并向你的电话发送一个一次性代码。 doc_auth.instructions.text4: 你的密码对你个人信息进行存储并加密。 +doc_auth.rate_limit_warning.plural_html: 出于安全原因,你还有 %{remaining_attempts} 次尝试机会。 +doc_auth.rate_limit_warning.singular_html: 出于安全原因,你还有 1 次尝试机会。 doc_auth.tips.document_capture_hint: 必须是 JPG 或 PNG doc_auth.tips.document_capture_id_text1: 使用水平和深色表面 doc_auth.tips.document_capture_id_text2: 在光线明亮的地方拍照 diff --git a/config/routes.rb b/config/routes.rb index a01b1257562..f30ff1f5936 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -361,6 +361,8 @@ put '/document_capture' => 'document_capture#update' get '/socure/document_capture' => 'socure/document_capture#show' get '/socure/document_capture_update' => 'socure/document_capture#update', as: :socure_document_capture_update + get '/socure/document_capture_errors' => 'socure/document_capture#errors', as: :socure_document_capture_errors + get '/socure/document_capture_goto_in_person' => 'socure/document_capture#goto_in_person', as: :socure_document_capture_goto_in_person # 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 @@ -369,6 +371,8 @@ get '/hybrid_mobile/capture_complete' => 'hybrid_mobile/capture_complete#show' get '/hybrid_mobile/socure/document_capture' => 'hybrid_mobile/socure/document_capture#show' get '/hybrid_mobile/socure/document_capture_update' => 'hybrid_mobile/socure/document_capture#update', as: :hybrid_mobile_socure_document_capture_update + get '/hybrid_mobile/socure/document_capture_errors' => 'hybrid_mobile/socure/document_capture#errors', as: :hybrid_mobile_socure_document_capture_errors + get '/hybrid_mobile/socure/document_capture_goto_in_person' => 'hybrid_mobile/socure/document_capture#goto_in_person', as: :hybrid_mobile_socure_document_capture_goto_in_person get '/hybrid_handoff' => 'hybrid_handoff#show' put '/hybrid_handoff' => 'hybrid_handoff#update' get '/link_sent' => 'link_sent#show' diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 88d1ca61523..f3792613efc 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -365,6 +365,7 @@ expect(controller).to receive(:selfie_requirement_met?). and_return(performed_if_needed) allow(result).to receive(:success?).and_return(true) + allow(result).to receive(:errors).and_return(result[:errors]) allow(subject).to receive(:stored_result).and_return(result) allow(subject).to receive(:extract_pii_from_doc) end @@ -374,6 +375,7 @@ it 'stays on document capture' do put :update + expect(response).to redirect_to idv_document_capture_url end end diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index ba96dbcfbb4..0f0a7fa17bf 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -297,10 +297,10 @@ ) end - it 'redirects back to the capture page' do + it 'redirects to the error page' do get(:update) - expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_url) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index fea657a0e01..f45a7413c5d 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -153,6 +153,7 @@ allow(load_result).to receive(:success?).and_return(load_result_success) allow(load_result).to receive(:selfie_check_performed?).and_return(false) + allow(load_result).to receive(:errors).and_return({ message: 'an error message' }) document_capture_session = DocumentCaptureSession.create!( user: user, @@ -195,7 +196,6 @@ it 'flashes an error and does not redirect' do put :update - expect(flash[:error]).to eq t('doc_auth.errors.phone_step_incomplete') expect(response.status).to eq(204) end end @@ -227,7 +227,6 @@ put :update expect(response).to redirect_to(idv_hybrid_handoff_url) - expect(flash[:error]).to eq(error_message) end end @@ -238,7 +237,6 @@ put :update expect(response).to have_http_status(204) - expect(flash[:error]).to eq(t('doc_auth.errors.phone_step_incomplete')) end end end diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb index e431b493a6a..fd2c9a5fa52 100644 --- a/spec/controllers/idv/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb @@ -272,9 +272,11 @@ end describe '#update' do - it 'returns FOUND (302) and redirects to SSN' do - get(:update) + before do + get :update + end + it 'returns FOUND (302) and redirects to SSN' do expect(response).to redirect_to(idv_ssn_path) expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end @@ -282,10 +284,10 @@ context 'when doc auth fails' do let(:doc_auth_success) { false } - it 'redirects to document capture' do + it 'renders the errors' do get(:update) - expect(response).to redirect_to(idv_socure_document_capture_path) + expect(response).to redirect_to idv_socure_document_capture_errors_url expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end end @@ -316,8 +318,6 @@ let(:socure_docv_enabled) { false } it 'the webhook route does not exist' do - get(:update) - expect(response).to be_not_found end end diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb index ca3e1702bb9..0ef78772666 100644 --- a/spec/features/idv/doc_auth/socure_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -24,7 +24,6 @@ allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) @docv_transaction_token = stub_docv_document_request - stub_docv_verification_data_pass end before(:all) do @@ -33,115 +32,181 @@ after(:all) { @user.destroy } - context 'standard desktop flow' do + context 'happy path' do before do - visit_idp_from_oidc_sp_with_ial2 - sign_in_and_2fa_user(@user) - complete_doc_auth_steps_before_document_capture_step - click_idv_continue + stub_docv_verification_data_pass end - context 'rate limits calls to backend docauth vendor', allow_browser_log: true do + context 'standard desktop flow' do before do - allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) - (max_attempts - 1).times do - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + click_idv_continue + end + + context 'rate limits calls to backend docauth vendor', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + (max_attempts - 1).times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end + end + + it 'redirects to the rate limited error page' do + expect(page).to have_current_path(fake_socure_document_capture_app_url) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_socure_document_capture_path) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + expect(fake_analytics).to have_logged_event( + 'Rate Limit Reached', + limiter_type: :idv_doc_auth, + ) + end + + context 'successfully processes image on last attempt' do + before do + DocAuth::Mock::DocAuthMockClient.reset! + end + + it 'proceeds to the next page with valid info' do + expect(page).to have_current_path(fake_socure_document_capture_app_url) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_socure_document_capture_path) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + visit idv_socure_document_capture_update_path + expect(page).to have_current_path(idv_ssn_url) + + visit idv_socure_document_capture_path + + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + end + end + end + + # ToDo post LG-14010 + context 'network connection errors' do + xit 'catches network connection errors on document request', allow_browser_log: true do + # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end + + xit 'catches network connection errors on verification data request', + allow_browser_log: true do + # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) end end - it 'redirects to the rate limited error page' do - expect(page).to have_current_path(fake_socure_document_capture_app_url) - visit idv_socure_document_capture_path - expect(page).to have_current_path(idv_socure_document_capture_path) + it 'does not track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false) socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) - visit idv_socure_document_capture_path - expect(page).to have_current_path(idv_session_errors_rate_limited_path) - expect(fake_analytics).to have_logged_event( - 'Rate Limit Reached', - limiter_type: :idv_doc_auth, + + expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil + end + + xit 'does track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(true) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, ) + + expect(DocAuthLog.find_by(user_id: @user.id).state).not_to be_nil end + end - context 'successfully processes image on last attempt' do - before do - DocAuth::Mock::DocAuthMockClient.reset! - end + context 'standard mobile flow' do + it 'proceeds to the next page with valid info' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step - it 'proceeds to the next page with valid info' do - expect(page).to have_current_path(fake_socure_document_capture_app_url) - visit idv_socure_document_capture_path - expect(page).to have_current_path(idv_socure_document_capture_path) + expect(page).to have_current_path(idv_socure_document_capture_url) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + click_idv_continue socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) - visit idv_socure_document_capture_update_path expect(page).to have_current_path(idv_ssn_url) - visit idv_socure_document_capture_path + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') - expect(page).to have_current_path(idv_session_errors_rate_limited_path) + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) end end end + end - # ToDo post LG-14010 - context 'network connection errors' do - xit 'catches network connection errors on document request', allow_browser_log: true do - # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) - end + # ToDo post LG-14010 + xcontext 'network connection errors' do + it 'catches network connection errors on document request', allow_browser_log: true do + expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end - xit 'catches network connection errors on verification data request', - allow_browser_log: true do - # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) - end + it 'catches network connection errors on verification data request', + allow_browser_log: true do + expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) end + end - it 'does not track state if state tracking is disabled' do - allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false) - socure_docv_upload_documents( - docv_transaction_token: @docv_transaction_token, - ) + shared_examples 'a properly categorized Socure error' do |socure_error_code, expected_header_key| + before do + stub_docv_verification_data_fail_with([socure_error_code]) - expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil - end + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step - xit 'does track state if state tracking is disabled' do - allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(true) + click_idv_continue socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) + visit idv_socure_document_capture_update_path + end - expect(DocAuthLog.find_by(user_id: @user.id).state).not_to be_nil + it 'shows the correct error page' do + expect(page).to have_content(t(expected_header_key)) end end - context 'standard mobile flow' do - it 'proceeds to the next page with valid info' do - perform_in_browser(:mobile) do - visit_idp_from_oidc_sp_with_ial2 - sign_in_and_2fa_user(@user) - complete_doc_auth_steps_before_document_capture_step + context 'a type 1 error (because we do not recognize the code)' do + it_behaves_like 'a properly categorized Socure error', 'XXXX', 'doc_auth.headers.unreadable_id' + end - expect(page).to have_current_path(idv_socure_document_capture_url) - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - click_idv_continue - socure_docv_upload_documents( - docv_transaction_token: @docv_transaction_token, - ) - visit idv_socure_document_capture_update_path - expect(page).to have_current_path(idv_ssn_url) + context 'a type 1 error' do + it_behaves_like 'a properly categorized Socure error', 'I848', 'doc_auth.headers.unreadable_id' + end + + context 'a type 2 error' do + it_behaves_like 'a properly categorized Socure error', 'I849', 'doc_auth.headers.unaccepted_id_type' + end - expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') + context 'a type 3 error' do + it_behaves_like 'a properly categorized Socure error', 'R827', 'doc_auth.headers.expired_id' + end - fill_out_ssn_form_ok - click_idv_continue - complete_verify_step - expect(page).to have_current_path(idv_phone_url) - end - end + context 'a type 4 error' do + it_behaves_like 'a properly categorized Socure error', 'I808', 'doc_auth.headers.low_resolution' + end + + context 'a type 5 error' do + it_behaves_like 'a properly categorized Socure error', 'R845', 'doc_auth.headers.underage' + end + + context 'a type 6 error' do + it_behaves_like 'a properly categorized Socure error', 'I856', 'doc_auth.headers.id_not_found' end def expect_rate_limited_header(expected_to_be_present) @@ -169,13 +234,13 @@ def expect_rate_limited_header(expected_to_be_present) allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).and_return(false) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return( - in_person_proofing_opt_in_enabled, - ) + in_person_proofing_opt_in_enabled, + ) allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). and_return([service_provider.issuer]) allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([service_provider.issuer]) + :allowed_valid_authn_contexts_semantic_providers, + ).and_return([service_provider.issuer]) allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(false) allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true) diff --git a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb index d2da26e722f..63cbb90a69c 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb @@ -26,160 +26,253 @@ @docv_transaction_token = stub_docv_document_request end - it 'proofs and hands off to mobile', js: true do - user = nil + context 'happy path' do + it 'proofs and hands off to mobile', js: true do + user = nil - perform_in_browser(:desktop) do - visit_idp_from_sp_with_ial2(sp) - user = sign_up_and_2fa_ial1_user + perform_in_browser(:desktop) do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - expect(page).to have_content(t('doc_auth.headings.text_message')) - expect(page).to have_content(t('doc_auth.info.you_entered')) - expect(page).to have_content('+1 415-555-0199') + expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') - # Confirm that Continue button is not shown when polling is enabled - expect(page).not_to have_content(t('doc_auth.buttons.continue')) - end + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) + end - expect(@sms_link).to be_present + expect(@sms_link).to be_present - perform_in_browser(:mobile) do - visit @sms_link + perform_in_browser(:mobile) do + visit @sms_link - # Confirm that jumping to LinkSent page does not cause errors - visit idv_link_sent_url - expect(page).to have_current_path(root_url) + # Confirm that jumping to LinkSent page does not cause errors + visit idv_link_sent_url + expect(page).to have_current_path(root_url) - # Confirm that we end up on the LN / Mock page even if we try to - # go to the Socure one. - visit idv_hybrid_mobile_socure_document_capture_url - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_socure_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - # Confirm that clicking cancel and then coming back doesn't cause errors - click_link 'Cancel' - visit idv_hybrid_mobile_socure_document_capture_url + # Confirm that clicking cancel and then coming back doesn't cause errors + click_link 'Cancel' + visit idv_hybrid_mobile_socure_document_capture_url - # Confirm that jumping to Phone page does not cause errors - visit idv_phone_url - expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_socure_document_capture_url + # Confirm that jumping to Phone page does not cause errors + visit idv_phone_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url - # Confirm that jumping to Welcome page does not cause errors - visit idv_welcome_url - expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_socure_document_capture_url + # Confirm that jumping to Welcome page does not cause errors + visit idv_welcome_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - stub_docv_verification_data_pass - click_idv_continue - expect(page).to have_current_path(fake_socure_document_capture_app_url) - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) - visit idv_hybrid_mobile_socure_document_capture_update_url + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - # To be fixed in app: - # Confirm app disallows jumping back to DocumentCapture page - # visit idv_hybrid_mobile_socure_document_capture_url - # expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - end + # To be fixed in app: + # Confirm app disallows jumping back to DocumentCapture page + # visit idv_hybrid_mobile_socure_document_capture_url + # expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end - perform_in_browser(:desktop) do - expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) - expect(page).to have_current_path(idv_ssn_path) + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + expect(page).to have_current_path(idv_ssn_path) - fill_out_ssn_form_ok - click_idv_continue + fill_out_ssn_form_ok + click_idv_continue - expect(page).to have_content(t('headings.verify')) - complete_verify_step + expect(page).to have_content(t('headings.verify')) + complete_verify_step - prefilled_phone = page.find(id: 'idv_phone_form_phone').value + prefilled_phone = page.find(id: 'idv_phone_form_phone').value - expect( - PhoneFormatter.format(prefilled_phone), - ).to eq( - PhoneFormatter.format(user.default_phone_configuration.phone), - ) + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(user.default_phone_configuration.phone), + ) - fill_out_phone_form_ok - verify_phone_otp + fill_out_phone_form_ok + verify_phone_otp - fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD - click_idv_continue + fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD + click_idv_continue - acknowledge_and_confirm_personal_key + acknowledge_and_confirm_personal_key - validate_idv_completed_page(user) - click_agree_and_continue + validate_idv_completed_page(user) + click_agree_and_continue - validate_return_to_sp + validate_return_to_sp + end end - end - it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do - user = nil + it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do + user = nil - perform_in_browser(:desktop) do - user = sign_in_and_2fa_user - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - expect(page).to have_content(t('doc_auth.headings.text_message')) - end + expect(page).to have_content(t('doc_auth.headings.text_message')) + end - expect(@sms_link).to be_present + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + click_on t('links.cancel') + click_on t('forms.buttons.cancel') # Yes, cancel + end + + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - perform_in_browser(:mobile) do - visit @sms_link - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - click_on t('links.cancel') - click_on t('forms.buttons.cancel') # Yes, cancel + expect(page).to have_content(t('doc_auth.headings.text_message')) + end end - perform_in_browser(:desktop) do - expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + context 'user is rate limited on mobile' do + let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: DocAuth::Response.new( + success: false, + errors: { network: I18n.t('doc_auth.errors.general.network_error') }, + ), + ) + end + + it 'shows capture complete on mobile and error page on desktop', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present - expect(page).to have_content(t('doc_auth.headings.text_message')) + perform_in_browser(:mobile) do + visit @sms_link + + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + stub_docv_verification_data_pass + max_attempts.times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end + + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + end + end end - end - context 'user is rate limited on mobile' do - let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } - - before do - allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :post_front_image, - response: DocAuth::Response.new( - success: false, - errors: { network: I18n.t('doc_auth.errors.general.network_error') }, - ), - ) + it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do + user = create(:user, :with_authentication_app) + + perform_in_browser(:desktop) do + start_idv_from_sp(facial_match_required: false) + sign_in_and_2fa_user(user) + + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_content(t('headings.verify')) + complete_verify_step + + prefilled_phone = page.find(id: 'idv_phone_form_phone').value + + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(phone_number), + ) + end end + end - it 'shows capture complete on mobile and error page on desktop', js: true do + shared_examples 'a properly categorized Socure error' do |socure_error_code, expected_header_key| + it 'shows the correct error page', js: true do user = nil perform_in_browser(:desktop) do - user = sign_in_and_2fa_user + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user + complete_doc_auth_steps_before_hybrid_handoff_step clear_and_fill_in(:doc_auth_phone, phone_number) click_send_link expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') + + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) end expect(@sms_link).to be_present @@ -187,69 +280,96 @@ perform_in_browser(:mobile) do visit @sms_link + stub_docv_verification_data_fail_with([socure_error_code]) + click_idv_continue - expect(page).to have_current_path(fake_socure_document_capture_app_url) - stub_docv_verification_data_pass - max_attempts.times do - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) - end + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) visit idv_hybrid_mobile_socure_document_capture_update_url - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect(page).to have_text(t(expected_header_key)) + + click_try_again + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_path) end perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + expect(page).to have_current_path(idv_link_sent_path) end end end - it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do - user = create(:user, :with_authentication_app) + context 'a type 1 error (because we do not recognize the code)' do + it_behaves_like 'a properly categorized Socure error', 'XXXX', 'doc_auth.headers.unreadable_id' + end - perform_in_browser(:desktop) do - start_idv_from_sp(facial_match_required: false) - sign_in_and_2fa_user(user) + context 'a type 1 error' do + it_behaves_like 'a properly categorized Socure error', 'I848', 'doc_auth.headers.unreadable_id' + end - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link - end + context 'a type 2 error' do + it_behaves_like 'a properly categorized Socure error', 'I849', 'doc_auth.headers.unaccepted_id_type' + end - expect(@sms_link).to be_present + context 'a type 3 error' do + it_behaves_like 'a properly categorized Socure error', 'R827', 'doc_auth.headers.expired_id' + end - perform_in_browser(:mobile) do - visit @sms_link + context 'a type 4 error' do + it_behaves_like 'a properly categorized Socure error', 'I808', 'doc_auth.headers.low_resolution' + end - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - stub_docv_verification_data_pass - click_idv_continue - expect(page).to have_current_path(fake_socure_document_capture_app_url) - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) - visit idv_hybrid_mobile_socure_document_capture_update_url + context 'a type 5 error' do + it_behaves_like 'a properly categorized Socure error', 'R845', 'doc_auth.headers.underage' + end - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) - end + context 'a type 6 error' do + it_behaves_like 'a properly categorized Socure error', 'I856', 'doc_auth.headers.id_not_found' + end - perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_ssn_path, wait: 10) + context 'with a network error' do + it 'proofs and hands off to mobile', js: true do + user = nil + + perform_in_browser(:desktop) do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user - fill_out_ssn_form_ok - click_idv_continue + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - expect(page).to have_content(t('headings.verify')) - complete_verify_step + expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') - prefilled_phone = page.find(id: 'idv_phone_form_phone').value + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) + end - expect( - PhoneFormatter.format(prefilled_phone), - ).to eq( - PhoneFormatter.format(phone_number), - ) + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + stub_docv_network_error + + click_idv_continue + + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_text(t('doc_auth.errors.general.network_error')) + + click_try_again + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_path) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_link_sent_path) + end end end end diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index f5b82332515..9143136c3fe 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -107,6 +107,10 @@ def stub_docv_verification_data_pass stub_docv_verification_data(body: SocureDocvFixtures.pass_json) end + def stub_docv_verification_data_fail_with(errors) + stub_docv_verification_data(body: SocureDocvFixtures.fail_json(errors)) + end + def stub_docv_verification_data(body:) stub_request(:post, "#{IdentityConfig.store.socure_idplus_base_url}/api/3.0/EmailAuthScore"). to_return( diff --git a/spec/support/socure_docv_fixtures.rb b/spec/support/socure_docv_fixtures.rb index a0e6ab756c4..1a4bb4e256a 100644 --- a/spec/support/socure_docv_fixtures.rb +++ b/spec/support/socure_docv_fixtures.rb @@ -7,6 +7,16 @@ def pass_json JSON.parse(raw).to_json end + def fail_json(errors) + raw = read_fixture_file_at_path('pass.json') + body = JSON.parse(raw) + + body['documentVerification']['decision']['value'] = 'reject' + body['documentVerification']['reasonCodes'] = errors + + body.to_json + end + private def read_fixture_file_at_path(filepath) diff --git a/spec/views/idv/shared/_error.html.erb_spec.rb b/spec/views/idv/shared/_error.html.erb_spec.rb index 20a0fdf5a10..89cb5c1452f 100644 --- a/spec/views/idv/shared/_error.html.erb_spec.rb +++ b/spec/views/idv/shared/_error.html.erb_spec.rb @@ -5,7 +5,7 @@ let(:options) { [{ text: 'Example', url: '#example' }] } let(:heading) { 'Error' } let(:action) { nil } - let(:action_secondary) { nil } + let(:secondary_action) { nil } let(:type) { nil } let(:current_step) { nil } let(:step_indicator_steps) { nil } @@ -14,7 +14,7 @@ type: type, heading: heading, action: action, - action_secondary: action_secondary, + secondary_action: secondary_action, current_step: current_step, options: options, } @@ -73,7 +73,7 @@ end context 'with secondary action' do - let(:action_secondary) { { text: 'Secondary Action', url: '#secondary' } } + let(:secondary_action) { { text: 'Secondary Action', url: '#secondary' } } it 'renders secondary action button' do expect(rendered).to have_link('Secondary Action', href: '#secondary') @@ -81,7 +81,7 @@ end context 'with form action' do - let(:action_secondary) { { text: 'Delete', url: '#delete', method: :delete } } + let(:secondary_action) { { text: 'Delete', url: '#delete', method: :delete } } it 'renders action button' do expect(rendered).to have_button('Delete')