diff --git a/Gemfile b/Gemfile
index a6ea2e90942..a645422ddb6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -102,7 +102,7 @@ group :development, :test do
gem 'brakeman', require: false
gem 'bullet', '~> 7.0'
gem 'capybara-webmock', git: 'https://github.com/hashrocket/capybara-webmock.git', ref: 'd3f3b7c'
- gem 'erb_lint', '~> 0.4.0', require: false
+ gem 'erb_lint', '~> 0.5.0', require: false
gem 'i18n-tasks', '~> 1.0'
gem 'knapsack'
gem 'listen'
diff --git a/Gemfile.lock b/Gemfile.lock
index 0a7fa861485..26a02875269 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -153,8 +153,8 @@ GEM
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
- addressable (2.8.6)
- public_suffix (>= 2.0.2, < 6.0)
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
ahoy_matey (3.3.0)
activesupport (>= 5)
device_detector
@@ -197,11 +197,11 @@ GEM
aws-sigv4 (~> 1.1)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
- axe-core-api (4.9.0)
+ axe-core-api (4.9.1)
dumb_delegator
virtus
- axe-core-rspec (4.9.0)
- axe-core-api
+ axe-core-rspec (4.9.1)
+ axe-core-api (= 4.9.1)
dumb_delegator
virtus
axiom-types (0.1.1)
@@ -217,7 +217,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
- better_html (2.0.2)
+ better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
ast (~> 2.0)
@@ -230,7 +230,7 @@ GEM
msgpack (~> 1.2)
brakeman (6.1.0)
browser (6.0.0)
- builder (3.2.4)
+ builder (3.3.0)
bullet (7.1.4)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
@@ -257,12 +257,13 @@ GEM
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- concurrent-ruby (1.3.1)
+ concurrent-ruby (1.3.3)
connection_pool (2.4.1)
cose (1.3.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
- crack (0.4.5)
+ crack (1.0.0)
+ bigdecimal
rexml
crass (1.0.6)
css_parser (1.14.0)
@@ -304,7 +305,7 @@ GEM
htmlentities (~> 4.3.3)
launchy (~> 2.1)
mail (~> 2.7)
- erb_lint (0.4.0)
+ erb_lint (0.5.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
@@ -312,7 +313,7 @@ GEM
rubocop
smart_properties
errbase (0.2.1)
- erubi (1.12.0)
+ erubi (1.13.0)
et-orbi (1.2.7)
tzinfo
factory_bot (6.4.6)
@@ -349,11 +350,11 @@ GEM
railties (>= 6.0.0)
thor (>= 0.14.1)
google-protobuf (3.24.4)
- hashdiff (1.0.1)
+ hashdiff (1.1.0)
heapy (0.2.0)
thor
highline (2.1.0)
- htmlbeautifier (1.4.2)
+ htmlbeautifier (1.4.3)
htmlentities (4.3.4)
http_accept_language (2.1.1)
i18n (1.14.5)
@@ -371,13 +372,13 @@ GEM
terminal-table (>= 1.5.1)
ice_nine (0.11.2)
io-console (0.7.2)
- irb (1.13.1)
+ irb (1.13.2)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
jsbundling-rails (1.1.2)
railties (>= 6.0.0)
- json (2.7.1)
+ json (2.7.2)
jwe (0.4.0)
jwt (2.7.1)
knapsack (4.0.0)
@@ -390,6 +391,7 @@ GEM
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
+ logger (1.6.0)
lograge (0.11.2)
actionpack (>= 4)
activesupport (>= 4)
@@ -424,7 +426,7 @@ GEM
mini_histogram (0.3.1)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
- minitest (5.23.1)
+ minitest (5.24.1)
msgpack (1.7.2)
multiset (0.5.3)
mutex_m (0.2.0)
@@ -444,18 +446,18 @@ GEM
net-ssh (6.1.0)
newrelic_rpm (9.7.0)
nio4r (2.7.3)
- nokogiri (1.16.5)
+ nokogiri (1.16.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
openssl (3.0.2)
openssl-signature_algorithm (1.2.1)
openssl (> 2.0, < 3.1)
orm_adapter (0.5.0)
- parallel (1.24.0)
- parser (3.3.1.0)
+ parallel (1.25.1)
+ parser (3.3.3.0)
ast (~> 2.4.1)
racc
- pg (1.5.4)
+ pg (1.5.6)
pg_query (4.2.3)
google-protobuf (>= 3.22.3)
phonelib (0.8.9)
@@ -489,7 +491,7 @@ GEM
pry (>= 0.10.4)
psych (5.1.2)
stringio
- public_suffix (5.0.5)
+ public_suffix (6.0.0)
puma (6.4.2)
nio4r (~> 2.0)
raabro (1.4.0)
@@ -558,12 +560,12 @@ GEM
psych (>= 4.0.0)
redacted_struct (2.0.0)
redcarpet (3.6.0)
- redis (5.1.0)
- redis-client (>= 0.17.0)
- redis-client (0.22.0)
+ redis (5.2.0)
+ redis-client (>= 0.22.0)
+ redis-client (0.22.2)
connection_pool
- regexp_parser (2.9.1)
- reline (0.5.8)
+ regexp_parser (2.9.2)
+ reline (0.5.9)
io-console (~> 0.5)
request_store (1.5.1)
rack (>= 1.4)
@@ -571,8 +573,8 @@ GEM
actionpack (>= 5.0)
railties (>= 5.0)
retries (0.0.5)
- rexml (3.2.8)
- strscan (>= 3.0.9)
+ rexml (3.3.1)
+ strscan
rotp (6.3.0)
rouge (4.2.0)
rqrcode (2.1.0)
@@ -615,8 +617,8 @@ GEM
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.31.2)
- parser (>= 3.3.0.4)
+ rubocop-ast (1.31.3)
+ parser (>= 3.3.1.0)
rubocop-capybara (2.19.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.24.0)
@@ -647,8 +649,9 @@ GEM
jwt (~> 2.0)
scrypt (3.0.7)
ffi-compiler (>= 1.0, < 2.0)
- selenium-webdriver (4.20.1)
+ selenium-webdriver (4.22.0)
base64 (~> 0.2)
+ logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@@ -673,9 +676,9 @@ GEM
unf (~> 0.1.4)
smart_properties (1.17.0)
stringex (2.8.5)
- stringio (3.1.0)
- strong_migrations (1.6.4)
- activerecord (>= 5.2)
+ stringio (3.1.1)
+ strong_migrations (2.0.0)
+ activerecord (>= 6.1)
strscan (3.1.0)
tableparser (1.0.1)
terminal-table (3.0.2)
@@ -734,7 +737,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.36)
- zeitwerk (2.6.15)
+ zeitwerk (2.6.16)
zlib (3.0.0)
zonebie (0.6.1)
zxcvbn (0.1.9)
@@ -773,7 +776,7 @@ DEPENDENCIES
devise (~> 4.8)
dotiw (>= 4.0.1)
email_spec
- erb_lint (~> 0.4.0)
+ erb_lint (~> 0.5.0)
factory_bot_rails (>= 6.2.0)
faker
faraday (~> 2)
diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss
index fd485da9ec4..d3310b4f25d 100644
--- a/app/assets/stylesheets/email.css.scss
+++ b/app/assets/stylesheets/email.css.scss
@@ -119,14 +119,6 @@ h6 {
}
}
-@media only screen and (max-width: #{$global-breakpoint}) {
- table.body .columns,
- table.body .column {
- padding-left: $global-gutter-small !important;
- padding-right: $global-gutter-small !important;
- }
-}
-
.info-alert,
.warning-alert {
padding: 0 units(0.5);
diff --git a/app/components/badge_component.scss b/app/components/badge_component.scss
index 2dbbf933fc0..4958757cead 100644
--- a/app/components/badge_component.scss
+++ b/app/components/badge_component.scss
@@ -1,7 +1,2 @@
@use 'uswds-core' as *;
@forward 'usa-verification-badge';
-
-// Upstream: https://github.com/18F/identity-design-system/pull/445
-.lg-verification-badge .usa-icon {
- margin-right: units(1);
-}
diff --git a/app/components/one_time_code_input_component.scss b/app/components/one_time_code_input_component.scss
index 6bbf026d0e2..1d43f96a5a0 100644
--- a/app/components/one_time_code_input_component.scss
+++ b/app/components/one_time_code_input_component.scss
@@ -2,12 +2,13 @@
lg-one-time-code-input {
display: block;
-
- @include at-media('tablet') {
- width: 50%;
- }
}
.one-time-code-input__input {
+ &.usa-input {
+ @include at-media('tablet') {
+ max-width: 50%;
+ }
+ }
@include u-font-family('mono');
}
diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb
index fccc07a6f34..2fd730152ad 100644
--- a/app/controllers/concerns/idv_step_concern.rb
+++ b/app/controllers/concerns/idv_step_concern.rb
@@ -3,6 +3,7 @@
module IdvStepConcern
extend ActiveSupport::Concern
+ include VerifyProfileConcern
include IdvSessionConcern
include RateLimitConcern
include FraudReviewConcern
@@ -14,9 +15,7 @@ module IdvStepConcern
before_action :confirm_personal_key_acknowledged_if_needed
before_action :confirm_idv_needed
before_action :confirm_letter_recently_enqueued
- before_action :confirm_no_pending_gpo_profile
- before_action :confirm_no_pending_in_person_enrollment
- before_action :handle_fraud
+ before_action :confirm_no_pending_profile
before_action :check_for_mail_only_outage
end
@@ -35,13 +34,8 @@ def confirm_letter_recently_enqueued
return redirect_to idv_letter_enqueued_url if letter_recently_enqueued?
end
- def confirm_no_pending_gpo_profile
- redirect_to idv_verify_by_mail_enter_code_url if letter_not_recently_enqueued?
- end
-
- def confirm_no_pending_in_person_enrollment
- return if !IdentityConfig.store.in_person_proofing_enabled
- redirect_to idv_in_person_ready_to_verify_url if current_user&.pending_in_person_enrollment
+ def confirm_no_pending_profile
+ redirect_to url_for_pending_profile_reason if user_has_pending_profile?
end
def check_for_mail_only_outage
diff --git a/app/controllers/concerns/verify_profile_concern.rb b/app/controllers/concerns/verify_profile_concern.rb
index 7d9a779a0fe..e65f16f3832 100644
--- a/app/controllers/concerns/verify_profile_concern.rb
+++ b/app/controllers/concerns/verify_profile_concern.rb
@@ -6,15 +6,12 @@ module VerifyProfileConcern
def url_for_pending_profile_reason
return idv_verify_by_mail_enter_code_url if current_user.gpo_verification_pending_profile?
return idv_in_person_ready_to_verify_url if current_user.in_person_pending_profile?
- # We don't want to hit idv_please_call_url in cases where the user
- # has fraud review pending and not passed at the post office
- return idv_welcome_url if user_failed_ipp_with_fraud_review_pending?
return idv_please_call_url if current_user.fraud_review_pending?
idv_not_verified_url if current_user.fraud_rejection?
end
def user_has_pending_profile?
- pending_profile_policy.user_has_pending_profile?
+ pending_profile_policy.user_has_pending_profile? && !user_failed_ipp_with_fraud_review_pending?
end
def pending_profile_policy
@@ -25,6 +22,10 @@ def pending_profile_policy
)
end
+ # Returns true if the user has not passed IPP at the post office and is
+ # flagged for fraud review, or has been rejected for fraud.
+ # Ultimately this is to allow users who fail at the post office to create another enrollment
+ # bypassing the typical flow of showing the Please Call or Fraud Rejection screens.
def user_failed_ipp_with_fraud_review_pending?
IdentityConfig.store.in_person_proofing_enforce_tmx &&
current_user.ipp_enrollment_status_not_passed? &&
diff --git a/app/controllers/idv/in_person/public/usps_locations_controller.rb b/app/controllers/idv/in_person/public/usps_locations_controller.rb
index eafddf1924a..5ecb9c28ae1 100644
--- a/app/controllers/idv/in_person/public/usps_locations_controller.rb
+++ b/app/controllers/idv/in_person/public/usps_locations_controller.rb
@@ -18,26 +18,19 @@ def index
)
locations = proofer.request_facilities(candidate, false)
- render json: localized_locations(locations).to_json
+ render json: locations.to_json
end
def options
head :ok
end
- private
+ protected
def proofer
@proofer ||= UspsInPersonProofing::EnrollmentHelper.usps_proofer
end
- def localized_locations(locations)
- return nil if locations.nil?
- locations.map do |location|
- UspsInPersonProofing::EnrollmentHelper.localized_location(location)
- end
- end
-
def enabled?
IdentityConfig.store.in_person_public_address_search_enabled
end
diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb
index 45cac7ac484..4367b795ba3 100644
--- a/app/controllers/idv/in_person/usps_locations_controller.rb
+++ b/app/controllers/idv/in_person/usps_locations_controller.rb
@@ -27,18 +27,18 @@ def index
zip_code: search_params['zip_code']
)
is_enhanced_ipp = resolved_authn_context_result.enhanced_ipp?
- locations = proofer.request_facilities(candidate, is_enhanced_ipp)
- if locations.length > 0
+ response = proofer.request_facilities(candidate, is_enhanced_ipp)
+ if response.length > 0
analytics.idv_in_person_locations_searched(
success: true,
- result_total: locations.length,
+ result_total: response.length,
)
else
analytics.idv_in_person_locations_searched(
success: false, errors: 'No USPS locations found',
)
end
- render json: localized_locations(locations).to_json
+ render json: response.to_json
end
# save the Post Office location the user selected to an enrollment
@@ -64,13 +64,6 @@ def add_proofing_component
update(document_check: Idp::Constants::Vendors::USPS)
end
- def localized_locations(locations)
- return nil if locations.nil?
- locations.map do |location|
- EnrollmentHelper.localized_location(location)
- end
- end
-
def handle_error(err)
remapped_error = case err
when ActionController::InvalidAuthenticityToken,
diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb
index 4d18dcbc6b1..7cccf2d8924 100644
--- a/app/controllers/idv/personal_key_controller.rb
+++ b/app/controllers/idv/personal_key_controller.rb
@@ -16,8 +16,7 @@ class PersonalKeyController < ApplicationController
# standard before_actions and handle them in our own special way below.
skip_before_action :confirm_idv_needed
skip_before_action :confirm_personal_key_acknowledged_if_needed
- skip_before_action :confirm_no_pending_in_person_enrollment
- skip_before_action :handle_fraud
+ skip_before_action :confirm_no_pending_profile
def show
analytics.idv_personal_key_visited(
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
index 135ef08f9cd..159e784baf2 100644
--- a/app/controllers/users/sessions_controller.rb
+++ b/app/controllers/users/sessions_controller.rb
@@ -74,10 +74,21 @@ def increment_session_bad_password_count
def process_locked_out_session
warden.logout(:user)
warden.lock!
- flash[:error] = t('errors.sign_in.bad_password_limit')
+
+ flash[:error] = t(
+ 'errors.sign_in.bad_password_limit',
+ time_left: locked_out_time_remaining,
+ )
redirect_to root_url
end
+ def locked_out_time_remaining
+ locked_at = session[:max_bad_passwords_at]
+ window = IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds
+ time_lockout_expires = Time.zone.at(locked_at) + window
+ distance_of_time_in_words(Time.zone.now, time_lockout_expires, true)
+ end
+
def valid_captcha_result?
return @valid_captcha_result if defined?(@valid_captcha_result)
@valid_captcha_result = SignInRecaptchaForm.new(**recaptcha_form_args).submit(
diff --git a/app/javascript/packages/clipboard-button/package.json b/app/javascript/packages/clipboard-button/package.json
index 86ae7f2f7b1..bd1df2ce1e0 100644
--- a/app/javascript/packages/clipboard-button/package.json
+++ b/app/javascript/packages/clipboard-button/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"dependencies": {
- "@18f/identity-design-system": "^9.2.0"
+ "@18f/identity-design-system": "^9.3.0"
},
"sideEffects": [
"./clipboard-button-element.ts"
diff --git a/app/javascript/packages/components/alert.spec.tsx b/app/javascript/packages/components/alert.spec.tsx
index 917247d6f05..47a645048ef 100644
--- a/app/javascript/packages/components/alert.spec.tsx
+++ b/app/javascript/packages/components/alert.spec.tsx
@@ -5,15 +5,14 @@ import type { AlertType } from './alert';
describe('Alert', () => {
describe('role', () => {
- (
- [
- ['success', 'status'],
- ['warning', 'status'],
- ['error', 'alert'],
- ['info', 'status'],
- ['other', 'status'],
- ] as [AlertType, 'alert' | 'status'][]
- ).forEach(([type, role]) => {
+ const variants: [AlertType, 'alert' | 'status'][] = [
+ ['success', 'status'],
+ ['warning', 'status'],
+ ['error', 'alert'],
+ ['info', 'status'],
+ ];
+
+ variants.forEach(([type, role]) => {
context(`with ${type} type`, () => {
it(`should apply ${role} role`, () => {
const { getByRole } = render();
diff --git a/app/javascript/packages/components/alert.tsx b/app/javascript/packages/components/alert.tsx
index 033b5bafdbe..ca2f3e65620 100644
--- a/app/javascript/packages/components/alert.tsx
+++ b/app/javascript/packages/components/alert.tsx
@@ -1,11 +1,11 @@
import { forwardRef, createElement } from 'react';
import type { ReactNode, ForwardedRef } from 'react';
-export type AlertType = 'success' | 'warning' | 'error' | 'info' | 'other';
+export type AlertType = 'success' | 'warning' | 'error' | 'info';
interface AlertProps {
/**
- * Alert type. Defaults to "other".
+ * Alert type.
*/
type?: AlertType;
@@ -32,10 +32,10 @@ interface AlertProps {
}
function Alert(
- { type = 'other', className, isFocusable, children, textTag = 'p' }: AlertProps,
+ { type, className, isFocusable, children, textTag = 'p' }: AlertProps,
ref: ForwardedRef,
) {
- const classes = [`usa-alert usa-alert--${type}`, className].filter(Boolean).join(' ');
+ const classes = ['usa-alert', type && `usa-alert--${type}`, className].filter(Boolean).join(' ');
const role = type === 'error' ? 'alert' : 'status';
const inner = createElement(textTag, { className: 'usa-alert__text' }, children);
diff --git a/app/javascript/packages/stylelint-config/CHANGELOG.md b/app/javascript/packages/stylelint-config/CHANGELOG.md
index 93fb812367e..cbc85a1753c 100644
--- a/app/javascript/packages/stylelint-config/CHANGELOG.md
+++ b/app/javascript/packages/stylelint-config/CHANGELOG.md
@@ -1,4 +1,4 @@
-## 5.0.0-beta.1
+## Unreleased
### Breaking Changes
@@ -13,6 +13,8 @@
- [`scss/double-slash-comment-empty-line-before`](https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/double-slash-comment-empty-line-before/README.md)
- [`color-function-notation`](https://stylelint.io/user-guide/rules/color-function-notation/) (due to [Sass incompatibilities](https://github.com/sass/sass/issues/2831))
- The ruleset now configures [`"reportNeedlessDisables": true`](https://stylelint.io/user-guide/options/#reportneedlessdisables), which will report inline configuration that disables rules unnecessarily.
+- The [`declaration-no-important`](https://stylelint.io/user-guide/rules/declaration-no-important/) rule is now enabled, which disallows `!important` in stylesheets.
+ - `!important` is a sledgehammer solution which often causes more problems than it helps, and usually stems from misunderstandings of [CSS specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity). See related ["Best practices" MDN documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/important#best_practices).
## 4.1.0
diff --git a/app/javascript/packages/stylelint-config/index.js b/app/javascript/packages/stylelint-config/index.js
index 16f79869db9..01e5818316d 100644
--- a/app/javascript/packages/stylelint-config/index.js
+++ b/app/javascript/packages/stylelint-config/index.js
@@ -4,6 +4,7 @@ module.exports = {
'at-rule-empty-line-before': null,
'color-function-notation': null,
'declaration-empty-line-before': null,
+ 'declaration-no-important': true,
'no-descending-specificity': null,
'rule-empty-line-before': null,
'scss/comment-no-empty': null,
diff --git a/app/presenters/idv/in_person/ready_to_verify_presenter.rb b/app/presenters/idv/in_person/ready_to_verify_presenter.rb
index 61caeca8b5a..a5f8462f742 100644
--- a/app/presenters/idv/in_person/ready_to_verify_presenter.rb
+++ b/app/presenters/idv/in_person/ready_to_verify_presenter.rb
@@ -31,7 +31,7 @@ def formatted_due_date
def selected_location_hours(prefix)
return unless selected_location_details
hours = selected_location_details["#{prefix}_hours"]
- UspsInPersonProofing::EnrollmentHelper.localized_hours(hours) if hours
+ return localized_hours(hours) if hours
end
def service_provider
@@ -107,6 +107,18 @@ def format_outage_date(date)
I18n.l(date.to_date, format: :short)
end
+ def localized_hours(hours)
+ case hours
+ when 'Closed'
+ I18n.t('in_person_proofing.body.barcode.retail_hours_closed')
+ else
+ hours.
+ split(' - '). # Hyphen
+ map { |time| Time.zone.parse(time).strftime(I18n.t('time.formats.event_time')) }.
+ join(' – ') # Endash
+ end
+ end
+
def sp_return_url_resolver
SpReturnUrlResolver.new(service_provider: service_provider)
end
diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb
index cb8cc789c9f..7de2212dc26 100644
--- a/app/services/usps_in_person_proofing/enrollment_helper.rb
+++ b/app/services/usps_in_person_proofing/enrollment_helper.rb
@@ -101,40 +101,6 @@ def usps_proofer
end
end
- def localized_location(location)
- {
- address: location.address,
- city: location.city,
- distance: location.distance,
- name: location.name,
- saturday_hours: EnrollmentHelper.localized_hours(location.saturday_hours),
- state: location.state,
- sunday_hours: EnrollmentHelper.localized_hours(location.sunday_hours),
- weekday_hours: EnrollmentHelper.localized_hours(location.weekday_hours),
- zip_code_4: location.zip_code_4,
- zip_code_5: location.zip_code_5,
- is_pilot: location.is_pilot,
- }
- end
-
- def localized_hours(hours)
- if hours == 'Closed'
- I18n.t('in_person_proofing.body.barcode.retail_hours_closed')
- elsif hours.include?(' - ') # Hyphen
- hours.
- split(' - '). # Hyphen
- map { |time| Time.zone.parse(time).strftime(I18n.t('time.formats.event_time')) }.
- join(' – ') # Endash
- elsif hours.include?(' – ') # Endash
- hours.
- split(' – '). # Endash
- map { |time| Time.zone.parse(time).strftime(I18n.t('time.formats.event_time')) }.
- join(' – ') # Endash
- else
- hours
- end
- end
-
private
SECONDARY_ID_ADDRESS_MAP = {
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3c931ad39a4..27e0a67d630 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -717,7 +717,7 @@ errors.messages.invalid_voice_number: Invalid phone number. Check that you’ve
errors.messages.missing_field: Please fill in this field.
errors.messages.no_pending_profile: No profile is waiting for verification
errors.messages.not_a_number: is not a number
-errors.messages.otp_format: Enter your entire one-time code without spaces or special characters
+errors.messages.otp_format: Enter the one-time code sent to your phone. Do not use spaces or special characters.
errors.messages.password_incorrect: Incorrect password
errors.messages.password_mismatch: Your passwords don’t match
errors.messages.personal_key_incorrect: Incorrect personal key
@@ -744,7 +744,7 @@ errors.messages.wrong_length.one: is the wrong length (should be 1 character)
errors.messages.wrong_length.other: is the wrong length (should be %{count} characters)
errors.piv_cac_setup.unique_name: That name is already taken. Please choose a different name.
errors.registration.terms: Before you can continue, you must give us permission. Please check the box below and then click continue.
-errors.sign_in.bad_password_limit: You have exceeded the maximum sign in attempts.
+errors.sign_in.bad_password_limit: You have exceeded the maximum sign in attempts. You must wait %{time_left} before trying again.
errors.two_factor_auth_setup.must_select_additional_option: Select an additional authentication method.
errors.two_factor_auth_setup.must_select_option: Select an authentication method.
errors.verify_personal_key.rate_limited: You tried too many times, please try again in %{timeout}.
diff --git a/config/locales/es.yml b/config/locales/es.yml
index e32ab74ce44..e0d3ee442bc 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -728,7 +728,7 @@ errors.messages.invalid_voice_number: Número de teléfono no válido. Verifique
errors.messages.missing_field: Llene este campo.
errors.messages.no_pending_profile: No hay ningún perfil en espera de verificación
errors.messages.not_a_number: no es un número
-errors.messages.otp_format: Ingrese su código de un solo uso completo, sin espacios ni caracteres especiales.
+errors.messages.otp_format: Introduzca el código de un solo uso enviado a su teléfono. No utilice espacios ni caracteres especiales.
errors.messages.password_incorrect: Contraseña incorrecta
errors.messages.password_mismatch: Sus contraseñas no coinciden
errors.messages.personal_key_incorrect: Clave personal incorrecta
@@ -755,7 +755,7 @@ errors.messages.wrong_length.one: tiene la longitud incorrecta (debe ser de 1 c
errors.messages.wrong_length.other: tiene la longitud incorrecta (debe ser de %{count} caracteres)
errors.piv_cac_setup.unique_name: Ese nombre ya fue seleccionado. Elija un nombre diferente.
errors.registration.terms: Antes de continuar, debe darnos permiso. Marque la casilla a continuación y luego haga clic en continuar.
-errors.sign_in.bad_password_limit: Superó el número máximo de intentos de inicio de sesión.
+errors.sign_in.bad_password_limit: Superó el número máximo de intentos de inicio de sesión. Debe esperar %{time_left} antes de volver a intentarlo.
errors.two_factor_auth_setup.must_select_additional_option: Seleccione un método de autenticación adicional.
errors.two_factor_auth_setup.must_select_option: Seleccione un método de autenticación.
errors.verify_personal_key.rate_limited: Lo intentó demasiadas veces; vuelva a intentarlo en %{timeout}.
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index a70d855ed5f..c5f07b8e94d 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -717,7 +717,7 @@ errors.messages.invalid_voice_number: Numéro de téléphone non valide. Vérifi
errors.messages.missing_field: Veuillez remplir ce champ.
errors.messages.no_pending_profile: Aucun profil en attente de vérification
errors.messages.not_a_number: n’est pas un chiffre
-errors.messages.otp_format: Saisissez l’intégralité de votre code à usage unique sans espaces ni caractères spéciaux
+errors.messages.otp_format: Entrez le code à usage unique envoyé sur votre téléphone. N’utilisez pas d’espaces ou de caractères spéciaux.
errors.messages.password_incorrect: Mot de passe incorrect
errors.messages.password_mismatch: Vos mots de passe ne correspondent pas
errors.messages.personal_key_incorrect: Clé personnelle incorrecte
@@ -744,7 +744,7 @@ errors.messages.wrong_length.one: n’est pas de la bonne longueur (devrait êtr
errors.messages.wrong_length.other: n’est pas de la bonne longueur (devrait être de %{count} caractères)
errors.piv_cac_setup.unique_name: Ce nom est déjà pris. Veuillez choisir un autre nom.
errors.registration.terms: Avant de pouvoir continuer, vous devez nous donner la permission. Veuillez cocher la case ci-dessous, puis cliquez sur Suite.
-errors.sign_in.bad_password_limit: Vous avez dépassé le nombre maximal de tentatives de connexion.
+errors.sign_in.bad_password_limit: Vous avez dépassé le nombre maximal de tentatives de connexion. Vous devez attendre %{time_left} avant de réessayer.
errors.two_factor_auth_setup.must_select_additional_option: Sélectionnez une méthode d’authentification supplémentaire.
errors.two_factor_auth_setup.must_select_option: Sélectionnez une méthode d’authentification.
errors.verify_personal_key.rate_limited: Vous avez essayé trop de fois, veuillez réessayer dans %{timeout}.
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index a8b5be89b5e..adc2d825393 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -728,7 +728,7 @@ errors.messages.invalid_voice_number: 电话号码有误。检查一下你是否
errors.messages.missing_field: 请填写这一字段。
errors.messages.no_pending_profile: 没有等待验证的用户资料
errors.messages.not_a_number: 不是数字
-errors.messages.otp_format: 输入你完整的一次性代码(没有空白或特殊字符)
+errors.messages.otp_format: 输入发送到你手机的一次性代码。请勿使用空格或特殊字符。
errors.messages.password_incorrect: 密码不对。
errors.messages.password_mismatch: 你的密码不一致
errors.messages.personal_key_incorrect: 个人密钥不对
@@ -755,7 +755,7 @@ errors.messages.wrong_length.one: 长度不对(应当是 1 个字符)
errors.messages.wrong_length.other: 长度不对(应当是 %{count} 个字符)
errors.piv_cac_setup.unique_name: 这个名字已被使用。请选择一个不同的名字。
errors.registration.terms: 在你能继续之前,你必须授予我们你的同意。请在下面的框打勾然后点击继续。
-errors.sign_in.bad_password_limit: 你已超出登录尝试允许最多次数。
+errors.sign_in.bad_password_limit: 你已超出登录尝试允许最多次数。你必须等待 %{time_left} 才能重试。
errors.two_factor_auth_setup.must_select_additional_option: 请选择一个额外的身份证实方法。
errors.two_factor_auth_setup.must_select_option: 选择一个身份证实方法。
errors.verify_personal_key.rate_limited: 你尝试了太多次。请在 %{timeout}后再试。
diff --git a/package.json b/package.json
index 279d76117b7..c0c0c50e466 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"build:css": "build-sass app/assets/stylesheets/*.css.scss app/components/*.scss --load-path=app/assets/stylesheets --out-dir=app/assets/builds"
},
"dependencies": {
- "@18f/identity-design-system": "^9.2.0",
+ "@18f/identity-design-system": "^9.3.0",
"@babel/core": "^7.20.7",
"@babel/preset-env": "^7.15.6",
"@babel/preset-react": "^7.14.5",
diff --git a/spec/controllers/concerns/idv_step_concern_spec.rb b/spec/controllers/concerns/idv_step_concern_spec.rb
index b46c4ef423f..4ed251ef252 100644
--- a/spec/controllers/concerns/idv_step_concern_spec.rb
+++ b/spec/controllers/concerns/idv_step_concern_spec.rb
@@ -19,13 +19,6 @@ def show
end
describe 'before_actions' do
- it 'includes handle_fraud' do
- expect(idv_step_controller_class).to have_actions(
- :before,
- :handle_fraud,
- )
- end
-
it 'includes check_for_mail_only_outage before_action' do
expect(idv_step_controller_class).to have_actions(
:before,
@@ -176,6 +169,7 @@ def show
describe '#confirm_letter_recently_enqueued' do
controller(idv_step_controller_class) do
before_action :confirm_letter_recently_enqueued
+ before_action :confirm_no_pending_profile
end
before(:each) do
@@ -209,9 +203,9 @@ def show
end
end
- describe '#confirm_no_pending_in_person_enrollment' do
+ describe '#confirm_no_pending_profile' do
controller(idv_step_controller_class) do
- before_action :confirm_no_pending_in_person_enrollment
+ before_action :confirm_no_pending_profile
end
before(:each) do
@@ -245,20 +239,6 @@ def show
expect(response).to redirect_to idv_in_person_ready_to_verify_url
end
end
- end
-
- describe '#confirm_no_pending_gpo_profile' do
- controller(idv_step_controller_class) do
- before_action :confirm_no_pending_gpo_profile
- end
-
- before(:each) do
- sign_in(user)
- allow(subject).to receive(:current_user).and_return(user)
- routes.draw do
- get 'show' => 'anonymous#show'
- end
- end
context 'without pending gpo profile' do
it 'does not redirect' do
diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
index aed47ea128e..402ae40efa4 100644
--- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
+++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
@@ -172,32 +172,6 @@
response_status_code: nil,
)
end
-
- context 'with non-English locale' do
- let(:locale) { 'fr' }
-
- it 'returns content in selected locale' do
- json = response.body
-
- expect(json).to include(
- I18n.t('in_person_proofing.body.barcode.retail_hours_closed', locale: locale),
- )
- I18n.locale = locale
- facilities = JSON.parse(json)
-
- facilities.zip(locations).each do |facility, location|
- expect(facility['weekday_hours']).to eq(
- UspsInPersonProofing::EnrollmentHelper.localized_hours(location[:weekday_hours]),
- )
- expect(facility['saturday_hours']).to eq(
- UspsInPersonProofing::EnrollmentHelper.localized_hours(location[:saturday_hours]),
- )
- expect(facility['sunday_hours']).to eq(
- UspsInPersonProofing::EnrollmentHelper.localized_hours(location[:sunday_hours]),
- )
- end
- end
- end
end
context 'with a timeout from Faraday' do
diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb
index 2899e92299c..8b3ba37bef2 100644
--- a/spec/controllers/idv/personal_key_controller_spec.rb
+++ b/spec/controllers/idv/personal_key_controller_spec.rb
@@ -180,7 +180,7 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs)
:confirm_idv_needed,
:confirm_personal_key_acknowledged_if_needed,
:confirm_no_pending_in_person_enrollment,
- :handle_fraud,
+ :confirm_no_pending_profile,
)
end
diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb
index e50c63953fe..2495cf64fcc 100644
--- a/spec/controllers/users/sessions_controller_spec.rb
+++ b/spec/controllers/users/sessions_controller_spec.rb
@@ -116,6 +116,35 @@
end
end
+ context 'locked out session' do
+ let(:locked_at) { Time.zone.now }
+ let(:user) { create(:user, :fully_registered) }
+ let(:bad_password_window) { IdentityConfig.store.max_bad_passwords_window_in_seconds }
+
+ before do
+ session[:bad_password_count] = IdentityConfig.store.max_bad_passwords + 1
+ session[:max_bad_passwords_at] = locked_at.to_i
+ end
+
+ it 'renders an error letting user know they are locked out for a period of time' do
+ post :create, params: { user: { email: user.email.upcase, password: user.password } }
+ current_time = Time.zone.now
+ time_in_hours = distance_of_time_in_words(
+ current_time,
+ (locked_at + bad_password_window.seconds),
+ true,
+ )
+
+ expect(response).to redirect_to root_url
+ expect(flash[:error]).to eq(
+ t(
+ 'errors.sign_in.bad_password_limit',
+ time_left: time_in_hours,
+ ),
+ )
+ end
+ end
+
it 'tracks the unsuccessful authentication for existing user' do
user = create(:user, :fully_registered)
@@ -157,13 +186,6 @@
post :create, params: { user: { email: 'foo@example.com', password: 'password' } }
end
- it 'tracks unsuccessful authentication for too many auth failures' do
- allow(subject).to receive(:session_bad_password_count_max_exceeded?).and_return(true)
- mock_email_parameter = { email: 'bob@example.com' }
-
- post :create, params: { user: { **mock_email_parameter, password: 'eatCake!' } }
- end
-
it 'tracks unsuccessful authentication for locked out user' do
user = create(
:user,
diff --git a/spec/features/idv/verify_by_mail_pending_spec.rb b/spec/features/idv/verify_by_mail_pending_spec.rb
new file mode 100644
index 00000000000..dee065f1dff
--- /dev/null
+++ b/spec/features/idv/verify_by_mail_pending_spec.rb
@@ -0,0 +1,39 @@
+require 'rails_helper'
+
+RSpec.feature 'a user that is pending verify by mail', allowed_extra_analytics: [:*] do
+ include IdvStepHelper
+
+ it 'requires them to enter code or cancel to enter the proofing flow' do
+ user = create(:user, :fully_registered)
+ profile = create(:profile, :with_pii, :verify_by_mail_pending, user: user)
+ create(:gpo_confirmation_code, profile: profile, created_at: 2.days.ago, updated_at: 2.days.ago)
+
+ start_idv_from_sp(biometric_comparison_required: false)
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ # Attempting to start IdV should require enter-code to be completed
+ visit idv_welcome_path
+ expect(current_path).to eq(idv_verify_by_mail_enter_code_path)
+
+ # Cancelling redirects to IdV flow start
+ click_on t('idv.gpo.address_accordion.cta_link')
+ click_idv_continue
+
+ expect(current_path).to eq(idv_welcome_path)
+ end
+
+ it 'does not require them to enter their code if they are upgrading to biometric' do
+ user = create(:user, :fully_registered)
+ profile = create(:profile, :with_pii, :verify_by_mail_pending, user: user)
+ create(:gpo_confirmation_code, profile: profile, created_at: 2.days.ago, updated_at: 2.days.ago)
+
+ start_idv_from_sp(biometric_comparison_required: true)
+ sign_in_live_with_2fa(user)
+
+ # The user is redirected to proofing since their pending profile does not meet
+ # the biometric comparison requirement
+ expect(current_path).to eq(idv_welcome_path)
+ end
+end
diff --git a/spec/features/visitors/bad_password_spec.rb b/spec/features/visitors/bad_password_spec.rb
index 53fe0364040..1493b96a04c 100644
--- a/spec/features/visitors/bad_password_spec.rb
+++ b/spec/features/visitors/bad_password_spec.rb
@@ -1,8 +1,10 @@
require 'rails_helper'
RSpec.feature 'Visitor signs in with bad passwords and gets locked out' do
+ include ActionView::Helpers::DateHelper
let(:user) { create(:user, :fully_registered) }
let(:bad_password) { 'badpassword' }
+ let(:window) { IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds }
scenario 'visitor tries too many bad passwords gets locked out then waits window seconds' do
visit new_user_session_path
@@ -15,14 +17,33 @@
expect(page).to have_content(error_message)
expect(page).to have_current_path(new_user_session_path)
end
+ locked_at = Time.zone.at(page.get_rack_session['max_bad_passwords_at'])
+ # Need to do this because getting rack session changes the url.
+ visit new_user_session_path
2.times do
fill_in_credentials_and_submit(user.email, bad_password)
+
expect(page).to have_current_path(new_user_session_path)
- expect(page).to have_content(t('errors.sign_in.bad_password_limit'))
+ new_time = Time.zone.at(locked_at) + window
+ time_left = distance_of_time_in_words(Time.zone.now, new_time, true)
+ expect(page).to have_content(
+ t(
+ 'errors.sign_in.bad_password_limit',
+ time_left: time_left,
+ ),
+ )
end
fill_in_credentials_and_submit(user.email, user.password)
expect(page).to have_current_path(new_user_session_path)
- expect(page).to have_content(t('errors.sign_in.bad_password_limit'))
+ new_time = Time.zone.at(locked_at) + window
+ time_left = distance_of_time_in_words(Time.zone.now, new_time, true)
+ expect(page).to have_content(
+ t(
+ 'errors.sign_in.bad_password_limit',
+ time_left: time_left,
+ ),
+ )
+
travel_to(IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds.from_now) do
fill_in_credentials_and_submit(user.email, bad_password)
expect(page).to have_content(error_message)
diff --git a/yarn.lock b/yarn.lock
index 24ae052d682..1bacc1cdecc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,13 +2,13 @@
# yarn lockfile v1
-"@18f/identity-design-system@^9.2.0":
- version "9.2.0"
- resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-9.2.0.tgz#36f1e4c4c68cae52c0cd4d5256ac33e6ef872763"
- integrity sha512-gzzcRtxRPKdxcbdgYKBS+IEmBielCwbxB9KkUTwNyXTqDMJoWmscSODPEpmegIEB8Tg/LXwxJQfr+LyEePYewQ==
+"@18f/identity-design-system@^9.3.0":
+ version "9.3.0"
+ resolved "https://registry.yarnpkg.com/@18f/identity-design-system/-/identity-design-system-9.3.0.tgz#990089ea93c5ac773cf87ac61d175d739ec89ae7"
+ integrity sha512-vOhBNu20rtSUUIx0bpyLjUTcOzyzLpq9/Oyot+vWPYxyatTg2m+7dpQK8xSNVDtnNtpVEwOhf1GnOGF+v7n+nA==
dependencies:
"@types/uswds__uswds" "^3.8.0"
- "@uswds/uswds" "^3.8.0"
+ "@uswds/uswds" "^3.8.1"
"@aashutoshrathi/word-wrap@^1.2.3":
version "1.2.6"
@@ -1895,10 +1895,10 @@
"@typescript-eslint/types" "6.7.5"
eslint-visitor-keys "^3.4.1"
-"@uswds/uswds@^3.8.0":
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/@uswds/uswds/-/uswds-3.8.0.tgz#dba0b0b38182053779276f9ae6809474bd31d548"
- integrity sha512-rMwCXe/u4HGkfskvS1Iuabapi/EXku3ChaIFW7y/dUhc7R1TXQhbbfp8YXEjmXPF0yqJnv9T08WPgS0fQqWZ8w==
+"@uswds/uswds@^3.8.1":
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/@uswds/uswds/-/uswds-3.8.1.tgz#3d834559498ae1bb7d3a618f3f85a5f4e9818497"
+ integrity sha512-bKG/B9mJF1v0yoqth48wQDzST5Xyu3OxxpePIPDyhKWS84oDrCehnu3Z88JhSjdIAJMl8dtjtH8YvdO9kZUpAg==
dependencies:
classlist-polyfill "1.2.0"
object-assign "4.1.1"