Properly countdown time to account unlocking#772
Conversation
**Why**: The time remaining displayed on the Account Locked screen never changes, leading to a poor user experience **How**: Refactor JS to make the countdown and hours-minutes-seconds formatting reusable
|
If someone knows how to render the nonced_javascript_tag in slim that would be awesome! Couldn't get it to work outside of erb. |
| var remainingSeconds = parseInt(seconds % 60, 10); | ||
|
|
||
| var displayMinutes = minutes == 0 ? '' : | ||
| minutes + ' minute' + (minutes !== 1 ? 's' : '') + ' and '; |
There was a problem hiding this comment.
I see this didn't really change in the diff, but I'm now seeing this is not i18n-friendly. I'll make an issue to track i18n-ing this
config/locales/devise/en.yml
Outdated
| Your account is temporarily locked because you have entered the | ||
| one-time passcode incorrectly too many times. | ||
| please_try_again: Please try again in %{time_remaining}. | ||
| please_try_again: Please try again in <span id="countdown">%{time_remaining}</span>. |
There was a problem hiding this comment.
If we rename this key please_try_again_html we don't need to manually .html_safe this in the template.
Also, I consider it a best practie to keep HTML markup outside of i18n (because of human- and machine-introduced bugs that appear in translation pipelines) so I would recommend creating the span in the template:
<%= t('devise.two_factor_authentication.please_try_again_html',
time_remaining: content_tag(:span, decorated_user.lockout_time_remaining_in_words)) %>There was a problem hiding this comment.
That seems cleaner. I saw markup in another translation file and figured it was OK to put there.
I can remove the tag from that file as well, if you don't think it's outside the scope of this PR.
There was a problem hiding this comment.
Yeah I'm 100% ok with fixing that translation in the other file, just didn't want to distract you accidentally
**Why**: CodeClimate doesnt like errors
**Why**: Cleaner separation of concerns
a663540 to
1ae4ece
Compare
**Why**: When the account locked countdown reaches zero, the signed in modal flashes briefly, and then reloads the page. This produces an unattractive UI state to the user **How**: uses `user_fully_authenticated` helper method instead of `current_user`
35157ad to
52f1a2b
Compare
52f1a2b to
7a5366f
Compare
**Why**: Makes shared javascript functions available under correct namespace
7a5366f to
2c43600
Compare
| return `${seconds} ${pluralize('second', seconds)}`; | ||
| } | ||
|
|
||
| root.hmsFormatter = function(milliseconds) { |
There was a problem hiding this comment.
why do we want these methods attached to our LoginGov object (vs. just a function that we export / import within the js modules that need it)?
There was a problem hiding this comment.
as in export default hmsFormatter at the bottom of this file and then import hmsFormatter from './hms-formatter' in the other js file(s) that need it
There was a problem hiding this comment.
using import filename from 'file' didn't work, it gave me a compilation error. Specifically, they didn't work in the .js.erb files.I think we at least need the countdownTimer and autoLogout functions in the LoginGov namespace on the window object, if not the hmsFormatter
| @@ -1,3 +1,6 @@ | |||
| import 'app/autoLogout'; | |||
| import 'app/hms-formatter'; | |||
| import 'app/countdownTimer'; | |||
There was a problem hiding this comment.
we should probably be consistent with our js filenames (dashes vs camelcase)
There was a problem hiding this comment.
yep, you are right, I payed attention for the formatter but named on autopilot for the other two 🙁
| @@ -1,3 +1,6 @@ | |||
| import 'app/autoLogout'; | |||
There was a problem hiding this comment.
we should probably be consistent with our js filenames (dashes vs camelcase)
jessieay
left a comment
There was a problem hiding this comment.
Just a few comments from me! 🎉
| if (show_warning & !el) { | ||
| cntnr.insertAdjacentHTML('afterbegin', warning_info); | ||
| initTimer(warning); | ||
| LoginGov.countdownTimer('#countdown', warning); |
There was a problem hiding this comment.
so much better! as the last person who touched this js, I really appreciate the refactor
| </p> | ||
| <p> | ||
| <%= t('devise.two_factor_authentication.please_try_again_html', | ||
| time_remaining: content_tag(:span, lockout_time_in_words, id: 'countdown')) %> |
There was a problem hiding this comment.
The upside of this (vs having the HTML in the i18n string itself):
- feels more railsy
- easier to read
Downside:
- when just looking at the translation itself (
please_try_again_html), it is not obvious that there is HTML used in the translation. Need to look at where it is called.
Because of this downside, I think we've mostly been including the HTML directly in I18n translation. At least that was the case the last time I checked.
What do you think?
There was a problem hiding this comment.
I do agree with the downside, I originally had the html directly in the locale file. However, per @zachmargolis comment above, I do think that it is a little cleaner to have the html live in the view.
There was a problem hiding this comment.
Do we, strictly speaking, need to know that the translation includes HTML?
There was a problem hiding this comment.
We do not, but if I were buzzing around in the locale files and saw an _html translation without HTML, I might be tempted to refactor that and remove the _html, which would break this.
I am fine with keeping as-is and will try to move in this direction when I am using html-ified translations in the future in this proj.
There was a problem hiding this comment.
Yeah that part does require one to know that the _html automatically calls html_safe on the output, but rails is already so full of magic what's one more 😬
There was a problem hiding this comment.
Yes, that is the magic ✨ -- what I meant to convey is that, as someone who does know this magic, if I saw a translation without HTML but with _html, I might want to refactor to remove _html since it would not be necessary.
There was a problem hiding this comment.
ahhh I see! The ✨ was not known to me. 😢
| @@ -0,0 +1,18 @@ | |||
| <% lockout_time_in_words = decorated_user.lockout_time_remaining_in_words %> | |||
There was a problem hiding this comment.
I am not a huge fan of declaring vars in views like this (because I would expect this to be a helper method and woud look for it by grepping for def lockout_time_in_words), but this may be the style of the rest of the app
There was a problem hiding this comment.
I used a variable here because the view originally had the decorated_user.lockout_time_remaining_in_words sitting directly in the markup taking up a bunch of characters 😄. I saw variables in other views, and since I'm still learning how you all write rails code, I just went with that. I certainly don't mind making a helper method in this case if that's the style we're should to be adhering to!
There was a problem hiding this comment.
I don't think a helper method is needed, but I do think that using that veryyyy long decorated_user.lockout_time_remaining_in_words directly in the markup would be fine by me!
| if (!countdownTarget) return; | ||
|
|
||
| (function tick() { | ||
| countdownTarget.innerHTML = window.LoginGov.hmsFormatter(remaining); |
There was a problem hiding this comment.
can window.LoginGov.hmsFormatter(remaining); now become root.hmsFormatter(remaining)?
brendansudol
left a comment
There was a problem hiding this comment.
left a few questions/comments
**Why**: because comments
There was a problem hiding this comment.
thanks, this is looking good. i have one more thought / recommendation around organization. it might be nice / cleaner to have these utility functions separated from the binding of them to the LoginGov object -- what are your thoughts on this? https://gist.github.com/brendansudol/feef1dc02c764bd4a01379fad905b93e (making the functions a bit more self contained and consolidating the root = window.LoginGov bizness to one place)
|
ps, i'm also happy to do this reorg in a new PR too if you'd like to get this merged in |
|
Yeah proposed changed seem fine |
|
@brendansudol updated! |
f1c830d to
a74e1bb
Compare
**Why**: keeps code neater
a74e1bb to
a83a870
Compare
* Properly coountdown time to account unlocking **Why**: The time remaining displayed on the Account Locked screen never changes, leading to a poor user experience **How**: Refactor JS to make the countdown and hours-minutes-seconds formatting reusable * Fixes CodeClimate errors **Why**: CodeClimate doesnt like errors * Removes markup from translation files **Why**: Cleaner separation of concerns * Only show signed in modal after 2fa code entered **Why**: When the account locked countdown reaches zero, the signed in modal flashes briefly, and then reloads the page. This produces an unattractive UI state to the user **How**: uses `user_fully_authenticated` helper method instead of `current_user` * Ensure LoginGov object is properly set on window **Why**: Makes shared javascript functions available under correct namespace * Tweaks to JS files per brendans comments **Why**: because comments * Organizes javascript files **Why**: keeps code neater
* Properly coountdown time to account unlocking **Why**: The time remaining displayed on the Account Locked screen never changes, leading to a poor user experience **How**: Refactor JS to make the countdown and hours-minutes-seconds formatting reusable * Fixes CodeClimate errors **Why**: CodeClimate doesnt like errors * Removes markup from translation files **Why**: Cleaner separation of concerns * Only show signed in modal after 2fa code entered **Why**: When the account locked countdown reaches zero, the signed in modal flashes briefly, and then reloads the page. This produces an unattractive UI state to the user **How**: uses `user_fully_authenticated` helper method instead of `current_user` * Ensure LoginGov object is properly set on window **Why**: Makes shared javascript functions available under correct namespace * Tweaks to JS files per brendans comments **Why**: because comments * Organizes javascript files **Why**: keeps code neater
|
|
||
| #session-timeout-cntnr | ||
| - if current_user | ||
| - if user_fully_authenticated? |
There was a problem hiding this comment.
This change means that a user who signs in with email and password and sits on the 2FA delivery page will never auto time out. The consequence is that if I leave my computer and phone unattended, someone can sign in to my account without needing to know my email and password. That's probably an edge case, but still something to think about and decide whether or not this is what we want.
The solution to keeping the user on the account locked screen is to reverse the order of these 2 lines: https://github.com/18F/identity-idp/blob/master/app/controllers/concerns/two_factor_authenticatable.rb#L16-L18
The reason is that application.html.slim wasn't called after signing the user out, and therefore the JS was still active. By reversing the order, we force a new call to the layout template, which then sees that a current user does not exist, and therefore does not trigger the timeout JS.
I will submit a PR shortly.
There was a problem hiding this comment.
Disregard my first comment about not timing out. I forgot that Devise will time you out regardless of any JS we have. The JS is just for the modal to appear, but I think we do want it to appear during 2FA, right?
Why:
The time remaining displayed on the Account Locked screen never changes,
leading to a poor user experience
How:
Refactor JS to make the countdown and hours-minutes-seconds formatting
reusable