diff --git a/app/components/vendor_outage_alert_component.html.erb b/app/components/vendor_outage_alert_component.html.erb new file mode 100644 index 00000000000..abc68baa15f --- /dev/null +++ b/app/components/vendor_outage_alert_component.html.erb @@ -0,0 +1,6 @@ +<% if content %> + <%= render(AlertComponent.new(type: :error, class: 'margin-bottom-4')) do %> + <%= content %> + <%= new_window_link_to t('vendor_outage.get_updates'), StatusPage.base_url %> + <% end %> +<% end %> diff --git a/app/components/vendor_outage_alert_component.rb b/app/components/vendor_outage_alert_component.rb new file mode 100644 index 00000000000..dbf7a21d104 --- /dev/null +++ b/app/components/vendor_outage_alert_component.rb @@ -0,0 +1,42 @@ +class VendorOutageAlertComponent < BaseComponent + include LinkHelper + + def initialize(vendors:, only_if_all: false, context: 'default') + @vendors = vendors + @only_if_all = only_if_all + @context = context + end + + def content + case outages.sort + when [:sms, :voice] + # i18n-tasks-use t('vendor_outage.alerts.phone.default') + # i18n-tasks-use t('vendor_outage.alerts.phone.idv') + t(context, scope: 'vendor_outage.alerts.phone', default: :default) + when [:sms] + # i18n-tasks-use t('vendor_outage.alerts.sms.default') + # i18n-tasks-use t('vendor_outage.alerts.sms.idv') + t(context, scope: 'vendor_outage.alerts.sms', default: :default) + when [:voice] + # i18n-tasks-use t('vendor_outage.alerts.voice.default') + # i18n-tasks-use t('vendor_outage.alerts.voice.idv') + t(context, scope: 'vendor_outage.alerts.voice', default: :default) + end + end + + private + + attr_reader :vendors, :only_if_all, :context + + def outages + if only_if_all + vendor_status.all_vendor_outage?(vendors) ? vendors : [] + else + vendors.select { |vendor| vendor_status.vendor_outage?(vendor) } + end + end + + def vendor_status + @vendor_status ||= VendorStatus.new + end +end diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index e53ad15e7b2..dc18bbd8cef 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -49,7 +49,11 @@ def max_attempts_reached def redirect_to_next_step if phone_confirmation_required? - redirect_to idv_otp_delivery_method_url + if VendorStatus.new.all_phone_vendor_outage? + redirect_to vendor_outage_path(from: :idv_phone) + else + redirect_to idv_otp_delivery_method_url + end else redirect_to idv_review_url end diff --git a/app/controllers/users/phones_controller.rb b/app/controllers/users/phones_controller.rb index 4b83ae2b489..acf547bd741 100644 --- a/app/controllers/users/phones_controller.rb +++ b/app/controllers/users/phones_controller.rb @@ -3,6 +3,7 @@ class PhonesController < ReauthnRequiredController include PhoneConfirmation before_action :confirm_two_factor_authenticated + before_action :redirect_if_phone_vendor_outage def add user_session[:phone_id] = nil @@ -21,6 +22,11 @@ def create private + def redirect_if_phone_vendor_outage + return unless VendorStatus.new.all_phone_vendor_outage? + redirect_to vendor_outage_path(from: :users_phones) + end + def user_params params.require(:new_phone_form).permit( :phone, :international_code, diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index a2e9627f067..a3a991191b2 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -3,6 +3,7 @@ class TwoFactorAuthenticationController < ApplicationController include TwoFactorAuthenticatable before_action :check_remember_device_preference + before_action :redirect_to_vendor_outage_if_phone_only, only: [:show] def show service_provider_mfa_requirement_redirect || non_phone_redirect || phone_redirect || @@ -33,7 +34,7 @@ def non_phone_redirect end def phone_redirect - return unless phone_enabled? + return unless phone_enabled? && !VendorStatus.new.any_phone_vendor_outage? validate_otp_delivery_preference_and_send_code true end @@ -44,7 +45,15 @@ def backup_code_redirect end def redirect_on_nothing_enabled - redirect_to two_factor_options_url + # "Nothing enabled" can mean one of two things: + # 1. The user hasn't yet set up MFA, and should be redirected to setup path. + # 2. The user has set up MFA, but none of the redirect options are currently available (e.g. + # vendor outage), and they should be sent to the MFA selection path. + if MfaPolicy.new(current_user).two_factor_enabled? + redirect_to login_two_factor_options_path + else + redirect_to two_factor_options_url + end end def phone_enabled? @@ -117,6 +126,13 @@ def redirect_to_otp_verification_with_error ) end + def redirect_to_vendor_outage_if_phone_only + return unless VendorStatus.new.all_phone_vendor_outage? && + phone_enabled? && + !MfaPolicy.new(current_user).multiple_factors_enabled? + redirect_to vendor_outage_path(from: :two_factor_authentication) + end + def capture_analytics_for_exception(telephony_error) attributes = { error: telephony_error.class.to_s, diff --git a/app/controllers/vendor_outage_controller.rb b/app/controllers/vendor_outage_controller.rb index 55f20ff42ea..ab47989c85e 100644 --- a/app/controllers/vendor_outage_controller.rb +++ b/app/controllers/vendor_outage_controller.rb @@ -6,6 +6,19 @@ def show from_idv: session.delete(:vendor_outage_redirect_from_idv), ) @specific_message = vendor_status.outage_message + @show_gpo_option = from_idv_phone? && gpo_letter_available? vendor_status.track_event(analytics) end + + private + + def from_idv_phone? + params[:from] == 'idv_phone' + end + + def gpo_letter_available? + FeatureManagement.enable_gpo_verification? && + current_user && + !Idv::GpoMail.new(current_user).mail_spammed? + end end diff --git a/app/forms/new_phone_form.rb b/app/forms/new_phone_form.rb index f4789b727a7..ddfe3ac6f59 100644 --- a/app/forms/new_phone_form.rb +++ b/app/forms/new_phone_form.rb @@ -33,11 +33,11 @@ def submit(params) end def delivery_preference_sms? - true + !VendorStatus.new.vendor_outage?(:sms) end def delivery_preference_voice? - false + VendorStatus.new.vendor_outage?(:sms) end def already_has_phone? diff --git a/app/javascript/packs/form-steps-wait.jsx b/app/javascript/packs/form-steps-wait.jsx index aecfae17475..fc0f8bf404a 100644 --- a/app/javascript/packs/form-steps-wait.jsx +++ b/app/javascript/packs/form-steps-wait.jsx @@ -106,7 +106,8 @@ export class FormStepsWait { this.scheduleNextPollFetch(); } else { const message = getPageErrorMessage(dom); - if (message) { + const isSamePage = new URL(response.url).pathname === window.location.pathname; + if (message && isSamePage) { this.renderError(message); this.stopSpinner(); } else { diff --git a/app/presenters/two_factor_authentication/phone_selection_presenter.rb b/app/presenters/two_factor_authentication/phone_selection_presenter.rb index 4f581630180..c8f9dddcaa5 100644 --- a/app/presenters/two_factor_authentication/phone_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/phone_selection_presenter.rb @@ -31,6 +31,10 @@ def security_level t('two_factor_authentication.two_factor_choice_options.less_secure_label') end + def disabled? + VendorStatus.new.all_phone_vendor_outage? + end + private def masked_number(number) diff --git a/app/presenters/two_factor_authentication/selection_presenter.rb b/app/presenters/two_factor_authentication/selection_presenter.rb index 4d9d1553e9d..8894bd716f5 100644 --- a/app/presenters/two_factor_authentication/selection_presenter.rb +++ b/app/presenters/two_factor_authentication/selection_presenter.rb @@ -26,6 +26,10 @@ def html_class '' end + def disabled? + false + end + private def option_mode diff --git a/app/presenters/two_factor_authentication/sms_selection_presenter.rb b/app/presenters/two_factor_authentication/sms_selection_presenter.rb index 41cdc209005..3b0c122c0f7 100644 --- a/app/presenters/two_factor_authentication/sms_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/sms_selection_presenter.rb @@ -3,5 +3,9 @@ class SmsSelectionPresenter < PhoneSelectionPresenter def method :sms end + + def disabled? + VendorStatus.new.vendor_outage?(:sms) + end end end diff --git a/app/presenters/two_factor_authentication/voice_selection_presenter.rb b/app/presenters/two_factor_authentication/voice_selection_presenter.rb index d4ddb0ec883..6144f27f023 100644 --- a/app/presenters/two_factor_authentication/voice_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/voice_selection_presenter.rb @@ -3,5 +3,9 @@ class VoiceSelectionPresenter < PhoneSelectionPresenter def method :voice end + + def disabled? + VendorStatus.new.vendor_outage?(:voice) + end end end diff --git a/app/presenters/two_factor_login_options_presenter.rb b/app/presenters/two_factor_login_options_presenter.rb index 35cc27d30fe..283d650bee4 100644 --- a/app/presenters/two_factor_login_options_presenter.rb +++ b/app/presenters/two_factor_login_options_presenter.rb @@ -71,6 +71,10 @@ def cancel_link end end + def first_enabled_option_index + options.find_index { |option| !option.disabled? } || 0 + end + private def account_reset_link diff --git a/app/services/vendor_status.rb b/app/services/vendor_status.rb index 7be45a7a3bb..b4ea9f07f08 100644 --- a/app/services/vendor_status.rb +++ b/app/services/vendor_status.rb @@ -1,4 +1,6 @@ class VendorStatus + include ActionView::Helpers::TranslationHelper + def initialize(from: nil, from_idv: nil, sp: nil) @from = from @from_idv = from_idv @@ -6,7 +8,8 @@ def initialize(from: nil, from_idv: nil, sp: nil) end IAL2_VENDORS = %i[acuant lexisnexis_instant_verify lexisnexis_trueid].freeze - ALL_VENDORS = (IAL2_VENDORS + %i[sms voice]).freeze + PHONE_VENDORS = %i[sms voice].freeze + ALL_VENDORS = (IAL2_VENDORS + PHONE_VENDORS).freeze def vendor_outage?(vendor) status = case vendor @@ -30,10 +33,22 @@ def any_vendor_outage?(vendors = ALL_VENDORS) vendors.any? { |vendor| vendor_outage?(vendor) } end + def all_vendor_outage?(vendors = ALL_VENDORS) + vendors.all? { |vendor| vendor_outage?(vendor) } + end + def any_ial2_vendor_outage? any_vendor_outage?(IAL2_VENDORS) end + def any_phone_vendor_outage? + any_vendor_outage?(PHONE_VENDORS) + end + + def all_phone_vendor_outage? + all_vendor_outage?(PHONE_VENDORS) + end + def from_idv? from_idv end @@ -46,16 +61,19 @@ def outage_message if any_ial2_vendor_outage? if from_idv? if sp - return I18n.t( - 'vendor_outage.idv_blocked.with_sp', - service_provider: sp.friendly_name, - ) + t('vendor_outage.blocked.idv.with_sp', service_provider: sp.friendly_name) else - return I18n.t('vendor_outage.idv_blocked.without_sp') + t('vendor_outage.blocked.idv.without_sp') end + else + t('vendor_outage.blocked.idv.generic') + end + elsif any_phone_vendor_outage? + if from_idv? + t('vendor_outage.blocked.phone.idv') + else + t('vendor_outage.blocked.phone.default') end - - return I18n.t('vendor_outage.idv_blocked.generic') end end diff --git a/app/views/idv/doc_auth/welcome.html.erb b/app/views/idv/doc_auth/welcome.html.erb index d0d1f2f28c8..5274156be89 100644 --- a/app/views/idv/doc_auth/welcome.html.erb +++ b/app/views/idv/doc_auth/welcome.html.erb @@ -1,5 +1,7 @@ <% title t('titles.doc_auth.verify') %> +<%= render(VendorOutageAlertComponent.new(vendors: [:sms, :voice], only_if_all: true, context: :idv)) %> + <% step = 0 %> <%= render 'shared/maintenance_window_alert' do %> diff --git a/app/views/idv/otp_delivery_method/new.html.erb b/app/views/idv/otp_delivery_method/new.html.erb index d052d637fb0..593e2ddc863 100644 --- a/app/views/idv/otp_delivery_method/new.html.erb +++ b/app/views/idv/otp_delivery_method/new.html.erb @@ -9,6 +9,8 @@ <% title t('titles.doc_auth.otp_delivery') %> +<%= render(VendorOutageAlertComponent.new(vendors: [:sms, :voice], context: :idv)) %> +

<%= t('idv.titles.otp_delivery_method') %>

@@ -28,6 +30,7 @@ 'otp_delivery_preference', :sms, false, + disabled: VendorStatus.new.vendor_outage?(:sms), class: 'usa-radio__input usa-radio__input--tile', ) %>