Port recovery key upload button to new designs
This commit is contained in:
parent
f4460ca78f
commit
15ebaa1470
3 changed files with 198 additions and 33 deletions
|
@ -38,11 +38,56 @@ limitations under the License.
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessSecretStorageDialog_passPhraseInput,
|
.mx_AccessSecretStorageDialog_passPhraseInput {
|
||||||
.mx_AccessSecretStorageDialog_recoveryKeyInput {
|
|
||||||
width: 300px;
|
width: 300px;
|
||||||
border: 1px solid $accent-color;
|
border: 1px solid $accent-color;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessSecretStorageDialog_recoveryKeyEntry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessSecretStorageDialog_recoveryKeyFeedback {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid {
|
||||||
|
color: $input-valid-border-color;
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/check.svg');
|
||||||
|
background-color: $input-valid-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid {
|
||||||
|
color: $input-invalid-border-color;
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/x.svg');
|
||||||
|
background-color: $input-invalid-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -15,13 +15,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
|
import Field from '../../elements/Field';
|
||||||
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
|
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||||
|
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// maybe that's a thing people would do?
|
||||||
|
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;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Access Secure Secret Storage by requesting the user's passphrase.
|
* Access Secure Secret Storage by requesting the user's passphrase.
|
||||||
*/
|
*/
|
||||||
|
@ -35,9 +48,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this._fileUpload = React.createRef();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
recoveryKey: "",
|
recoveryKey: "",
|
||||||
recoveryKeyValid: false,
|
recoveryKeyValid: null,
|
||||||
|
recoveryKeyCorrect: null,
|
||||||
|
recoveryKeyFileError: null,
|
||||||
forceRecoveryKey: false,
|
forceRecoveryKey: false,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
keyMatches: null,
|
keyMatches: null,
|
||||||
|
@ -54,12 +72,89 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_validateRecoveryKeyOnChange = debounce(() => {
|
||||||
|
this._validateRecoveryKey();
|
||||||
|
}, VALIDATION_THROTTLE_MS);
|
||||||
|
|
||||||
|
async _validateRecoveryKey() {
|
||||||
|
if (this.state.recoveryKey === '') {
|
||||||
|
this.setState({
|
||||||
|
recoveryKeyValid: null,
|
||||||
|
recoveryKeyCorrect: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedKey = decodeRecoveryKey(this.state.recoveryKey);
|
||||||
|
const correct = await MatrixClientPeg.get().checkSecretStorageKey(
|
||||||
|
decodedKey, this.props.keyInfo,
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
recoveryKeyValid: true,
|
||||||
|
recoveryKeyCorrect: correct,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({
|
||||||
|
recoveryKeyValid: false,
|
||||||
|
recoveryKeyCorrect: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onRecoveryKeyChange = (e) => {
|
_onRecoveryKeyChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
recoveryKey: e.target.value,
|
recoveryKey: e.target.value,
|
||||||
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
|
recoveryKeyFileError: null,
|
||||||
keyMatches: 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)
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRecoveryKeyFileChange = async e => {
|
||||||
|
if (e.target.files.length === 0) return;
|
||||||
|
|
||||||
|
const f = e.target.files[0];
|
||||||
|
|
||||||
|
if (f.size > KEY_FILE_MAX_SIZE) {
|
||||||
|
this.setState({
|
||||||
|
recoveryKeyFileError: true,
|
||||||
|
recoveryKeyCorrect: false,
|
||||||
|
recoveryKeyValid: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const contents = await f.text();
|
||||||
|
// test it's within the base58 alphabet. We could be more strict here, eg. require the
|
||||||
|
// right number of characters, but it's really just to make sure that what we're reading is
|
||||||
|
// text because we'll put it in the text field.
|
||||||
|
if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) {
|
||||||
|
this.setState({
|
||||||
|
recoveryKeyFileError: null,
|
||||||
|
recoveryKey: contents.trim(),
|
||||||
|
});
|
||||||
|
this._validateRecoveryKey();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
recoveryKeyFileError: true,
|
||||||
|
recoveryKeyCorrect: false,
|
||||||
|
recoveryKeyValid: false,
|
||||||
|
recoveryKey: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRecoveryKeyFileUploadClick = () => {
|
||||||
|
this._fileUpload.current.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseNext = async (e) => {
|
_onPassPhraseNext = async (e) => {
|
||||||
|
@ -99,6 +194,20 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getKeyValidationText() {
|
||||||
|
if (this.state.recoveryKeyFileError) {
|
||||||
|
return _t("Wrong file type");
|
||||||
|
} else if (this.state.recoveryKeyCorrect) {
|
||||||
|
return _t("Looks good!");
|
||||||
|
} else if (this.state.recoveryKeyValid) {
|
||||||
|
return _t("Wrong Recovery Key");
|
||||||
|
} else if (this.state.recoveryKeyValid === null) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return _t("Invalid Recovery Key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
|
@ -169,36 +278,43 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
|
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
let keyStatus;
|
const feedbackClasses = classNames({
|
||||||
if (this.state.recoveryKey.length === 0) {
|
'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
|
'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true,
|
||||||
} else if (this.state.keyMatches === false) {
|
'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
});
|
||||||
{"\uD83D\uDC4E "}{_t(
|
const recoveryKeyFeedback = <div className={feedbackClasses}>
|
||||||
"Unable to access secret storage. " +
|
{this.getKeyValidationText()}
|
||||||
"Please verify that you entered the correct recovery key.",
|
|
||||||
)}
|
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.recoveryKeyValid) {
|
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
|
||||||
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
|
||||||
</div>;
|
|
||||||
} else {
|
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
|
||||||
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
content = <div>
|
content = <div>
|
||||||
<p>{_t("Use your Security Key to continue.")}</p>
|
<p>{_t("Use your Security Key to continue.")}</p>
|
||||||
|
|
||||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext}>
|
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext}>
|
||||||
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
|
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
|
||||||
onChange={this._onRecoveryKeyChange}
|
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
label={_t('Recovery Key')}
|
||||||
value={this.state.recoveryKey}
|
value={this.state.recoveryKey}
|
||||||
autoFocus={true}
|
onChange={this._onRecoveryKeyChange}
|
||||||
/>
|
/>
|
||||||
{keyStatus}
|
</div>
|
||||||
|
<span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">
|
||||||
|
{_t("or")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<input type="file"
|
||||||
|
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
|
||||||
|
ref={this._fileUpload}
|
||||||
|
onChange={this._onRecoveryKeyFileChange}
|
||||||
|
/>
|
||||||
|
<AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}>
|
||||||
|
{_t("Upload")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{recoveryKeyFeedback}
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t('Continue')}
|
primaryButton={_t('Continue')}
|
||||||
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
||||||
|
|
|
@ -1787,14 +1787,16 @@
|
||||||
"Remember my selection for this widget": "Remember my selection for this widget",
|
"Remember my selection for this widget": "Remember my selection for this widget",
|
||||||
"Allow": "Allow",
|
"Allow": "Allow",
|
||||||
"Deny": "Deny",
|
"Deny": "Deny",
|
||||||
|
"Wrong file type": "Wrong file type",
|
||||||
|
"Looks good!": "Looks good!",
|
||||||
|
"Wrong Recovery Key": "Wrong Recovery Key",
|
||||||
|
"Invalid Recovery Key": "Invalid Recovery Key",
|
||||||
"Security Phrase": "Security Phrase",
|
"Security Phrase": "Security Phrase",
|
||||||
"Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.",
|
"Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.",
|
||||||
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Enter your Security Phrase or <button>Use your Security Key</button> to continue.",
|
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Enter your Security Phrase or <button>Use your Security Key</button> to continue.",
|
||||||
"Security Key": "Security Key",
|
"Security Key": "Security Key",
|
||||||
"Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.",
|
|
||||||
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
|
|
||||||
"Not a valid recovery key": "Not a valid recovery key",
|
|
||||||
"Use your Security Key to continue.": "Use your Security Key to continue.",
|
"Use your Security Key to continue.": "Use your Security Key to continue.",
|
||||||
|
"Recovery Key": "Recovery Key",
|
||||||
"Restoring keys from backup": "Restoring keys from backup",
|
"Restoring keys from backup": "Restoring keys from backup",
|
||||||
"Fetching keys from server...": "Fetching keys from server...",
|
"Fetching keys from server...": "Fetching keys from server...",
|
||||||
"%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored",
|
"%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored",
|
||||||
|
@ -1813,6 +1815,8 @@
|
||||||
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
|
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
|
||||||
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
|
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
|
||||||
"Enter recovery key": "Enter recovery key",
|
"Enter recovery key": "Enter recovery key",
|
||||||
|
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
|
||||||
|
"Not a valid recovery key": "Not a valid recovery key",
|
||||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
|
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
|
||||||
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
|
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
|
||||||
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
|
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue