diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb
index b451f3e5a62..91eed3491e8 100644
--- a/app/controllers/idv/in_person/address_controller.rb
+++ b/app/controllers/idv/in_person/address_controller.rb
@@ -109,7 +109,11 @@ def redirect_to_next_page
def confirm_in_person_state_id_step_complete
return if pii_from_user&.has_key?(:identity_doc_address1)
- redirect_to idv_in_person_step_url(step: :state_id)
+ if IdentityConfig.store.in_person_state_id_controller_enabled
+ redirect_to idv_in_person_proofing_state_id_url
+ else
+ redirect_to idv_in_person_step_url(step: :state_id)
+ end
end
def confirm_in_person_address_step_needed
diff --git a/app/controllers/idv/in_person/state_id_controller.rb b/app/controllers/idv/in_person/state_id_controller.rb
new file mode 100644
index 00000000000..e4fc9e1fa1f
--- /dev/null
+++ b/app/controllers/idv/in_person/state_id_controller.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Idv
+ module InPerson
+ class StateIdController < ApplicationController
+ include Idv::AvailabilityConcern
+ include IdvStepConcern
+
+ before_action :render_404_if_controller_not_enabled
+ before_action :redirect_unless_enrollment # confirm previous step is complete
+
+ def show
+ flow_session[:pii_from_user] ||= {}
+ analytics.idv_in_person_proofing_state_id_visited(**analytics_arguments)
+
+ render :show, locals: extra_view_variables
+ end
+
+ def extra_view_variables
+ {
+ form:,
+ pii:,
+ parsed_dob:,
+ updating_state_id: updating_state_id?,
+ }
+ end
+
+ private
+
+ def render_404_if_controller_not_enabled
+ render_not_found unless
+ IdentityConfig.store.in_person_state_id_controller_enabled
+ end
+
+ def redirect_unless_enrollment
+ redirect_to idv_document_capture_url unless current_user.establishing_in_person_enrollment
+ end
+
+ def flow_session
+ user_session.fetch('idv/in_person', {})
+ end
+
+ def analytics_arguments
+ {
+ flow_path: idv_session.flow_path,
+ step: 'state_id',
+ analytics_id: 'In Person Proofing',
+ irs_reproofing: irs_reproofing?,
+ }.merge(ab_test_analytics_buckets).
+ merge(extra_analytics_properties)
+ end
+
+ def updating_state_id?
+ flow_session[:pii_from_user].has_key?(:first_name)
+ end
+
+ def parsed_dob
+ form_dob = pii[:dob]
+ if form_dob.instance_of?(String)
+ dob_str = form_dob
+ elsif form_dob.instance_of?(Hash)
+ dob_str = MemorableDateComponent.extract_date_param(form_dob)
+ end
+ Date.parse(dob_str) unless dob_str.nil?
+ rescue StandardError
+ # Catch date parsing errors
+ end
+
+ def pii
+ data = flow_session[:pii_from_user]
+ data = data.merge(flow_params) if params.has_key?(:state_id)
+ data.deep_symbolize_keys
+ end
+
+ def flow_params
+ params.require(:state_id).permit(
+ *Idv::StateIdForm::ATTRIBUTES,
+ dob: [
+ :month,
+ :day,
+ :year,
+ ],
+ )
+ end
+
+ def form
+ @form ||= Idv::StateIdForm.new(current_user)
+ end
+ end
+ end
+ end
diff --git a/app/services/flow/flow_state_machine.rb b/app/services/flow/flow_state_machine.rb
index 496467167a5..074f08bcf97 100644
--- a/app/services/flow/flow_state_machine.rb
+++ b/app/services/flow/flow_state_machine.rb
@@ -173,7 +173,15 @@ def flow_finish
def redirect_to_step(step)
flow_finish and return unless next_step
- redirect_to send(@step_url, step: step)
+ redirect_url(step)
+ end
+
+ def redirect_url(step)
+ if IdentityConfig.store.in_person_state_id_controller_enabled
+ redirect_to idv_in_person_proofing_state_id_url
+ else
+ redirect_to send(@step_url, step: step)
+ end
end
def analytics_properties
diff --git a/app/views/idv/in_person/state_id/show.html.erb b/app/views/idv/in_person/state_id/show.html.erb
new file mode 100644
index 00000000000..fa3a4b8fccd
--- /dev/null
+++ b/app/views/idv/in_person/state_id/show.html.erb
@@ -0,0 +1,244 @@
+<% content_for(:pre_flash_content) do %>
+ <%= render StepIndicatorComponent.new(
+ steps: Idv::Flows::InPersonFlow::STEP_INDICATOR_STEPS,
+ current_step: :verify_info,
+ locale_scope: 'idv',
+ class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4',
+ ) %>
+<% end %>
+<% if updating_state_id %>
+ <% self.title = t('in_person_proofing.headings.update_state_id') %>
+ <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.update_state_id')) %>
+<% else %>
+ <% self.title = t('in_person_proofing.headings.state_id_milestone_2') %>
+ <%= render PageHeadingComponent.new.with_content(t('in_person_proofing.headings.state_id_milestone_2')) %>
+<% end %>
+
+
+ <%= t('in_person_proofing.body.state_id.info_html') %>
+
+
+<%= render AlertComponent.new(
+ type: :info,
+ class: 'margin-bottom-4',
+ text_tag: 'div',
+ ) do %>
+ <%= t('in_person_proofing.body.state_id.alert_message') %>
+
+ <% t('in_person_proofing.body.state_id.id_types').each do | id_type | %>
+ -
+ <%= id_type %>
+
+ <% end %>
+
+
+ <%= t('in_person_proofing.body.state_id.questions') %>
+ <%= link_to(
+ MarketingSite.help_center_article_url(
+ category: 'verify-your-identity',
+ article: 'accepted-state-issued-identification',
+ ),
+ class: 'display-inline',
+ ) do %>
+ <%= t('in_person_proofing.body.state_id.learn_more_link') %>
+ <% end %>
+
+<% end %>
+<%= simple_form_for form,
+ url: url_for,
+ method: 'put',
+ html: { autocomplete: 'off', class: 'margin-y-5' } do |f| %>
+
+
+ <%= render ValidatedFieldComponent.new(
+ name: :first_name,
+ form: f,
+ input_html: { value: pii[:first_name] },
+ label: t('in_person_proofing.form.state_id.first_name'),
+ label_html: { class: 'usa-label' },
+ maxlength: 255,
+ required: true,
+ ) %>
+
+
+
+ <%= render ValidatedFieldComponent.new(
+ name: :last_name,
+ form: f,
+ input_html: { value: pii[:last_name] },
+ label: t('in_person_proofing.form.state_id.last_name'),
+ label_html: { class: 'usa-label' },
+ maxlength: 255,
+ required: true,
+ ) %>
+
+
+
+ <%= render MemorableDateComponent.new(
+ content_tag: 'memorable-date',
+ name: :dob,
+ day: parsed_dob&.day,
+ month: parsed_dob&.month,
+ year: parsed_dob&.year,
+ required: true,
+ min: '1900-01-01',
+ max: Time.zone.today,
+ hint: t('in_person_proofing.form.state_id.dob_hint'),
+ label: t('in_person_proofing.form.state_id.dob'),
+ form: f,
+ error_messages: {
+ missing_month_day_year: t('in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.missing_month_day_year'),
+ range_overflow: t('in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_overflow'),
+ },
+ range_errors: [
+ {
+ max: Time.zone.today - 13.years,
+ message: t(
+ 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age',
+ app_name: APP_NAME,
+ ),
+ },
+ ],
+ )
+ %>
+
+
+
+ <%= render ValidatedFieldComponent.new(
+ name: :state_id_jurisdiction,
+ collection: us_states_territories,
+ form: f,
+ hint: t('in_person_proofing.form.state_id.state_id_jurisdiction_hint'),
+ input_html: { class: 'jurisdiction-state-selector' },
+ label: t('in_person_proofing.form.state_id.state_id_jurisdiction'),
+ label_html: { class: 'usa-label' },
+ prompt: t('in_person_proofing.form.state_id.state_id_jurisdiction_prompt'),
+ required: true,
+ selected: pii[:state_id_jurisdiction],
+ ) %>
+
+
+ <% state_id_number_hint_default = capture do %>
+ <%= t('in_person_proofing.form.state_id.state_id_number_hint') %>
+ <% [
+ [t('in_person_proofing.form.state_id.state_id_number_hint_spaces'), ' '],
+ [t('in_person_proofing.form.state_id.state_id_number_hint_forward_slashes'), '/'],
+ [t('in_person_proofing.form.state_id.state_id_number_hint_asterisks'), '*'],
+ [t('in_person_proofing.form.state_id.state_id_number_hint_dashes'), '-', true],
+ ].each do |text, symbol, last| %>
+ <%= text %><%= ',' if !last %>
+ <%= symbol %>
+ <% end %>
+ <% end %>
+ <% state_id_number_hint = capture do %>
+ <% [
+ [:default, state_id_number_hint_default],
+ ['FL', t('in_person_proofing.form.state_id.state_id_number_florida_hint_html')],
+ ['TX', t('in_person_proofing.form.state_id.state_id_number_texas_hint')],
+ ].each do |state, hint| %>
+ <%= content_tag(
+ :span,
+ hint,
+ class: state == :default ? nil : 'display-none',
+ data: { state: },
+ ) %>
+ <% end %>
+ <% end %>
+ <%= render ValidatedFieldComponent.new(
+ name: :state_id_number,
+ form: f,
+ hint: state_id_number_hint,
+ hint_html: { class: ['tablet:grid-col-10', 'jurisdiction-extras'] },
+ input_html: { value: pii[:state_id_number] },
+ label: t('in_person_proofing.form.state_id.state_id_number'),
+ label_html: { class: 'usa-label' },
+ maxlength: 255,
+ required: true,
+ ) %>
+
+
+ <%= t('in_person_proofing.headings.id_address') %>
+ <%= render ValidatedFieldComponent.new(
+ name: :identity_doc_address_state,
+ collection: us_states_territories,
+ form: f,
+ input_html: { class: 'address-state-selector' },
+ label: t('in_person_proofing.form.state_id.identity_doc_address_state'),
+ label_html: { class: 'usa-label' },
+ prompt: t('in_person_proofing.form.state_id.identity_doc_address_state_prompt'),
+ required: true,
+ selected: pii[:identity_doc_address_state],
+ ) %>
+ <%= render ValidatedFieldComponent.new(
+ name: :identity_doc_address1,
+ form: f,
+ hint_html: { class: ['display-none', 'puerto-rico-extras'] },
+ hint: t('in_person_proofing.form.state_id.address1_hint'),
+ input_html: { value: pii[:identity_doc_address1] },
+ label: t('in_person_proofing.form.state_id.address1'),
+ label_html: { class: 'usa-label' },
+ maxlength: 255,
+ required: true,
+ ) %>
+ <%= render ValidatedFieldComponent.new(
+ name: :identity_doc_address2,
+ form: f,
+ hint: t('in_person_proofing.form.state_id.address2_hint'),
+ hint_html: { class: ['display-none', 'puerto-rico-extras'] },
+ input_html: { value: pii[:identity_doc_address2] },
+ label: t('in_person_proofing.form.state_id.address2'),
+ label_html: { class: 'usa-label' },
+ maxlength: 255,
+ required: false,
+ ) %>
+ <%= render ValidatedFieldComponent.new(
+ name: :identity_doc_city,
+ form: f,
+ input_html: { value: pii[:identity_doc_city] },
+ label: t('in_person_proofing.form.state_id.city'),
+ label_html: { class: 'usa-label' },
+ maxlength: 255,
+ required: true,
+ ) %>
+
+ <%# using :tel for mobile numeric keypad %>
+ <%= render ValidatedFieldComponent.new(
+ as: :tel,
+ error_messages: { patternMismatch: t('idv.errors.pattern_mismatch.zipcode') },
+ form: f,
+ input_html: { value: pii[:identity_doc_zipcode], class: 'zipcode' },
+ label: t('in_person_proofing.form.state_id.zipcode'),
+ label_html: { class: 'usa-label' },
+ name: :identity_doc_zipcode,
+ pattern: '\d{5}([\-]\d{4})?',
+ required: true,
+ ) %>
+
+ <%= render ValidatedFieldComponent.new(
+ as: :radio_buttons,
+ checked: pii[:same_address_as_id],
+ collection: [
+ [t('in_person_proofing.form.state_id.same_address_as_id_yes'), true],
+ [t('in_person_proofing.form.state_id.same_address_as_id_no'), false],
+ ],
+ form: f,
+ label: t('in_person_proofing.form.state_id.same_address_as_id'),
+ legend_html: { class: 'h2' },
+ name: :same_address_as_id,
+ required: true,
+ wrapper: :uswds_radio_buttons,
+ ) %>
+ <%= f.submit do %>
+ <% if updating_state_id %>
+ <%= t('forms.buttons.submit.update') %>
+ <% else %>
+ <%= t('forms.buttons.continue') %>
+ <% end %>
+ <% end %>
+<% end %>
+<% if updating_state_id %>
+ <%= render 'idv/shared/back', action: 'cancel_update_state_id' %>
+<% else %>
+ <%= render 'idv/doc_auth/cancel', step: 'state_id' %>
+<% end %>
+<%= javascript_packs_tag_once('formatted-fields', 'state-guidance') %>
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index be4a54a2ac5..2f79a9ea44b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -397,6 +397,7 @@
# during the deprecation process.
get '/in_person_proofing/address' => redirect('/verify/in_person/address', status: 307)
put '/in_person_proofing/address' => redirect('/verify/in_person/address', status: 307)
+ get '/in_person_proofing/state_id' => 'in_person/state_id#show'
get '/in_person' => 'in_person#index'
get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show',
diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb
index bf5644c2e7f..df06ee89e2f 100644
--- a/spec/controllers/idv/in_person/address_controller_spec.rb
+++ b/spec/controllers/idv/in_person/address_controller_spec.rb
@@ -27,11 +27,27 @@
describe 'before_actions' do
context '#confirm_in_person_state_id_step_complete' do
- it 'redirects to state id page if not complete' do
- subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1)
- get :show
+ context 'in_person_state_id_controller_enabled is enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
+ and_return(true)
+ end
- expect(response).to redirect_to idv_in_person_step_url(step: :state_id)
+ it 'redirects to state id page if not complete' do
+ subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1)
+ get :show
+
+ expect(response).to redirect_to idv_in_person_proofing_state_id_url
+ end
+ end
+
+ context 'in_person_state_id_controller_enabled is not enabled' do
+ it 'redirects to state id page if not complete' do
+ subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1)
+ get :show
+
+ expect(response).to redirect_to idv_in_person_step_url(step: :state_id)
+ end
end
end
diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb
new file mode 100644
index 00000000000..5bd6d2babd3
--- /dev/null
+++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb
@@ -0,0 +1,115 @@
+require 'rails_helper'
+
+RSpec.describe Idv::InPerson::StateIdController do
+ include FlowPolicyHelper
+ include InPersonHelper
+
+ let(:user) { build(:user) }
+ let(:enrollment) { InPersonEnrollment.new }
+
+ let(:ab_test_args) do
+ { sample_bucket1: :sample_value1, sample_bucket2: :sample_value2 }
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
+ and_return(true)
+ allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).
+ and_return(true)
+ stub_sign_in(user)
+ stub_up_to(:hybrid_handoff, idv_session: subject.idv_session)
+ allow(user).to receive(:establishing_in_person_enrollment).and_return(enrollment)
+ subject.user_session['idv/in_person'] = { pii_from_user: {} }
+ subject.idv_session.ssn = nil # This made specs pass. Might need more investigation.
+ stub_analytics
+ allow(@analytics).to receive(:track_event)
+ allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args)
+ end
+
+ describe 'before_actions' do
+ context '#render_404_if_controller_not_enabled' do
+ context 'flag not set' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
+ and_return(nil)
+ end
+ it 'renders a 404' do
+ get :show
+
+ expect(response).to be_not_found
+ end
+ end
+
+ context 'flag not enabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_state_id_controller_enabled).
+ and_return(false)
+ end
+ it 'renders a 404' do
+ get :show
+
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ context '#confirm_establishing_enrollment' do
+ let(:enrollment) { nil }
+ it 'redirects to document capture if not complete' do
+ get :show
+
+ expect(response).to redirect_to idv_document_capture_url
+ end
+ end
+ end
+
+ describe '#show' do
+ let(:analytics_name) { 'IdV: in person proofing state_id visited' }
+ let(:analytics_args) do
+ {
+ analytics_id: 'In Person Proofing',
+ flow_path: 'standard',
+ irs_reproofing: false,
+ opted_in_to_in_person_proofing: nil,
+ step: 'state_id',
+ pii_like_keypaths: [[:same_address_as_id],
+ [:proofing_results, :context, :stages, :state_id,
+ :state_id_jurisdiction]],
+ }.merge(ab_test_args)
+ end
+
+ it 'renders the show template' do
+ get :show
+
+ expect(response).to render_template :show
+ end
+
+ context 'pii_from_user is nil' do
+ it 'renders the show template' do
+ subject.user_session['idv/in_person'].delete(:pii_from_user)
+ get :show
+
+ expect(response).to render_template :show
+ end
+ end
+
+ it 'logs idv_in_person_proofing_state_id_visited' do
+ get :show
+
+ expect(@analytics).to have_received(
+ :track_event,
+ ).with(analytics_name, analytics_args)
+ end
+
+ it 'has correct extra_view_variables' do
+ expect(subject.extra_view_variables).to include(
+ form: Idv::StateIdForm,
+ updating_state_id: false,
+ )
+
+ expect(subject.extra_view_variables[:pii]).to_not have_key(
+ :address1,
+ )
+ end
+ end
+end