You've already forked matrix-react-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-react-sdk.git
				synced 2025-11-03 00:33:22 +03:00 
			
		
		
		
	Merge pull request #4599 from matrix-org/t3chguy/progress_colour
Consolidate password/passphrase fields into a component & add dynamic colour to progress
This commit is contained in:
		@@ -120,6 +120,7 @@
 | 
			
		||||
    "@types/modernizr": "^3.5.3",
 | 
			
		||||
    "@types/qrcode": "^1.3.4",
 | 
			
		||||
    "@types/react": "16.9",
 | 
			
		||||
    "@types/zxcvbn": "^4.4.0",
 | 
			
		||||
    "babel-eslint": "^10.0.3",
 | 
			
		||||
    "babel-jest": "^24.9.0",
 | 
			
		||||
    "chokidar": "^3.3.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,7 @@
 | 
			
		||||
@import "./views/auth/_CountryDropdown.scss";
 | 
			
		||||
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
 | 
			
		||||
@import "./views/auth/_LanguageSelector.scss";
 | 
			
		||||
@import "./views/auth/_PassphraseField.scss";
 | 
			
		||||
@import "./views/auth/_ServerConfig.scss";
 | 
			
		||||
@import "./views/auth/_ServerTypeSelector.scss";
 | 
			
		||||
@import "./views/auth/_Welcome.scss";
 | 
			
		||||
 
 | 
			
		||||
@@ -146,27 +146,3 @@ 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								res/css/views/auth/_PassphraseField.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								res/css/views/auth/_PassphraseField.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
$PassphraseStrengthHigh: $accent-color;
 | 
			
		||||
$PassphraseStrengthMedium: $username-variant5-color;
 | 
			
		||||
$PassphraseStrengthLow: $notice-primary-color;
 | 
			
		||||
 | 
			
		||||
@define-mixin ProgressBarColour $colour {
 | 
			
		||||
    color: $colour;
 | 
			
		||||
    &::-moz-progress-bar {
 | 
			
		||||
        background-color: $colour;
 | 
			
		||||
    }
 | 
			
		||||
    &::-webkit-progress-value {
 | 
			
		||||
        background-color: $colour;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
progress.mx_PassphraseField_progress {
 | 
			
		||||
    appearance: none;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    height: 4px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: -12px;
 | 
			
		||||
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
    &::-moz-progress-bar {
 | 
			
		||||
        border-radius: 2px;
 | 
			
		||||
    }
 | 
			
		||||
    &::-webkit-progress-bar,
 | 
			
		||||
    &::-webkit-progress-value {
 | 
			
		||||
        border-radius: 2px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @mixin ProgressBarColour $PassphraseStrengthLow;
 | 
			
		||||
    &[value="2"], &[value="3"] {
 | 
			
		||||
        @mixin ProgressBarColour $PassphraseStrengthMedium;
 | 
			
		||||
    }
 | 
			
		||||
    &[value="4"] {
 | 
			
		||||
        @mixin ProgressBarColour $PassphraseStrengthHigh;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -35,17 +35,6 @@ limitations under the License.
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateKeyBackupDialog_passPhraseHelp {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    height: 85px;
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    font-size: 80%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateKeyBackupDialog_passPhraseHelp progress {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateKeyBackupDialog_passPhraseInput {
 | 
			
		||||
    flex: none;
 | 
			
		||||
    width: 250px;
 | 
			
		||||
 
 | 
			
		||||
@@ -68,17 +68,6 @@ limitations under the License.
 | 
			
		||||
    margin-top: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateSecretStorageDialog_passPhraseHelp {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    height: 64px;
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    font-size: 80%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateSecretStorageDialog_passPhraseHelp progress {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_CreateSecretStorageDialog_passPhraseMatch {
 | 
			
		||||
    width: 200px;
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import React, {createRef} from 'react';
 | 
			
		||||
import FileSaver from 'file-saver';
 | 
			
		||||
import * as sdk from '../../../../index';
 | 
			
		||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { scorePassword } from '../../../../utils/PasswordScorer';
 | 
			
		||||
import { _t } from '../../../../languageHandler';
 | 
			
		||||
import {_t, _td} from '../../../../languageHandler';
 | 
			
		||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
 | 
			
		||||
import SettingsStore from '../../../../settings/SettingsStore';
 | 
			
		||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
 | 
			
		||||
import {copyNode} from "../../../../utils/strings";
 | 
			
		||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
 | 
			
		||||
 | 
			
		||||
const PHASE_PASSPHRASE = 0;
 | 
			
		||||
const PHASE_PASSPHRASE_CONFIRM = 1;
 | 
			
		||||
@@ -36,7 +36,6 @@ const PHASE_DONE = 5;
 | 
			
		||||
const PHASE_OPTOUT_CONFIRM = 6;
 | 
			
		||||
 | 
			
		||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
 | 
			
		||||
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Walks the user through the process of creating an e2e key backup
 | 
			
		||||
@@ -52,17 +51,18 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
        this._recoveryKeyNode = null;
 | 
			
		||||
        this._keyBackupInfo = null;
 | 
			
		||||
        this._setZxcvbnResultTimeout = null;
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            secureSecretStorage: null,
 | 
			
		||||
            phase: PHASE_PASSPHRASE,
 | 
			
		||||
            passPhrase: '',
 | 
			
		||||
            passPhraseValid: false,
 | 
			
		||||
            passPhraseConfirm: '',
 | 
			
		||||
            copied: false,
 | 
			
		||||
            downloaded: false,
 | 
			
		||||
            zxcvbnResult: null,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this._passphraseField = createRef();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async componentDidMount() {
 | 
			
		||||
@@ -81,12 +81,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        if (this._setZxcvbnResultTimeout !== null) {
 | 
			
		||||
            clearTimeout(this._setZxcvbnResultTimeout);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _collectRecoveryKeyNode = (n) => {
 | 
			
		||||
        this._recoveryKeyNode = n;
 | 
			
		||||
    }
 | 
			
		||||
@@ -180,22 +174,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseNextClick = async (e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        if (!this._passphraseField.current) return; // unmounting
 | 
			
		||||
 | 
			
		||||
        // If we're waiting for the timeout before updating the result at this point,
 | 
			
		||||
        // skip ahead and do it now, otherwise we'll deny the attempt to proceed
 | 
			
		||||
        // even if the user entered a valid passphrase
 | 
			
		||||
        if (this._setZxcvbnResultTimeout !== null) {
 | 
			
		||||
            clearTimeout(this._setZxcvbnResultTimeout);
 | 
			
		||||
            this._setZxcvbnResultTimeout = null;
 | 
			
		||||
            await new Promise((resolve) => {
 | 
			
		||||
                this.setState({
 | 
			
		||||
                    zxcvbnResult: scorePassword(this.state.passPhrase),
 | 
			
		||||
                }, resolve);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        if (this._passPhraseIsValid()) {
 | 
			
		||||
            this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
 | 
			
		||||
        await this._passphraseField.current.validate({ allowEmpty: false });
 | 
			
		||||
        if (!this._passphraseField.current.state.valid) {
 | 
			
		||||
            this._passphraseField.current.focus();
 | 
			
		||||
            this._passphraseField.current.validate({ allowEmpty: false, focused: true });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseConfirmNextClick = async (e) => {
 | 
			
		||||
@@ -214,9 +202,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
    _onSetAgainClick = () => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhrase: '',
 | 
			
		||||
            passPhraseValid: false,
 | 
			
		||||
            passPhraseConfirm: '',
 | 
			
		||||
            phase: PHASE_PASSPHRASE,
 | 
			
		||||
            zxcvbnResult: null,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -226,23 +214,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseValidate = (result) => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhraseValid: result.valid,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseChange = (e) => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhrase: e.target.value,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (this._setZxcvbnResultTimeout !== null) {
 | 
			
		||||
            clearTimeout(this._setZxcvbnResultTimeout);
 | 
			
		||||
        }
 | 
			
		||||
        this._setZxcvbnResultTimeout = setTimeout(() => {
 | 
			
		||||
            this._setZxcvbnResultTimeout = null;
 | 
			
		||||
            this.setState({
 | 
			
		||||
                // precompute this and keep it in state: zxcvbn is fast but
 | 
			
		||||
                // we use it in a couple of different places so no point recomputing
 | 
			
		||||
                // it unnecessarily.
 | 
			
		||||
                zxcvbnResult: scorePassword(this.state.passPhrase),
 | 
			
		||||
            });
 | 
			
		||||
        }, PASSPHRASE_FEEDBACK_DELAY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseConfirmChange = (e) => {
 | 
			
		||||
@@ -251,35 +232,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _passPhraseIsValid() {
 | 
			
		||||
        return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _renderPhasePassPhrase() {
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
 | 
			
		||||
        let strengthMeter;
 | 
			
		||||
        let helpText;
 | 
			
		||||
        if (this.state.zxcvbnResult) {
 | 
			
		||||
            if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
 | 
			
		||||
                helpText = _t("Great! This recovery passphrase looks strong enough.");
 | 
			
		||||
            } else {
 | 
			
		||||
                const suggestions = [];
 | 
			
		||||
                for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
 | 
			
		||||
                    suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
 | 
			
		||||
                }
 | 
			
		||||
                const suggestionBlock = <div>{suggestions.length > 0 ? suggestions : _t("Keep going...")}</div>;
 | 
			
		||||
 | 
			
		||||
                helpText = <div>
 | 
			
		||||
                    {this.state.zxcvbnResult.feedback.warning}
 | 
			
		||||
                    {suggestionBlock}
 | 
			
		||||
                </div>;
 | 
			
		||||
            }
 | 
			
		||||
            strengthMeter = <div>
 | 
			
		||||
                <progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
 | 
			
		||||
            </div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return <form onSubmit={this._onPassPhraseNextClick}>
 | 
			
		||||
            <p>{_t(
 | 
			
		||||
                "<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
 | 
			
		||||
@@ -293,17 +248,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
            <div className="mx_CreateKeyBackupDialog_primaryContainer">
 | 
			
		||||
                <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
 | 
			
		||||
                    <input type="password"
 | 
			
		||||
                        onChange={this._onPassPhraseChange}
 | 
			
		||||
                        value={this.state.passPhrase}
 | 
			
		||||
                    <PassphraseField
 | 
			
		||||
                        className="mx_CreateKeyBackupDialog_passPhraseInput"
 | 
			
		||||
                        placeholder={_t("Enter a recovery passphrase...")}
 | 
			
		||||
                        onChange={this._onPassPhraseChange}
 | 
			
		||||
                        minScore={PASSWORD_MIN_SCORE}
 | 
			
		||||
                        value={this.state.passPhrase}
 | 
			
		||||
                        onValidate={this._onPassPhraseValidate}
 | 
			
		||||
                        fieldRef={this._passphraseField}
 | 
			
		||||
                        autoFocus={true}
 | 
			
		||||
                        label={_td("Enter a recovery passphrase")}
 | 
			
		||||
                        labelEnterPassword={_td("Enter a recovery passphrase")}
 | 
			
		||||
                        labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
 | 
			
		||||
                        labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
 | 
			
		||||
                    />
 | 
			
		||||
                    <div className="mx_CreateKeyBackupDialog_passPhraseHelp">
 | 
			
		||||
                        {strengthMeter}
 | 
			
		||||
                        {helpText}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@@ -311,7 +268,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 | 
			
		||||
                primaryButton={_t('Next')}
 | 
			
		||||
                onPrimaryButtonClick={this._onPassPhraseNextClick}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
                disabled={!this._passPhraseIsValid()}
 | 
			
		||||
                disabled={!this.state.passPhraseValid}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <details>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import React, {createRef} from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import * as sdk from '../../../../index';
 | 
			
		||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
 | 
			
		||||
import { scorePassword } from '../../../../utils/PasswordScorer';
 | 
			
		||||
import FileSaver from 'file-saver';
 | 
			
		||||
import { _t } from '../../../../languageHandler';
 | 
			
		||||
import {_t, _td} from '../../../../languageHandler';
 | 
			
		||||
import Modal from '../../../../Modal';
 | 
			
		||||
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
 | 
			
		||||
import {copyNode} from "../../../../utils/strings";
 | 
			
		||||
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
 | 
			
		||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
 | 
			
		||||
 | 
			
		||||
const PHASE_LOADING = 0;
 | 
			
		||||
const PHASE_LOADERROR = 1;
 | 
			
		||||
@@ -39,7 +39,6 @@ const PHASE_DONE = 8;
 | 
			
		||||
const PHASE_CONFIRM_SKIP = 9;
 | 
			
		||||
 | 
			
		||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
 | 
			
		||||
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Walks the user through the process of creating a passphrase to guard Secure
 | 
			
		||||
@@ -62,16 +61,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
        this._recoveryKey = null;
 | 
			
		||||
        this._recoveryKeyNode = null;
 | 
			
		||||
        this._setZxcvbnResultTimeout = null;
 | 
			
		||||
        this._backupKey = null;
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            phase: PHASE_LOADING,
 | 
			
		||||
            passPhrase: '',
 | 
			
		||||
            passPhraseValid: false,
 | 
			
		||||
            passPhraseConfirm: '',
 | 
			
		||||
            copied: false,
 | 
			
		||||
            downloaded: false,
 | 
			
		||||
            zxcvbnResult: null,
 | 
			
		||||
            backupInfo: null,
 | 
			
		||||
            backupSigStatus: null,
 | 
			
		||||
            // does the server offer a UI auth flow with just m.login.password
 | 
			
		||||
@@ -83,6 +81,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
            useKeyBackup: true,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this._passphraseField = createRef();
 | 
			
		||||
 | 
			
		||||
        this._fetchBackupInfo();
 | 
			
		||||
        if (this.state.accountPassword) {
 | 
			
		||||
            // If we have an account password in memory, let's simplify and
 | 
			
		||||
@@ -99,9 +99,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
 | 
			
		||||
        if (this._setZxcvbnResultTimeout !== null) {
 | 
			
		||||
            clearTimeout(this._setZxcvbnResultTimeout);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _fetchBackupInfo() {
 | 
			
		||||
@@ -364,22 +361,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseNextClick = async (e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        if (!this._passphraseField.current) return; // unmounting
 | 
			
		||||
 | 
			
		||||
        // If we're waiting for the timeout before updating the result at this point,
 | 
			
		||||
        // skip ahead and do it now, otherwise we'll deny the attempt to proceed
 | 
			
		||||
        // even if the user entered a valid passphrase
 | 
			
		||||
        if (this._setZxcvbnResultTimeout !== null) {
 | 
			
		||||
            clearTimeout(this._setZxcvbnResultTimeout);
 | 
			
		||||
            this._setZxcvbnResultTimeout = null;
 | 
			
		||||
            await new Promise((resolve) => {
 | 
			
		||||
                this.setState({
 | 
			
		||||
                    zxcvbnResult: scorePassword(this.state.passPhrase),
 | 
			
		||||
                }, resolve);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        if (this._passPhraseIsValid()) {
 | 
			
		||||
            this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
 | 
			
		||||
        await this._passphraseField.current.validate({ allowEmpty: false });
 | 
			
		||||
        if (!this._passphraseField.current.state.valid) {
 | 
			
		||||
            this._passphraseField.current.focus();
 | 
			
		||||
            this._passphraseField.current.validate({ allowEmpty: false, focused: true });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseConfirmNextClick = async (e) => {
 | 
			
		||||
@@ -399,9 +390,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
    _onSetAgainClick = () => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhrase: '',
 | 
			
		||||
            passPhraseValid: false,
 | 
			
		||||
            passPhraseConfirm: '',
 | 
			
		||||
            phase: PHASE_PASSPHRASE,
 | 
			
		||||
            zxcvbnResult: null,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -411,23 +402,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseValidate = (result) => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhraseValid: result.valid,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseChange = (e) => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passPhrase: e.target.value,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (this._setZxcvbnResultTimeout !== null) {
 | 
			
		||||
            clearTimeout(this._setZxcvbnResultTimeout);
 | 
			
		||||
        }
 | 
			
		||||
        this._setZxcvbnResultTimeout = setTimeout(() => {
 | 
			
		||||
            this._setZxcvbnResultTimeout = null;
 | 
			
		||||
            this.setState({
 | 
			
		||||
                // precompute this and keep it in state: zxcvbn is fast but
 | 
			
		||||
                // we use it in a couple of different places so no point recomputing
 | 
			
		||||
                // it unnecessarily.
 | 
			
		||||
                zxcvbnResult: scorePassword(this.state.passPhrase),
 | 
			
		||||
            });
 | 
			
		||||
        }, PASSPHRASE_FEEDBACK_DELAY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onPassPhraseConfirmChange = (e) => {
 | 
			
		||||
@@ -436,10 +420,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _passPhraseIsValid() {
 | 
			
		||||
        return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _onAccountPasswordChange = (e) => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            accountPassword: e.target.value,
 | 
			
		||||
@@ -502,37 +482,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
    _renderPhasePassPhrase() {
 | 
			
		||||
        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 | 
			
		||||
        const Field = sdk.getComponent('views.elements.Field');
 | 
			
		||||
        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 | 
			
		||||
        const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
 | 
			
		||||
 | 
			
		||||
        let strengthMeter;
 | 
			
		||||
        let helpText;
 | 
			
		||||
        if (this.state.zxcvbnResult) {
 | 
			
		||||
            if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
 | 
			
		||||
                helpText = _t("Great! This recovery passphrase looks strong enough.");
 | 
			
		||||
            } else {
 | 
			
		||||
                // We take the warning from zxcvbn or failing that, the first
 | 
			
		||||
                // suggestion. In practice The first is generally the most relevant
 | 
			
		||||
                // and it's probably better to present the user with one thing to
 | 
			
		||||
                // improve about their password than a whole collection - it can
 | 
			
		||||
                // spit out a warning and multiple suggestions which starts getting
 | 
			
		||||
                // very information-dense.
 | 
			
		||||
                const suggestion = (
 | 
			
		||||
                    this.state.zxcvbnResult.feedback.warning ||
 | 
			
		||||
                    this.state.zxcvbnResult.feedback.suggestions[0]
 | 
			
		||||
                );
 | 
			
		||||
                const suggestionBlock = <div>{suggestion || _t("Keep going...")}</div>;
 | 
			
		||||
 | 
			
		||||
                helpText = <div>
 | 
			
		||||
                    {suggestionBlock}
 | 
			
		||||
                </div>;
 | 
			
		||||
            }
 | 
			
		||||
            strengthMeter = <div>
 | 
			
		||||
                <progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
 | 
			
		||||
            </div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return <form onSubmit={this._onPassPhraseNextClick}>
 | 
			
		||||
            <p>{_t(
 | 
			
		||||
                "Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
 | 
			
		||||
@@ -540,19 +492,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
            )}</p>
 | 
			
		||||
 | 
			
		||||
            <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
 | 
			
		||||
                <Field
 | 
			
		||||
                    type="password"
 | 
			
		||||
                <PassphraseField
 | 
			
		||||
                    className="mx_CreateSecretStorageDialog_passPhraseField"
 | 
			
		||||
                    onChange={this._onPassPhraseChange}
 | 
			
		||||
                    minScore={PASSWORD_MIN_SCORE}
 | 
			
		||||
                    value={this.state.passPhrase}
 | 
			
		||||
                    label={_t("Enter a recovery passphrase")}
 | 
			
		||||
                    onValidate={this._onPassPhraseValidate}
 | 
			
		||||
                    fieldRef={this._passphraseField}
 | 
			
		||||
                    autoFocus={true}
 | 
			
		||||
                    autoComplete="new-password"
 | 
			
		||||
                    label={_td("Enter a recovery passphrase")}
 | 
			
		||||
                    labelEnterPassword={_td("Enter a recovery passphrase")}
 | 
			
		||||
                    labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
 | 
			
		||||
                    labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
 | 
			
		||||
                />
 | 
			
		||||
                <div className="mx_CreateSecretStorageDialog_passPhraseHelp">
 | 
			
		||||
                    {strengthMeter}
 | 
			
		||||
                    {helpText}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <LabelledToggleSwitch
 | 
			
		||||
@@ -564,7 +516,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 | 
			
		||||
                primaryButton={_t('Continue')}
 | 
			
		||||
                onPrimaryButtonClick={this._onPassPhraseNextClick}
 | 
			
		||||
                hasCancel={false}
 | 
			
		||||
                disabled={!this._passPhraseIsValid()}
 | 
			
		||||
                disabled={!this.state.passPhraseValid}
 | 
			
		||||
            >
 | 
			
		||||
                <button type="button"
 | 
			
		||||
                    onClick={this._onSkipSetupClick}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										125
									
								
								src/components/views/auth/PassphraseField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/components/views/auth/PassphraseField.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,125 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, {PureComponent, RefCallback, RefObject} from "react";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
import zxcvbn from "zxcvbn";
 | 
			
		||||
 | 
			
		||||
import SdkConfig from "../../../SdkConfig";
 | 
			
		||||
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
 | 
			
		||||
import {_t, _td} from "../../../languageHandler";
 | 
			
		||||
import Field from "../elements/Field";
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
    autoFocus?: boolean;
 | 
			
		||||
    id?: string;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    minScore: 0 | 1 | 2 | 3 | 4;
 | 
			
		||||
    value: string;
 | 
			
		||||
    fieldRef?: RefCallback<Field> | RefObject<Field>;
 | 
			
		||||
 | 
			
		||||
    label?: string;
 | 
			
		||||
    labelEnterPassword?: string;
 | 
			
		||||
    labelStrongPassword?: string;
 | 
			
		||||
    labelAllowedButUnsafe?: string;
 | 
			
		||||
 | 
			
		||||
    onChange(ev: KeyboardEvent);
 | 
			
		||||
    onValidate(result: IValidationResult);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
    complexity: zxcvbn.ZXCVBNResult;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PassphraseField extends PureComponent<IProps, IState> {
 | 
			
		||||
    static defaultProps = {
 | 
			
		||||
        label: _td("Password"),
 | 
			
		||||
        labelEnterPassword: _td("Enter password"),
 | 
			
		||||
        labelStrongPassword: _td("Nice, strong password!"),
 | 
			
		||||
        labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    state = { complexity: null };
 | 
			
		||||
 | 
			
		||||
    public readonly validate = withValidation<this>({
 | 
			
		||||
        description: function() {
 | 
			
		||||
            const complexity = this.state.complexity;
 | 
			
		||||
            const score = complexity ? complexity.score : 0;
 | 
			
		||||
            return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
 | 
			
		||||
        },
 | 
			
		||||
        rules: [
 | 
			
		||||
            {
 | 
			
		||||
                key: "required",
 | 
			
		||||
                test: ({ value, allowEmpty }) => allowEmpty || !!value,
 | 
			
		||||
                invalid: () => _t(this.props.labelEnterPassword),
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                key: "complexity",
 | 
			
		||||
                test: async function({ value }) {
 | 
			
		||||
                    if (!value) {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    const { scorePassword } = await import('../../../utils/PasswordScorer');
 | 
			
		||||
                    const complexity = scorePassword(value);
 | 
			
		||||
                    this.setState({ complexity });
 | 
			
		||||
                    const safe = complexity.score >= this.props.minScore;
 | 
			
		||||
                    const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
 | 
			
		||||
                    return allowUnsafe || safe;
 | 
			
		||||
                },
 | 
			
		||||
                valid: function() {
 | 
			
		||||
                    // Unsafe passwords that are valid are only possible through a
 | 
			
		||||
                    // configuration flag. We'll print some helper text to signal
 | 
			
		||||
                    // to the user that their password is allowed, but unsafe.
 | 
			
		||||
                    if (this.state.complexity.score >= this.props.minScore) {
 | 
			
		||||
                        return _t(this.props.labelStrongPassword);
 | 
			
		||||
                    }
 | 
			
		||||
                    return _t(this.props.labelAllowedButUnsafe);
 | 
			
		||||
                },
 | 
			
		||||
                invalid: function() {
 | 
			
		||||
                    const complexity = this.state.complexity;
 | 
			
		||||
                    if (!complexity) {
 | 
			
		||||
                        return null;
 | 
			
		||||
                    }
 | 
			
		||||
                    const { feedback } = complexity;
 | 
			
		||||
                    return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    onValidate = async (fieldState: IFieldState) => {
 | 
			
		||||
        const result = await this.validate(fieldState);
 | 
			
		||||
        this.props.onValidate(result);
 | 
			
		||||
        return result;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return <Field
 | 
			
		||||
            id={this.props.id}
 | 
			
		||||
            autoFocus={this.props.autoFocus}
 | 
			
		||||
            className={classNames("mx_PassphraseField", this.props.className)}
 | 
			
		||||
            ref={this.props.fieldRef}
 | 
			
		||||
            type="password"
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            label={_t(this.props.label)}
 | 
			
		||||
            value={this.props.value}
 | 
			
		||||
            onChange={this.props.onChange}
 | 
			
		||||
            onValidate={this.onValidate}
 | 
			
		||||
        />
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PassphraseField;
 | 
			
		||||
@@ -29,6 +29,7 @@ import SdkConfig from '../../../SdkConfig';
 | 
			
		||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
 | 
			
		||||
import withValidation from '../elements/Validation';
 | 
			
		||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 | 
			
		||||
import PassphraseField from "./PassphraseField";
 | 
			
		||||
 | 
			
		||||
const FIELD_EMAIL = 'field_email';
 | 
			
		||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
 | 
			
		||||
@@ -78,7 +79,6 @@ export default createReactClass({
 | 
			
		||||
            password: this.props.defaultPassword || "",
 | 
			
		||||
            passwordConfirm: this.props.defaultPassword || "",
 | 
			
		||||
            passwordComplexity: null,
 | 
			
		||||
            passwordSafe: false,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@@ -264,65 +264,10 @@ export default createReactClass({
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async onPasswordValidate(fieldState) {
 | 
			
		||||
        const result = await this.validatePasswordRules(fieldState);
 | 
			
		||||
    onPasswordValidate(result) {
 | 
			
		||||
        this.markFieldValid(FIELD_PASSWORD, result.valid);
 | 
			
		||||
        return result;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    validatePasswordRules: withValidation({
 | 
			
		||||
        description: function() {
 | 
			
		||||
            const complexity = this.state.passwordComplexity;
 | 
			
		||||
            const score = complexity ? complexity.score : 0;
 | 
			
		||||
            return <progress
 | 
			
		||||
                className="mx_AuthBody_passwordScore"
 | 
			
		||||
                max={PASSWORD_MIN_SCORE}
 | 
			
		||||
                value={score}
 | 
			
		||||
            />;
 | 
			
		||||
        },
 | 
			
		||||
        rules: [
 | 
			
		||||
            {
 | 
			
		||||
                key: "required",
 | 
			
		||||
                test: ({ value, allowEmpty }) => allowEmpty || !!value,
 | 
			
		||||
                invalid: () => _t("Enter password"),
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                key: "complexity",
 | 
			
		||||
                test: async function({ value }) {
 | 
			
		||||
                    if (!value) {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    const { scorePassword } = await import('../../../utils/PasswordScorer');
 | 
			
		||||
                    const complexity = scorePassword(value);
 | 
			
		||||
                    const safe = complexity.score >= PASSWORD_MIN_SCORE;
 | 
			
		||||
                    const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
 | 
			
		||||
                    this.setState({
 | 
			
		||||
                        passwordComplexity: complexity,
 | 
			
		||||
                        passwordSafe: safe,
 | 
			
		||||
                    });
 | 
			
		||||
                    return allowUnsafe || safe;
 | 
			
		||||
                },
 | 
			
		||||
                valid: function() {
 | 
			
		||||
                    // Unsafe passwords that are valid are only possible through a
 | 
			
		||||
                    // configuration flag. We'll print some helper text to signal
 | 
			
		||||
                    // to the user that their password is allowed, but unsafe.
 | 
			
		||||
                    if (!this.state.passwordSafe) {
 | 
			
		||||
                        return _t("Password is allowed, but unsafe");
 | 
			
		||||
                    }
 | 
			
		||||
                    return _t("Nice, strong password!");
 | 
			
		||||
                },
 | 
			
		||||
                invalid: function() {
 | 
			
		||||
                    const complexity = this.state.passwordComplexity;
 | 
			
		||||
                    if (!complexity) {
 | 
			
		||||
                        return null;
 | 
			
		||||
                    }
 | 
			
		||||
                    const { feedback } = complexity;
 | 
			
		||||
                    return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    onPasswordConfirmChange(ev) {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            passwordConfirm: ev.target.value,
 | 
			
		||||
@@ -484,13 +429,10 @@ export default createReactClass({
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    renderPassword() {
 | 
			
		||||
        const Field = sdk.getComponent('elements.Field');
 | 
			
		||||
        return <Field
 | 
			
		||||
        return <PassphraseField
 | 
			
		||||
            id="mx_RegistrationForm_password"
 | 
			
		||||
            ref={field => this[FIELD_PASSWORD] = field}
 | 
			
		||||
            type="password"
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            label={_t("Password")}
 | 
			
		||||
            fieldRef={field => this[FIELD_PASSWORD] = field}
 | 
			
		||||
            minScore={PASSWORD_MIN_SCORE}
 | 
			
		||||
            value={this.state.password}
 | 
			
		||||
            onChange={this.onPasswordChange}
 | 
			
		||||
            onValidate={this.onPasswordValidate}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2019 New Vector Ltd
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
@@ -15,11 +16,39 @@ limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
/* eslint-disable babel/no-invalid-this */
 | 
			
		||||
import React from "react";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
type Data = Pick<IFieldState, "value" | "allowEmpty">;
 | 
			
		||||
 | 
			
		||||
interface IRule<T> {
 | 
			
		||||
    key: string;
 | 
			
		||||
    final?: boolean;
 | 
			
		||||
    skip?(this: T, data: Data): boolean;
 | 
			
		||||
    test(this: T, data: Data): boolean | Promise<boolean>;
 | 
			
		||||
    valid?(this: T): string;
 | 
			
		||||
    invalid?(this: T): string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IArgs<T> {
 | 
			
		||||
    rules: IRule<T>[];
 | 
			
		||||
    description(this: T): React.ReactChild;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFieldState {
 | 
			
		||||
    value: string;
 | 
			
		||||
    focused: boolean;
 | 
			
		||||
    allowEmpty: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IValidationResult {
 | 
			
		||||
    valid?: boolean;
 | 
			
		||||
    feedback?: React.ReactChild;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a validation function from a set of rules describing what to validate.
 | 
			
		||||
 * Generic T is the "this" type passed to the rule methods
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Function} description
 | 
			
		||||
 *     Function that returns a string summary of the kind of value that will
 | 
			
		||||
@@ -37,8 +66,8 @@ import classNames from 'classnames';
 | 
			
		||||
 *     A validation function that takes in the current input value and returns
 | 
			
		||||
 *     the overall validity and a feedback UI that can be rendered for more detail.
 | 
			
		||||
 */
 | 
			
		||||
export default function withValidation({ description, rules }) {
 | 
			
		||||
    return async function onValidate({ value, focused, allowEmpty = true }) {
 | 
			
		||||
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
 | 
			
		||||
    return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
 | 
			
		||||
        if (!value && allowEmpty) {
 | 
			
		||||
            return {
 | 
			
		||||
                valid: null,
 | 
			
		||||
@@ -1889,6 +1889,10 @@
 | 
			
		||||
    "Your Modular server": "Your Modular server",
 | 
			
		||||
    "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.",
 | 
			
		||||
    "Server Name": "Server Name",
 | 
			
		||||
    "Enter password": "Enter password",
 | 
			
		||||
    "Nice, strong password!": "Nice, strong password!",
 | 
			
		||||
    "Password is allowed, but unsafe": "Password is allowed, but unsafe",
 | 
			
		||||
    "Keep going...": "Keep going...",
 | 
			
		||||
    "The email field must not be blank.": "The email field must not be blank.",
 | 
			
		||||
    "The username field must not be blank.": "The username field must not be blank.",
 | 
			
		||||
    "The phone number field must not be blank.": "The phone number field must not be blank.",
 | 
			
		||||
@@ -1903,10 +1907,6 @@
 | 
			
		||||
    "Use an email address to recover your account": "Use an email address to recover your account",
 | 
			
		||||
    "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
 | 
			
		||||
    "Doesn't look like a valid email address": "Doesn't look like a valid email address",
 | 
			
		||||
    "Enter password": "Enter password",
 | 
			
		||||
    "Password is allowed, but unsafe": "Password is allowed, but unsafe",
 | 
			
		||||
    "Nice, strong password!": "Nice, strong password!",
 | 
			
		||||
    "Keep going...": "Keep going...",
 | 
			
		||||
    "Passwords don't match": "Passwords don't match",
 | 
			
		||||
    "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
 | 
			
		||||
    "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
 | 
			
		||||
@@ -2200,9 +2200,9 @@
 | 
			
		||||
    "Restore": "Restore",
 | 
			
		||||
    "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
 | 
			
		||||
    "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
 | 
			
		||||
    "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
 | 
			
		||||
    "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:",
 | 
			
		||||
    "Enter a recovery passphrase": "Enter a recovery passphrase",
 | 
			
		||||
    "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
 | 
			
		||||
    "Back up encrypted message keys": "Back up encrypted message keys",
 | 
			
		||||
    "Set up with a recovery key": "Set up with a recovery key",
 | 
			
		||||
    "That matches!": "That matches!",
 | 
			
		||||
@@ -2230,7 +2230,6 @@
 | 
			
		||||
    "Unable to set up secret storage": "Unable to set up secret storage",
 | 
			
		||||
    "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
 | 
			
		||||
    "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
 | 
			
		||||
    "Enter a recovery passphrase...": "Enter a recovery passphrase...",
 | 
			
		||||
    "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
 | 
			
		||||
    "Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
 | 
			
		||||
    "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,7 @@ _td("Short keyboard patterns are easy to guess");
 | 
			
		||||
 * @param {string} password Password to score
 | 
			
		||||
 * @returns {object} Score result with `score` and `feedback` properties
 | 
			
		||||
 */
 | 
			
		||||
export function scorePassword(password) {
 | 
			
		||||
export function scorePassword(password: string) {
 | 
			
		||||
    if (password.length === 0) return null;
 | 
			
		||||
 | 
			
		||||
    const userInputs = ZXCVBN_USER_INPUTS.slice();
 | 
			
		||||
@@ -1325,6 +1325,11 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/yargs-parser" "*"
 | 
			
		||||
 | 
			
		||||
"@types/zxcvbn@^4.4.0":
 | 
			
		||||
  version "4.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609"
 | 
			
		||||
  integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==
 | 
			
		||||
 | 
			
		||||
"@typescript-eslint/experimental-utils@^2.5.0":
 | 
			
		||||
  version "2.27.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user