From c5bd1fb32d04cb2e3394ed64ebb950a9064408d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 23 Oct 2021 05:32:16 +0200 Subject: [PATCH] Convert `/src/async-components/views/dialogs/security` to TS (#6923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Convert RecoveryMethodRemovedDialog to TS Signed-off-by: Šimon Brandner * Convert NewRecoveryMethodDialog to TS Signed-off-by: Šimon Brandner * Convert ImportE2eKeysDialog to TS Signed-off-by: Šimon Brandner * Convert ExportE2eKeysDialog to TS Signed-off-by: Šimon Brandner * Convert CreateSecretStorageDialog to TS Signed-off-by: Šimon Brandner * Convert CreateKeyBackupDialog to TS Signed-off-by: Šimon Brandner * Fix types This is somewhat hacky though I don't know of a better way to do this Signed-off-by: Šimon Brandner --- src/SecurityManager.ts | 5 +- ...kupDialog.js => CreateKeyBackupDialog.tsx} | 276 +++++----- ...ialog.js => CreateSecretStorageDialog.tsx} | 493 +++++++++--------- ...eKeysDialog.js => ExportE2eKeysDialog.tsx} | 76 +-- ...eKeysDialog.js => ImportE2eKeysDialog.tsx} | 85 +-- ...dDialog.js => NewRecoveryMethodDialog.tsx} | 33 +- ...log.js => RecoveryMethodRemovedDialog.tsx} | 28 +- src/components/structures/MatrixChat.tsx | 10 +- src/components/views/dialogs/LogoutDialog.tsx | 10 +- .../views/settings/ChangePassword.tsx | 6 +- .../views/settings/CryptographyPanel.tsx | 10 +- .../views/settings/SecureBackupPanel.tsx | 6 +- 12 files changed, 534 insertions(+), 504 deletions(-) rename src/async-components/views/dialogs/security/{CreateKeyBackupDialog.js => CreateKeyBackupDialog.tsx} (67%) rename src/async-components/views/dialogs/security/{CreateSecretStorageDialog.js => CreateSecretStorageDialog.tsx} (67%) rename src/async-components/views/dialogs/security/{ExportE2eKeysDialog.js => ExportE2eKeysDialog.tsx} (79%) rename src/async-components/views/dialogs/security/{ImportE2eKeysDialog.js => ImportE2eKeysDialog.tsx} (74%) rename src/async-components/views/dialogs/security/{NewRecoveryMethodDialog.js => NewRecoveryMethodDialog.tsx} (84%) rename src/async-components/views/dialogs/security/{RecoveryMethodRemovedDialog.js => RecoveryMethodRemovedDialog.tsx} (82%) diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index a184e6e9cbd..7c65c38e0a4 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -32,6 +32,7 @@ import SecurityCustomisations from "./customisations/Security"; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { logger } from "matrix-js-sdk/src/logger"; +import { ComponentType } from "react"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -335,7 +336,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', - import("./async-components/views/dialogs/security/CreateSecretStorageDialog"), + import( + "./async-components/views/dialogs/security/CreateSecretStorageDialog" + ) as unknown as Promise>, { forceReset, }, diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx similarity index 67% rename from src/async-components/views/dialogs/security/CreateKeyBackupDialog.js rename to src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 8527f788959..c0aff7c8b91 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -17,56 +17,70 @@ limitations under the License. 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 { _t, _td } from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../SecurityManager'; import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import { copyNode } from "../../../../utils/strings"; import PassphraseField from "../../../../components/views/auth/PassphraseField"; - +import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import Field from "../../../../components/views/elements/Field"; +import Spinner from "../../../../components/views/elements/Spinner"; +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; +import { IValidationResult } from "../../../../components/views/elements/Validation"; +import { IPreparedKeyBackupVersion } from "matrix-js-sdk/src/crypto/backup"; import { logger } from "matrix-js-sdk/src/logger"; -const PHASE_PASSPHRASE = 0; -const PHASE_PASSPHRASE_CONFIRM = 1; -const PHASE_SHOWKEY = 2; -const PHASE_KEEPITSAFE = 3; -const PHASE_BACKINGUP = 4; -const PHASE_DONE = 5; -const PHASE_OPTOUT_CONFIRM = 6; +enum Phase { + Passphrase = "passphrase", + PassphraseConfirm = "passphrase_confirm", + ShowKey = "show_key", + KeepItSafe = "keep_it_safe", + BackingUp = "backing_up", + Done = "done", + OptOutConfirm = "opt_out_confirm", +} const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. +interface IProps extends IDialogProps {} + +interface IState { + secureSecretStorage: boolean; + phase: Phase; + passPhrase: string; + passPhraseValid: boolean; + passPhraseConfirm: string; + copied: boolean; + downloaded: boolean; + error?: string; +} + /* * Walks the user through the process of creating an e2e key backup * on the server. */ -export default class CreateKeyBackupDialog extends React.PureComponent { - static propTypes = { - onFinished: PropTypes.func.isRequired, - } +export default class CreateKeyBackupDialog extends React.PureComponent { + private keyBackupInfo: Pick; + private recoveryKeyNode = createRef(); + private passphraseField = createRef(); - constructor(props) { + constructor(props: IProps) { super(props); - this._recoveryKeyNode = null; - this._keyBackupInfo = null; - this.state = { secureSecretStorage: null, - phase: PHASE_PASSPHRASE, + phase: Phase.Passphrase, passPhrase: '', passPhraseValid: false, passPhraseConfirm: '', copied: false, downloaded: false, }; - - this._passphraseField = createRef(); } - async componentDidMount() { + public async componentDidMount(): Promise { const cli = MatrixClientPeg.get(); const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); this.setState({ secureSecretStorage }); @@ -74,41 +88,37 @@ export default class CreateKeyBackupDialog extends React.PureComponent { // If we're using secret storage, skip ahead to the backing up step, as // `accessSecretStorage` will handle passphrases as needed. if (secureSecretStorage) { - this.setState({ phase: PHASE_BACKINGUP }); - this._createBackup(); + this.setState({ phase: Phase.BackingUp }); + this.createBackup(); } } - _collectRecoveryKeyNode = (n) => { - this._recoveryKeyNode = n; - } - - _onCopyClick = () => { - const successful = copyNode(this._recoveryKeyNode); + private onCopyClick = (): void => { + const successful = copyNode(this.recoveryKeyNode.current); if (successful) { this.setState({ copied: true, - phase: PHASE_KEEPITSAFE, + phase: Phase.KeepItSafe, }); } - } + }; - _onDownloadClick = () => { - const blob = new Blob([this._keyBackupInfo.recovery_key], { + private onDownloadClick = (): void => { + const blob = new Blob([this.keyBackupInfo.recovery_key], { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'security-key.txt'); this.setState({ downloaded: true, - phase: PHASE_KEEPITSAFE, + phase: Phase.KeepItSafe, }); - } + }; - _createBackup = async () => { + private createBackup = async (): Promise => { const { secureSecretStorage } = this.state; this.setState({ - phase: PHASE_BACKINGUP, + phase: Phase.BackingUp, error: null, }); let info; @@ -123,12 +133,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent { }); } else { info = await MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, + this.keyBackupInfo, ); } await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); this.setState({ - phase: PHASE_DONE, + phase: Phase.Done, }); } catch (e) { logger.error("Error creating key backup", e); @@ -143,97 +153,91 @@ export default class CreateKeyBackupDialog extends React.PureComponent { error: e, }); } - } + }; - _onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); - } + }; - _onDone = () => { + private onDone = (): void => { this.props.onFinished(true); - } - - _onOptOutClick = () => { - this.setState({ phase: PHASE_OPTOUT_CONFIRM }); - } + }; - _onSetUpClick = () => { - this.setState({ phase: PHASE_PASSPHRASE }); - } + private onSetUpClick = (): void => { + this.setState({ phase: Phase.Passphrase }); + }; - _onSkipPassPhraseClick = async () => { - this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); + private onSkipPassPhraseClick = async (): Promise => { + this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); this.setState({ copied: false, downloaded: false, - phase: PHASE_SHOWKEY, + phase: Phase.ShowKey, }); - } + }; - _onPassPhraseNextClick = async (e) => { + private onPassPhraseNextClick = async (e: React.FormEvent): Promise => { e.preventDefault(); - if (!this._passphraseField.current) return; // unmounting + if (!this.passphraseField.current) return; // unmounting - 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 }); + 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 }); + this.setState({ phase: Phase.PassphraseConfirm }); }; - _onPassPhraseConfirmNextClick = async (e) => { + private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise => { e.preventDefault(); if (this.state.passPhrase !== this.state.passPhraseConfirm) return; - this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); + this.keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ copied: false, downloaded: false, - phase: PHASE_SHOWKEY, + phase: Phase.ShowKey, }); }; - _onSetAgainClick = () => { + private onSetAgainClick = (): void => { this.setState({ passPhrase: '', passPhraseValid: false, passPhraseConfirm: '', - phase: PHASE_PASSPHRASE, + phase: Phase.Passphrase, }); - } + }; - _onKeepItSafeBackClick = () => { + private onKeepItSafeBackClick = (): void => { this.setState({ - phase: PHASE_SHOWKEY, + phase: Phase.ShowKey, }); - } + }; - _onPassPhraseValidate = (result) => { + private onPassPhraseValidate = (result: IValidationResult): void => { this.setState({ passPhraseValid: result.valid, }); }; - _onPassPhraseChange = (e) => { + private onPassPhraseChange = (e: React.ChangeEvent): void => { this.setState({ passPhrase: e.target.value, }); - } + }; - _onPassPhraseConfirmChange = (e) => { + private onPassPhraseConfirmChange = (e: React.ChangeEvent): void => { this.setState({ passPhraseConfirm: e.target.value, }); - } - - _renderPhasePassPhrase() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + }; - return
+ private renderPhasePassPhrase(): JSX.Element { + return

{ _t( "Warning: You should only set up key backup from a trusted computer.", {}, { b: sub => { sub } }, @@ -248,11 +252,11 @@ export default class CreateKeyBackupDialog extends React.PureComponent {

{ _t("Advanced") } - + { _t("Set up with a Security Key") }
; } - _renderPhasePassPhraseConfirm() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - + private renderPhasePassPhraseConfirm(): JSX.Element { let matchText; let changeText; if (this.state.passPhraseConfirm === this.state.passPhrase) { @@ -303,14 +305,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent { passPhraseMatch =
{ matchText }
- + { changeText }
; } - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
+ return

{ _t( "Enter your Security Phrase a second time to confirm it.", ) }

@@ -318,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
; } - _renderPhaseShowKey() { + private renderPhaseShowKey(): JSX.Element { return

{ _t( "Your Security Key is a safety net - you can use it to restore " + @@ -352,13 +353,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {

- { this._keyBackupInfo.recovery_key } + { this.keyBackupInfo.recovery_key }
- -
@@ -367,7 +368,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
; } - _renderPhaseKeepItSafe() { + private renderPhaseKeepItSafe(): JSX.Element { let introText; if (this.state.copied) { introText = _t( @@ -380,7 +381,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent { {}, { b: s => { s } }, ); } - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
{ introText }
    @@ -389,107 +389,101 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
  • { _t("Copy it to your personal cloud storage", {}, { b: s => { s } }) }
- +
; } - _renderBusyPhase(text) { - const Spinner = sdk.getComponent('views.elements.Spinner'); + private renderBusyPhase(): JSX.Element { return
; } - _renderPhaseDone() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + private renderPhaseDone(): JSX.Element { return

{ _t( "Your keys are being backed up (the first backup could take a few minutes).", ) }

; } - _renderPhaseOptOutConfirm() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + private renderPhaseOptOutConfirm(): JSX.Element { return
{ _t( "Without setting up Secure Message Recovery, you won't be able to restore your " + "encrypted message history if you log out or use another session.", ) } - +
; } - _titleForPhase(phase) { + private titleForPhase(phase: Phase): string { switch (phase) { - case PHASE_PASSPHRASE: + case Phase.Passphrase: return _t('Secure your backup with a Security Phrase'); - case PHASE_PASSPHRASE_CONFIRM: + case Phase.PassphraseConfirm: return _t('Confirm your Security Phrase'); - case PHASE_OPTOUT_CONFIRM: + case Phase.OptOutConfirm: return _t('Warning!'); - case PHASE_SHOWKEY: - case PHASE_KEEPITSAFE: + case Phase.ShowKey: + case Phase.KeepItSafe: return _t('Make a copy of your Security Key'); - case PHASE_BACKINGUP: + case Phase.BackingUp: return _t('Starting backup...'); - case PHASE_DONE: + case Phase.Done: return _t('Success!'); default: return _t("Create key backup"); } } - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - + public render(): JSX.Element { let content; if (this.state.error) { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); content =

{ _t("Unable to create key backup") }

; } else { switch (this.state.phase) { - case PHASE_PASSPHRASE: - content = this._renderPhasePassPhrase(); + case Phase.Passphrase: + content = this.renderPhasePassPhrase(); break; - case PHASE_PASSPHRASE_CONFIRM: - content = this._renderPhasePassPhraseConfirm(); + case Phase.PassphraseConfirm: + content = this.renderPhasePassPhraseConfirm(); break; - case PHASE_SHOWKEY: - content = this._renderPhaseShowKey(); + case Phase.ShowKey: + content = this.renderPhaseShowKey(); break; - case PHASE_KEEPITSAFE: - content = this._renderPhaseKeepItSafe(); + case Phase.KeepItSafe: + content = this.renderPhaseKeepItSafe(); break; - case PHASE_BACKINGUP: - content = this._renderBusyPhase(); + case Phase.BackingUp: + content = this.renderBusyPhase(); break; - case PHASE_DONE: - content = this._renderPhaseDone(); + case Phase.Done: + content = this.renderPhaseDone(); break; - case PHASE_OPTOUT_CONFIRM: - content = this._renderPhaseOptOutConfirm(); + case Phase.OptOutConfirm: + content = this.renderPhaseOptOutConfirm(); break; } } @@ -497,8 +491,8 @@ export default class CreateKeyBackupDialog extends React.PureComponent { return (
{ content } diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx similarity index 67% rename from src/async-components/views/dialogs/security/CreateSecretStorageDialog.js rename to src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 7a21b7075bb..145d3bcede5 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -16,8 +16,6 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../../index'; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import FileSaver from 'file-saver'; import { _t, _td } from '../../../../languageHandler'; @@ -31,52 +29,105 @@ import AccessibleButton from "../../../../components/views/elements/AccessibleBu import DialogButtons from "../../../../components/views/elements/DialogButtons"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; -import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; +import { + getSecureBackupSetupMethods, + isSecureBackupRequired, + SecureBackupSetupMethod, +} from '../../../../utils/WellKnownUtils'; import SecurityCustomisations from "../../../../customisations/Security"; import { logger } from "matrix-js-sdk/src/logger"; - -const PHASE_LOADING = 0; -const PHASE_LOADERROR = 1; -const PHASE_CHOOSE_KEY_PASSPHRASE = 2; -const PHASE_MIGRATE = 3; -const PHASE_PASSPHRASE = 4; -const PHASE_PASSPHRASE_CONFIRM = 5; -const PHASE_SHOWKEY = 6; -const PHASE_STORING = 8; -const PHASE_CONFIRM_SKIP = 10; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; +import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import Field from "../../../../components/views/elements/Field"; +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import Spinner from "../../../../components/views/elements/Spinner"; +import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; +import { CrossSigningKeys } from "matrix-js-sdk"; +import InteractiveAuthDialog from "../../../../components/views/dialogs/InteractiveAuthDialog"; +import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api"; +import { IValidationResult } from "../../../../components/views/elements/Validation"; + +// I made a mistake while converting this and it has to be fixed! +enum Phase { + Loading = "loading", + LoadError = "load_error", + ChooseKeyPassphrase = "choose_key_passphrase", + Migrate = "migrate", + Passphrase = "passphrase", + PassphraseConfirm = "passphrase_confirm", + ShowKey = "show_key", + Storing = "storing", + ConfirmSkip = "confirm_skip", +} const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -// these end up as strings from being values in the radio buttons, so just use strings -const CREATE_STORAGE_OPTION_KEY = 'key'; -const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase'; +interface IProps extends IDialogProps { + hasCancel: boolean; + accountPassword: string; + forceReset: boolean; +} + +interface IState { + phase: Phase; + passPhrase: string; + passPhraseValid: boolean; + passPhraseConfirm: string; + copied: boolean; + downloaded: boolean; + setPassphrase: boolean; + backupInfo: IKeyBackupInfo; + backupSigStatus: TrustInfo; + // does the server offer a UI auth flow with just m.login.password + // for /keys/device_signing/upload? + canUploadKeysWithPasswordOnly: boolean; + accountPassword: string; + accountPasswordCorrect: boolean; + canSkip: boolean; + passPhraseKeySelected: string; + error?: string; +} /* * Walks the user through the process of creating a passphrase to guard Secure * Secret Storage in account data. */ -export default class CreateSecretStorageDialog extends React.PureComponent { - static propTypes = { - hasCancel: PropTypes.bool, - accountPassword: PropTypes.string, - forceReset: PropTypes.bool, - }; - - static defaultProps = { +export default class CreateSecretStorageDialog extends React.PureComponent { + public static defaultProps: Partial = { hasCancel: true, forceReset: false, }; + private recoveryKey: IRecoveryKey; + private backupKey: Uint8Array; + private recoveryKeyNode = createRef(); + private passphraseField = createRef(); - constructor(props) { + constructor(props: IProps) { super(props); - this._recoveryKey = null; - this._recoveryKeyNode = null; - this._backupKey = null; + let passPhraseKeySelected; + const setupMethods = getSecureBackupSetupMethods(); + if (setupMethods.includes(SecureBackupSetupMethod.Key)) { + passPhraseKeySelected = SecureBackupSetupMethod.Key; + } else { + passPhraseKeySelected = SecureBackupSetupMethod.Passphrase; + } + + const accountPassword = props.accountPassword || ""; + let canUploadKeysWithPasswordOnly = null; + if (accountPassword) { + // If we have an account password in memory, let's simplify and + // assume it means password auth is also supported for device + // signing key upload as well. This avoids hitting the server to + // test auth flows, which may be slow under high load. + canUploadKeysWithPasswordOnly = true; + } else { + this.queryKeyUploadAuth(); + } this.state = { - phase: PHASE_LOADING, + phase: Phase.Loading, passPhrase: '', passPhraseValid: false, passPhraseConfirm: '', @@ -87,55 +138,37 @@ export default class CreateSecretStorageDialog extends React.PureComponent { backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password // for /keys/device_signing/upload? - canUploadKeysWithPasswordOnly: null, - accountPassword: props.accountPassword || "", accountPasswordCorrect: null, canSkip: !isSecureBackupRequired(), + canUploadKeysWithPasswordOnly, + passPhraseKeySelected, + accountPassword, }; - const setupMethods = getSecureBackupSetupMethods(); - if (setupMethods.includes("key")) { - this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_KEY; - } else { - this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_PASSPHRASE; - } - - this._passphraseField = createRef(); - - MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); - - if (this.state.accountPassword) { - // If we have an account password in memory, let's simplify and - // assume it means password auth is also supported for device - // signing key upload as well. This avoids hitting the server to - // test auth flows, which may be slow under high load. - this.state.canUploadKeysWithPasswordOnly = true; - } else { - this._queryKeyUploadAuth(); - } + MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatusChange); - this._getInitialPhase(); + this.getInitialPhase(); } - componentWillUnmount() { - MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); + public componentWillUnmount(): void { + MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatusChange); } - _getInitialPhase() { + private getInitialPhase(): void { const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.(); if (keyFromCustomisations) { logger.log("Created key via customisations, jumping to bootstrap step"); - this._recoveryKey = { + this.recoveryKey = { privateKey: keyFromCustomisations, }; - this._bootstrapSecretStorage(); + this.bootstrapSecretStorage(); return; } - this._fetchBackupInfo(); + this.fetchBackupInfo(); } - async _fetchBackupInfo() { + private async fetchBackupInfo(): Promise<{ backupInfo: IKeyBackupInfo, backupSigStatus: TrustInfo }> { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupSigStatus = ( @@ -144,7 +177,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ); const { forceReset } = this.props; - const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; + const phase = (backupInfo && !forceReset) ? Phase.Migrate : Phase.ChooseKeyPassphrase; this.setState({ phase, @@ -157,13 +190,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent { backupSigStatus, }; } catch (e) { - this.setState({ phase: PHASE_LOADERROR }); + this.setState({ phase: Phase.LoadError }); } } - async _queryKeyUploadAuth() { + private async queryKeyUploadAuth(): Promise { try { - await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); + await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys); // We should never get here: the server should always require // UI auth to upload device signing keys. If we do, we upload // no keys which would be a no-op. @@ -182,59 +215,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - _onKeyBackupStatusChange = () => { - if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); - } + private onKeyBackupStatusChange = (): void => { + if (this.state.phase === Phase.Migrate) this.fetchBackupInfo(); + }; - _onKeyPassphraseChange = e => { + private onKeyPassphraseChange = (e: React.ChangeEvent): void => { this.setState({ passPhraseKeySelected: e.target.value, }); - } - - _collectRecoveryKeyNode = (n) => { - this._recoveryKeyNode = n; - } + }; - _onChooseKeyPassphraseFormSubmit = async () => { - if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) { - this._recoveryKey = + private onChooseKeyPassphraseFormSubmit = async (): Promise => { + if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) { + this.recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); this.setState({ copied: false, downloaded: false, setPassphrase: false, - phase: PHASE_SHOWKEY, + phase: Phase.ShowKey, }); } else { this.setState({ copied: false, downloaded: false, - phase: PHASE_PASSPHRASE, + phase: Phase.Passphrase, }); } - } + }; - _onMigrateFormSubmit = (e) => { + private onMigrateFormSubmit = (e: React.FormEvent): void => { e.preventDefault(); if (this.state.backupSigStatus.usable) { - this._bootstrapSecretStorage(); + this.bootstrapSecretStorage(); } else { - this._restoreBackup(); + this.restoreBackup(); } - } + }; - _onCopyClick = () => { - const successful = copyNode(this._recoveryKeyNode); + private onCopyClick = (): void => { + const successful = copyNode(this.recoveryKeyNode.current); if (successful) { this.setState({ copied: true, }); } - } + }; - _onDownloadClick = () => { - const blob = new Blob([this._recoveryKey.encodedPrivateKey], { + private onDownloadClick = (): void => { + const blob = new Blob([this.recoveryKey.encodedPrivateKey], { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'security-key.txt'); @@ -242,9 +271,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ downloaded: true, }); - } + }; - _doBootstrapUIAuth = async (makeRequest) => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ type: 'm.login.password', @@ -258,8 +287,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { password: this.state.accountPassword, }); } else { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), @@ -292,11 +319,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent { throw new Error("Cross-signing key upload auth canceled"); } } - } + }; - _bootstrapSecretStorage = async () => { + private bootstrapSecretStorage = async (): Promise => { this.setState({ - phase: PHASE_STORING, + phase: Phase.Storing, error: null, }); @@ -308,7 +335,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (forceReset) { logger.log("Forcing secret storage reset"); await cli.bootstrapSecretStorage({ - createSecretStorageKey: async () => this._recoveryKey, + createSecretStorageKey: async () => this.recoveryKey, setupNewKeyBackup: true, setupNewSecretStorage: true, }); @@ -321,18 +348,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // keys (and also happen to skip all post-authentication flows at the // moment via token login) await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + authUploadDeviceSigningKeys: this.doBootstrapUIAuth, }); await cli.bootstrapSecretStorage({ - createSecretStorageKey: async () => this._recoveryKey, + createSecretStorageKey: async () => this.recoveryKey, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo, - getKeyBackupPassphrase: () => { + getKeyBackupPassphrase: async () => { // We may already have the backup key if we earlier went // through the restore backup path, so pass it along // rather than prompting again. - if (this._backupKey) { - return this._backupKey; + if (this.backupKey) { + return this.backupKey; } return promptForBackupPassphrase(); }, @@ -344,27 +371,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ accountPassword: '', accountPasswordCorrect: false, - phase: PHASE_MIGRATE, + phase: Phase.Migrate, }); } else { this.setState({ error: e }); } logger.error("Error bootstrapping secret storage", e); } - } + }; - _onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); - } - - _onDone = () => { - this.props.onFinished(true); - } + }; - _restoreBackup = async () => { + private restoreBackup = async (): Promise => { // It's possible we'll need the backup key later on for bootstrapping, // so let's stash it here, rather than prompting for it twice. - const keyCallback = k => this._backupKey = k; + const keyCallback = k => this.backupKey = k; const { finished } = Modal.createTrackedDialog( 'Restore Backup', '', RestoreKeyBackupDialog, @@ -376,103 +399,103 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ); await finished; - const { backupSigStatus } = await this._fetchBackupInfo(); + const { backupSigStatus } = await this.fetchBackupInfo(); if ( backupSigStatus.usable && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword ) { - this._bootstrapSecretStorage(); + this.bootstrapSecretStorage(); } - } + }; - _onLoadRetryClick = () => { - this.setState({ phase: PHASE_LOADING }); - this._fetchBackupInfo(); - } + private onLoadRetryClick = (): void => { + this.setState({ phase: Phase.Loading }); + this.fetchBackupInfo(); + }; - _onShowKeyContinueClick = () => { - this._bootstrapSecretStorage(); - } + private onShowKeyContinueClick = (): void => { + this.bootstrapSecretStorage(); + }; - _onCancelClick = () => { - this.setState({ phase: PHASE_CONFIRM_SKIP }); - } + private onCancelClick = (): void => { + this.setState({ phase: Phase.ConfirmSkip }); + }; - _onGoBackClick = () => { - this.setState({ phase: PHASE_CHOOSE_KEY_PASSPHRASE }); - } + private onGoBackClick = (): void => { + this.setState({ phase: Phase.ChooseKeyPassphrase }); + }; - _onPassPhraseNextClick = async (e) => { + private onPassPhraseNextClick = async (e: React.FormEvent) => { e.preventDefault(); - if (!this._passphraseField.current) return; // unmounting + if (!this.passphraseField.current) return; // unmounting - 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 }); + 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 }); + this.setState({ phase: Phase.PassphraseConfirm }); }; - _onPassPhraseConfirmNextClick = async (e) => { + private onPassPhraseConfirmNextClick = async (e: React.FormEvent) => { e.preventDefault(); if (this.state.passPhrase !== this.state.passPhraseConfirm) return; - this._recoveryKey = + this.recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); this.setState({ copied: false, downloaded: false, setPassphrase: true, - phase: PHASE_SHOWKEY, + phase: Phase.ShowKey, }); - } + }; - _onSetAgainClick = () => { + private onSetAgainClick = (): void => { this.setState({ passPhrase: '', passPhraseValid: false, passPhraseConfirm: '', - phase: PHASE_PASSPHRASE, + phase: Phase.Passphrase, }); - } + }; - _onPassPhraseValidate = (result) => { + private onPassPhraseValidate = (result: IValidationResult): void => { this.setState({ passPhraseValid: result.valid, }); }; - _onPassPhraseChange = (e) => { + private onPassPhraseChange = (e: React.ChangeEvent): void => { this.setState({ passPhrase: e.target.value, }); - } + }; - _onPassPhraseConfirmChange = (e) => { + private onPassPhraseConfirmChange = (e: React.ChangeEvent): void => { this.setState({ passPhraseConfirm: e.target.value, }); - } + }; - _onAccountPasswordChange = (e) => { + private onAccountPasswordChange = (e: React.ChangeEvent): void => { this.setState({ accountPassword: e.target.value, }); - } + }; - _renderOptionKey() { + private renderOptionKey(): JSX.Element { return (
@@ -484,14 +507,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ); } - _renderOptionPassphrase() { + private renderOptionPassphrase(): JSX.Element { return (
@@ -503,12 +526,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ); } - _renderPhaseChooseKeyPassphrase() { + private renderPhaseChooseKeyPassphrase(): JSX.Element { const setupMethods = getSecureBackupSetupMethods(); - const optionKey = setupMethods.includes("key") ? this._renderOptionKey() : null; - const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null; + const optionKey = setupMethods.includes(SecureBackupSetupMethod.Key) ? this.renderOptionKey() : null; + const optionPassphrase = setupMethods.includes(SecureBackupSetupMethod.Passphrase) + ? this.renderOptionPassphrase() + : null; - return
+ return

{ _t( "Safeguard against losing access to encrypted messages & data by " + "backing up encryption keys on your server.", @@ -519,20 +544,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

; } - _renderPhaseMigrate() { + private renderPhaseMigrate(): JSX.Element { // TODO: This is a temporary screen so people who have the labs flag turned on and // click the button are aware they're making a change to their account. // Once we're confident enough in this (and it's supported enough) we can do // it automatically. // https://github.com/vector-im/element-web/issues/11696 - const Field = sdk.getComponent('views.elements.Field'); let authPrompt; let nextCaption = _t("Next"); @@ -543,7 +567,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { type="password" label={_t("Password")} value={this.state.accountPassword} - onChange={this._onAccountPasswordChange} + onChange={this.onAccountPasswordChange} forceValidity={this.state.accountPasswordCorrect === false ? false : null} autoFocus={true} />
@@ -559,7 +583,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

; } - return
+ return

{ _t( "Upgrade this session to allow it to verify other sessions, " + "granting them access to encrypted messages and marking them " + @@ -568,19 +592,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

{ authPrompt }
-
; } - _renderPhasePassPhrase() { - return
+ private renderPhasePassPhrase(): JSX.Element { + return

{ _t( "Enter a security phrase only you know, as it’s used to safeguard your data. " + "To be secure, you shouldn’t re-use your account password.", @@ -589,11 +613,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

; } - _renderPhasePassPhraseConfirm() { - const Field = sdk.getComponent('views.elements.Field'); - + private renderPhasePassPhraseConfirm(): JSX.Element { let matchText; let changeText; if (this.state.passPhraseConfirm === this.state.passPhrase) { @@ -641,20 +663,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent { passPhraseMatch =
{ matchText }
- + { changeText }
; } - return
+ return

{ _t( "Enter your Security Phrase a second time to confirm it.", ) }

; } - _renderPhaseShowKey() { + private renderPhaseShowKey(): JSX.Element { let continueButton; - if (this.state.phase === PHASE_SHOWKEY) { + if (this.state.phase === Phase.ShowKey) { continueButton = ; } else { @@ -700,13 +722,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
- { this._recoveryKey.encodedPrivateKey } + { this.recoveryKey.encodedPrivateKey }
{ _t("Download") } @@ -714,8 +736,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { { this.state.copied ? _t("Copied!") : _t("Copy") } @@ -726,27 +748,26 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } - _renderBusyPhase() { - const Spinner = sdk.getComponent('views.elements.Spinner'); + private renderBusyPhase(): JSX.Element { return
; } - _renderPhaseLoadError() { + private renderPhaseLoadError(): JSX.Element { return

{ _t("Unable to query secret storage status") }

; } - _renderPhaseSkipConfirm() { + private renderPhaseSkipConfirm(): JSX.Element { return

{ _t( "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", @@ -755,98 +776,96 @@ export default class CreateSecretStorageDialog extends React.PureComponent { "You can also set up Secure Backup & manage your keys in Settings.", ) }

- +
; } - _titleForPhase(phase) { + private titleForPhase(phase: Phase): string { switch (phase) { - case PHASE_CHOOSE_KEY_PASSPHRASE: + case Phase.ChooseKeyPassphrase: return _t('Set up Secure Backup'); - case PHASE_MIGRATE: + case Phase.Migrate: return _t('Upgrade your encryption'); - case PHASE_PASSPHRASE: + case Phase.Passphrase: return _t('Set a Security Phrase'); - case PHASE_PASSPHRASE_CONFIRM: + case Phase.PassphraseConfirm: return _t('Confirm Security Phrase'); - case PHASE_CONFIRM_SKIP: + case Phase.ConfirmSkip: return _t('Are you sure?'); - case PHASE_SHOWKEY: + case Phase.ShowKey: return _t('Save your Security Key'); - case PHASE_STORING: + case Phase.Storing: return _t('Setting up keys'); default: return ''; } } - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - + public render(): JSX.Element { let content; if (this.state.error) { content =

{ _t("Unable to set up secret storage") }

; } else { switch (this.state.phase) { - case PHASE_LOADING: - content = this._renderBusyPhase(); + case Phase.Loading: + content = this.renderBusyPhase(); break; - case PHASE_LOADERROR: - content = this._renderPhaseLoadError(); + case Phase.LoadError: + content = this.renderPhaseLoadError(); break; - case PHASE_CHOOSE_KEY_PASSPHRASE: - content = this._renderPhaseChooseKeyPassphrase(); + case Phase.ChooseKeyPassphrase: + content = this.renderPhaseChooseKeyPassphrase(); break; - case PHASE_MIGRATE: - content = this._renderPhaseMigrate(); + case Phase.Migrate: + content = this.renderPhaseMigrate(); break; - case PHASE_PASSPHRASE: - content = this._renderPhasePassPhrase(); + case Phase.Passphrase: + content = this.renderPhasePassPhrase(); break; - case PHASE_PASSPHRASE_CONFIRM: - content = this._renderPhasePassPhraseConfirm(); + case Phase.PassphraseConfirm: + content = this.renderPhasePassPhraseConfirm(); break; - case PHASE_SHOWKEY: - content = this._renderPhaseShowKey(); + case Phase.ShowKey: + content = this.renderPhaseShowKey(); break; - case PHASE_STORING: - content = this._renderBusyPhase(); + case Phase.Storing: + content = this.renderBusyPhase(); break; - case PHASE_CONFIRM_SKIP: - content = this._renderPhaseSkipConfirm(); + case Phase.ConfirmSkip: + content = this.renderPhaseSkipConfirm(); break; } } let titleClass = null; switch (this.state.phase) { - case PHASE_PASSPHRASE: - case PHASE_PASSPHRASE_CONFIRM: + case Phase.Passphrase: + case Phase.PassphraseConfirm: titleClass = [ 'mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_securePhraseTitle', ]; break; - case PHASE_SHOWKEY: + case Phase.ShowKey: titleClass = [ 'mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_secureBackupTitle', ]; break; - case PHASE_CHOOSE_KEY_PASSPHRASE: + case Phase.ChooseKeyPassphrase: titleClass = 'mx_CreateSecretStorageDialog_centeredTitle'; break; } @@ -854,9 +873,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return (
diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx similarity index 79% rename from src/async-components/views/dialogs/security/ExportE2eKeysDialog.js rename to src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx index c21e17a7a12..2ba78da90e3 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx @@ -16,47 +16,51 @@ limitations under the License. import FileSaver from 'file-saver'; import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; -import * as sdk from '../../../../index'; - +import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import { logger } from "matrix-js-sdk/src/logger"; -const PHASE_EDIT = 1; -const PHASE_EXPORTING = 2; +enum Phase { + Edit = "edit", + Exporting = "exporting", +} -export default class ExportE2eKeysDialog extends React.Component { - static propTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, - onFinished: PropTypes.func.isRequired, - }; +interface IProps extends IDialogProps { + matrixClient: MatrixClient; +} - constructor(props) { - super(props); +interface IState { + phase: Phase; + errStr: string; +} - this._unmounted = false; +export default class ExportE2eKeysDialog extends React.Component { + private unmounted = false; + private passphrase1 = createRef(); + private passphrase2 = createRef(); - this._passphrase1 = createRef(); - this._passphrase2 = createRef(); + constructor(props: IProps) { + super(props); this.state = { - phase: PHASE_EDIT, + phase: Phase.Edit, errStr: null, }; } - componentWillUnmount() { - this._unmounted = true; + public componentWillUnmount(): void { + this.unmounted = true; } - _onPassphraseFormSubmit = (ev) => { + private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => { ev.preventDefault(); - const passphrase = this._passphrase1.current.value; - if (passphrase !== this._passphrase2.current.value) { + const passphrase = this.passphrase1.current.value; + if (passphrase !== this.passphrase2.current.value) { this.setState({ errStr: _t('Passphrases must match') }); return false; } @@ -65,11 +69,11 @@ export default class ExportE2eKeysDialog extends React.Component { return false; } - this._startExport(passphrase); + this.startExport(passphrase); return false; }; - _startExport(passphrase) { + private startExport(passphrase: string): void { // extra Promise.resolve() to turn synchronous exceptions into // asynchronous ones. Promise.resolve().then(() => { @@ -86,39 +90,37 @@ export default class ExportE2eKeysDialog extends React.Component { this.props.onFinished(true); }).catch((e) => { logger.error("Error exporting e2e keys:", e); - if (this._unmounted) { + if (this.unmounted) { return; } const msg = e.friendlyText || _t('Unknown error'); this.setState({ errStr: msg, - phase: PHASE_EDIT, + phase: Phase.Edit, }); }); this.setState({ errStr: null, - phase: PHASE_EXPORTING, + phase: Phase.Exporting, }); } - _onCancelClick = (ev) => { + private onCancelClick = (ev: React.MouseEvent): boolean => { ev.preventDefault(); this.props.onFinished(false); return false; }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - - const disableForm = (this.state.phase === PHASE_EXPORTING); + public render(): JSX.Element { + const disableForm = (this.state.phase === Phase.Exporting); return ( -
+

{ _t( @@ -151,10 +153,10 @@ export default class ExportE2eKeysDialog extends React.Component {

@@ -167,9 +169,9 @@ export default class ExportE2eKeysDialog extends React.Component {
- @@ -184,7 +186,7 @@ export default class ExportE2eKeysDialog extends React.Component { value={_t('Export')} disabled={disableForm} /> -
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx similarity index 74% rename from src/async-components/views/dialogs/security/ImportE2eKeysDialog.js rename to src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index 51d2861396f..fccc7308128 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -15,20 +15,19 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; -import * as sdk from '../../../../index'; import { _t } from '../../../../languageHandler'; - +import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import { logger } from "matrix-js-sdk/src/logger"; -function readFileAsArrayBuffer(file) { +function readFileAsArrayBuffer(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { - resolve(e.target.result); + resolve(e.target.result as ArrayBuffer); }; reader.onerror = reject; @@ -36,51 +35,57 @@ function readFileAsArrayBuffer(file) { }); } -const PHASE_EDIT = 1; -const PHASE_IMPORTING = 2; +enum Phase { + Edit = "edit", + Importing = "importing", +} -export default class ImportE2eKeysDialog extends React.Component { - static propTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, - onFinished: PropTypes.func.isRequired, - }; +interface IProps extends IDialogProps { + matrixClient: MatrixClient; +} - constructor(props) { - super(props); +interface IState { + enableSubmit: boolean; + phase: Phase; + errStr: string; +} - this._unmounted = false; +export default class ImportE2eKeysDialog extends React.Component { + private unmounted = false; + private file = createRef(); + private passphrase = createRef(); - this._file = createRef(); - this._passphrase = createRef(); + constructor(props: IProps) { + super(props); this.state = { enableSubmit: false, - phase: PHASE_EDIT, + phase: Phase.Edit, errStr: null, }; } - componentWillUnmount() { - this._unmounted = true; + public componentWillUnmount(): void { + this.unmounted = true; } - _onFormChange = (ev) => { - const files = this._file.current.files || []; + private onFormChange = (ev: React.FormEvent): void => { + const files = this.file.current.files || []; this.setState({ - enableSubmit: (this._passphrase.current.value !== "" && files.length > 0), + enableSubmit: (this.passphrase.current.value !== "" && files.length > 0), }); }; - _onFormSubmit = (ev) => { + private onFormSubmit = (ev: React.FormEvent): boolean => { ev.preventDefault(); - this._startImport(this._file.current.files[0], this._passphrase.current.value); + this.startImport(this.file.current.files[0], this.passphrase.current.value); return false; }; - _startImport(file, passphrase) { + private startImport(file: File, passphrase: string) { this.setState({ errStr: null, - phase: PHASE_IMPORTING, + phase: Phase.Importing, }); return readFileAsArrayBuffer(file).then((arrayBuffer) => { @@ -94,34 +99,32 @@ export default class ImportE2eKeysDialog extends React.Component { this.props.onFinished(true); }).catch((e) => { logger.error("Error importing e2e keys:", e); - if (this._unmounted) { + if (this.unmounted) { return; } const msg = e.friendlyText || _t('Unknown error'); this.setState({ errStr: msg, - phase: PHASE_EDIT, + phase: Phase.Edit, }); }); } - _onCancelClick = (ev) => { + private onCancelClick = (ev: React.MouseEvent): boolean => { ev.preventDefault(); this.props.onFinished(false); return false; }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - - const disableForm = (this.state.phase !== PHASE_EDIT); + public render(): JSX.Element { + const disableForm = (this.state.phase !== Phase.Edit); return ( - +

{ _t( @@ -149,11 +152,11 @@ export default class ImportE2eKeysDialog extends React.Component {

@@ -165,11 +168,11 @@ export default class ImportE2eKeysDialog extends React.Component {
@@ -182,7 +185,7 @@ export default class ImportE2eKeysDialog extends React.Component { value={_t('Import')} disabled={!this.state.enableSubmit || disableForm} /> -
diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx similarity index 84% rename from src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js rename to src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index 263d25c98c5..105d12f3d78 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -16,43 +16,40 @@ limitations under the License. */ import React from "react"; -import PropTypes from "prop-types"; -import * as sdk from "../../../../index"; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import { Action } from "../../../../dispatcher/actions"; +import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; -export default class NewRecoveryMethodDialog extends React.PureComponent { - static propTypes = { - // As returned by js-sdk getKeyBackupVersion() - newVersionInfo: PropTypes.object, - onFinished: PropTypes.func.isRequired, - } +interface IProps extends IDialogProps { + newVersionInfo: IKeyBackupInfo; +} - onOkClick = () => { +export default class NewRecoveryMethodDialog extends React.PureComponent { + private onOkClick = (): void => { this.props.onFinished(); - } + }; - onGoToSettingsClick = () => { + private onGoToSettingsClick = (): void => { this.props.onFinished(); dis.fire(Action.ViewUserSettings); - } + }; - onSetupClick = async () => { + private onSetupClick = async (): Promise => { Modal.createTrackedDialog( 'Restore Backup', '', RestoreKeyBackupDialog, { onFinished: this.props.onFinished, }, null, /* priority = */ false, /* static = */ true, ); - } - - render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); - const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + }; + public render(): JSX.Element { const title = { _t("New Recovery Method") } ; diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx similarity index 82% rename from src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js rename to src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index f586c9430a3..8ed6eb233e0 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -15,36 +15,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import PropTypes from "prop-types"; -import * as sdk from "../../../../index"; +import React, { ComponentType } from "react"; import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; import { Action } from "../../../../dispatcher/actions"; +import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; -export default class RecoveryMethodRemovedDialog extends React.PureComponent { - static propTypes = { - onFinished: PropTypes.func.isRequired, - } +interface IProps extends IDialogProps {} - onGoToSettingsClick = () => { +export default class RecoveryMethodRemovedDialog extends React.PureComponent { + private onGoToSettingsClick = (): void => { this.props.onFinished(); dis.fire(Action.ViewUserSettings); - } + }; - onSetupClick = () => { + private onSetupClick = (): void => { this.props.onFinished(); Modal.createTrackedDialogAsync("Key Backup", "Key Backup", - import("./CreateKeyBackupDialog"), + import("./CreateKeyBackupDialog") as unknown as Promise>, null, null, /* priority = */ false, /* static = */ true, ); - } - - render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); - const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + }; + public render(): JSX.Element { const title = { _t("Recovery Method Removed") } ; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0c68f2013cb..eb60506589c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { ComponentType, createRef } from 'react'; import { createClient } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; @@ -1601,12 +1601,16 @@ export default class MatrixChat extends React.PureComponent { if (haveNewVersion) { Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', - import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'), + import( + '../../async-components/views/dialogs/security/NewRecoveryMethodDialog' + ) as unknown as Promise>, { newVersionInfo }, ); } else { Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed', - import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'), + import( + '../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog' + ) as unknown as Promise>, ); } }); diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 5ac31412699..759952a0481 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import Modal from '../../../Modal'; import * as sdk from '../../../index'; @@ -85,7 +85,9 @@ export default class LogoutDialog extends React.Component { private onExportE2eKeysClicked = (): void => { Modal.createTrackedDialogAsync('Export E2E Keys', '', - import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), + import( + '../../../async-components/views/dialogs/security/ExportE2eKeysDialog' + ) as unknown as Promise>, { matrixClient: MatrixClientPeg.get(), }, @@ -111,7 +113,9 @@ export default class LogoutDialog extends React.Component { ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", - import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"), + import( + "../../../async-components/views/dialogs/security/CreateKeyBackupDialog" + ) as unknown as Promise>, null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/settings/ChangePassword.tsx b/src/components/views/settings/ChangePassword.tsx index 4bde294aa44..f009d1121ea 100644 --- a/src/components/views/settings/ChangePassword.tsx +++ b/src/components/views/settings/ChangePassword.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import Field from "../elements/Field"; -import React from 'react'; +import React, { ComponentType } from 'react'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import AccessibleButton from '../elements/AccessibleButton'; import Spinner from '../elements/Spinner'; @@ -186,7 +186,9 @@ export default class ChangePassword extends React.Component { private onExportE2eKeysClicked = (): void => { Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', - import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), + import( + '../../../async-components/views/dialogs/security/ExportE2eKeysDialog' + ) as unknown as Promise>, { matrixClient: MatrixClientPeg.get(), }, diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index 67a3e8aa75e..353645fd055 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; @@ -92,14 +92,18 @@ export default class CryptographyPanel extends React.Component { private onExportE2eKeysClicked = (): void => { Modal.createTrackedDialogAsync('Export E2E Keys', '', - import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), + import( + '../../../async-components/views/dialogs/security/ExportE2eKeysDialog' + ) as unknown as Promise>, { matrixClient: MatrixClientPeg.get() }, ); }; private onImportE2eKeysClicked = (): void => { Modal.createTrackedDialogAsync('Import E2E Keys', '', - import('../../../async-components/views/dialogs/security/ImportE2eKeysDialog'), + import( + '../../../async-components/views/dialogs/security/ImportE2eKeysDialog' + ) as unknown as Promise>, { matrixClient: MatrixClientPeg.get() }, ); }; diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index d69cf250a6b..d44a7a78b1a 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; @@ -170,7 +170,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { private startNewBackup = (): void => { Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', - import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'), + import( + '../../../async-components/views/dialogs/security/CreateKeyBackupDialog' + ) as unknown as Promise>, { onFinished: () => { this.loadBackupStatus();