Skip to content

Commit

Permalink
LG-13665: Add option to change selected email shared with partner (#1…
Browse files Browse the repository at this point in the history
…1196)

* LG-13665: Add option to change selected email shared with partner

changelog: Upcoming Features, Partner Shared Email, Add option to change selected email shared with partner

* Use nil-safe access to email with fallback

Co-authored-by: Zach Margolis <[email protected]>

* Sort analytics methods

* Update view spec to use revised constructor signature

* Add failing regression spec for email language regression

* Fix regression for email language preference label

* Remove unnecessary footer condition

See: #11196 (comment)

* Sort analytics methods

* Eager load email address on connected accounts

* Add view spec for selected email

* Add feature spec for changing selected email

* Add ticket number

* Restrict controller access based on feature flag

---------

Co-authored-by: Zach Margolis <[email protected]>
  • Loading branch information
aduth and zachmargolis authored Sep 5, 2024
1 parent ae1454e commit 76dee31
Show file tree
Hide file tree
Showing 28 changed files with 606 additions and 77 deletions.
2 changes: 2 additions & 0 deletions app/components/status_page_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@
<%= troubleshooting_options %>
</div>
<% end %>

<%= footer %>
1 change: 1 addition & 0 deletions app/components/status_page_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class StatusPageComponent < BaseComponent
ButtonComponent.new(**button_options, big: true, wide: true)
end
renders_one :troubleshooting_options, TroubleshootingOptionsComponent
renders_one :footer, PageFooterComponent

attr_reader :status, :icon

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module Accounts
module ConnectedAccounts
class SelectedEmailController < ApplicationController
include RenderConditionConcern

check_or_render_not_found -> { IdentityConfig.store.feature_select_email_to_share_enabled }
before_action :confirm_two_factor_authenticated
before_action :validate_identity

def edit
@identity = identity
@select_email_form = build_select_email_form
analytics.sp_select_email_visited
end

def update
@select_email_form = build_select_email_form

result = @select_email_form.submit(form_params)

analytics.sp_select_email_submitted(**result.to_h)

if result.success?
redirect_to account_connected_accounts_path
else
flash[:error] = result.first_error_message
redirect_to edit_connected_account_selected_email_path(identity.id)
end
end

private

def form_params
params.require(:select_email_form).permit(:selected_email_id)
end

def build_select_email_form
SelectEmailForm.new(user: current_user, identity:)
end

def validate_identity
render_not_found if identity.blank?
end

def identity
return @identity if defined?(@identity)
@identity = current_user.identities.find_by(id: params[:identity_id])
end
end
end
end
5 changes: 4 additions & 1 deletion app/controllers/sign_up/select_email_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

module SignUp
class SelectEmailController < ApplicationController
include RenderConditionConcern

check_or_render_not_found -> { IdentityConfig.store.feature_select_email_to_share_enabled }
before_action :confirm_two_factor_authenticated
before_action :verify_needs_completions_screen

Expand Down Expand Up @@ -32,7 +35,7 @@ def user_emails
private

def build_select_email_form
SelectEmailForm.new(current_user)
SelectEmailForm.new(user: current_user)
end

def form_params
Expand Down
14 changes: 7 additions & 7 deletions app/forms/select_email_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@ class SelectEmailForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper

attr_reader :user, :selected_email_id
attr_reader :user, :identity, :selected_email_id

validate :validate_owns_selected_email

def initialize(user)
def initialize(user:, identity: nil)
@user = user
@identity = identity
end

def submit(params)
@selected_email_id = params[:selected_email_id]

success = valid?
FormResponse.new(success:, errors:)
identity.update(email_address_id: selected_email_id) if success && identity

FormResponse.new(success:, errors:, serialize_error_details_only: true)
end

private

def validate_owns_selected_email
return if user.confirmed_email_addresses.exists?(id: selected_email_id)

errors.add :email, I18n.t(
'email_address.not_found',
), type: :selected_email_id
errors.add(:selected_email_id, :not_found, message: t('email_address.not_found'))
end
end
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ def second_last_signed_in_at
end

def connected_apps
identities.not_deleted.includes(:service_provider_record).order('created_at DESC')
identities.not_deleted.order('created_at DESC')
end

def delete_account_bullet_key
Expand Down
6 changes: 5 additions & 1 deletion app/presenters/account_show_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ def totp_content
end
end

delegate :recent_events, :recent_devices, :connected_apps, to: :user
def connected_apps
user.connected_apps.includes([:service_provider_record, :email_address])
end

delegate :recent_events, :recent_devices, to: :user

private

Expand Down
27 changes: 27 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6150,6 +6150,33 @@ def sp_revoke_consent_visited(issuer:, **extra)
)
end

# User submitted form to change email shared with service provider
# @param [Boolean] success Whether form validation was successful
# @param [Hash] error_details Details for errors that occurred in unsuccessful submission
# @param [String, nil] needs_completion_screen_reason Reason for the consent screen being shown,
# if user is changing email in consent flow
def sp_select_email_submitted(
success:,
error_details: nil,
needs_completion_screen_reason: nil,
**extra
)
track_event(
:sp_select_email_submitted,
success:,
error_details:,
needs_completion_screen_reason:,
**extra,
)
end

# User visited form to change email shared with service provider
# @param [String, nil] needs_completion_screen_reason Reason for the consent screen being shown,
# if user is changing email in consent flow
def sp_select_email_visited(needs_completion_screen_reason: nil, **extra)
track_event(:sp_select_email_visited, needs_completion_screen_reason:, **extra)
end

# @param [String] area_code Area code of phone number
# @param [String] country_code Abbreviated 2-letter country code associated with phone number
# @param [String] phone_fingerprint HMAC fingerprint of the phone number formatted as E.164
Expand Down
23 changes: 19 additions & 4 deletions app/views/accounts/_connected_app.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,25 @@
<% end %>
</h2>

<%= t(
'account.connected_apps.associated_html',
timestamp_html: render(TimeComponent.new(time: identity.created_at)),
) %>
<% if IdentityConfig.store.feature_select_email_to_share_enabled %>
<%= t(
'account.connected_apps.associated_attributes_html',
timestamp_html: render(TimeComponent.new(time: identity.created_at)),
) %>
<br />
<strong>
<%= identity.email_address&.email || t('account.connected_apps.email_not_selected') %>
</strong>
<%= link_to(
t('help_text.requested_attributes.change_email_link'),
edit_connected_account_selected_email_path(identity_id: identity.id),
) %>
<% else %>
<%= t(
'account.connected_apps.associated_html',
timestamp_html: render(TimeComponent.new(time: identity.created_at)),
) %>
<% end %>

<% if identity.service_provider_id %>
<div class="margin-y-1">
Expand Down
45 changes: 45 additions & 0 deletions app/views/accounts/connected_accounts/selected_email/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<% self.title = t('titles.select_email') %>

<%= render StatusPageComponent.new(status: :info, icon: :question) do |c| %>
<% c.with_header(id: 'select-email-heading') { t('titles.select_email') } %>

<p id="select-email-intro">
<%= t('help_text.select_preferred_email', sp: @identity.display_name, app_name: APP_NAME) %>
</p>

<%= simple_form_for(
@select_email_form,
url: connected_account_selected_email_path(identity_id: @identity.id),
method: :patch,
) do |f| %>
<%= f.input(
:selected_email_id,
as: :radio_buttons,
label: false,
wrapper_html: {
aria: {
labelledby: 'select-email-heading',
describedby: 'select-email-intro',
},
},
collection: current_user.confirmed_email_addresses.map do |email|
[
email.email,
email.id,
checked: email.id == @identity.email_address_id,
]
end,
) %>
<%= f.submit(t('help_text.requested_attributes.change_email_link'), class: 'margin-top-1') %>
<% end %>

<%= render ButtonComponent.new(
url: add_email_path,
outline: true,
big: true,
wide: true,
class: 'margin-top-2',
).with_content(t('account.index.email_add')) %>

<% c.with_footer { link_to t('forms.buttons.back'), account_connected_accounts_path } %>
<% end %>
4 changes: 1 addition & 3 deletions config/initializers/simple_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@
item_wrapper_tag: nil,
item_label_class: item_label_class do |b|
b.use :html5_no_aria_required
b.wrapper :legend, tag: 'legend', class: legend_class do |ba|
ba.use :label_text
end
b.optional :label, wrap_with: { tag: 'legend', class: legend_class }
b.optional :hint, wrap_with: { tag: 'div', class: 'usa-hint margin-bottom-05' }
b.wrapper :grid_row, tag: :div, class: 'grid-row margin-bottom-neg-1' do |gr|
gr.wrapper :grid_column_radios, tag: :div, class: 'grid-col-fill' do |gc|
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ account_reset.request.info:
account_reset.request.no_cancel: Cancel
account_reset.request.title: Account deletion and reset
account_reset.request.yes_continue: Yes, continue deletion
account.connected_apps.associated_attributes_html: 'Connected %{timestamp_html} with:'
account.connected_apps.associated_html: Connected %{timestamp_html}
account.connected_apps.description: With your %{app_name} account, you can securely connect to multiple government accounts online. Below is a list of all the accounts you currently have connected.
account.connected_apps.email_not_selected: Email not yet selected
account.email_language.default: '%{language} (default)'
account.email_language.edit_title: Edit email language preference
account.email_language.languages_list: You will receive emails from %{app_name} in the language you choose.
Expand Down
2 changes: 2 additions & 0 deletions config/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ account_reset.request.info:
account_reset.request.no_cancel: Cancelar
account_reset.request.title: Eliminación y restablecimiento de cuenta
account_reset.request.yes_continue: Sí, continuar con la eliminación
account.connected_apps.associated_attributes_html: 'Conectado el %{timestamp_html} con:'
account.connected_apps.associated_html: 'Conectado: %{timestamp_html}'
account.connected_apps.description: Con su cuenta de %{app_name}, puede conectarse de manera segura a muchas cuentas del gobierno en línea. Esta es una lista de todas las cuentas que tiene conectadas actualmente.
account.connected_apps.email_not_selected: Email not yet selected
account.email_language.default: '%{language} (predeterminado)'
account.email_language.edit_title: Editar la preferencia de idioma del correo electrónico
account.email_language.languages_list: Recibirá mensajes de correo electrónico de %{app_name} en el idioma que elija.
Expand Down
2 changes: 2 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ account_reset.request.info:
account_reset.request.no_cancel: Annuler
account_reset.request.title: Suppression et réinitialisation de compte
account_reset.request.yes_continue: Oui, continuer la suppression
account.connected_apps.associated_attributes_html: 'Connecté %{timestamp_html} avec :'
account.connected_apps.associated_html: Connecté %{timestamp_html}
account.connected_apps.description: Avec votre compte %{app_name}, vous pouvez vous connecter en toute sécurité à plusieurs comptes de l’administration en ligne. Vous trouverez ci-dessous une liste de tous vos comptes actuellement connectés.
account.connected_apps.email_not_selected: Email not yet selected
account.email_language.default: '%{language} (par défaut)'
account.email_language.edit_title: Modifier la langue dans laquelle vous préférez recevoir les e-mails
account.email_language.languages_list: Vous recevrez des e-mails de %{app_name} dans la langue de votre choix.
Expand Down
2 changes: 2 additions & 0 deletions config/locales/zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ account_reset.request.info:
account_reset.request.no_cancel: 取消
account_reset.request.title: 账户删除和重设
account_reset.request.yes_continue: 是的,继续删除
account.connected_apps.associated_attributes_html: '在 %{timestamp_html} 用以下电邮做的连接:'
account.connected_apps.associated_html: 已连接 %{timestamp_html}
account.connected_apps.description: 使用你的 %{app_name} 账户,你可以安全地连接到网上多个政府账户。以下列出了你目前已连接的所有账户。
account.connected_apps.email_not_selected: Email not yet selected
account.email_language.default: '%{language} (默认)'
account.email_language.edit_title: 编辑电邮语言选择
account.email_language.languages_list: 您将收到%{app_name}用您选择的语言发送的电子邮件。
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@

get '/account' => 'accounts#show'
get '/account/connected_accounts' => 'accounts/connected_accounts#show'
get '/account/connected_accounts/:identity_id/selected_email' => 'accounts/connected_accounts/selected_email#edit',
as: :edit_connected_account_selected_email
patch '/account/connected_accounts/:identity_id/selected_email' => 'accounts/connected_accounts/selected_email#update',
as: :connected_account_selected_email
post '/account/reauthentication' => 'accounts#reauthentication'
get '/account/devices/:id/events' => 'events#show', as: :account_events
get '/account/delete' => 'users/delete#show', as: :account_delete
Expand Down
16 changes: 16 additions & 0 deletions spec/components/status_page_component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
expect(rendered).to have_link('Option', href: '/')
end

it 'does not render page footer' do
rendered = render_inline(StatusPageComponent.new)

expect(rendered).not_to have_css('.page-footer')
end

it 'validates status' do
expect do
render_inline StatusPageComponent.new(status: :foo)
Expand All @@ -76,4 +82,14 @@
render_inline StatusPageComponent.new(status: :info)
end.to raise_error(ActiveModel::ValidationError)
end

context 'with footer' do
it 'renders page footer' do
rendered = render_inline(StatusPageComponent.new) do |c|
c.with_footer.with_content('Footer')
end

expect(rendered).to have_css('.page-footer', text: 'Footer')
end
end
end
Loading

0 comments on commit 76dee31

Please sign in to comment.