Skip to content

Commit 29e84b0

Browse files
Me/dpc 5451 last used csp indicator (#3015)
## 🎫 Ticket https://jira.cms.gov/browse/DPC-5451 ## 🛠 Changes - Added "Last Used" CSP indicator at login screen. - Added "Last Used" CSP toggle to LookBook preview. ## ℹ️ Context When a user comes back to our site, we want to highlight the CSP they last used to login. On a successful login we write a cookie named `last_used_csp`. If that cookie is present when the user returns to the login page, we use it to highlight the CSP as "Last Used". ## 🧪 Validation - Deployed to dev [here](https://github.com/CMSgov/dpc-app/actions/runs/26452726499). - Ran locally. - All tests passing. Currently, only Login.gov is supported in the dpc-portal. If you want to see how the other CSPs look you have two options: 1. Just use LookBook. There's a drop down parameter that lets you set which CSP should be "Last Used". 2. Run locally, go the the login page, and then open your browser's developer mode and edit the `last_used_csp` cookie. <img width="824" height="452" alt="image" src="https://github.com/user-attachments/assets/2a21260b-48ff-4534-be5a-a60da6ce3dac" />
1 parent 783ed1f commit 29e84b0

10 files changed

Lines changed: 143 additions & 12 deletions

File tree

dpc-portal-test.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ make portal
1919
# Prepare the environment
2020
docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml up db --wait
2121
docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rails db:create db:migrate RAILS_ENV=test" dpc_portal
22+
docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rails assets:clobber" dpc_portal
2223

2324
# Run the tests
2425
echo "┌─────────────────────────------─┐"
@@ -28,6 +29,7 @@ echo "│ │"
2829
echo "└────────────────────────-----───┘"
2930

3031
docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rubocop" dpc_portal
32+
docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "rails assets:clobber tmp:clear" dpc_portal
3133
docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rspec" dpc_portal
3234
docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint docker/system-tests.sh dpc_portal
3335
echo "┌────────────────────────────────┐"

dpc-portal/app/assets/stylesheets/login.scss

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,29 @@
3434
line-height: 1.8;
3535
}
3636
}
37+
38+
.last-used-login-wrapper {
39+
display: block;
40+
text-align: center;
41+
padding: 0.5rem;
42+
background-color: #e1f3f8;
43+
border: 1px solid #a8ddec;
44+
border-radius: 4px;
45+
box-sizing: border-box;
46+
width: 100%;
47+
48+
// Make the button stretch across the full with.
49+
.usa-button {
50+
width: 100%;
51+
margin-bottom: 0.5rem;
52+
}
53+
54+
.last-used-login-wrapper__badge {
55+
font-size: 0.875rem;
56+
font-weight: bold;
57+
color: #005ea2;
58+
letter-spacing: 0.5px;
59+
margin: 0;
60+
line-height: 1;
61+
}
62+
}

dpc-portal/app/components/page/session/login_component.html.erb

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,25 @@
77
<div class="bg-white padding-y-3 padding-x-5 border border-base-lighter">
88
<h1 class="margin-bottom-0">DPC Portal sign in</h1>
99
<p>Sign in with your DPC Portal Login.gov account</p>
10-
<%= button_to @login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %>
11-
<span class="clear-login-button__logo">CLEAR</span>
12-
<% end %>
13-
<%= button_to @login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %>
14-
<span class="idme-login-button__logo">ID.me</span>
15-
<% end %>
16-
<%= button_to @login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %>
17-
<span class="lg-login-button__logo">Login.gov</span>
10+
11+
<%# Build a login button for each CSP %>
12+
<%
13+
csps = [
14+
{ id: :clear, logo_class: 'clear-login-button__logo', text: 'CLEAR' },
15+
{ id: :id_me, logo_class: 'idme-login-button__logo', text: 'ID.me' },
16+
{ id: :login_dot_gov, logo_class: 'lg-login-button__logo', text: 'Login.gov' }
17+
]
18+
%>
19+
<% csps.each do |csp| %>
20+
<% is_last_used = (@last_used_csp == csp[:id]) %>
21+
<div class="<%= class_names('last-used-login-wrapper margin-bottom-1': is_last_used) %>">
22+
<%= button_to @login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %>
23+
<span class="<%= csp[:logo_class] %>"><%= csp[:text] %></span>
24+
<% end %>
25+
<%= content_tag(:span, 'LAST USED', class: 'last-used-login-wrapper__badge') if is_last_used %>
26+
</div>
1827
<% end %>
28+
1929
<%= render(Core::Navigation::SystemUseAgreementLinkComponent.new) %>
2030
<div class="line-separator"></div>
2131
<p>

dpc-portal/app/components/page/session/login_component.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ module Page
44
module Session
55
# Renders the log in page
66
class LoginComponent < ViewComponent::Base
7-
def initialize(login_path)
7+
def initialize(login_path, last_used_csp: nil)
88
super()
99
@login_path = login_path
10+
@last_used_csp = last_used_csp
1011
end
1112
end
1213
end

dpc-portal/app/components/page/session/login_component_preview.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ module Page
44
module Session
55
# Previews the log in page
66
class LoginComponentPreview < ViewComponent::Preview
7-
def default
8-
render(Page::Session::LoginComponent.new(root_path))
7+
# The really long @param line fails a Rubocop check, but most of the alternatives I tried
8+
# broke LookBook so I turned the check off.
9+
# rubocop:disable Layout/LineLength
10+
# @param last_used_csp select { choices: { "None": "", "CLEAR": "clear", "ID.me": "id_me", "Login.gov": "login_dot_gov" } }
11+
# rubocop:enable Layout/LineLength
12+
def default(last_used_csp: nil)
13+
# Make sure if the user selects "None" that the value passed is actually nil and not "".
14+
csp_value = last_used_csp.presence
15+
16+
render(Page::Session::LoginComponent.new(root_path, last_used_csp: csp_value&.to_sym))
917
end
1018
end
1119
end

dpc-portal/app/controllers/login_dot_gov_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def sign_in_and_log(user)
5656

5757
sign_in(user)
5858
session[:logged_in_at] = Time.now
59+
cookies.permanent[:last_used_csp] = :login_dot_gov
5960
Rails.logger.info(['User logged in',
6061
{ actionContext: LoggingConstants::ActionContext::Authentication,
6162
actionType: LoggingConstants::ActionType::UserLoggedIn }])
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:login_dot_gov))) %>
1+
<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:login_dot_gov), last_used_csp: cookies[:last_used_csp]&.to_sym)) %>

dpc-portal/spec/components/page/session/login_component_spec.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,61 @@
4646
expect(page.find_all('.grid-col-12').size).to eq 2
4747
end
4848
end
49+
50+
describe 'last used csp was CLEAR' do
51+
let(:url) { '/' }
52+
let(:component) { described_class.new(url, last_used_csp: :clear) }
53+
before { render_inline(component) }
54+
55+
it 'wraps only the CLEAR button' do
56+
# Check that the right button has the "last used" wrapper.
57+
expect(page).to have_css('.last-used-login-wrapper .clear-login-button__logo')
58+
59+
# Make sure the other two don't.
60+
expect(page).not_to have_css('.last-used-login-wrapper .idme-login-button__logo')
61+
expect(page).not_to have_css('.last-used-login-wrapper .lg-login-button__logo')
62+
end
63+
end
64+
65+
describe 'last used csp was ID.me' do
66+
let(:url) { '/' }
67+
let(:component) { described_class.new(url, last_used_csp: :id_me) }
68+
before { render_inline(component) }
69+
70+
it 'wraps only the ID.me button' do
71+
# Check that the right button has the "last used" wrapper.
72+
expect(page).to have_css('.last-used-login-wrapper .idme-login-button__logo')
73+
74+
# Make sure the other two don't.
75+
expect(page).not_to have_css('.last-used-login-wrapper .clear-login-button__logo')
76+
expect(page).not_to have_css('.last-used-login-wrapper .lg-login-button__logo')
77+
end
78+
end
79+
80+
describe 'last used csp was Login.gov' do
81+
let(:url) { '/' }
82+
let(:component) { described_class.new(url, last_used_csp: :login_dot_gov) }
83+
before { render_inline(component) }
84+
85+
it 'wraps only the Login.gov button' do
86+
# Check that the right button has the "last used" wrapper.
87+
expect(page).to have_css('.last-used-login-wrapper .lg-login-button__logo')
88+
89+
# Make sure the other two don't.
90+
expect(page).not_to have_css('.last-used-login-wrapper .clear-login-button__logo')
91+
expect(page).not_to have_css('.last-used-login-wrapper .idme-login-button__logo')
92+
end
93+
94+
describe 'no last used csp' do
95+
let(:url) { '/' }
96+
let(:component) { described_class.new(url, last_used_csp: nil) }
97+
before { render_inline(component) }
98+
99+
it "doesn't wrap any buttons" do
100+
expect(page).not_to have_css('.last-used-login-wrapper .clear-login-button__logo')
101+
expect(page).not_to have_css('.last-used-login-wrapper .idme-login-button__logo')
102+
expect(page).not_to have_css('.last-used-login-wrapper .lg-login-button__logo')
103+
end
104+
end
105+
end
49106
end

dpc-portal/spec/requests/login_dot_gov_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
post '/auth/login_dot_gov'
3232
follow_redirect!
3333
end
34+
it 'should write a cookie with the last used csp' do
35+
post '/auth/login_dot_gov'
36+
follow_redirect!
37+
expect(cookies[:last_used_csp]).to eq 'login_dot_gov'
38+
end
3439
it 'should not add another user credential' do
3540
expect(CspUser.where(uuid:, csp:).count).to eq 1
3641
expect do

dpc-portal/spec/requests/users/sessions_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,25 @@
4949
end
5050
end
5151
end
52+
53+
describe 'loads last_used_csp from cookies' do
54+
let(:last_used_csp) { :login_dot_gov }
55+
before do
56+
cookies[:last_used_csp] = last_used_csp.to_s
57+
get sign_in_path
58+
end
59+
60+
# The functionality of which button is wrapped is handled in spec/components/page/session/login_component_spec.rb.
61+
# Here I just wanted to make sure the cookie is read and the value is passed.
62+
it 'should set last_used_csp' do
63+
expect(response.body).to include('last-used-login-wrapper')
64+
end
65+
end
66+
67+
describe 'handles no last_used_csp cookie set' do
68+
it 'should not wrap a csp button' do
69+
get sign_in_path
70+
expect(response.body).not_to include('last-used-login-wrapper')
71+
end
72+
end
5273
end

0 commit comments

Comments
 (0)