diff --git a/app/controllers/idv/by_mail/sp_follow_up_controller.rb b/app/controllers/idv/by_mail/sp_follow_up_controller.rb
new file mode 100644
index 00000000000..86381f6e2da
--- /dev/null
+++ b/app/controllers/idv/by_mail/sp_follow_up_controller.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Idv
+ module ByMail
+ class SpFollowUpController < ApplicationController
+ include Idv::AvailabilityConcern
+
+ before_action :confirm_two_factor_authenticated
+ before_action :confirm_needs_sp_follow_up
+
+ def new
+ analytics.track_event(:idv_by_mail_sp_follow_up_visited, **analytics_params)
+ @presenter = Idv::ByMail::SpFollowUpPresenter.new(current_user:)
+ end
+
+ def show
+ analytics.track_event(:idv_by_mail_sp_follow_up_submitted, **analytics_params)
+
+ sp_return_url_resolver = SpReturnUrlResolver.new(
+ service_provider: current_user.active_profile.initiating_service_provider,
+ )
+ redirect_url = sp_return_url_resolver.post_idv_follow_up_url ||
+ sp_return_url_resolver.return_to_sp_url
+ redirect_to(redirect_url, allow_other_host: true)
+ end
+
+ def cancel
+ analytics.track_event(:idv_by_mail_sp_follow_up_cancelled, **analytics_params)
+ redirect_to account_url
+ end
+
+ private
+
+ def analytics_params
+ initiating_service_provider = current_user.active_profile.initiating_service_provider
+ {
+ initiating_service_provider: initiating_service_provider.issuer,
+ }
+ end
+
+ def confirm_needs_sp_follow_up
+ return if current_user.identity_verified? &&
+ current_user.active_profile.initiating_service_provider.present? &&
+ !current_sp.present?
+ redirect_to account_url
+ end
+ end
+ end
+end
diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb
index 358b1f97fd0..8599d8291b8 100644
--- a/app/controllers/idv/personal_key_controller.rb
+++ b/app/controllers/idv/personal_key_controller.rb
@@ -72,6 +72,8 @@ def next_step
idv_please_call_url
elsif session[:sp]
sign_up_completed_url
+ elsif idv_session.address_verification_mechanism == 'gpo'
+ idv_sp_follow_up_path
else
after_sign_in_path_for(current_user)
end
diff --git a/app/presenters/idv/by_mail/sp_follow_up_presenter.rb b/app/presenters/idv/by_mail/sp_follow_up_presenter.rb
new file mode 100644
index 00000000000..b63cc7b2a78
--- /dev/null
+++ b/app/presenters/idv/by_mail/sp_follow_up_presenter.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Idv
+ module ByMail
+ class SpFollowUpPresenter
+ include ActionView::Helpers::TranslationHelper
+
+ attr_reader :current_user
+
+ def initialize(current_user:)
+ @current_user = current_user
+ end
+
+ def heading
+ t(
+ 'idv.by_mail.sp_follow_up.heading',
+ service_provider: initiating_service_provider_name,
+ )
+ end
+
+ def body
+ t(
+ 'idv.by_mail.sp_follow_up.body',
+ service_provider: initiating_service_provider_name,
+ app_name: APP_NAME,
+ )
+ end
+
+ private
+
+ def initiating_service_provider_name
+ initiating_service_provider.friendly_name
+ end
+
+ def initiating_service_provider
+ @initiating_service_provider ||= current_user.active_profile.initiating_service_provider
+ end
+ end
+ end
+end
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index d5f8016451d..82bb250b27d 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -820,15 +820,32 @@ def gpo_confirmation_upload(
# User visited sign-in URL from the "You've been successfully verified email" CTA button
# @param issuer [String] the ServiceProvider.issuer
# @param campaign_id [String] the email campaign ID
+ # @param [Hash,nil] proofing_components User's current proofing components
+ # @option proofing_components [String,nil] 'document_check' Vendor that verified the user's ID
+ # @option proofing_components [String,nil] 'document_type' Type of ID used to verify
+ # @option proofing_components [String,nil] 'source_check' Source used to verify user's PII
+ # @option proofing_components [String,nil] 'resolution_check' Vendor for identity resolution check
+ # @option proofing_components [String,nil] 'address_check' Method used to verify user's address
+ # @option proofing_components [Boolean,nil] 'threatmetrix' Whether ThreatMetrix check was done
+ # @option proofing_components [String,nil] 'threatmetrix_review_status' TMX decision on the user
+ # @param [String,nil] active_profile_idv_level ID verification level of user's active profile.
+ # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile.
def idv_account_verified_cta_visited(
issuer:,
campaign_id:,
+ proofing_components: nil,
+ active_profile_idv_level: nil,
+ pending_profile_idv_level: nil,
**extra
)
track_event(
:idv_account_verified_cta_visited,
issuer:,
campaign_id:,
+ proofing_components:,
+ active_profile_idv_level:,
+ pending_profile_idv_level:,
+
**extra,
)
end
@@ -1032,6 +1049,36 @@ def idv_barcode_warning_retake_photos_clicked(liveness_checking_required:, **ext
)
end
+ # @param [String] initiating_service_provider The service provider the user needs to connect to
+ # The user chose not to connect their account from the SP follow-up page
+ def idv_by_mail_sp_follow_up_cancelled(initiating_service_provider:, **extra)
+ track_event(
+ :idv_by_mail_sp_follow_up_cancelled,
+ initiating_service_provider:,
+ **extra,
+ )
+ end
+
+ # @param [String] initiating_service_provider The service provider the user needs to connect to
+ # The user chose to connect their account from the SP follow-up page
+ def idv_by_mail_sp_follow_up_submitted(initiating_service_provider:, **extra)
+ track_event(
+ :idv_by_mail_sp_follow_up_submitted,
+ initiating_service_provider:,
+ **extra,
+ )
+ end
+
+ # @param [String] initiating_service_provider The service provider the user needs to connect to
+ # The user visited the SP follow-up page
+ def idv_by_mail_sp_follow_up_visited(initiating_service_provider:, **extra)
+ track_event(
+ :idv_by_mail_sp_follow_up_visited,
+ initiating_service_provider:,
+ **extra,
+ )
+ end
+
# @param [Hash] error
def idv_camera_info_error(error:, **_extra)
track_event(:idv_camera_info_error, error: error)
diff --git a/app/views/idv/by_mail/sp_follow_up/new.html.erb b/app/views/idv/by_mail/sp_follow_up/new.html.erb
new file mode 100644
index 00000000000..972f905773c
--- /dev/null
+++ b/app/views/idv/by_mail/sp_follow_up/new.html.erb
@@ -0,0 +1,24 @@
+<% self.title = @presenter.heading %>
+
+
+ <%= image_tag(asset_url('user-access.svg'), width: '280', height: '91', alt: '', aria: { hidden: true }) %>
+
<%= @presenter.heading %>
+
+
+<%= @presenter.body %>
+
+
+ <%= render ButtonComponent.new(
+ url: idv_sp_follow_up_connect_path,
+ big: true,
+ wide: true,
+ ).with_content(t('idv.by_mail.sp_follow_up.connect_account')) %>
+
+
+ <%= render ButtonComponent.new(
+ url: idv_sp_follow_up_cancel_path,
+ big: true,
+ wide: true,
+ outline: true,
+ ).with_content(t('idv.by_mail.sp_follow_up.go_to_account')) %>
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7593fbd2ff2..45a49d27a8e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1011,6 +1011,10 @@ idv.buttons.change_ssn_label: Update Social Security number
idv.buttons.change_state_id_label: Update state ID
idv.buttons.continue_plain: Continue
idv.buttons.mail.send: Request a letter
+idv.by_mail.sp_follow_up.body: Sign back in to %{service_provider} to connect your verified %{app_name} account and access services.
+idv.by_mail.sp_follow_up.connect_account: Connect your account
+idv.by_mail.sp_follow_up.go_to_account: Go to account
+idv.by_mail.sp_follow_up.heading: Connect to %{service_provider}
idv.cancel.actions.account_page: Go to account page
idv.cancel.actions.exit: Exit %{app_name}
idv.cancel.actions.keep_going: No, keep going
diff --git a/config/locales/es.yml b/config/locales/es.yml
index baffc5ec289..9457e7e8892 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1022,6 +1022,10 @@ idv.buttons.change_ssn_label: Actualizar número de Seguro Social
idv.buttons.change_state_id_label: Actualizar identificación estatal
idv.buttons.continue_plain: Continuar
idv.buttons.mail.send: Solicitar una carta
+idv.by_mail.sp_follow_up.body: Vuelva a iniciar sesión en %{service_provider} para conectar su cuenta verificada de %{app_name} y acceder a los servicios.
+idv.by_mail.sp_follow_up.connect_account: Conecte su cuenta
+idv.by_mail.sp_follow_up.go_to_account: Ir a la cuenta
+idv.by_mail.sp_follow_up.heading: Conéctese a %{service_provider}
idv.cancel.actions.account_page: Ir a la página de la cuenta
idv.cancel.actions.exit: Salir de %{app_name}
idv.cancel.actions.keep_going: No, continuar
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index dc2ae99a5ed..cf5e61f9ecb 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1011,6 +1011,10 @@ idv.buttons.change_ssn_label: Mettre à jour votre numéro de sécurité sociale
idv.buttons.change_state_id_label: Mettre à jour votre pièce d’identité
idv.buttons.continue_plain: Suite
idv.buttons.mail.send: Demander une lettre
+idv.by_mail.sp_follow_up.body: Connectez-vous à nouveau à %{service_provider} pour y associer votre compte %{app_name} vérifié et accéder aux services de cet organisme.
+idv.by_mail.sp_follow_up.connect_account: Associer votre compte
+idv.by_mail.sp_follow_up.go_to_account: Aller sur le compte
+idv.by_mail.sp_follow_up.heading: Vous connecter à %{service_provider}
idv.cancel.actions.account_page: Aller à la page de votre compte
idv.cancel.actions.exit: Quitter %{app_name}
idv.cancel.actions.keep_going: Non, continuer
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index ea75d8f839a..3eb536b37d5 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -1024,6 +1024,10 @@ idv.buttons.change_ssn_label: 更新社会保障号码
idv.buttons.change_state_id_label: 更新州颁发的身份证件
idv.buttons.continue_plain: 继续
idv.buttons.mail.send: 要求发一封信
+idv.by_mail.sp_follow_up.body: 重新登录 %{service_provider}以连接你验证过的%{app_name}账户并获得服务。
+idv.by_mail.sp_follow_up.connect_account: 连接你的账户
+idv.by_mail.sp_follow_up.go_to_account: 前往账户
+idv.by_mail.sp_follow_up.heading: 连接 %{service_provider}
idv.cancel.actions.account_page: 到账户页面
idv.cancel.actions.exit: 退出 %{app_name}
idv.cancel.actions.keep_going: 不是,继续
diff --git a/config/routes.rb b/config/routes.rb
index f30ff1f5936..78c4010b282 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -451,6 +451,9 @@
end
get '/by_mail/letter_enqueued' => 'by_mail/letter_enqueued#show', as: :letter_enqueued
+ get '/by_mail/sp_follow_up' => 'by_mail/sp_follow_up#new', as: :sp_follow_up
+ get '/by_mail/sp_follow_up/connect' => 'by_mail/sp_follow_up#show', as: :sp_follow_up_connect
+ get '/by_mail/sp_follow_up/cancel' => 'by_mail/sp_follow_up#cancel', as: :sp_follow_up_cancel
# We re-mapped `/verify/by_mail` to `/verify/by_mail/enter_code`. However, we sent emails to
# users with a link to `/verify/by_mail?did_not_receive_letter=1`. We need to continue
diff --git a/spec/controllers/idv/by_mail/sp_follow_up_controller_spec.rb b/spec/controllers/idv/by_mail/sp_follow_up_controller_spec.rb
new file mode 100644
index 00000000000..9005379e672
--- /dev/null
+++ b/spec/controllers/idv/by_mail/sp_follow_up_controller_spec.rb
@@ -0,0 +1,83 @@
+require 'rails_helper'
+
+RSpec.describe Idv::ByMail::SpFollowUpController do
+ let(:post_idv_follow_up_url) { 'https://example.com/follow_up' }
+ let(:initiating_service_provider) { create(:service_provider, post_idv_follow_up_url:) }
+ let(:user) { create(:user, :fully_registered) }
+ let!(:profile) { create(:profile, :active, user:, initiating_service_provider:) }
+
+ before do
+ stub_sign_in(user) if user.present?
+ stub_analytics
+ end
+
+ describe '#new' do
+ context 'the user has not finished verification' do
+ let(:profile) do
+ create(:profile, :verify_by_mail_pending, user:, initiating_service_provider:)
+ end
+
+ it 'redirects to the account page' do
+ get :new
+
+ expect(response).to redirect_to(account_url)
+ end
+ end
+
+ context 'the user has an SP in the session' do
+ before do
+ allow(controller).to receive(:current_sp).and_return(initiating_service_provider)
+ end
+
+ it 'redirects to the account page' do
+ get :new
+
+ expect(response).to redirect_to(account_url)
+ end
+ end
+
+ context 'the user does not have an initiating service provider' do
+ let(:profile) { create(:profile, :active, user:, initiating_service_provider: nil) }
+
+ it 'redirects to the account page' do
+ get :new
+
+ expect(response).to redirect_to(account_url)
+ end
+ end
+
+ it 'logs analytics and renders the template' do
+ get :new
+
+ expect(response).to render_template(:new)
+ expect(@analytics).to have_logged_event(
+ :idv_by_mail_sp_follow_up_visited,
+ initiating_service_provider: initiating_service_provider.issuer,
+ )
+ end
+ end
+
+ describe '#show' do
+ it 'logs analytics and redirects to the service provider' do
+ get :show
+
+ expect(response).to redirect_to(post_idv_follow_up_url)
+ expect(@analytics).to have_logged_event(
+ :idv_by_mail_sp_follow_up_submitted,
+ initiating_service_provider: initiating_service_provider.issuer,
+ )
+ end
+ end
+
+ describe '#cancel' do
+ it 'logs analytics and redirects to the account URL' do
+ get :cancel
+
+ expect(response).to redirect_to(account_url)
+ expect(@analytics).to have_logged_event(
+ :idv_by_mail_sp_follow_up_cancelled,
+ initiating_service_provider: initiating_service_provider.issuer,
+ )
+ end
+ end
+end
diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb
index 8238bc1b0f6..6f37ec787c1 100644
--- a/spec/controllers/idv/personal_key_controller_spec.rb
+++ b/spec/controllers/idv/personal_key_controller_spec.rb
@@ -483,14 +483,43 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs)
context 'user selected gpo verification' do
let(:address_verification_mechanism) { 'gpo' }
- it 'redirects to correct url' do
- patch :update
- expect(response).to redirect_to idv_letter_enqueued_url
+ context 'when the user requested a letter this session' do
+ it 'redirects to correct url' do
+ patch :update
+ expect(response).to redirect_to idv_letter_enqueued_url
+ end
+
+ it 'does not log any events' do
+ expect(@analytics).not_to have_logged_event
+ patch :update
+ end
end
- it 'does not log any events' do
- expect(@analytics).not_to have_logged_event
- patch :update
+ context 'when the user entered a GPO code' do
+ before do
+ pending_profile = user.pending_profile
+ pending_profile.remove_gpo_deactivation_reason
+ pending_profile.activate
+ end
+
+ it 'redirects to correct url' do
+ patch :update
+ expect(response).to redirect_to idv_sp_follow_up_url
+ end
+
+ it 'logs analytics' do
+ patch :update
+
+ expect(@analytics).to have_logged_event(
+ 'IdV: personal key submitted',
+ hash_including(
+ address_verification_method: 'gpo',
+ fraud_review_pending: false,
+ fraud_rejection: false,
+ in_person_verification_pending: false,
+ ),
+ )
+ end
end
end
diff --git a/spec/features/idv/sp_follow_up_spec.rb b/spec/features/idv/sp_follow_up_spec.rb
new file mode 100644
index 00000000000..a8f5da4e6c7
--- /dev/null
+++ b/spec/features/idv/sp_follow_up_spec.rb
@@ -0,0 +1,106 @@
+require 'rails_helper'
+require 'action_account'
+
+RSpec.feature 'returning to an SP after out-of-band proofing' do
+ scenario 'receiving an email after entering a verify-by-mail code' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :verify_by_mail_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+ otp = 'ABC123'
+ create(
+ :gpo_confirmation_code,
+ profile: profile,
+ otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
+ created_at: 2.days.ago,
+ updated_at: 2.days.ago,
+ )
+
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ fill_in t('idv.gpo.form.otp_label'), with: otp
+ click_button t('idv.gpo.form.submit')
+ open_last_email
+ click_email_link_matching(/return_to_sp\/account_verified_cta/)
+
+ expect(current_url).to eq(post_idv_follow_up_url)
+ end
+
+ scenario 'receiving an email after passing fraud review' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :fraud_review_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+
+ expect(FraudReviewChecker.new(user).fraud_review_pending?).to eq(true)
+
+ review_pass = ActionAccount::ReviewPass.new
+ review_pass_config = ScriptBase::Config.new(reason: 'feature-test')
+ review_pass.run(args: [user.uuid], config: review_pass_config)
+
+ open_last_email
+ click_email_link_matching(/return_to_sp\/account_verified_cta/)
+
+ expect(current_url).to eq(post_idv_follow_up_url)
+ end
+
+ context 'after entering a verify-by-mail code' do
+ scenario 'clicking on the CTA' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :verify_by_mail_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+ otp = 'ABC123'
+ create(
+ :gpo_confirmation_code,
+ profile: profile,
+ otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
+ created_at: 2.days.ago,
+ updated_at: 2.days.ago,
+ )
+
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ fill_in t('idv.gpo.form.otp_label'), with: otp
+ click_button t('idv.gpo.form.submit')
+ acknowledge_and_confirm_personal_key
+
+ expect(current_path).to eq(idv_sp_follow_up_path)
+ click_on t('idv.by_mail.sp_follow_up.connect_account')
+
+ expect(current_url).to eq(post_idv_follow_up_url)
+ end
+
+ scenario 'canceling on the CTA' do
+ post_idv_follow_up_url = 'https://example.com/idv_follow_up'
+ initiating_service_provider = create(:service_provider, post_idv_follow_up_url:)
+ profile = create(:profile, :verify_by_mail_pending, :with_pii, initiating_service_provider:)
+ user = profile.user
+ otp = 'ABC123'
+ create(
+ :gpo_confirmation_code,
+ profile: profile,
+ otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
+ created_at: 2.days.ago,
+ updated_at: 2.days.ago,
+ )
+
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ fill_in t('idv.gpo.form.otp_label'), with: otp
+ click_button t('idv.gpo.form.submit')
+ acknowledge_and_confirm_personal_key
+
+ expect(current_path).to eq(idv_sp_follow_up_path)
+ click_on t('idv.by_mail.sp_follow_up.go_to_account')
+
+ expect(current_url).to eq(account_url)
+ end
+ end
+end
diff --git a/spec/presenters/idv/by_mail/sp_follow_up_presenter_spec.rb b/spec/presenters/idv/by_mail/sp_follow_up_presenter_spec.rb
new file mode 100644
index 00000000000..cf58b449906
--- /dev/null
+++ b/spec/presenters/idv/by_mail/sp_follow_up_presenter_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+RSpec.describe Idv::ByMail::SpFollowUpPresenter do
+ let(:sp_name) { 'Test SP' }
+ let(:service_provider) { create(:service_provider, friendly_name: sp_name) }
+ let(:user) do
+ create(
+ :profile,
+ :active,
+ initiating_service_provider: service_provider,
+ ).user
+ end
+
+ subject(:presenter) { described_class.new(current_user: user) }
+
+ describe '#heading' do
+ it 'interpolates the SP name' do
+ expect(presenter.heading).to eq(
+ t('idv.by_mail.sp_follow_up.heading', service_provider: sp_name),
+ )
+ end
+ end
+
+ describe '#body' do
+ it 'interpolates the SP name' do
+ expect(presenter.body).to eq(
+ t('idv.by_mail.sp_follow_up.body', service_provider: sp_name, app_name: APP_NAME),
+ )
+ end
+ end
+end