diff --git a/res/css/_components.scss b/res/css/_components.scss index 1f896d270d..8bea138acb 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -100,6 +100,7 @@ @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_Tooltip.scss"; +@import "./views/elements/_Validation.scss"; @import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index fa034095b6..16ac876869 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -130,3 +130,27 @@ limitations under the License. .mx_AuthBody_spinner { margin: 1em 0; } + +.mx_AuthBody_passwordScore { + width: 100%; + appearance: none; + height: 4px; + border: 0; + border-radius: 2px; + position: absolute; + top: -12px; + + &::-moz-progress-bar { + border-radius: 2px; + background-color: $accent-color; + } + + &::-webkit-progress-bar, + &::-webkit-progress-value { + border-radius: 2px; + } + + &::-webkit-progress-value { + background-color: $accent-color; + } +} diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 20b1efd28b..147bb3b471 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -168,6 +168,7 @@ limitations under the License. .mx_Field_tooltip { margin-top: -12px; margin-left: 4px; + width: 200px; } .mx_Field_tooltip.mx_Field_valid { diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 2f35bd338e..43ddf6dde5 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -50,7 +50,6 @@ limitations under the License. .mx_Tooltip { display: none; - animation: mx_fadein 0.2s; position: fixed; border: 1px solid $menu-border-color; border-radius: 4px; @@ -66,4 +65,12 @@ limitations under the License. max-width: 200px; word-break: break-word; margin-right: 50px; + + &.mx_Tooltip_visible { + animation: mx_fadein 0.2s forwards; + } + + &.mx_Tooltip_invisible { + animation: mx_fadeout 0.1s forwards; + } } diff --git a/res/css/views/elements/_Validation.scss b/res/css/views/elements/_Validation.scss new file mode 100644 index 0000000000..1f9bd880e6 --- /dev/null +++ b/res/css/views/elements/_Validation.scss @@ -0,0 +1,69 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_Validation { + position: relative; +} + +.mx_Validation_details { + padding-left: 20px; + margin: 0; +} + +.mx_Validation_description + .mx_Validation_details { + margin: 1em 0 0; +} + +.mx_Validation_detail { + position: relative; + font-weight: normal; + list-style: none; + margin-bottom: 0.5em; + + &:last-child { + margin-bottom: 0; + } + + &::before { + content: ""; + position: absolute; + width: 14px; + height: 14px; + top: 0; + left: -18px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &.mx_Validation_valid { + color: $input-valid-border-color; + + &::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); + background-color: $input-valid-border-color; + } + } + + &.mx_Validation_invalid { + color: $input-invalid-border-color; + + &::before { + mask-image: url('$(res)/img/feather-customised/x.svg'); + background-color: $input-invalid-border-color; + } + } +} diff --git a/res/img/feather-customised/check.svg b/res/img/feather-customised/check.svg new file mode 100644 index 0000000000..5c600f8649 --- /dev/null +++ b/res/img/feather-customised/check.svg @@ -0,0 +1,3 @@ + diff --git a/res/img/feather-customised/x.svg b/res/img/feather-customised/x.svg new file mode 100644 index 0000000000..5468caa8aa --- /dev/null +++ b/res/img/feather-customised/x.svg @@ -0,0 +1,4 @@ + diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 0d36e592f8..df87c1b9ca 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -28,8 +28,6 @@ import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import * as ServerType from '../../views/auth/ServerTypeSelector'; -const MIN_PASSWORD_LENGTH = 6; - // Phases // Show controls to configure server details const PHASE_SERVER_DETAILS = 0; @@ -308,58 +306,6 @@ module.exports = React.createClass({ }); }, - onFormValidationChange: function(fieldErrors) { - // `fieldErrors` is an object mapping field IDs to error codes when there is an - // error or `null` for no error, so the values array will be something like: - // `[ null, "RegistrationForm.ERR_PASSWORD_MISSING", null]` - // Find the first non-null error code and show that. - const errCode = Object.values(fieldErrors).find(value => !!value); - if (!errCode) { - this.setState({ - errorText: null, - }); - return; - } - - let errMsg; - switch (errCode) { - case "RegistrationForm.ERR_PASSWORD_MISSING": - errMsg = _t('Missing password.'); - break; - case "RegistrationForm.ERR_PASSWORD_MISMATCH": - errMsg = _t('Passwords don\'t match.'); - break; - case "RegistrationForm.ERR_PASSWORD_LENGTH": - errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH}); - break; - case "RegistrationForm.ERR_EMAIL_INVALID": - errMsg = _t('This doesn\'t look like a valid email address.'); - break; - case "RegistrationForm.ERR_PHONE_NUMBER_INVALID": - errMsg = _t('This doesn\'t look like a valid phone number.'); - break; - case "RegistrationForm.ERR_MISSING_EMAIL": - errMsg = _t('An email address is required to register on this homeserver.'); - break; - case "RegistrationForm.ERR_MISSING_PHONE_NUMBER": - errMsg = _t('A phone number is required to register on this homeserver.'); - break; - case "RegistrationForm.ERR_USERNAME_INVALID": - errMsg = _t("A username can only contain lower case letters, numbers and '=_-./'"); - break; - case "RegistrationForm.ERR_USERNAME_BLANK": - errMsg = _t('You need to enter a username.'); - break; - default: - console.error("Unknown error code: %s", errCode); - errMsg = _t('An unknown error occurred.'); - break; - } - this.setState({ - errorText: errMsg, - }); - }, - onLoginClick: function(ev) { ev.preventDefault(); ev.stopPropagation(); @@ -534,8 +480,6 @@ module.exports = React.createClass({ defaultPhoneCountry={this.state.formVals.phoneCountry} defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPassword={this.state.formVals.password} - minPasswordLength={MIN_PASSWORD_LENGTH} - onValidationChange={this.onFormValidationChange} onRegisterClick={this.onFormSubmit} onEditServerDetailsClick={onEditServerDetailsClick} flows={this.state.flows} diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 1784ab61c3..6e55581af0 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -25,6 +25,7 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; +import withValidation from '../elements/Validation'; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_NUMBER = 'field_phone_number'; @@ -32,6 +33,8 @@ const FIELD_USERNAME = 'field_username'; const FIELD_PASSWORD = 'field_password'; const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. + /** * A pure UI component which displays a registration form. */ @@ -45,8 +48,6 @@ module.exports = React.createClass({ defaultPhoneNumber: PropTypes.string, defaultUsername: PropTypes.string, defaultPassword: PropTypes.string, - minPasswordLength: PropTypes.number, - onValidationChange: PropTypes.func, onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -59,7 +60,6 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - minPasswordLength: 6, onValidationChange: console.error, }; }, @@ -67,7 +67,7 @@ module.exports = React.createClass({ getInitialState: function() { return { // Field error codes by field ID - fieldErrors: {}, + fieldValid: {}, // The ISO2 country code selected in the phone number entry phoneCountry: this.props.defaultPhoneCountry, username: "", @@ -75,44 +75,37 @@ module.exports = React.createClass({ phoneNumber: "", password: "", passwordConfirm: "", + passwordComplexity: null, }; }, - onSubmit: function(ev) { + onSubmit: async function(ev) { ev.preventDefault(); - // validate everything, in reverse order so - // the error that ends up being displayed - // is the one from the first invalid field. - // It's not super ideal that this just calls - // onValidationChange once for each invalid field. - this.validateField(FIELD_PHONE_NUMBER, ev.type); - this.validateField(FIELD_EMAIL, ev.type); - this.validateField(FIELD_PASSWORD_CONFIRM, ev.type); - this.validateField(FIELD_PASSWORD, ev.type); - this.validateField(FIELD_USERNAME, ev.type); + const allFieldsValid = await this.verifyFieldsBeforeSubmit(); + if (!allFieldsValid) { + return; + } const self = this; - if (this.allFieldsValid()) { - if (this.state.email == '') { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, { - title: _t("Warning!"), - description: -