diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.js b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx similarity index 79% rename from src/components/views/dialogs/security/AccessSecretStorageDialog.js rename to src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index f54a053984..3c09470b39 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -1,6 +1,5 @@ /* -Copyright 2018, 2019 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2018-2021 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. @@ -17,14 +16,15 @@ limitations under the License. import {debounce} from "lodash"; import classNames from 'classnames'; -import React from 'react'; -import PropTypes from "prop-types"; +import React, {ChangeEvent, FormEvent} from 'react'; +import {ISecretStorageKeyInfo} from "matrix-js-sdk/src"; + import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import Field from '../../elements/Field'; import AccessibleButton from '../../elements/AccessibleButton'; - -import { _t } from '../../../../languageHandler'; +import {_t} from '../../../../languageHandler'; +import {IDialogProps} from "../IDialogProps"; // Maximum acceptable size of a key file. It's 59 characters including the spaces we encode, // so this should be plenty and allow for people putting extra whitespace in the file because @@ -34,22 +34,30 @@ const KEY_FILE_MAX_SIZE = 128; // Don't shout at the user that their key is invalid every time they type a key: wait a short time const VALIDATION_THROTTLE_MS = 200; +interface IProps extends IDialogProps { + keyInfo: ISecretStorageKeyInfo; + checkPrivateKey: (k: {passphrase?: string, recoveryKey?: string}) => boolean; +} + +interface IState { + recoveryKey: string; + recoveryKeyValid: boolean | null; + recoveryKeyCorrect: boolean | null; + recoveryKeyFileError: boolean | null; + forceRecoveryKey: boolean; + passPhrase: string; + keyMatches: boolean | null; +} + /* * Access Secure Secret Storage by requesting the user's passphrase. */ -export default class AccessSecretStorageDialog extends React.PureComponent { - static propTypes = { - // { passphrase, pubkey } - keyInfo: PropTypes.object.isRequired, - // Function from one of { passphrase, recoveryKey } -> boolean - checkPrivateKey: PropTypes.func.isRequired, - } +export default class AccessSecretStorageDialog extends React.PureComponent { + private fileUpload = React.createRef(); constructor(props) { super(props); - this._fileUpload = React.createRef(); - this.state = { recoveryKey: "", recoveryKeyValid: null, @@ -61,21 +69,21 @@ export default class AccessSecretStorageDialog extends React.PureComponent { }; } - _onCancel = () => { + private onCancel = () => { this.props.onFinished(false); - } + }; - _onUseRecoveryKeyClick = () => { + private onUseRecoveryKeyClick = () => { this.setState({ forceRecoveryKey: true, }); - } + }; - _validateRecoveryKeyOnChange = debounce(() => { - this._validateRecoveryKey(); + private validateRecoveryKeyOnChange = debounce(async () => { + await this.validateRecoveryKey(); }, VALIDATION_THROTTLE_MS); - async _validateRecoveryKey() { + private async validateRecoveryKey() { if (this.state.recoveryKey === '') { this.setState({ recoveryKeyValid: null, @@ -102,27 +110,27 @@ export default class AccessSecretStorageDialog extends React.PureComponent { } } - _onRecoveryKeyChange = (e) => { + private onRecoveryKeyChange = (ev: ChangeEvent) => { this.setState({ - recoveryKey: e.target.value, + recoveryKey: ev.target.value, recoveryKeyFileError: null, }); // also clear the file upload control so that the user can upload the same file // the did before (otherwise the onchange wouldn't fire) - if (this._fileUpload.current) this._fileUpload.current.value = null; + if (this.fileUpload.current) this.fileUpload.current.value = null; // We don't use Field's validation here because a) we want it in a separate place rather // than in a tooltip and b) we want it to display feedback based on the uploaded file // as well as the text box. Ideally we would refactor Field's validation logic so we could // re-use some of it. - this._validateRecoveryKeyOnChange(); - } + this.validateRecoveryKeyOnChange(); + }; - _onRecoveryKeyFileChange = async e => { - if (e.target.files.length === 0) return; + private onRecoveryKeyFileChange = async (ev: ChangeEvent) => { + if (ev.target.files.length === 0) return; - const f = e.target.files[0]; + const f = ev.target.files[0]; if (f.size > KEY_FILE_MAX_SIZE) { this.setState({ @@ -140,7 +148,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { recoveryKeyFileError: null, recoveryKey: contents.trim(), }); - this._validateRecoveryKey(); + await this.validateRecoveryKey(); } else { this.setState({ recoveryKeyFileError: true, @@ -150,14 +158,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent { }); } } + }; + + private onRecoveryKeyFileUploadClick = () => { + this.fileUpload.current.click(); } - _onRecoveryKeyFileUploadClick = () => { - this._fileUpload.current.click(); - } - - _onPassPhraseNext = async (e) => { - e.preventDefault(); + private onPassPhraseNext = async (ev: FormEvent) => { + ev.preventDefault(); if (this.state.passPhrase.length <= 0) return; @@ -169,10 +177,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent { } else { this.setState({ keyMatches }); } - } + }; - _onRecoveryKeyNext = async (e) => { - e.preventDefault(); + private onRecoveryKeyNext = async (ev: FormEvent) => { + ev.preventDefault(); if (!this.state.recoveryKeyValid) return; @@ -184,16 +192,16 @@ export default class AccessSecretStorageDialog extends React.PureComponent { } else { this.setState({ keyMatches }); } - } + }; - _onPassPhraseChange = (e) => { + private onPassPhraseChange = (ev: ChangeEvent) => { this.setState({ - passPhrase: e.target.value, + passPhrase: ev.target.value, keyMatches: null, }); - } + }; - getKeyValidationText() { + private getKeyValidationText(): string { if (this.state.recoveryKeyFileError) { return _t("Wrong file type"); } else if (this.state.recoveryKeyCorrect) { @@ -208,7 +216,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent { } render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + // Caution: Making this an import will break tests. + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); const hasPassphrase = ( this.props.keyInfo && @@ -244,18 +253,18 @@ export default class AccessSecretStorageDialog extends React.PureComponent { { button: s => {s} , }, )}

-
+ @@ -291,7 +300,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { @@ -301,7 +310,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { type="password" label={_t('Security Key')} value={this.state.recoveryKey} - onChange={this._onRecoveryKeyChange} + onChange={this.onRecoveryKeyChange} forceValidity={this.state.recoveryKeyCorrect} autoComplete="off" /> @@ -312,10 +321,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
- + {_t("Upload")}
@@ -323,11 +332,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent { {recoveryKeyFeedback} @@ -341,9 +350,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent { title={title} titleClass={titleClass} > -
- {content} -
+
+ {content} +
); } diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.js b/test/components/views/dialogs/AccessSecretStorageDialog-test.js index 76412a6a82..13b39ab0d0 100644 --- a/test/components/views/dialogs/AccessSecretStorageDialog-test.js +++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.js @@ -37,7 +37,7 @@ describe("AccessSecretStorageDialog", function() { recoveryKey: "a", }); const e = { preventDefault: () => {} }; - testInstance.getInstance()._onRecoveryKeyNext(e); + testInstance.getInstance().onRecoveryKeyNext(e); }); it("Considers a valid key to be valid", async function() { @@ -51,9 +51,9 @@ describe("AccessSecretStorageDialog", function() { stubClient(); MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => 'a raw key'; MatrixClientPeg.get().checkSecretStorageKey = () => true; - testInstance.getInstance()._onRecoveryKeyChange(e); + testInstance.getInstance().onRecoveryKeyChange(e); // force a validation now because it debounces - await testInstance.getInstance()._validateRecoveryKey(); + await testInstance.getInstance().validateRecoveryKey(); const { recoveryKeyValid } = testInstance.getInstance().state; expect(recoveryKeyValid).toBe(true); }); @@ -69,9 +69,9 @@ describe("AccessSecretStorageDialog", function() { MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => { throw new Error("that's no key"); }; - testInstance.getInstance()._onRecoveryKeyChange(e); + testInstance.getInstance().onRecoveryKeyChange(e); // force a validation now because it debounces - await testInstance.getInstance()._validateRecoveryKey(); + await testInstance.getInstance().validateRecoveryKey(); const { recoveryKeyValid, recoveryKeyCorrect } = testInstance.getInstance().state; expect(recoveryKeyValid).toBe(false); @@ -98,8 +98,8 @@ describe("AccessSecretStorageDialog", function() { const e = { target: { value: "a" } }; stubClient(); MatrixClientPeg.get().isValidRecoveryKey = () => false; - testInstance.getInstance()._onPassPhraseChange(e); - await testInstance.getInstance()._onPassPhraseNext({ preventDefault: () => {} }); + testInstance.getInstance().onPassPhraseChange(e); + await testInstance.getInstance().onPassPhraseNext({ preventDefault: () => {} }); const notification = testInstance.root.findByProps({ className: "mx_AccessSecretStorageDialog_keyStatus", });