Convert ChangePassword to TS

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-09-21 14:39:56 +02:00
parent 073eff4c71
commit 9051df45c1
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D

View file

@ -17,78 +17,81 @@ limitations under the License.
import Field from "../elements/Field"; import Field from "../elements/Field";
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner'; import Spinner from '../elements/Spinner';
import withValidation from '../elements/Validation'; import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from "../../../index";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import PassphraseField from "../auth/PassphraseField"; import PassphraseField from "../auth/PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm'; import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
import { MatrixClient } from "matrix-js-sdk/src/client";
import SetEmailDialog from "../dialogs/SetEmailDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
const FIELD_OLD_PASSWORD = 'field_old_password'; const FIELD_OLD_PASSWORD = 'field_old_password';
const FIELD_NEW_PASSWORD = 'field_new_password'; const FIELD_NEW_PASSWORD = 'field_new_password';
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm'; const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
enum Phase {
Edit = "edit",
Uploading = "uploading",
Error = "error",
}
interface IProps {
onFinished?: ({ didSetEmail: boolean }?) => void;
onError?: (error: {error: string}) => void;
rowClassName?: string;
buttonClassName?: string;
buttonKind?: string;
buttonLabel?: string;
confirm?: boolean;
// Whether to autoFocus the new password input
autoFocusNewPasswordInput?: boolean;
className?: string;
shouldAskForEmail?: boolean;
}
interface IState {
fieldValid: {};
phase: Phase;
oldPassword: string;
newPassword: string;
newPasswordConfirm: string;
}
@replaceableComponent("views.settings.ChangePassword") @replaceableComponent("views.settings.ChangePassword")
export default class ChangePassword extends React.Component { export default class ChangePassword extends React.Component<IProps, IState> {
static propTypes = { public static defaultProps: Partial<IProps> = {
onFinished: PropTypes.func,
onError: PropTypes.func,
onCheckPassword: PropTypes.func,
rowClassName: PropTypes.string,
buttonClassName: PropTypes.string,
buttonKind: PropTypes.string,
buttonLabel: PropTypes.string,
confirm: PropTypes.bool,
// Whether to autoFocus the new password input
autoFocusNewPasswordInput: PropTypes.bool,
};
static Phases = {
Edit: "edit",
Uploading: "uploading",
Error: "error",
};
static defaultProps = {
onFinished() {}, onFinished() {},
onError() {}, onError() {},
onCheckPassword(oldPass, newPass, confirmPass) {
if (newPass !== confirmPass) {
return {
error: _t("New passwords don't match"),
};
} else if (!newPass || newPass.length === 0) {
return {
error: _t("Passwords can't be empty"),
};
}
},
confirm: true,
}
state = { confirm: true,
fieldValid: {},
phase: ChangePassword.Phases.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
}; };
changePassword(oldPassword, newPassword) { constructor(props: IProps) {
super(props);
this.state = {
fieldValid: {},
phase: Phase.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
};
}
private onChangePassword(oldPassword: string, newPassword: string): void {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!this.props.confirm) { if (!this.props.confirm) {
this._changePassword(cli, oldPassword, newPassword); this.changePassword(cli, oldPassword, newPassword);
return; return;
} }
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Change Password', '', QuestionDialog, { Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
title: _t("Warning!"), title: _t("Warning!"),
description: description:
@ -109,20 +112,20 @@ export default class ChangePassword extends React.Component {
<button <button
key="exportRoomKeys" key="exportRoomKeys"
className="mx_Dialog_primary" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked} onClick={this.onExportE2eKeysClicked}
> >
{ _t('Export E2E room keys') } { _t('Export E2E room keys') }
</button>, </button>,
], ],
onFinished: (confirmed) => { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
this._changePassword(cli, oldPassword, newPassword); this.changePassword(cli, oldPassword, newPassword);
} }
}, },
}); });
} }
_changePassword(cli, oldPassword, newPassword) { private changePassword(cli: MatrixClient, oldPassword: string, newPassword: string): void {
const authDict = { const authDict = {
type: 'm.login.password', type: 'm.login.password',
identifier: { identifier: {
@ -136,12 +139,12 @@ export default class ChangePassword extends React.Component {
}; };
this.setState({ this.setState({
phase: ChangePassword.Phases.Uploading, phase: Phase.Uploading,
}); });
cli.setPassword(authDict, newPassword).then(() => { cli.setPassword(authDict, newPassword).then(() => {
if (this.props.shouldAskForEmail) { if (this.props.shouldAskForEmail) {
return this._optionallySetEmail().then((confirmed) => { return this.optionallySetEmail().then((confirmed) => {
this.props.onFinished({ this.props.onFinished({
didSetEmail: confirmed, didSetEmail: confirmed,
}); });
@ -153,7 +156,7 @@ export default class ChangePassword extends React.Component {
this.props.onError(err); this.props.onError(err);
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
phase: ChangePassword.Phases.Edit, phase: Phase.Edit,
oldPassword: "", oldPassword: "",
newPassword: "", newPassword: "",
newPasswordConfirm: "", newPasswordConfirm: "",
@ -161,16 +164,27 @@ export default class ChangePassword extends React.Component {
}); });
} }
_optionallySetEmail() { private checkPassword(oldPass: string, newPass: string, confirmPass: string): {error: string} {
if (newPass !== confirmPass) {
return {
error: _t("New passwords don't match"),
};
} else if (!newPass || newPass.length === 0) {
return {
error: _t("Passwords can't be empty"),
};
}
}
private optionallySetEmail(): Promise<boolean> {
// Ask for an email otherwise the user has no way to reset their password // Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
title: _t('Do you want to set an email address?'), title: _t('Do you want to set an email address?'),
}); });
return modal.finished.then(([confirmed]) => confirmed); return modal.finished.then(([confirmed]) => confirmed);
} }
_onExportE2eKeysClicked = () => { private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{ {
@ -179,7 +193,7 @@ export default class ChangePassword extends React.Component {
); );
}; };
markFieldValid(fieldID, valid) { private markFieldValid(fieldID: string, valid: boolean): void {
const { fieldValid } = this.state; const { fieldValid } = this.state;
fieldValid[fieldID] = valid; fieldValid[fieldID] = valid;
this.setState({ this.setState({
@ -187,19 +201,19 @@ export default class ChangePassword extends React.Component {
}); });
} }
onChangeOldPassword = (ev) => { private onChangeOldPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
oldPassword: ev.target.value, oldPassword: ev.target.value,
}); });
}; };
onOldPasswordValidate = async fieldState => { private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.validateOldPasswordRules(fieldState); const result = await this.validateOldPasswordRules(fieldState);
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid); this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
return result; return result;
}; };
validateOldPasswordRules = withValidation({ private validateOldPasswordRules = withValidation({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -209,29 +223,29 @@ export default class ChangePassword extends React.Component {
], ],
}); });
onChangeNewPassword = (ev) => { private onChangeNewPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
newPassword: ev.target.value, newPassword: ev.target.value,
}); });
}; };
onNewPasswordValidate = result => { private onNewPasswordValidate = (result: IValidationResult): void => {
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid); this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
}; };
onChangeNewPasswordConfirm = (ev) => { private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
newPasswordConfirm: ev.target.value, newPasswordConfirm: ev.target.value,
}); });
}; };
onNewPasswordConfirmValidate = async fieldState => { private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.validatePasswordConfirmRules(fieldState); const result = await this.validatePasswordConfirmRules(fieldState);
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid); this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
return result; return result;
}; };
validatePasswordConfirmRules = withValidation({ private validatePasswordConfirmRules = withValidation<this>({
rules: [ rules: [
{ {
key: "required", key: "required",
@ -248,7 +262,7 @@ export default class ChangePassword extends React.Component {
], ],
}); });
onClickChange = async (ev) => { private onClickChange = async (ev: React.MouseEvent | React.FormEvent): Promise<void> => {
ev.preventDefault(); ev.preventDefault();
const allFieldsValid = await this.verifyFieldsBeforeSubmit(); const allFieldsValid = await this.verifyFieldsBeforeSubmit();
@ -260,20 +274,20 @@ export default class ChangePassword extends React.Component {
const oldPassword = this.state.oldPassword; const oldPassword = this.state.oldPassword;
const newPassword = this.state.newPassword; const newPassword = this.state.newPassword;
const confirmPassword = this.state.newPasswordConfirm; const confirmPassword = this.state.newPasswordConfirm;
const err = this.props.onCheckPassword( const err = this.checkPassword(
oldPassword, newPassword, confirmPassword, oldPassword, newPassword, confirmPassword,
); );
if (err) { if (err) {
this.props.onError(err); this.props.onError(err);
} else { } else {
this.changePassword(oldPassword, newPassword); this.onChangePassword(oldPassword, newPassword);
} }
}; };
async verifyFieldsBeforeSubmit() { private async verifyFieldsBeforeSubmit(): Promise<boolean> {
// Blur the active element if any, so we first run its blur validation, // Blur the active element if any, so we first run its blur validation,
// which is less strict than the pass we're about to do below for all fields. // which is less strict than the pass we're about to do below for all fields.
const activeElement = document.activeElement; const activeElement = document.activeElement as HTMLElement;
if (activeElement) { if (activeElement) {
activeElement.blur(); activeElement.blur();
} }
@ -300,7 +314,7 @@ export default class ChangePassword extends React.Component {
// Validation and state updates are async, so we need to wait for them to complete // Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve. // first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve)); await new Promise<void>((resolve) => this.setState({}, resolve));
if (this.allFieldsValid()) { if (this.allFieldsValid()) {
return true; return true;
@ -319,7 +333,7 @@ export default class ChangePassword extends React.Component {
return false; return false;
} }
allFieldsValid() { private allFieldsValid(): boolean {
const keys = Object.keys(this.state.fieldValid); const keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
if (!this.state.fieldValid[keys[i]]) { if (!this.state.fieldValid[keys[i]]) {
@ -329,7 +343,7 @@ export default class ChangePassword extends React.Component {
return true; return true;
} }
findFirstInvalidField(fieldIDs) { private findFirstInvalidField(fieldIDs: string[]): Field {
for (const fieldID of fieldIDs) { for (const fieldID of fieldIDs) {
if (!this.state.fieldValid[fieldID] && this[fieldID]) { if (!this.state.fieldValid[fieldID] && this[fieldID]) {
return this[fieldID]; return this[fieldID];
@ -338,12 +352,12 @@ export default class ChangePassword extends React.Component {
return null; return null;
} }
render() { public render(): JSX.Element {
const rowClassName = this.props.rowClassName; const rowClassName = this.props.rowClassName;
const buttonClassName = this.props.buttonClassName; const buttonClassName = this.props.buttonClassName;
switch (this.state.phase) { switch (this.state.phase) {
case ChangePassword.Phases.Edit: case Phase.Edit:
return ( return (
<form className={this.props.className} onSubmit={this.onClickChange}> <form className={this.props.className} onSubmit={this.onClickChange}>
<div className={rowClassName}> <div className={rowClassName}>
@ -385,7 +399,7 @@ export default class ChangePassword extends React.Component {
</AccessibleButton> </AccessibleButton>
</form> </form>
); );
case ChangePassword.Phases.Uploading: case Phase.Uploading:
return ( return (
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<Spinner /> <Spinner />