Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Change password hint color to red when invalid, else grey #5130

Merged
merged 26 commits into from Nov 22, 2021

Conversation

mananjadhav
Copy link
Collaborator

@mananjadhav mananjadhav commented Sep 8, 2021

@Jag96 Can you please review this?

Details

  • Show the password hint color to red when it is invalid otherwise always show the message in grey

Fixed Issues

$ #4995

Tests

  1. Blank password field should show grey hint
  2. Invalid Password should show the hint in red
  3. Valid Password should show grey hint

QA Steps

  1. Open the Change Password
  2. Check the empty New Password field to show grey hint
  3. Enter the invalid password and hint text should show it in red
  4. Enter the valid password and the hint should show back in grey.

Tested On

  • Web
  • Mobile Web
  • Desktop
  • iOS
  • Android

Screenshots

Web

v1

https://user-images.githubusercontent.com/3069065/135293821-e1f56e42-5e7c-4e49-b211-a1799de306eb.mov
web-just-mismatch-errors
web-password-mistmatch
web-all-errors

Latest
https://user-images.githubusercontent.com/3069065/137942020-5fefb22e-6dd2-4962-82a1-4aa981841e57.mov

Mobile Web

v1
mweb-all-errors
mweb-mismatch-error

Latest
mweb-password-hint-color-02
mweb-password-hint-color-01
mweb-password-hint-color-03

Desktop

v1
desktop-all-errors
desktop-mismatch-errors

Latest
desktop-password-hint-color-03
desktop-password-hint-color-02
desktop-password-hint-color-01

iOS

v1
ios-all-errors
ios-password-error
ios-password-mismatch

Latest

ios-password-hint-color-03

ios-password-hint-color-02

ios-password-hint-color-01

Android

v1
android-all-errors
android-mismatch-error

Latest
android-password-hint-color-03
android-password-hint-color-02
android-password-hint-color-01

@mananjadhav mananjadhav requested a review from a team as a code owner September 8, 2021 05:27
@MelvinBot MelvinBot requested review from marcaaron and removed request for a team September 8, 2021 05:27
@@ -86,8 +80,8 @@ class PasswordPage extends Component {
}
}

isValidPassword() {
return this.state.newPassword.match(CONST.PASSWORD_COMPLEXITY_REGEX_STRING);
isInvalidPassword() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change from isValidPassword to isInvalidPassword? We usually use the "positive" version of things rather than check if something is "not" something like isNotValid() or isInvalid().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, earlier this function was used for a flag that we don’t use anymore. I’ll update the logic to use ‘isValidPassword’

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcaaron Are you suggesting we have

isValidPassword() {
        return this.state.newPassword.match(CONST.PASSWORD_COMPLEXITY_REGEX_STRING);
}

and then `this.state.newPassword && !isValidPassword()`

That is what we initially thought to use but wanted to move this && to a separate function. Let me know and I'll accordingly update.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point on keeping the function positive, in that case, yes I think we'd have to leave that logic in the render function the way you specified (maybe split out the style array into separate lines like we do here to avoid a giant line).

Alternatively, you could change the function to be called something like shouldApplyPasswordErrorStyles and include the empty state check, but I don't like that solution as much.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agree and hence, lets just split into lines with an && check

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why we need two separate lines. If there's no password it's not valid, right?

return this.state.newPassword && this.state.newPassword.match(CONST.PASSWORD_COMPLEXITY_REGEX_STRING);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe we just mean syntactically use a line break? That seems fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wanted to match the same behavior from the screenshots in #5093 where the field shows as grey initially if the user hasn't typed anything.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to isValidPassword

@mananjadhav
Copy link
Collaborator Author

@Jag96 @marcaaron PR updated

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of style nitpicks, otherwise looks good!

>
{this.props.translate('passwordPage.newPasswordPrompt')}
</Text>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove spaces between elements

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

)}

<Text style={[
styles.textLabelSupporting, styles.mt1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put each of these on their own line?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move the style prop to its own line and indent accordingly? There's an example in the // Good section of https://github.com/Expensify/App/blob/main/STYLING.md#when-to-create-a-new-style

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Thanks for the share. I wasn't sure if every style will be moved to a new line. Do you think this should be added as a lint config?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a good improvement, I think it's worth bringing up in slack to at least get a discussion going, but we'll keep that separate from this PR.

@mananjadhav
Copy link
Collaborator Author

PR updated

@Jag96
Copy link
Contributor

Jag96 commented Sep 10, 2021

Thanks for the update @mananjadhav!

In slack, we just agreed on some updated ways to handle displaying errors, so if you'd like to update this PR to match the new standard we're happy to provide a bonus (equal to the current price) for the updated scope. Otherwise, we can merge this PR as it is and open a separate issue to update the code to the new standard. Either is fine, just let us know what you prefer!

The updates we're looking to implement are:

  1. Update the form so the Save button is always enabled/clickable.
  2. Always show the new password requirements text, and only update it to have formError styles when the user submits the form and the new password doesn't meet the requirements.
  3. The onBlur functions can be removed, if the passwords don't match after the user submits the form display the Passwords must match error where it is now.

Feel free to ask any questions as well!

@mananjadhav
Copy link
Collaborator Author

Hi it makes sense to update the code in this PR only. I’ll do the needful.

@mananjadhav
Copy link
Collaborator Author

@Jag96 Two questions based on the slack discussion here

  1. Should I add border color to formError or that is already handled?
  2. If I type the valid password after the error message is shown, should we have onBlur to remove the message?

@mananjadhav
Copy link
Collaborator Author

mananjadhav commented Sep 12, 2021

@Jag96 I started with the implementation and have one more question. Once we get rid of the following block, we'll also have to show Required field validations, right?

isDisabled={!this.state.currentPassword || !this.state.newPassword
                                || !this.state.confirmNewPassword
                                || (this.state.newPassword !== this.state.confirmNewPassword)
                                || (this.state.currentPassword === this.state.newPassword)
                                || !this.state.newPassword.match(CONST.PASSWORD_COMPLEXITY_REGEX_STRING)}

@Jag96
Copy link
Contributor

Jag96 commented Sep 14, 2021

@mananjadhav sounds great! To answer your questions

  1. The errorText prop on the ExpensiTextInput component is what adds the red border I believe, but in this case since we want the message to show up as gray as well as red, it may not be as simple as just using errorText. If that is the case, I think it's fine to leave the field without a red border for the new password input for now and we can update it later when we create a generic form component. If you can get it working w/ ExpensiTextInput though, that works!
  2. onBlur isn't required so I'd say let's leave it out for now per this message.

@Jag96 I started with the implementation and have one more question. Once we get rid of the following block, we'll also have to show Required field validations, right?

Yes, that's correct, the required field validations will have to move into handleChangePassword or another function. We can use the errorText prop on the ExpensiTextInput for displaying the error messages, and if you need to add a new error for something like Password is a required field you can add it to this obj and ask for a Spanish translation in slack.

@kadiealexander
Copy link
Contributor

Hi @mananjadhav, I've placed this issue on hold as per this update, as we are prioritising issues related to a feature release scheduled for 9/24. As an apology for the delay, we will add a $100 bonus as a thank you for waiting.

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one request to update JS docs, otherwise looks good!


this.setState(stateToUpdate);
clearErrorAndSetValue(field, value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add jsdocs here and to other functions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Sorry, not sure how I missed these 🤔

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conflicts and a couple of things, otherwise looking good

currentPassword: 'Current password is required',
confirmNewPassword: 'Confirm password is required',
newPasswordSameAsOld: 'New password must be different than your old password',
newPassword: 'Your password must have at least 8 characters,\n1 capital letter, 1 lowercase letter, 1 number.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
newPassword: 'Your password must have at least 8 characters,\n1 capital letter, 1 lowercase letter, 1 number.',
newPassword: 'Your password must have at least 8 characters,\n1 capital letter, 1 lowercase letter, and 1 number.',

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the Spanish translation

@@ -248,6 +258,7 @@ function isValidLengthForFirstOrLastName(name) {
return name.length <= 50;
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary extra space

} else {
this.setState({shouldShowPasswordConfirmError: false});
/**
* Validate all fields and submit the form if no errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update comment, this function no longer submits the form

@mananjadhav
Copy link
Collaborator Author

@Jag96 PR updated with the changes.

Jag96
Jag96 previously approved these changes Nov 4, 2021
Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, @marcaaron all yours

Copy link
Contributor

@marcaaron marcaaron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good, but I did have some suggestions. I think the logic around which errors are getting set is a lot more difficult to suss out now - but I don't have a great suggestion for that other than extract some of the conditions into variables so there's less to parse mentally.

@@ -230,6 +230,16 @@ function isValidUSPhone(phoneNumber) {
return CONST.REGEX.PHONE_E164_PLUS.test(phoneNumber.replace(CONST.REGEX.NON_ALPHA_NUMERIC, '')) && CONST.REGEX.US_PHONE.test(phoneNumber);
}

/**
* Checks if the password matches regex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment please

} else {
stateToUpdate.isPasswordRequirementsVisible = false;
/**
* Return the error message for the field
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment please

}
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the doc says we are returning a string then don't return null return an empty string.

stateToUpdate.shouldShowPasswordConfirmError = false;

/**
* Set value for the field in state and clear error flag
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment

} else {
this.setState({shouldShowPasswordConfirmError: false});
/**
* Validate all fields
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

* @param {String} value
* @param {String[]} additionalErrorFlags
*/
clearErrorAndSetValue(field, value, additionalErrorFlags) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

additionalErrorsToClear would be a better name

onFocus={() => this.setState({isPasswordRequirementsVisible: true})}
onBlur={() => this.onBlurNewPassword()}
hasError={this.state.errors.newPassword || this.state.errors.newPasswordSameAsOld}
errorText={!this.state.errors.newPassword && this.state.errors.newPasswordSameAsOld
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check both of these conditions to determine which errorText to show? If there's no error it will not return anything?

</Text>
)}
{
!this.state.errors.newPassword && !this.state.errors.newPasswordSameAsOld
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB, I would probably extract this to a variable for readability like const shouldShowNewPasswordPrompt

/>
</View>
{!this.state.shouldShowPasswordConfirmError && !isEmpty(this.props.account.error) && (
{!_.some(_.values(this.state.errors)) && !_.isEmpty(this.props.account.error) && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inverting _.some() is a bit confusing and the _.values() isn't necessary.

Maybe clearer to just do something like....

_.every(this.state.errors, error => !error);

@mananjadhav
Copy link
Collaborator Author

@marcaaron PR updated

marcaaron
marcaaron previously approved these changes Nov 8, 2021
@marcaaron
Copy link
Contributor

These changes look good, however there is one strange thing happening... after changing the password the fields are not cleared and we see this:

2021-11-08_10-06-07

Should we clear the state here? I think my expectation is that the old passwords would not be sticking around in these fields, but it looks like we are pushing the settings page on top of the change password page instead of popping it off the stack and returning us to the settings page.

Maybe things already work this way on production, but should be fixed. Thoughts @Jag96 ?

@mananjadhav
Copy link
Collaborator Author

Should we clear the state here

Agreed. Let me know what you and @Jag96 decide. Will do the needful.

@mananjadhav
Copy link
Collaborator Author

Any updates for me on this one?

@Jag96
Copy link
Contributor

Jag96 commented Nov 9, 2021

Should we clear the state here? I think my expectation is that the old passwords would not be sticking around in these fields, but it looks like we are pushing the settings page on top of the change password page instead of popping it off the stack and returning us to the settings page.

I think going back is better than navigating to the settings page, so I agree with the improvement being proposed.

Also, per this message we're putting all non-critical issues on merge hold including this one, and adding a $250 bonus.

@mananjadhav
Copy link
Collaborator Author

@marcaaron Can you tell me in which scenario are the fields retained? I just tried and it didn't. I tried clicking on back, etc. too. One area where I saw the values being retained in the fields is when there's an error. Can you help me with the steps so that I can fix and do the needful.

change-password-redirect.mov

@Jag96
Copy link
Contributor

Jag96 commented Nov 9, 2021

Hmm, it looks like if you load the page by typing http://localhost:8080/settings/password (or whatever URL you use to test with settings/password at the end) in the URL bar and then submit the form, you get the behavior where the previous values stick around. If you load up the app without having the settings page already open the bug doesn't happen. Using onBackButtonPress={() => Navigation.goBack()} will navigate back if there is a settings menu to go back to, and if there isn't then it will just close the modal, so I think that is an ok solution here.

For the actual submit, we can do the same thing, since it seems to behave the same.

@Jag96
Copy link
Contributor

Jag96 commented Nov 18, 2021

This issue has been taken off hold! @mananjadhav if you have any questions on #5130 (comment) let me know!

@mananjadhav
Copy link
Collaborator Author

@Jag96 PR was updated yesterday

Copy link
Contributor

@Jag96 Jag96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcaaron all yours

isValidPassword() {
return this.state.newPassword.match(CONST.PASSWORD_COMPLEXITY_REGEX_STRING);
}
if (!this.state.newPassword || !isValidPassword(this.state.newPassword)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB, !this.state.newPassword check should be performed inside isValidPassword()?

@marcaaron marcaaron merged commit e27a916 into Expensify:main Nov 22, 2021
@OSBotify
Copy link
Contributor

🚀 Deployed to staging by @marcaaron in version: 1.1.15-19 🚀

platform result
🤖 android 🤖 failure ❌
🖥 desktop 🖥 failure ❌
🍎 iOS 🍎 success ✅
🕸 web 🕸 failure ❌

@OSBotify
Copy link
Contributor

🚀 Deployed to production by @roryabraham in version: 1.1.16-10 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants