diff --git a/app/components/badge_component.html.erb b/app/components/badge_component.html.erb index e0bbd3bc4a5..e87eafbe2a4 100644 --- a/app/components/badge_component.html.erb +++ b/app/components/badge_component.html.erb @@ -1,4 +1,4 @@ -<%= content_tag('div', **tag_options, class: ['lg-verification-badge', *tag_options[:class]]) do %> - <%= render IconComponent.new(icon:, class: 'text-success') %> +<%= content_tag('div', **tag_options, class: ['lg-verification-badge', border_css_class, *tag_options[:class]]) do %> + <%= render IconComponent.new(icon:, class: icon_css_class) %> <%= content %> <% end %> diff --git a/app/components/badge_component.rb b/app/components/badge_component.rb index 6f5f8a75c2c..f42563c8479 100644 --- a/app/components/badge_component.rb +++ b/app/components/badge_component.rb @@ -4,6 +4,8 @@ class BadgeComponent < BaseComponent ICONS = %i[ lock check_circle + warning + info ].to_set.freeze attr_reader :icon, :tag_options @@ -13,4 +15,23 @@ def initialize(icon:, **tag_options) @icon = icon @tag_options = tag_options end + + def color_token + case icon + when :check_circle, :lock + 'success' + when :warning + 'warning' + else + 'info' + end + end + + def border_css_class + "border-#{color_token}" + end + + def icon_css_class + "text-#{color_token}" + end end diff --git a/app/components/tooltip_component.rb b/app/components/tooltip_component.rb new file mode 100644 index 00000000000..c5b6ee77471 --- /dev/null +++ b/app/components/tooltip_component.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class TooltipComponent < BaseComponent + attr_reader :tooltip_text, :tag_options + + def initialize(tooltip_text:, **tag_options) + @tooltip_text = tooltip_text + @tag_options = tag_options + end + + def call + content_tag(:'lg-tooltip', content, **tag_options, 'tooltip-text': tooltip_text) + end +end diff --git a/app/components/tooltip_component.scss b/app/components/tooltip_component.scss new file mode 100644 index 00000000000..1b48fc8121b --- /dev/null +++ b/app/components/tooltip_component.scss @@ -0,0 +1,9 @@ +@forward 'usa-tooltip'; + +// The USWDS tooltip component does not handle child elements gracefully. Prevent non-interactive +// child elements from triggering event handlers to avoid errors. +// +// See: https://github.com/uswds/uswds/pull/5263#issuecomment-2191808834 +lg-tooltip .usa-icon { + pointer-events: none; +} diff --git a/app/components/tooltip_component.ts b/app/components/tooltip_component.ts new file mode 100644 index 00000000000..99358f6e9c6 --- /dev/null +++ b/app/components/tooltip_component.ts @@ -0,0 +1 @@ +import '@18f/identity-tooltip/tooltip-element'; diff --git a/app/controllers/accounts/connected_accounts_controller.rb b/app/controllers/accounts/connected_accounts_controller.rb index 1a01f884a7c..401ef2eda2a 100644 --- a/app/controllers/accounts/connected_accounts_controller.rb +++ b/app/controllers/accounts/connected_accounts_controller.rb @@ -11,6 +11,7 @@ def show @presenter = AccountShowPresenter.new( decrypted_pii: nil, sp_session_request_url: sp_session_request_url_with_updated_params, + authn_context: resolved_authn_context_result, sp_name: decorated_sp_session.sp_name, user: current_user, locked_for_session: pii_locked_for_session?(current_user), diff --git a/app/controllers/accounts/history_controller.rb b/app/controllers/accounts/history_controller.rb index 3a25dddd783..cbdfb77350f 100644 --- a/app/controllers/accounts/history_controller.rb +++ b/app/controllers/accounts/history_controller.rb @@ -11,6 +11,7 @@ def show @presenter = AccountShowPresenter.new( decrypted_pii: nil, sp_session_request_url: sp_session_request_url_with_updated_params, + authn_context: resolved_authn_context_result, sp_name: decorated_sp_session.sp_name, user: current_user, locked_for_session: pii_locked_for_session?(current_user), diff --git a/app/controllers/accounts/two_factor_authentication_controller.rb b/app/controllers/accounts/two_factor_authentication_controller.rb index b97e273fa7e..d8dfbdf9803 100644 --- a/app/controllers/accounts/two_factor_authentication_controller.rb +++ b/app/controllers/accounts/two_factor_authentication_controller.rb @@ -12,6 +12,7 @@ def show @presenter = AccountShowPresenter.new( decrypted_pii: nil, sp_session_request_url: sp_session_request_url_with_updated_params, + authn_context: resolved_authn_context_result, sp_name: decorated_sp_session.sp_name, user: current_user, locked_for_session: pii_locked_for_session?(current_user), diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 53b68f4ab3b..90d78f33bf3 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -14,6 +14,7 @@ def show @presenter = AccountShowPresenter.new( decrypted_pii: cacher.fetch(current_user.active_or_pending_profile&.id), sp_session_request_url: sp_session_request_url_with_updated_params, + authn_context: resolved_authn_context_result, sp_name: decorated_sp_session.sp_name, user: current_user, locked_for_session: pii_locked_for_session?(current_user), diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 9c4c879c753..d950e090f40 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -12,6 +12,7 @@ def show @presenter = AccountShowPresenter.new( decrypted_pii: nil, sp_session_request_url: sp_session_request_url_with_updated_params, + authn_context: resolved_authn_context_result, sp_name: decorated_sp_session.sp_name, user: current_user, locked_for_session: pii_locked_for_session?(current_user), diff --git a/app/javascript/packages/tooltip/README.md b/app/javascript/packages/tooltip/README.md new file mode 100644 index 00000000000..99d034417ec --- /dev/null +++ b/app/javascript/packages/tooltip/README.md @@ -0,0 +1,24 @@ +# `@18f/identity-tooltip` + +Custom element for a tooltip component. + +## Usage + +Importing the element will register the `` custom element: + +```ts +import '@18f/identity-tooltip/tooltip-element'; +``` + +The custom element will implement behaviors for showing tooltip text on hover or focus, but all markup must already exist. + +> [!WARNING] +> Due to existing issues with the U.S. Web Design System Tooltip component, there are a few limitations to be aware of: +> 1. Content must be wrapped in a wrapper element, such as a `` tag. +> 2. Any other nested child elements must be non-interactive, using `pointer-events: none;`. + +```html + + Verified + +``` diff --git a/app/javascript/packages/tooltip/package.json b/app/javascript/packages/tooltip/package.json new file mode 100644 index 00000000000..bc0959c9b56 --- /dev/null +++ b/app/javascript/packages/tooltip/package.json @@ -0,0 +1,11 @@ +{ + "name": "@18f/identity-tooltip", + "version": "1.0.0", + "private": true, + "dependencies": { + "@18f/identity-design-system": "^9.3.0" + }, + "sideEffects": [ + "./tooltip-element.ts" + ] +} diff --git a/app/javascript/packages/tooltip/tooltip-element.spec.ts b/app/javascript/packages/tooltip/tooltip-element.spec.ts new file mode 100644 index 00000000000..f1227d6d302 --- /dev/null +++ b/app/javascript/packages/tooltip/tooltip-element.spec.ts @@ -0,0 +1,27 @@ +import { screen, getByText, waitFor } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import { computeAccessibleDescription } from 'dom-accessibility-api'; +import './tooltip-element'; + +describe('TooltipElement', () => { + function createAndConnectElement({ tooltipText = '', innerHTML = 'Verified' } = {}) { + const element = document.createElement('lg-tooltip'); + element.setAttribute('tooltip-text', tooltipText); + element.innerHTML = innerHTML; + document.body.appendChild(element); + return element; + } + + it('initializes tooltip element', async () => { + const tooltipText = 'Your identity has been verified'; + const element = createAndConnectElement({ tooltipText }); + + const content = getByText(element, 'Verified'); + + await userEvent.hover(content); + expect(computeAccessibleDescription(content)).to.be.equal(tooltipText); + await waitFor(() => { + expect(screen.getByText(tooltipText).classList.contains('is-visible')).to.be.true(); + }); + }); +}); diff --git a/app/javascript/packages/tooltip/tooltip-element.ts b/app/javascript/packages/tooltip/tooltip-element.ts new file mode 100644 index 00000000000..fe5e4372d96 --- /dev/null +++ b/app/javascript/packages/tooltip/tooltip-element.ts @@ -0,0 +1,32 @@ +import { tooltip } from '@18f/identity-design-system'; + +class TooltipElement extends HTMLElement { + connectedCallback() { + this.tooltipElement.setAttribute('title', this.tooltipText); + this.tooltipElement.classList.add('usa-tooltip'); + tooltip.on(this.tooltipElement); + } + + get tooltipElement(): HTMLElement { + return this.firstElementChild as HTMLElement; + } + + /** + * Retrieves the text to be shown in the tooltip. + */ + get tooltipText(): string { + return this.getAttribute('tooltip-text')!; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'lg-tooltip': TooltipElement; + } +} + +if (!customElements.get('lg-tooltip')) { + customElements.define('lg-tooltip', TooltipElement); +} + +export default TooltipElement; diff --git a/app/presenters/account_show_presenter.rb b/app/presenters/account_show_presenter.rb index 109bc1286c5..d2f9e70c804 100644 --- a/app/presenters/account_show_presenter.rb +++ b/app/presenters/account_show_presenter.rb @@ -1,14 +1,29 @@ # frozen_string_literal: true class AccountShowPresenter - attr_reader :user, :decrypted_pii, :locked_for_session, :pii, :sp_session_request_url, :sp_name - - def initialize(decrypted_pii:, sp_session_request_url:, sp_name:, user:, - locked_for_session:) + attr_reader :user, + :decrypted_pii, + :locked_for_session, + :pii, + :sp_session_request_url, + :authn_context, + :sp_name + + delegate :identity_verified_with_biometric_comparison?, to: :user + + def initialize( + decrypted_pii:, + sp_session_request_url:, + authn_context:, + sp_name:, + user:, + locked_for_session: + ) @decrypted_pii = decrypted_pii @user = user @sp_name = sp_name @sp_session_request_url = sp_session_request_url + @authn_context = authn_context @locked_for_session = locked_for_session @pii = determine_pii end @@ -17,10 +32,6 @@ def show_password_reset_partial? user.password_reset_profile.present? end - def show_pii_partial? - decrypted_pii.present? || user.identity_verified? - end - def show_manage_personal_key_partial? user.encrypted_recovery_code_digest.present? && user.password_reset_profile.blank? @@ -30,14 +41,45 @@ def show_service_provider_continue_partial? sp_name.present? && sp_session_request_url.present? end - def show_gpo_partial? + def showing_alerts? + show_service_provider_continue_partial? || + show_password_reset_partial? + end + + def active_profile? + user.active_profile.present? + end + + def active_profile_for_authn_context? + return @active_profile_for_authn_context if defined?(@active_profile_for_authn_context) + + @active_profile_for_authn_context = active_profile? && ( + !authn_context.biometric_comparison? || identity_verified_with_biometric_comparison? + ) + end + + def pending_idv? + authn_context.identity_proofing? && !active_profile_for_authn_context? + end + + def pending_ipp? + user.pending_in_person_enrollment.present? + end + + def pending_gpo? user.gpo_verification_pending_profile? end - def showing_any_partials? - show_service_provider_continue_partial? || - show_password_reset_partial? || - show_gpo_partial? + def show_idv_partial? + active_profile? || pending_idv? || pending_ipp? || pending_gpo? + end + + def formatted_ipp_due_date + I18n.l(user.pending_in_person_enrollment.due_date, format: :event_date) + end + + def formatted_nonbiometric_idv_date + I18n.l(user.active_profile.created_at, format: :event_date) end def show_unphishable_badge? @@ -119,7 +161,7 @@ def decrypted_pii_accessor end def determine_pii - return PiiAccessor.new unless show_pii_partial? + return PiiAccessor.new unless active_profile? if decrypted_pii.present? && !@locked_for_session decrypted_pii_accessor else diff --git a/app/views/accounts/_badges.html.erb b/app/views/accounts/_badges.html.erb index 1dfdaf4f1c0..0b12c8fbc93 100644 --- a/app/views/accounts/_badges.html.erb +++ b/app/views/accounts/_badges.html.erb @@ -1,7 +1,3 @@ <% if @presenter.show_unphishable_badge? %> <%= render BadgeComponent.new(icon: :lock).with_content(t('headings.account.unphishable')) %> <% end %> - -<% if @presenter.show_verified_badge? %> - <%= render BadgeComponent.new(icon: :check_circle).with_content(t('headings.account.verified_account')) %> -<% end %> diff --git a/app/views/accounts/_identity_verification.html.erb b/app/views/accounts/_identity_verification.html.erb new file mode 100644 index 00000000000..5ef2ebd5f60 --- /dev/null +++ b/app/views/accounts/_identity_verification.html.erb @@ -0,0 +1,88 @@ +
+

+ <%= t('account.index.verification.identity_verification') %> +

+
+ <% if @presenter.active_profile_for_authn_context? %> + <%= render TooltipComponent.new(tooltip_text: @presenter.identity_verified_with_biometric_comparison? ? t('account.index.verification.verified_biometric_badge_tooltip') : t('account.index.verification.verified_badge_tooltip')) do %> + <%= render BadgeComponent.new(icon: :check_circle).with_content(t('account.index.verification.verified_badge')) %> + <% end %> + <% elsif @presenter.pending_gpo? || @presenter.pending_ipp? %> + <%= render TooltipComponent.new(tooltip_text: t('account.index.verification.pending_badge_tooltip')) do %> + <%= render BadgeComponent.new(icon: :info).with_content(t('account.index.verification.pending_badge')) %> + <% end %> + <% else %> + <%= render TooltipComponent.new(tooltip_text: t('account.index.verification.unverified_badge_tooltip')) do %> + <%= render BadgeComponent.new(icon: :warning).with_content(t('account.index.verification.unverified_badge')) %> + <% end %> + <% end %> +
+
+ +<% if @presenter.active_profile? || @presenter.pending_ipp? || @presenter.pending_gpo? %> +

+ <% if @presenter.active_profile_for_authn_context? %> + <% if @presenter.identity_verified_with_biometric_comparison? %> + <%= t('account.index.verification.you_verified_your_biometric_identity', app_name: APP_NAME) %> + <% else %> + <%= t('account.index.verification.you_verified_your_identity_html', sp_name: @presenter.user.active_profile.initiating_service_provider&.friendly_name || APP_NAME) %> + <% end %> + <% elsif @presenter.active_profile? %> + <%= t('account.index.verification.nonbiometric_verified_html', app_name: APP_NAME, date: @presenter.formatted_nonbiometric_idv_date) %> + <% elsif @presenter.sp_name || @presenter.user.pending_profile.initiating_service_provider %> + <%= t('account.index.verification.finish_verifying_html', sp_name: @presenter.sp_name || @presenter.user.pending_profile.initiating_service_provider.friendly_name) %> + <% else %> + <%= t('account.index.verification.finish_verifying_no_sp', app_name: APP_NAME) %> + <% end %> + + <% if !@presenter.active_profile? || @presenter.active_profile_for_authn_context? %> + <%= new_tab_link_to t('account.index.verification.learn_more_link'), help_center_redirect_path(category: 'verify-your-identity', article: 'overview', flow: :account_show, location: :idv) %> + <% end %> +

+<% end %> + +<% if @presenter.active_profile? && !@presenter.active_profile_for_authn_context? %> +

+ <%= t('account.index.verification.verify_with_biometric_html', sp_name: @presenter.sp_name) %> +

+

+ <%= new_tab_link_to t('account.index.verification.learn_more_link'), help_center_redirect_path(category: 'verify-your-identity', article: 'overview', flow: :account_show, location: :idv) %> +

+<% end %> + +<% if @presenter.pending_gpo? %> + <%= render AlertComponent.new(type: :info, text_tag: 'div') do %> +

+ <%= t('account.index.verification.instructions') %> +

+

+ <%= link_to(t('account.index.verification.reactivate_button'), idv_verify_by_mail_enter_code_path) %> +

+ <% end %> +<% elsif @presenter.pending_ipp? %> + <%= render AlertComponent.new(type: :info, text_tag: 'div') do %> +

+ <%= t('account.index.verification.in_person_instructions_html', deadline: @presenter.formatted_ipp_due_date) %> +

+

+ <%= link_to(t('account.index.verification.show_bar_code', app_name: APP_NAME), idv_in_person_ready_to_verify_url) %> +

+ <% end %> +<% elsif !@presenter.active_profile_for_authn_context? %> + <%= render AlertComponent.new(type: :warning, text_tag: 'div') do %> +

+ <% if @presenter.sp_name && !@presenter.active_profile? %> + <%= t('account.index.verification.finish_verifying_html', sp_name: @presenter.sp_name) %> + <% else %> + <%= t('account.index.verification.finish_verifying_no_sp', app_name: APP_NAME) %> + <% end %> +

+

+ <%= link_to(t('account.index.verification.continue_idv'), idv_path) %> +

+ <% end %> +<% end %> + +<% if @presenter.active_profile_for_authn_context? %> + <%= render 'accounts/pii', pii: @presenter.pii, locked_for_session: @presenter.locked_for_session %> +<% end %> diff --git a/app/views/accounts/_pending_profile_gpo.html.erb b/app/views/accounts/_pending_profile_gpo.html.erb deleted file mode 100644 index 5ca40bc2edc..00000000000 --- a/app/views/accounts/_pending_profile_gpo.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%= render AlertComponent.new(type: :warning, text_tag: 'div') do %> -

- <%= t('account.index.verification.instructions') %> -

-

- <%= link_to t('account.index.verification.reactivate_button'), idv_verify_by_mail_enter_code_path %> -

-<% end %> diff --git a/app/views/accounts/_pii.html.erb b/app/views/accounts/_pii.html.erb index 8decfe81eb5..a2badd478d2 100644 --- a/app/views/accounts/_pii.html.erb +++ b/app/views/accounts/_pii.html.erb @@ -14,64 +14,55 @@ <% end %> -
-
-
-

- <%= t('headings.account.verified_information') %> - <%= image_tag asset_url('lock.svg'), width: 8, height: 10 %> -

+ +
+
+
+ <%= t('account.verified_information.full_name') %> +
+
+ <%= pii.full_name %>
-
-
-
- <%= t('account.verified_information.full_name') %> -
-
- <%= pii.full_name %> -
+
+
+ <%= t('account.verified_information.address') %>
-
-
- <%= t('account.verified_information.address') %> -
-
- <%= render 'shared/address', address: pii %> -
+
+ <%= render 'shared/address', address: pii %>
-
-
- <%= t('account.verified_information.dob') %> -
-
- <%= pii.dob %> -
+
+
+
+ <%= t('account.verified_information.dob') %>
-
-
- <%= t('account.verified_information.ssn') %> -
-
- ***-**-**** -
+
+ <%= pii.dob %>
-
-
- <%= t('account.verified_information.phone_number') %> -
-
- <%= PhoneFormatter.format(pii.phone) %> -
+
+
+
+ <%= t('account.verified_information.ssn') %> +
+
+ ***-**-****
- <% unless locked_for_session %> -
-
- <%= image_tag asset_url('lock.svg'), width: 8, height: 10, class: 'margin-right-1' %> - <%= t('account.security.text') %> -
- <%= link_to t('account.security.link'), MarketingSite.help_url %> +
+
+ <%= t('account.verified_information.phone_number') %> +
+
+ <%= PhoneFormatter.format(pii.phone) %>
- <% end %> +
+<% unless locked_for_session %> +
+
+ <%= image_tag asset_url('lock.svg'), width: 8, height: 10, class: 'margin-right-1' %> + <%= t('account.security.text') %> +
+ <%= link_to t('account.security.link'), MarketingSite.help_url %> +
+<% end %> diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index a0188aa4db5..f9227fccbe5 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -1,22 +1,23 @@ <% self.title = t('titles.account') %> -<% if @presenter.showing_any_partials? %> +<%= render 'accounts/header', presenter: @presenter %> + +<% if @presenter.showing_alerts? %>
<% if @presenter.show_password_reset_partial? %> <%= render 'accounts/password_reset', presenter: @presenter %> <% end %> - - <% if @presenter.show_gpo_partial? %> - <%= render 'accounts/pending_profile_gpo' %> - <% end %> - <% if @presenter.show_service_provider_continue_partial? %> <%= render 'accounts/service_provider_continue', presenter: @presenter %> <% end %>
<% end %> -<%= render 'accounts/header', presenter: @presenter %> +<% if @presenter.show_idv_partial? %> +
+ <%= render 'accounts/identity_verification' %> +
+<% end %>

<%= t('account.index.email_preferences') %>

@@ -89,8 +90,3 @@ <%= render 'accounts/backup_codes' %>
<% end %> - -<% if @presenter.show_pii_partial? %> - <%= render 'accounts/pii', pii: @presenter.pii, - locked_for_session: @presenter.locked_for_session %> -<% end %> diff --git a/app/views/layouts/component_preview.html.erb b/app/views/layouts/component_preview.html.erb index 5a73c75a763..d5662395c30 100644 --- a/app/views/layouts/component_preview.html.erb +++ b/app/views/layouts/component_preview.html.erb @@ -2,8 +2,8 @@ Component Preview - <%= stylesheet_link_tag 'application', media: 'all' %> <%= render_stylesheet_once_tags %> + <%= stylesheet_link_tag 'application', media: 'all' %> <% if params.dig(:lookbook, :display, :form) == true %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 27e0a67d630..39b0502ab1f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -72,9 +72,27 @@ account.index.reactivation.instructions: Your profile was recently deactivated d account.index.reactivation.link: Reactivate your profile now. account.index.sign_in_location_and_ip: From %{ip} (IP address potentially located in %{location}) account.index.unknown_location: unknown location -account.index.verification.instructions: Your account requires a verification code to be verified. -account.index.verification.reactivate_button: Enter the code you received via US mail +account.index.verification.continue_idv: Continue identity verification +account.index.verification.finish_verifying_html: Finish verifying your identity to access %{sp_name}. +account.index.verification.finish_verifying_no_sp: Finish the identity verification process to gain access to all %{app_name} partners. +account.index.verification.identity_verification: Identity verification +account.index.verification.in_person_instructions_html: You must visit any participating Post Office by %{deadline} to verify your identity. +account.index.verification.instructions: Enter your verification code to finish verifying your identity. +account.index.verification.learn_more_link: Learn more about verifying your identity +account.index.verification.nonbiometric_verified_html: You verified your identity with %{app_name} on %{date} using your state ID. +account.index.verification.pending_badge: Pending +account.index.verification.pending_badge_tooltip: Your identity is pending verification. +account.index.verification.reactivate_button: Enter the verification code you received via US mail +account.index.verification.show_bar_code: Show me my %{app_name} barcode account.index.verification.success: We verified your information +account.index.verification.unverified_badge: Unverified +account.index.verification.unverified_badge_tooltip: Finish verifying your identity. +account.index.verification.verified_badge: Verified +account.index.verification.verified_badge_tooltip: Your identity has been verified. +account.index.verification.verified_biometric_badge_tooltip: Your identity and photo have both been verified. +account.index.verification.verify_with_biometric_html: To access %{sp_name}, verify your identity again using a photo of yourself. +account.index.verification.you_verified_your_biometric_identity: You have verified your identity with the information below and verified a photo of yourself which gives you access to all %{app_name} partners. +account.index.verification.you_verified_your_identity_html: You verified your identity for %{sp_name} with the information below. account.index.webauthn: Security key account.index.webauthn_add: Add security key account.index.webauthn_platform: Face or touch unlock @@ -887,8 +905,6 @@ headings.account.login_info: Your account headings.account.reactivate: Reactivate your account headings.account.two_factor: Your authentication methods headings.account.unphishable: Unphishable -headings.account.verified_account: Verified Account -headings.account.verified_information: Verified information headings.add_email: Add a new email address headings.add_info.phone: Add a phone number headings.cancellations.login_cancel_prompt: Are you sure you want to cancel and exit %{app_name}? diff --git a/config/locales/es.yml b/config/locales/es.yml index e0d3ee442bc..0628995d7ae 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -72,9 +72,27 @@ account.index.reactivation.instructions: Su perfil fue desactivado recientemente account.index.reactivation.link: Reactive su perfil ahora. account.index.sign_in_location_and_ip: Desde %{ip} (la dirección IP se encuentra posiblemente en %{location}) account.index.unknown_location: ubicación desconocida -account.index.verification.instructions: Su cuenta requiere un código de verificación para poderla verificar. -account.index.verification.reactivate_button: Ingrese el código que recibió por correo de los EE. UU. +account.index.verification.continue_idv: Continuar la verificación de identidad +account.index.verification.finish_verifying_html: Termine de verificar su identidad para acceder a la %{sp_name}. +account.index.verification.finish_verifying_no_sp: Termine el proceso de verificación de identidad para obtener acceso a todos los asociados de %{app_name}. +account.index.verification.identity_verification: Verificación de identidad +account.index.verification.in_person_instructions_html: Para terminar de verificar su identidad, debe acudir a una oficina de correos participante antes del %{deadline}. +account.index.verification.instructions: Ingrese su código de verificación para terminar de verificar su identidad. +account.index.verification.learn_more_link: Obtenga más información sobre la verificación de su identidad +account.index.verification.nonbiometric_verified_html: El %{date}, usted verificó su identidad con %{app_name} usando su identificación estatal. +account.index.verification.pending_badge: Pendiente +account.index.verification.pending_badge_tooltip: La verificación de su identidad está pendiente +account.index.verification.reactivate_button: Ingrese el código de verificación que recibió por correo de los EE. UU. +account.index.verification.show_bar_code: Mostrar mi código de barras de %{app_name} account.index.verification.success: Verificamos su información +account.index.verification.unverified_badge: No verificada +account.index.verification.unverified_badge_tooltip: Termine de verificar su identidad. +account.index.verification.verified_badge: Verificada +account.index.verification.verified_badge_tooltip: Se verificó su identidad. +account.index.verification.verified_biometric_badge_tooltip: Se verificó tanto su identidad como su fotografía. +account.index.verification.verify_with_biometric_html: Para acceder a la %{sp_name}, verifique su identidad de nuevo usando una foto de usted. +account.index.verification.you_verified_your_biometric_identity: Usted verificó su identidad con la información siguiente y verificó una fotografía suya, lo cual le da acceso a todos los asociados de %{app_name}. +account.index.verification.you_verified_your_identity_html: Usted verificó su identidad para %{sp_name} con la información siguiente. account.index.webauthn: Clave de seguridad account.index.webauthn_add: Agregar clave de seguridad account.index.webauthn_platform: Desbloqueo facial o táctil @@ -898,8 +916,6 @@ headings.account.login_info: Su cuenta headings.account.reactivate: Reactive su cuenta headings.account.two_factor: Sus métodos de autenticación headings.account.unphishable: No vulnerable al phishing -headings.account.verified_account: Cuenta verificada -headings.account.verified_information: Información verificada headings.add_email: Agregar una nueva dirección de correo electrónico headings.add_info.phone: Agregar un número de teléfono headings.cancellations.login_cancel_prompt: '¿Está seguro de que desea cancelar y salir de %{app_name}?' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c5f07b8e94d..ac466847ff6 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -72,9 +72,27 @@ account.index.reactivation.instructions: Votre profil a été récemment désact account.index.reactivation.link: Réactiver votre profil maintenant. account.index.sign_in_location_and_ip: De %{ip} (adresse IP éventuellement située dans %{location}) account.index.unknown_location: lieu inconnu -account.index.verification.instructions: Votre compte nécessite un code de vérification pour être vérifié. -account.index.verification.reactivate_button: Saisissez le code que vous avez reçu par la poste +account.index.verification.continue_idv: Poursuivre la vérification d’identité +account.index.verification.finish_verifying_html: Terminez la procédure de vérification d’identité pour pouvoir accéder à %{sp_name}. +account.index.verification.finish_verifying_no_sp: Terminer la procédure de vérification d’identité pour pouvoir accéder à tous les organismes partenaires de %{app_name}. +account.index.verification.identity_verification: Vérification de l’identité +account.index.verification.in_person_instructions_html: Vous devez vous rendre à un bureau de poste participant d’ici le %{deadline} pour terminer la procédure de vérification de votre identité. +account.index.verification.instructions: Saisissez votre code de vérification pour terminer la procédure de vérification d’identité. +account.index.verification.learn_more_link: En savoir plus sur la vérification de votre identité +account.index.verification.nonbiometric_verified_html: Vous avez confirmé votre identité sur %{app_name} le %{date} avec votre pièce d’identité d’État. +account.index.verification.pending_badge: En cours +account.index.verification.pending_badge_tooltip: Votre identité est en cours de vérification. +account.index.verification.reactivate_button: Saisissez le code de vérification que vous avez reçu par la poste. +account.index.verification.show_bar_code: Me montrer mon code-barre %{app_name} account.index.verification.success: Nous avons vérifié vos informations +account.index.verification.unverified_badge: Non vérifiée +account.index.verification.unverified_badge_tooltip: Terminer la vérification de votre identité. +account.index.verification.verified_badge: Vérifiée +account.index.verification.verified_badge_tooltip: Votre identité a été vérifiée. +account.index.verification.verified_biometric_badge_tooltip: Votre identité et votre photo ont été vérifiées. +account.index.verification.verify_with_biometric_html: Pour accéder à %{sp_name}, confirmez à nouveau votre identité à l’aide d’une photo de vous-même. +account.index.verification.you_verified_your_biometric_identity: Vous avez confirmé votre identité à l’aide des informations ci-dessous et d’une photo de vous-même, ce qui vous permet d’accéder à tous les organismes partenaires de %{app_name}. +account.index.verification.you_verified_your_identity_html: Vous avez confirmé votre identité auprès de %{sp_name} à l’aide des informations ci-dessous. account.index.webauthn: Clé de sécurité account.index.webauthn_add: Ajouter une clé de sécurité account.index.webauthn_platform: Déverrouillage facial ou tactile @@ -887,8 +905,6 @@ headings.account.login_info: Votre compte headings.account.reactivate: Réactiver votre compte headings.account.two_factor: Vos méthodes d’authentification headings.account.unphishable: Non hameçonnable -headings.account.verified_account: Compte vérifié -headings.account.verified_information: Informations vérifiées headings.add_email: Ajouter une nouvelle adresse e-mail headings.add_info.phone: Ajouter un numéro de téléphone headings.cancellations.login_cancel_prompt: Êtes-vous sûr de vouloir annuler et quitter %{app_name}? diff --git a/config/locales/zh.yml b/config/locales/zh.yml index adc2d825393..e277884889e 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -72,9 +72,27 @@ account.index.reactivation.instructions: 你的用户资料因为重设密码最 account.index.reactivation.link: 现在重新激活你的用户资料。 account.index.sign_in_location_and_ip: 从 %{ip}(IP 地址可能位于 %{location})。 account.index.unknown_location: 未知地点 -account.index.verification.instructions: 你账户的验证需要一个验证码。 -account.index.verification.reactivate_button: 输入你通过邮局邮件收到的代码。 +account.index.verification.continue_idv: 继续身份验证 +account.index.verification.finish_verifying_html: 完成身份验证流程来获得访问 %{sp_name} 的权限。 +account.index.verification.finish_verifying_no_sp: 完成身份验证流程来获得访问%{app_name} 合作伙伴机构的权限。 +account.index.verification.identity_verification: 身份验证 +account.index.verification.in_person_instructions_html: 你必须在 %{deadline} 之前去邮局完成验证你的身份。 +account.index.verification.instructions: 输入你的验证码来完成身份验证。 +account.index.verification.learn_more_link: 了解更多有关验证你身份的信息。 +account.index.verification.nonbiometric_verified_html: 你在 %{date} 使用州颁发的身份证件在%{app_name} 验证了身份。 +account.index.verification.pending_badge: 待验证 +account.index.verification.pending_badge_tooltip: 你的身份有待验证。 +account.index.verification.reactivate_button: 输入你通过邮局收到的验证码。 +account.index.verification.show_bar_code: 显示我的%{app_name} 条形码 account.index.verification.success: 我们验证了你的信息 +account.index.verification.unverified_badge: 未验证 +account.index.verification.unverified_badge_tooltip: 完成验证你的身份。 +account.index.verification.verified_badge: 已验证 +account.index.verification.verified_badge_tooltip: 你的身份已经验证。 +account.index.verification.verified_biometric_badge_tooltip: 你的身份和照片都已验证。 +account.index.verification.verify_with_biometric_html: 要访问 %{sp_name},请使用你本人照片再次验证身份。 +account.index.verification.you_verified_your_biometric_identity: 你使用以下信息验证了身份并验证了一张你本人的照片,从而获得了访问%{app_name}所有合作伙伴机构的权限。 +account.index.verification.you_verified_your_identity_html: 你使用以下信息向 %{sp_name} 验证了身份。 account.index.webauthn: 安全密钥 account.index.webauthn_add: 添加安全密钥 account.index.webauthn_platform: 人脸或触摸解锁 @@ -902,8 +920,6 @@ headings.account.login_info: 你的账户 headings.account.reactivate: 重新激活你的账户 headings.account.two_factor: 你的身份证实方法。 headings.account.unphishable: 无法网络钓鱼 -headings.account.verified_account: 验证过的账户 -headings.account.verified_information: 验证过的信息 headings.add_email: 添加一个新电邮地址 headings.add_info.phone: 添加一个电话号码 headings.cancellations.login_cancel_prompt: 你确定要取消并退出 %{app_name} 吗? diff --git a/lib/action_account.rb b/lib/action_account.rb index 01ca3984e45..61bb6e8fc13 100644 --- a/lib/action_account.rb +++ b/lib/action_account.rb @@ -80,6 +80,7 @@ def log_text user_reinstated: 'User has been reinstated and the user has been emailed', user_already_suspended: 'User has already been suspended', user_is_not_suspended: 'User is not suspended', + user_already_reinstated: 'User has already been reinstated', } end end @@ -108,6 +109,8 @@ def perform_user_action(args:, config:, action:) if user.suspended? user.reinstate! log_texts << log_text[:user_reinstated] + elsif user.reinstated? + log_texts << (log_text[:user_already_reinstated] + " (at #{user.reinstated_at})") else log_texts << log_text[:user_is_not_suspended] end diff --git a/spec/components/badge_component_spec.rb b/spec/components/badge_component_spec.rb index 356213a6a69..cfebd37187a 100644 --- a/spec/components/badge_component_spec.rb +++ b/spec/components/badge_component_spec.rb @@ -29,7 +29,7 @@ let(:icon) { :check_circle } it 'renders badge with icon and content' do - expect(rendered).to have_css('.lg-verification-badge .usa-icon.text-success') + expect(rendered).to have_css('.lg-verification-badge .usa-icon') inline_icon_style = rendered.at_css('.usa-icon style').text.strip expect(inline_icon_style).to match(%r{url\([^)]+?/check_circle-\w+\.svg\)}) end @@ -41,5 +41,37 @@ expect(rendered).to have_css('.lg-verification-badge.example-class[data-foo="bar"]') end end + + context 'with lock icon' do + let(:icon) { :lock } + + it 'renders with success color' do + expect(rendered).to have_css('.lg-verification-badge.border-success .usa-icon.text-success') + end + end + + context 'with check_circle icon' do + let(:icon) { :check_circle } + + it 'renders with success color' do + expect(rendered).to have_css('.lg-verification-badge.border-success .usa-icon.text-success') + end + end + + context 'with warning icon' do + let(:icon) { :warning } + + it 'renders with warning color' do + expect(rendered).to have_css('.lg-verification-badge.border-warning .usa-icon.text-warning') + end + end + + context 'with info icon' do + let(:icon) { :info } + + it 'renders with info color' do + expect(rendered).to have_css('.lg-verification-badge.border-info .usa-icon.text-info') + end + end end end diff --git a/spec/components/previews/badge_component_preview.rb b/spec/components/previews/badge_component_preview.rb index 3df85d751f7..bfa983bf187 100644 --- a/spec/components/previews/badge_component_preview.rb +++ b/spec/components/previews/badge_component_preview.rb @@ -1,11 +1,23 @@ class BadgeComponentPreview < BaseComponentPreview # @!group Preview - def default + def check_circle_icon render(BadgeComponent.new(icon: :check_circle).with_content('Verified Account')) end + + def lock_icon + render(BadgeComponent.new(icon: :lock).with_content('Unphishable')) + end + + def warning_icon + render(BadgeComponent.new(icon: :warning).with_content('Unverified')) + end + + def info_icon + render(BadgeComponent.new(icon: :info).with_content('Pending')) + end # @!endgroup - # @param icon select [check_circle,lock] + # @param icon select [check_circle,lock,warning,info] # @param content text def workbench(icon: :check_circle, content: 'Verified Account') render(BadgeComponent.new(icon: icon&.to_sym).with_content(content)) diff --git a/spec/components/previews/tooltip_component_preview.rb b/spec/components/previews/tooltip_component_preview.rb new file mode 100644 index 00000000000..8ebf488239b --- /dev/null +++ b/spec/components/previews/tooltip_component_preview.rb @@ -0,0 +1,22 @@ +class TooltipComponentPreview < BaseComponentPreview + # @!group Preview + # @display body_class padding-10 + def default + render( + TooltipComponent. + new(tooltip_text: 'Finish verifying your identity.'). + with_content(content_tag(:span, 'Unverified')), + ) + end + # @!endgroup + + # @param content text + # @param tooltip_text text + # @display body_class padding-10 + def workbench( + content: 'Unverified', + tooltip_text: 'Finish verifying your identity.' + ) + render(TooltipComponent.new(tooltip_text:).with_content(content_tag(:span, content))) + end +end diff --git a/spec/components/tooltip_component_spec.rb b/spec/components/tooltip_component_spec.rb new file mode 100644 index 00000000000..d08db6738ed --- /dev/null +++ b/spec/components/tooltip_component_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +RSpec.describe TooltipComponent, type: :component do + let(:tooltip_text) { 'Your identity has been verified.' } + let(:options) { {} } + let(:content) { 'Verified' } + + subject(:rendered) do + render_inline TooltipComponent.new(tooltip_text:, **options).with_content(content) + end + + it 'renders badge as tooltip, with content and tooltip text' do + expect(rendered).to have_css("lg-tooltip[tooltip-text='#{tooltip_text}']", text: content) + end + + context 'with additional tag options' do + let(:options) { super().merge(data: { foo: 'bar' }) } + + it 'renders tag options on root wrapper element' do + expect(rendered).to have_css('lg-tooltip[data-foo="bar"]') + end + end +end diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 5e796a918ce..767e654ffc0 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -95,6 +95,7 @@ presenter = AccountShowPresenter.new( decrypted_pii: nil, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, user: user, locked_for_session: false, @@ -107,23 +108,6 @@ end end - context 'when a profile is pending' do - render_views - it 'renders the pending profile banner' do - user = create( - :user, - :fully_registered, - profiles: [build(:profile, gpo_verification_pending_at: 1.day.ago)], - ) - - sign_in user - get :show - - expect(response).to render_template(:show) - expect(response).to render_template(partial: 'accounts/_pending_profile_gpo') - end - end - context 'when a user is suspended' do it 'redirects to contact support page' do user = create(:user, :fully_registered, :suspended) @@ -149,6 +133,7 @@ presenter = AccountShowPresenter.new( decrypted_pii: nil, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, user: user, locked_for_session: false, diff --git a/spec/factories/users.rb b/spec/factories/users.rb index c6005b94f6e..85a56ceba1b 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -311,7 +311,7 @@ fully_registered after :build do |user| - create(:profile, :password_reset, :with_pii, user: user) + create(:profile, :verified, :password_reset, :with_pii, user: user) end end diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index f9cca491830..8f980c5e72d 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -313,7 +313,7 @@ click_idv_continue expect(page).to have_current_path(account_path) - expect(page).not_to have_content(t('headings.account.verified_account')) + expect(page).not_to have_content(t('account.index.verification.verified_badge')) click_on t('account.index.verification.reactivate_button') expect_in_person_gpo_step_indicator_current_step( t('step_indicator.flows.idv.verify_address'), diff --git a/spec/features/users/password_recovery_via_recovery_code_spec.rb b/spec/features/users/password_recovery_via_recovery_code_spec.rb index ef92a6cab7f..6c755692ba0 100644 --- a/spec/features/users/password_recovery_via_recovery_code_spec.rb +++ b/spec/features/users/password_recovery_via_recovery_code_spec.rb @@ -43,7 +43,7 @@ visit account_path - expect(page).not_to have_content(t('headings.account.verified_account')) + expect(page).not_to have_content(t('account.index.verification.verified_badge')) click_link t('account.index.reactivation.link') click_on t('links.account.reactivate.without_key') @@ -58,7 +58,7 @@ visit account_path - expect(page).to have_content(t('headings.account.verified_account')) + expect(page).to have_content(t('account.index.verification.verified_badge')) expect(current_path).to eq(account_path) end diff --git a/spec/features/users/user_profile_spec.rb b/spec/features/users/user_profile_spec.rb index 0c90faf9bab..a357145a0d0 100644 --- a/spec/features/users/user_profile_spec.rb +++ b/spec/features/users/user_profile_spec.rb @@ -16,7 +16,7 @@ let(:profile) { create(:profile, :active, :verified, pii: { ssn: '111', dob: '1920-01-01' }) } it 'shows a "Verified Account" badge with no tooltip' do - expect(page).to have_content(t('headings.account.verified_account')) + expect(page).to have_content(t('account.index.verification.verified_badge')) end end end diff --git a/spec/lib/action_account_spec.rb b/spec/lib/action_account_spec.rb index 474954a44d7..57467056afb 100644 --- a/spec/lib/action_account_spec.rb +++ b/spec/lib/action_account_spec.rb @@ -307,6 +307,21 @@ expect(result.subtask).to eq('reinstate-user') expect(result.uuids).to match_array([user.uuid, suspended_user.uuid]) end + + context 'with a reinstated user' do + let(:user) { create(:user, :reinstated) } + let(:args) { [user.uuid] } + + it 'gives a helpful error if the user has been reinstated' do + message = "User has already been reinstated (at #{user.reinstated_at})" + expect(result.table).to match_array( + [ + ['uuid', 'status', 'reason'], + [user.uuid, message, 'INV1234'], + ], + ) + end + end end end diff --git a/spec/presenters/account_show_presenter_spec.rb b/spec/presenters/account_show_presenter_spec.rb index 270e4b99e17..4ea28b060bc 100644 --- a/spec/presenters/account_show_presenter_spec.rb +++ b/spec/presenters/account_show_presenter_spec.rb @@ -1,6 +1,227 @@ require 'rails_helper' RSpec.describe AccountShowPresenter do + let(:vtr) { ['C2'] } + let(:decrypted_pii) { nil } + let(:sp_session_request_url) { nil } + let(:authn_context) do + AuthnContextResolver.new( + user:, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + end + let(:sp_name) { nil } + let(:user) { build(:user) } + let(:locked_for_session) { false } + subject(:presenter) do + AccountShowPresenter.new( + decrypted_pii:, + sp_session_request_url:, + authn_context:, + sp_name:, + user:, + locked_for_session:, + ) + end + + describe 'identity_verified_with_biometric_comparison?' do + subject(:identity_verified_with_biometric_comparison?) do + presenter.identity_verified_with_biometric_comparison? + end + + it 'delegates to user' do + expect(identity_verified_with_biometric_comparison?).to eq( + user.identity_verified_with_biometric_comparison?, + ) + end + end + + describe '#showing_alerts?' do + subject(:showing_alerts?) { presenter.showing_alerts? } + + it { is_expected.to eq(false) } + + context 'with associated sp' do + let(:sp_session_request_url) { 'http://example.test' } + let(:sp_name) { 'Example SP' } + + it { is_expected.to eq(true) } + end + + context 'with password reset profile' do + let(:user) { build(:user, :deactivated_password_reset_profile) } + + it { is_expected.to eq(true) } + end + end + + describe '#active_profile?' do + subject(:active_profile?) { presenter.active_profile? } + + it { is_expected.to eq(false) } + + context 'with proofed user' do + let(:user) { build(:user, :proofed) } + + it { is_expected.to eq(true) } + end + + context 'with user who proofed but has pending profile' do + let(:user) { build(:user, :deactivated_password_reset_profile) } + + it { is_expected.to eq(false) } + end + end + + describe '#active_profile_for_authn_context?' do + subject(:active_profile_for_authn_context?) { presenter.active_profile_for_authn_context? } + + it { is_expected.to eq(false) } + + context 'with non-biometric proofed user' do + let(:user) { build(:user, :proofed) } + + it { is_expected.to eq(true) } + + context 'with sp request for non-biometric' do + let(:vtr) { ['C2.P1'] } + + it { is_expected.to eq(true) } + end + + context 'with sp request for biometric' do + let(:vtr) { ['C2.Pb'] } + + it { is_expected.to eq(false) } + end + end + + context 'with biometric proofed user' do + let(:user) { build(:user, :proofed_with_selfie) } + + it { is_expected.to eq(true) } + + context 'with sp request for biometric' do + let(:vtr) { ['C2.Pb'] } + + it { is_expected.to eq(true) } + end + end + end + + context '#pending_idv?' do + subject(:pending_idv?) { presenter.pending_idv? } + + it { is_expected.to eq(false) } + + context 'with sp request for non-biometric' do + let(:vtr) { ['C2.P1'] } + + it { is_expected.to eq(true) } + + context 'with non-biometric proofed user' do + let(:user) { build(:user, :proofed) } + + it { is_expected.to eq(false) } + end + end + + context 'with sp request for biometric' do + let(:vtr) { ['C2.Pb'] } + + it { is_expected.to eq(true) } + + context 'with non-biometric proofed user' do + let(:user) { build(:user, :proofed) } + + it { is_expected.to eq(true) } + end + + context 'with biometric proofed user' do + let(:user) { build(:user, :proofed_with_selfie) } + + it { is_expected.to eq(false) } + end + end + end + + context '#pending_ipp?' do + subject(:pending_ipp?) { presenter.pending_ipp? } + + it { is_expected.to eq(false) } + + context 'with user pending ipp verification' do + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + it { is_expected.to eq(true) } + end + end + + context '#pending_gpo?' do + subject(:pending_gpo?) { presenter.pending_gpo? } + + it { is_expected.to eq(false) } + + context 'with user pending gpo verification' do + let(:user) { create(:user, :with_pending_gpo_profile) } + + it { is_expected.to eq(true) } + end + end + + context '#show_idv_partial?' do + subject(:show_idv_partial?) { presenter.show_idv_partial? } + + it { is_expected.to eq(false) } + + context 'with proofed user' do + let(:user) { build(:user, :proofed) } + + it { is_expected.to eq(true) } + end + + context 'with pending idv' do + let(:user) { build(:user, :proofed) } + let(:vtr) { ['C2.Pb'] } + + it { is_expected.to eq(true) } + end + + context 'with user pending ipp verification' do + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + it { is_expected.to eq(true) } + end + + context 'with user pending gpo verification' do + let(:user) { create(:user, :with_pending_gpo_profile) } + + it { is_expected.to eq(true) } + end + end + + describe '#formatted_ipp_due_date' do + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + subject(:formatted_ipp_due_date) { presenter.formatted_ipp_due_date } + + it 'formats a date string' do + expect { Date.parse(formatted_ipp_due_date) }.not_to raise_error + end + end + + describe '#formatted_nonbiometric_idv_date' do + let(:user) { build(:user, :proofed_with_selfie) } + + subject(:formatted_nonbiometric_idv_date) { presenter.formatted_nonbiometric_idv_date } + + it 'formats a date string' do + expect { Date.parse(formatted_nonbiometric_idv_date) }.not_to raise_error + end + end + describe '#header_personalization' do context 'AccountShowPresenter instance has decrypted_pii' do it "returns the user's first name" do @@ -16,6 +237,7 @@ decrypted_pii: decrypted_pii, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ) @@ -33,6 +255,7 @@ decrypted_pii: {}, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ) @@ -54,6 +277,7 @@ decrypted_pii: {}, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ) @@ -72,6 +296,7 @@ decrypted_pii: {}, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ) @@ -90,6 +315,7 @@ account_show = AccountShowPresenter.new( decrypted_pii: {}, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, user: user.reload, locked_for_session: false, @@ -108,6 +334,7 @@ account_show = AccountShowPresenter.new( decrypted_pii: {}, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, user: user.reload, locked_for_session: false, @@ -118,26 +345,17 @@ end describe '#pii' do - let(:user) { build(:user) } - let(:decrypted_pii) do - Pii::Attributes.new_from_hash(dob: dob) - end + let(:user) { build(:user, :proofed) } + let(:dob) { nil } + let(:decrypted_pii) { Pii::Attributes.new_from_hash(dob:) } - subject(:account_show) do - AccountShowPresenter.new( - decrypted_pii: decrypted_pii, - sp_session_request_url: nil, - sp_name: nil, - user: user, - locked_for_session: false, - ) - end + subject(:pii) { presenter.pii } context 'birthday is formatted as an american date' do let(:dob) { '12/31/1970' } it 'parses the birthday' do - expect(account_show.pii.dob).to eq('December 31, 1970') + expect(pii.dob).to eq('December 31, 1970') end end @@ -145,7 +363,7 @@ let(:dob) { '1970-01-01' } it 'parses the birthday' do - expect(account_show.pii.dob).to eq('January 01, 1970') + expect(pii.dob).to eq('January 01, 1970') end end end @@ -165,6 +383,7 @@ decrypted_pii: {}, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ) @@ -188,6 +407,7 @@ decrypted_pii: {}, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ) @@ -206,6 +426,7 @@ decrypted_pii: {}, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ) diff --git a/spec/support/idv_examples/clearing_and_restarting.rb b/spec/support/idv_examples/clearing_and_restarting.rb index 3379fab7257..b5390a9a420 100644 --- a/spec/support/idv_examples/clearing_and_restarting.rb +++ b/spec/support/idv_examples/clearing_and_restarting.rb @@ -48,7 +48,7 @@ visit account_path - expect(page).to_not have_content(t('headings.account.verified_information')) + expect(page).to_not have_content(t('account.index.verification.verified_badge')) expect(page).to_not have_content(t('account.verified_information.address')) expect(page).to_not have_content(t('account.verified_information.dob')) expect(page).to_not have_content(t('account.verified_information.full_name')) diff --git a/spec/views/accounts/_badges.html.erb_spec.rb b/spec/views/accounts/_badges.html.erb_spec.rb new file mode 100644 index 00000000000..f0557adbf7c --- /dev/null +++ b/spec/views/accounts/_badges.html.erb_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +RSpec.describe 'accounts/_badges.html.erb' do + let(:user) { build(:user) } + subject(:rendered) { render partial: 'accounts/badges' } + + before do + @presenter = AccountShowPresenter.new( + decrypted_pii: nil, + sp_session_request_url: nil, + authn_context: nil, + sp_name: nil, + user:, + locked_for_session: false, + ) + end + + it 'does not render anything' do + expect(rendered).to be_empty + end + + context 'with user having only non-phishable mfa methods' do + let(:user) { build(:user, :with_webauthn) } + + it 'renders unphishable badge' do + expect(rendered).to have_content(t('headings.account.unphishable')) + end + end +end diff --git a/spec/views/accounts/_identity_verification.html.erb_spec.rb b/spec/views/accounts/_identity_verification.html.erb_spec.rb new file mode 100644 index 00000000000..75f45a5ca5b --- /dev/null +++ b/spec/views/accounts/_identity_verification.html.erb_spec.rb @@ -0,0 +1,291 @@ +require 'rails_helper' + +RSpec.describe 'accounts/_identity_verification.html.erb' do + let(:vtr) { ['C2'] } + let(:sp_name) { nil } + let(:authn_context) do + AuthnContextResolver.new( + user:, + service_provider: nil, + vtr: vtr, + acr_values: nil, + ).resolve + end + let(:user) { build(:user) } + subject(:rendered) { render partial: 'accounts/identity_verification' } + + before do + @presenter = AccountShowPresenter.new( + decrypted_pii: nil, + sp_session_request_url: nil, + authn_context:, + sp_name:, + user:, + locked_for_session: false, + ) + end + + context 'with user pending gpo verification' do + let(:gpo_sp_name) { 'GPO SP' } + let(:gpo_sp_issuer) { 'urn:gov:gsa:openidconnect:sp:gpo_sp' } + let(:gpo_sp) { create(:service_provider, issuer: gpo_sp_issuer, friendly_name: gpo_sp_name) } + let(:user) { create(:user, :with_pending_gpo_profile) } + + before do + gpo_sp + user.pending_profile.update(initiating_service_provider_issuer: gpo_sp_issuer) + end + + it 'references initiating sp with prompt to finish verifying their identity' do + expect(rendered).to have_content( + strip_tags(t('account.index.verification.finish_verifying_html', sp_name: gpo_sp_name)), + ) + end + end + + context 'with user pending ipp verification' do + let(:ipp_sp_name) { 'IPP SP' } + let(:ipp_sp_issuer) { 'urn:gov:gsa:openidconnect:sp:ipp_sp' } + let(:ipp_sp) { create(:service_provider, issuer: ipp_sp_issuer, friendly_name: ipp_sp_name) } + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + before do + ipp_sp + user.pending_profile.update(initiating_service_provider_issuer: ipp_sp_issuer) + end + + it 'references initiating sp with prompt to finish verifying their identity' do + expect(rendered).to have_content( + strip_tags(t('account.index.verification.finish_verifying_html', sp_name: ipp_sp_name)), + ) + end + end + + context 'with partner requesting non-biometric verification' do + let(:sp_name) { 'Example SP' } + let(:vtr) { ['C2.P1'] } + + context 'with unproofed user' do + let(:user) { build(:user) } + + it 'shows unverified badge' do + expect(rendered).to have_content(t('account.index.verification.unverified_badge')) + end + + it 'shows warning alert instructing user to complete identity verificaton' do + expect(rendered).to have_css('.usa-alert.usa-alert--warning') + expect(rendered).to have_content( + strip_tags(t('account.index.verification.finish_verifying_html', sp_name:)), + ) + expect(rendered).to have_link(t('account.index.verification.continue_idv'), href: idv_path) + end + end + + context 'with user pending ipp verification' do + let(:user) { build(:user, :with_pending_in_person_enrollment) } + + it 'shows pending badge' do + expect(rendered).to have_content(t('account.index.verification.pending_badge')) + end + + it 'shows content explaining that the user needs to finish verifying their identity' do + expect(rendered).to have_content( + strip_tags(t('account.index.verification.finish_verifying_html', sp_name:)), + ) + expect(rendered).to have_link( + t('account.index.verification.learn_more_link'), + href: help_center_redirect_path( + category: 'verify-your-identity', + article: 'overview', + flow: :account_show, + location: :idv, + ), + ) + end + + it 'shows info alert instructing user to go to the post office to complete verification' do + expect(rendered).to have_css('.usa-alert.usa-alert--info') + expect(rendered).to have_content( + strip_tags( + t( + 'account.index.verification.in_person_instructions_html', + deadline: @presenter.formatted_ipp_due_date, + ), + ), + ) + expect(rendered).to have_link( + t('account.index.verification.show_bar_code', app_name: APP_NAME), + href: idv_in_person_ready_to_verify_url, + ) + end + end + + context 'with user pending gpo verification' do + let(:user) { create(:user, :with_pending_gpo_profile) } + + it 'shows pending badge' do + expect(rendered).to have_content(t('account.index.verification.pending_badge')) + end + + it 'shows content explaining that the user needs to finish verifying their identity' do + expect(rendered).to have_content( + strip_tags(t('account.index.verification.finish_verifying_html', sp_name:)), + ) + expect(rendered).to have_link( + t('account.index.verification.learn_more_link'), + href: help_center_redirect_path( + category: 'verify-your-identity', + article: 'overview', + flow: :account_show, + location: :idv, + ), + ) + end + + it 'shows info alert instructing user to enter their gpo verification code' do + expect(rendered).to have_css('.usa-alert.usa-alert--info') + expect(rendered).to have_content(t('account.index.verification.instructions')) + expect(rendered).to have_link( + t('account.index.verification.reactivate_button'), + href: idv_verify_by_mail_enter_code_path, + ) + end + end + + context 'with non-biometric proofed user' do + let(:user) { build(:user, :proofed) } + + it 'shows verified badge' do + expect(rendered).to have_content(t('account.index.verification.verified_badge')) + end + + it 'shows content confirming verified identity' do + expect(rendered).to have_content( + strip_tags( + t('account.index.verification.you_verified_your_identity_html', sp_name: APP_NAME), + ), + ) + expect(rendered).to have_link( + t('account.index.verification.learn_more_link'), + href: help_center_redirect_path( + category: 'verify-your-identity', + article: 'overview', + flow: :account_show, + location: :idv, + ), + ) + end + + it 'renders pii' do + expect(rendered).to render_template(partial: 'accounts/_pii') + end + + context 'with initiating sp for active profile' do + let(:sp_name) { 'Example SP' } + let(:sp_issuer) { 'urn:gov:gsa:openidconnect:sp:example_sp' } + let(:sp) { create(:service_provider, issuer: sp_issuer, friendly_name: sp_name) } + + before do + sp + user.active_profile.update(initiating_service_provider_issuer: sp_issuer) + end + + it 'shows content confirming verified identity for initiating sp' do + expect(rendered).to have_content( + strip_tags( + t('account.index.verification.you_verified_your_identity_html', sp_name:), + ), + ) + end + end + end + end + + context 'with partner requesting biometric verification' do + let(:sp_name) { 'Example SP' } + let(:vtr) { ['C2.Pb'] } + + context 'with unproofed user' do + let(:user) { build(:user) } + + it 'shows unverified badge' do + expect(rendered).to have_content(t('account.index.verification.unverified_badge')) + end + + it 'shows warning alert instructing user to complete identity verificaton' do + expect(rendered).to have_css('.usa-alert.usa-alert--warning') + expect(rendered).to have_content( + strip_tags(t('account.index.verification.finish_verifying_html', sp_name:)), + ) + expect(rendered).to have_link(t('account.index.verification.continue_idv'), href: idv_path) + end + end + + context 'with non-biometric proofed user' do + let(:user) { build(:user, :proofed) } + + it 'shows unverified badge' do + expect(rendered).to have_content(t('account.index.verification.unverified_badge')) + end + + it 'shows content explaining that the user needs to verify their identity again' do + expect(rendered).to have_content( + strip_tags( + t( + 'account.index.verification.nonbiometric_verified_html', + app_name: APP_NAME, + date: @presenter.formatted_nonbiometric_idv_date, + ), + ), + ) + expect(rendered).to have_content( + strip_tags(t('account.index.verification.verify_with_biometric_html', sp_name:)), + ) + expect(rendered).to have_link( + t('account.index.verification.learn_more_link'), + href: help_center_redirect_path( + category: 'verify-your-identity', + article: 'overview', + flow: :account_show, + location: :idv, + ), + ) + end + + it 'shows warning alert instructing user to complete identity verificaton' do + expect(rendered).to have_css('.usa-alert.usa-alert--warning') + expect(rendered).to have_content( + t('account.index.verification.finish_verifying_no_sp', app_name: APP_NAME), + ) + expect(rendered).to have_link(t('account.index.verification.continue_idv'), href: idv_path) + end + end + + context 'with biometric proofed user' do + let(:user) { build(:user, :proofed_with_selfie) } + + it 'shows verified badge' do + expect(rendered).to have_content(t('account.index.verification.verified_badge')) + end + + it 'shows content confirming verified identity' do + expect(rendered).to have_content( + t('account.index.verification.you_verified_your_biometric_identity', app_name: APP_NAME), + ) + expect(rendered).to have_link( + t('account.index.verification.learn_more_link'), + href: help_center_redirect_path( + category: 'verify-your-identity', + article: 'overview', + flow: :account_show, + location: :idv, + ), + ) + end + + it 'renders pii' do + expect(rendered).to render_template(partial: 'accounts/_pii') + end + end + end +end diff --git a/spec/views/accounts/connected_accounts/show.html.erb_spec.rb b/spec/views/accounts/connected_accounts/show.html.erb_spec.rb index 5046e71bbdc..04332b39429 100644 --- a/spec/views/accounts/connected_accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/connected_accounts/show.html.erb_spec.rb @@ -10,6 +10,7 @@ decrypted_pii: nil, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ), diff --git a/spec/views/accounts/history/show.html.erb_spec.rb b/spec/views/accounts/history/show.html.erb_spec.rb index f6547cc3851..d23507eca37 100644 --- a/spec/views/accounts/history/show.html.erb_spec.rb +++ b/spec/views/accounts/history/show.html.erb_spec.rb @@ -11,6 +11,7 @@ decrypted_pii: nil, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ), diff --git a/spec/views/accounts/show.html.erb_spec.rb b/spec/views/accounts/show.html.erb_spec.rb index 7e0cf6fbd3e..c2f53cfe647 100644 --- a/spec/views/accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/show.html.erb_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' RSpec.describe 'accounts/show.html.erb' do + let(:authn_context) { Vot::Parser::Result.no_sp_result } let(:user) { create(:user, :fully_registered, :with_personal_key) } before do @@ -12,6 +13,7 @@ decrypted_pii: nil, user: user, sp_session_request_url: nil, + authn_context:, sp_name: nil, locked_for_session: false, ), @@ -24,6 +26,14 @@ render end + context 'when current user has a verified account' do + let(:user) { build(:user, :proofed) } + + it 'renders idv partial' do + expect(render).to render_template(partial: 'accounts/_identity_verification') + end + end + context 'when current user has password_reset_profile' do before do allow(user).to receive(:password_reset_profile).and_return(true) @@ -59,25 +69,19 @@ end end - context 'when current user has pending_profile' do - before do - pending = create( - :profile, - gpo_verification_pending_at: 2.days.ago, - created_at: 2.days.ago, - user: user, - ) - allow(user).to receive(:pending_profile).and_return(pending) + context 'when current user has gpo pending profile' do + let(:user) { create(:user, :with_pending_gpo_profile) } + + it 'renders idv partial' do + expect(render).to render_template(partial: 'accounts/_identity_verification') end + end - it 'contains a link to activate profile' do - render + context 'when current user has ipp pending profile' do + let(:user) { build(:user, :with_pending_in_person_enrollment) } - expect(rendered). - to have_link( - t('account.index.verification.reactivate_button'), - href: idv_verify_by_mail_enter_code_path, - ) + it 'renders idv partial' do + expect(render).to render_template(partial: 'accounts/_identity_verification') end end @@ -168,6 +172,7 @@ decrypted_pii: nil, user: user, sp_session_request_url: sp.return_to_sp_url, + authn_context:, sp_name: sp.friendly_name, locked_for_session: false, ), diff --git a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb index a8295d9e709..00d95067643 100644 --- a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb +++ b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb @@ -11,6 +11,7 @@ decrypted_pii: nil, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ), @@ -36,6 +37,7 @@ decrypted_pii: nil, user: user, sp_session_request_url: nil, + authn_context: nil, sp_name: nil, locked_for_session: false, ),