1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-28 03:08:30 +00:00

Merge pull request #88 from satoshilabs/cleanup-passphrase-code

enhancement: rewrite passphrase modal
This commit is contained in:
Vladimir Volek 2018-09-27 14:10:28 +02:00 committed by GitHub
commit 2ff639fbdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 197 additions and 274 deletions

View File

@ -2,13 +2,13 @@ import React from 'react';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
import { TRANSITION } from 'config/variables'; import { TRANSITION, FONT_WEIGHT, FONT_SIZE } from 'config/variables';
const Wrapper = styled.button` const Wrapper = styled.button`
padding: ${props => (props.icon ? '4px 24px 4px 15px' : '11px 24px')}; padding: ${props => (props.icon ? '4px 24px 4px 15px' : '11px 24px')};
border-radius: 3px; border-radius: 3px;
font-size: 14px; font-size: ${FONT_SIZE.SMALL};
font-weight: 300; font-weight: ${FONT_WEIGHT.SMALLEST};
cursor: pointer; cursor: pointer;
background: ${colors.GREEN_PRIMARY}; background: ${colors.GREEN_PRIMARY};
color: ${colors.WHITE}; color: ${colors.WHITE};

View File

@ -26,18 +26,18 @@ const IconWrapper = styled.div`
border-radius: 2px; border-radius: 2px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: ${props => (props.checked ? colors.WHITE : colors.GREEN_PRIMARY)}; color: ${props => (props.isChecked ? colors.WHITE : colors.GREEN_PRIMARY)};
background: ${props => (props.checked ? colors.GREEN_PRIMARY : colors.WHITE)}; background: ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.WHITE)};
border: 1px solid ${props => (props.checked ? colors.GREEN_PRIMARY : colors.DIVIDER)}; border: 1px solid ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.DIVIDER)};
width: 24px; width: 24px;
height: 24px; height: 24px;
&:hover, &:hover,
&:focus { &:focus {
${props => !props.checked && css` ${props => !props.isChecked && css`
border: 1px solid ${colors.GREEN_PRIMARY}; border: 1px solid ${colors.GREEN_PRIMARY};
`} `}
background: ${props => (props.checked ? colors.GREEN_PRIMARY : colors.WHITE)}; background: ${props => (props.isChecked ? colors.GREEN_PRIMARY : colors.WHITE)};
} }
`; `;
@ -50,7 +50,7 @@ const Label = styled.div`
&:hover, &:hover,
&:focus { &:focus {
color: ${props => (props.checked ? colors.TEXT_PRIMARY : colors.TEXT_PRIMARY)}; color: ${props => (props.isChecked ? colors.TEXT_PRIMARY : colors.TEXT_PRIMARY)};
} }
`; `;
@ -63,7 +63,7 @@ class Checkbox extends PureComponent {
render() { render() {
const { const {
checked, isChecked,
children, children,
onClick, onClick,
} = this.props; } = this.props;
@ -73,20 +73,20 @@ class Checkbox extends PureComponent {
onKeyUp={e => this.handleKeyboard(e)} onKeyUp={e => this.handleKeyboard(e)}
tabIndex={0} tabIndex={0}
> >
<IconWrapper checked={checked}> <IconWrapper isChecked={isChecked}>
{checked && ( {isChecked && (
<Tick> <Tick>
<Icon <Icon
hoverColor={colors.WHITE} hoverColor={colors.WHITE}
size={26} size={26}
color={checked ? colors.WHITE : colors.GREEN_PRIMARY} color={isChecked ? colors.WHITE : colors.GREEN_PRIMARY}
icon={icons.SUCCESS} icon={icons.SUCCESS}
/> />
</Tick> </Tick>
) )
} }
</IconWrapper> </IconWrapper>
<Label checked={checked}>{children}</Label> <Label isChecked={isChecked}>{children}</Label>
</Wrapper> </Wrapper>
); );
} }
@ -94,7 +94,7 @@ class Checkbox extends PureComponent {
Checkbox.propTypes = { Checkbox.propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
checked: PropTypes.bool, isChecked: PropTypes.bool,
children: PropTypes.string, children: PropTypes.string,
}; };

View File

@ -117,14 +117,15 @@ class Input extends Component {
hasAddon={!!this.props.sideAddons} hasAddon={!!this.props.sideAddons}
type={this.props.type} type={this.props.type}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
autoComplete={this.props.autoComplete} autocomplete={this.props.autocomplete}
autoCorrect={this.props.autoCorrect} autocorrect={this.props.autocorrect}
autoCapitalize={this.props.autoCapitalize} autocapitalize={this.props.autocapitalize}
spellCheck={this.props.spellCheck} spellCheck={this.props.spellCheck}
value={this.props.value} value={this.props.value}
onChange={this.props.onChange} onChange={this.props.onChange}
borderColor={this.getColor(this.props.state)} borderColor={this.getColor(this.props.state)}
disabled={this.props.isDisabled} disabled={this.props.isDisabled}
name={this.props.name}
/> />
</InputIconWrapper> </InputIconWrapper>
{this.props.sideAddons && this.props.sideAddons.map(sideAddon => sideAddon)} {this.props.sideAddons && this.props.sideAddons.map(sideAddon => sideAddon)}
@ -146,9 +147,9 @@ Input.propTypes = {
innerRef: PropTypes.func, innerRef: PropTypes.func,
placeholder: PropTypes.string, placeholder: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
autoComplete: PropTypes.string, autocomplete: PropTypes.string,
autoCorrect: PropTypes.string, autocorrect: PropTypes.string,
autoCapitalize: PropTypes.string, autocapitalize: PropTypes.string,
spellCheck: PropTypes.string, spellCheck: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
@ -157,6 +158,7 @@ Input.propTypes = {
topLabel: PropTypes.node, topLabel: PropTypes.node,
sideAddons: PropTypes.arrayOf(PropTypes.node), sideAddons: PropTypes.arrayOf(PropTypes.node),
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
name: PropTypes.string,
}; };
Input.defaultProps = { Input.defaultProps = {

View File

@ -1,18 +1,25 @@
/* @flow */ /* @flow */
import React, { Component } from 'react'; import React, { Component } from 'react';
import raf from 'raf'; import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE, TRANSITION } from 'config/variables';
import { H2 } from 'components/Heading'; import { H2 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import { FONT_SIZE } from 'config/variables';
import Link from 'components/Link';
import Checkbox from 'components/Checkbox'; import Checkbox from 'components/Checkbox';
import Button from 'components/Button'; import Button from 'components/Button';
import Input from 'components/inputs/Input'; import Input from 'components/inputs/Input';
import styled from 'styled-components';
import type { Props } from '../../index'; import type { Props } from '../../index';
type State = {
deviceLabel: string,
shouldShowSingleInput: boolean,
passphraseInputValue: string,
passphraseCheckInputValue: string,
doPassphraseInputsMatch: boolean,
isPassphraseHidden: boolean,
};
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 24px 48px; padding: 24px 48px;
max-width: 390px; max-width: 390px;
@ -45,315 +52,229 @@ const Footer = styled.div`
justify-content: center; justify-content: center;
`; `;
type State = { const LinkButton = styled(Button)`
deviceLabel: string; padding: 0;
singleInput: boolean; margin: 0;
passphrase: string; text-decoration: none;
passphraseRevision: string; cursor: pointer;
passphraseFocused: boolean; transition: ${TRANSITION.HOVER};
passphraseRevisionFocused: boolean; font-size: ${FONT_SIZE.SMALLER};
passphraseRevisionTouched: boolean; border-radius: 0;
match: boolean; border-bottom: 1px solid ${colors.GREEN_PRIMARY};
visible: boolean; background: transparent;
}
export default class PinModal extends Component<Props, State> { &,
&:visited,
&:active,
&:hover {
color: ${colors.GREEN_PRIMARY};
}
&:hover {
border-color: transparent;
background: transparent;
}
`;
class Passphrase extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const device = props.modal.opened ? props.modal.device : null; const device = props.modal.opened ? props.modal.device : null;
if (!device) return; if (!device) {
return;
}
// check if this device is already known // Check if this device is already known
const selected = props.wallet.selectedDevice; // if device is already known then only one input is presented
const { selectedDevice } = props.wallet;
let deviceLabel = device.label; let deviceLabel = device.label;
let singleInput = false; let shouldShowSingleInput = false;
if (selected && selected.path === device.path) { if (selectedDevice && selectedDevice.path === device.path) {
deviceLabel = selected.instanceLabel; deviceLabel = selectedDevice.instanceLabel;
singleInput = selected.remember || selected.state !== null; shouldShowSingleInput = selectedDevice.remember || selectedDevice.state !== null;
} }
this.state = { this.state = {
deviceLabel, deviceLabel,
singleInput, shouldShowSingleInput,
passphrase: '', passphraseInputValue: '',
passphraseRevision: '', passphraseCheckInputValue: '',
passphraseFocused: false, doPassphraseInputsMatch: true,
passphraseRevisionFocused: false, isPassphraseHidden: true,
passphraseRevisionTouched: false,
match: true,
visible: false,
}; };
} }
keyboardHandler: (event: KeyboardEvent) => void;
state: State; state: State;
passphraseInput: ?HTMLInputElement; componentDidMount() {
this.passphraseInput.focus();
passphraseRevisionInput: ?HTMLInputElement; this.handleKeyPress = this.handleKeyPress.bind(this);
window.addEventListener('keypress', this.handleKeyPress, false);
keyboardHandler(event: KeyboardEvent): void {
if (event.keyCode === 13) {
event.preventDefault();
//this.passphraseInput.blur();
//this.passphraseRevisionInput.blur();
//this.passphraseInput.type = 'text';
//this.passphraseRevisionInput.type = 'text';
this.submit();
// TODO: set timeout, or wait for blur event
//onPassphraseSubmit(passphrase, passphraseCached);
//raf(() => onPassphraseSubmit(passphrase));
}
} }
componentWillUnmount() {
componentDidMount(): void { window.removeEventListener('keypress', this.handleKeyPress, false);
// one time autofocus
if (this.passphraseInput) this.passphraseInput.focus();
this.keyboardHandler = this.keyboardHandler.bind(this);
window.addEventListener('keydown', this.keyboardHandler, false);
// document.oncontextmenu = (event) => {
// const el = window.event.srcElement || event.target;
// const type = el.tagName.toLowerCase() || '';
// if (type === 'input') {
// return false;
// }
// };
} }
// we don't want to keep password inside "value" attribute, handleKeyPress: (event: KeyboardEvent) => void;
// so we need to replace it thru javascript
componentDidUpdate() {
const {
passphrase,
passphraseRevision,
passphraseFocused,
passphraseRevisionFocused,
visible,
} = this.state;
// } = this.props.modal;
const passphraseInputValue: string = passphrase; passphraseInput: HTMLInputElement;
const passphraseRevisionInputValue: string = passphraseRevision;
// if (!visible && !passphraseFocused) {
// passphraseInputValue = passphrase.replace(/./g, '•');
// }
// if (!visible && !passphraseRevisionFocused) {
// passphraseRevisionInputValue = passphraseRevision.replace(/./g, '•');
// }
handleInputChange(event: Event) {
const { target } = event;
if (target instanceof HTMLInputElement) {
const inputValue = target.value;
const inputName = target.name;
if (this.passphraseInput) { let doPassphraseInputsMatch = false;
// this.passphraseInput.value = passphraseInputValue; if (inputName === 'passphraseInputValue') {
// this.passphraseInput.setAttribute('type', visible || (!visible && !passphraseFocused) ? 'text' : 'password'); // If passphrase is not hidden the second input should get filled automatically
this.passphraseInput.setAttribute('type', visible ? 'text' : 'password'); // and should be disabled
} if (this.state.isPassphraseHidden) {
if (this.passphraseRevisionInput) { doPassphraseInputsMatch = inputValue === this.state.passphraseCheckInputValue;
// this.passphraseRevisionInput.value = passphraseRevisionInputValue; } else {
// this.passphraseRevisionInput.setAttribute('type', visible || (!visible && !passphraseRevisionFocused) ? 'text' : 'password'); // Since both inputs are same they really do match
this.passphraseRevisionInput.setAttribute('type', visible ? 'text' : 'password'); // see: comment above
} this.setState({
} passphraseCheckInputValue: inputValue,
});
componentWillUnmount(): void { doPassphraseInputsMatch = true;
window.removeEventListener('keydown', this.keyboardHandler, false); }
// this.passphraseInput.type = 'text'; } else if (inputName === 'passphraseCheckInputValue') {
// this.passphraseInput.style.display = 'none'; doPassphraseInputsMatch = inputValue === this.state.passphraseInputValue;
// this.passphraseRevisionInput.type = 'text';
// this.passphraseRevisionInput.style.display = 'none';
}
onPassphraseChange = (input: string, value: string): void => {
// https://codepen.io/MiDri/pen/PGqvrO
// or
// https://github.com/zakangelle/react-password-mask/blob/master/src/index.js
if (input === 'passphrase') {
this.setState(previousState => ({
match: previousState.singleInput || previousState.passphraseRevision === value,
passphrase: value,
}));
if (this.state.visible && this.passphraseRevisionInput) {
this.setState({
match: true,
passphraseRevision: value,
});
this.passphraseRevisionInput.value = value;
} }
if (this.state.shouldShowSingleInput) {
doPassphraseInputsMatch = true;
}
this.setState({
[inputName]: inputValue,
doPassphraseInputsMatch,
});
}
}
handleCheckboxClick() {
// If passphrase was visible and now shouldn't be --> delete the value of passphraseCheckInputValue
// doPassphraseInputsMatch
// - if passphrase was visible and now shouldn't be --> doPassphraseInputsMatch = false
// - because passphraseCheckInputValue will be empty string
// - if passphrase wasn't visibe and now should be --> doPassphraseInputsMatch = true
// - because passphraseCheckInputValue will be same as passphraseInputValue
let doInputsMatch = false;
if (this.state.shouldShowSingleInput || this.state.passphraseInputValue === this.state.passphraseCheckInputValue) {
doInputsMatch = true;
} else { } else {
this.setState(previousState => ({ doInputsMatch = !!this.state.isPassphraseHidden;
match: previousState.passphrase === value,
passphraseRevision: value,
passphraseRevisionTouched: true,
}));
} }
this.setState(previousState => ({
isPassphraseHidden: !previousState.isPassphraseHidden,
passphraseInputValue: previousState.isPassphraseHidden ? previousState.passphraseInputValue : '',
passphraseCheckInputValue: previousState.isPassphraseHidden ? previousState.passphraseInputValue : '',
doPassphraseInputsMatch: doInputsMatch,
}));
} }
onPassphraseFocus = (input: string): void => { submitPassphrase(shouldLeavePassphraseBlank: boolean = false) {
if (input === 'passphrase') {
this.setState({
passphraseFocused: true,
});
} else {
this.setState({
passphraseRevisionFocused: true,
});
}
}
onPassphraseBlur = (input: string): void => {
if (input === 'passphrase') {
this.setState({
passphraseFocused: false,
});
} else {
this.setState({
passphraseRevisionFocused: false,
});
}
}
onPassphraseShow = (): void => {
this.setState({
visible: true,
});
if (this.passphraseRevisionInput) {
this.passphraseRevisionInput.disabled = true;
this.passphraseRevisionInput.value = this.state.passphrase;
this.setState(previousState => ({
passphraseRevision: previousState.passphrase,
match: true,
}));
}
}
onPassphraseHide = (): void => {
this.setState({
visible: false,
});
if (this.passphraseRevisionInput) {
const emptyPassphraseRevisionValue = '';
this.passphraseRevisionInput.value = emptyPassphraseRevisionValue;
this.setState(previousState => ({
passphraseRevision: emptyPassphraseRevisionValue,
match: emptyPassphraseRevisionValue === previousState.passphrase,
}));
this.passphraseRevisionInput.disabled = false;
}
}
submit = (empty: boolean = false): void => {
const { onPassphraseSubmit } = this.props.modalActions; const { onPassphraseSubmit } = this.props.modalActions;
const { passphrase, match } = this.state; const passphrase = this.state.passphraseInputValue;
if (!match) return;
//this.passphraseInput.type = 'text';
// this.passphraseInput.style.display = 'none';
//this.passphraseInput.setAttribute('readonly', 'readonly');
// this.passphraseRevisionInput.type = 'text';
//this.passphraseRevisionInput.style.display = 'none';
//this.passphraseRevisionInput.setAttribute('readonly', 'readonly');
// const p = passphrase;
// Reset state so same passphrase isn't filled when the modal will be visible again
this.setState({ this.setState({
passphrase: '', passphraseInputValue: '',
passphraseRevision: '', passphraseCheckInputValue: '',
passphraseFocused: false, doPassphraseInputsMatch: true,
passphraseRevisionFocused: false, isPassphraseHidden: true,
visible: false,
}); });
raf(() => onPassphraseSubmit(empty ? '' : passphrase)); onPassphraseSubmit(shouldLeavePassphraseBlank ? '' : passphrase);
}
handleKeyPress(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
console.warn('ENTER', this.state);
if (this.state.doPassphraseInputsMatch) {
this.submitPassphrase();
}
}
} }
render() { render() {
if (!this.props.modal.opened) return null; if (!this.props.modal.opened) {
return null;
}
const {
device,
} = this.props.modal;
const {
deviceLabel,
singleInput,
passphrase,
passphraseRevision,
passphraseFocused,
passphraseRevisionFocused,
visible,
match,
passphraseRevisionTouched,
} = this.state;
let passphraseInputType: string = visible || (!visible && !passphraseFocused) ? 'text' : 'password';
let passphraseRevisionInputType: string = visible || (!visible && !passphraseRevisionFocused) ? 'text' : 'password';
passphraseInputType = passphraseRevisionInputType = 'text';
//let passphraseInputType: string = visible || passphraseFocused ? "text" : "password";
//let passphraseRevisionInputType: string = visible || passphraseRevisionFocused ? "text" : "password";
const showPassphraseCheckboxFn: Function = visible ? this.onPassphraseHide : this.onPassphraseShow;
return ( return (
<Wrapper> <Wrapper>
<H2>Enter { deviceLabel } passphrase</H2> <H2>Enter {this.state.deviceLabel} passphrase</H2>
<P isSmaller>Note that passphrase is case-sensitive.</P> <P isSmaller>Note that passphrase is case-sensitive.</P>
<Row> <Row>
<Label>Passphrase</Label> <Label>Passphrase</Label>
<Input <Input
innerRef={(element) => { this.passphraseInput = element; }} innerRef={(input) => { this.passphraseInput = input; }}
onChange={event => this.onPassphraseChange('passphrase', event.currentTarget.value)} name="passphraseInputValue"
type={passphraseInputType} type={this.state.isPassphraseHidden ? 'password' : 'text'}
autoComplete="off" autocorrect="off"
autoCorrect="off" autocapitalize="off"
autoCapitalize="off" autocomplete="off"
spellCheck="false" value={this.state.passphraseInputValue}
data-lpignore="true" onChange={event => this.handleInputChange(event)}
onFocus={() => this.onPassphraseFocus('passphrase')}
onBlur={() => this.onPassphraseBlur('passphrase')}
tabIndex="0"
/> />
</Row> </Row>
{!singleInput && ( {!this.state.shouldShowSingleInput && (
<Row> <Row>
<Label>Re-enter passphrase</Label> <Label>Re-enter passphrase</Label>
<Input <Input
innerRef={(element) => { this.passphraseRevisionInput = element; }} name="passphraseCheckInputValue"
onChange={event => this.onPassphraseChange('revision', event.currentTarget.value)} type={this.state.isPassphraseHidden ? 'password' : 'text'}
type={passphraseRevisionInputType} autocorrect="off"
autoComplete="off" autocapitalize="off"
autoCorrect="off" autocomplete="off"
autoCapitalize="off" value={this.state.passphraseCheckInputValue}
spellCheck="false" onChange={event => this.handleInputChange(event)}
data-lpignore="true" isDisabled={!this.state.isPassphraseHidden}
onFocus={() => this.onPassphraseFocus('revision')}
onBlur={() => this.onPassphraseBlur('revision')}
/> />
{!match && passphraseRevisionTouched && <PassphraseError>Passphrases do not match</PassphraseError> }
</Row> </Row>
) } )}
{!this.state.doPassphraseInputsMatch && (
<PassphraseError>Passphrases do not match</PassphraseError>
)}
<Row> <Row>
<Checkbox onClick={showPassphraseCheckboxFn} checked={visible}>Show passphrase</Checkbox> <Checkbox
isChecked={!this.state.isPassphraseHidden}
onClick={() => this.handleCheckboxClick()}
>
Show passphrase
</Checkbox>
</Row> </Row>
<Row> <Row>
<Button type="button" disabled={!match} onClick={() => this.submit()}>Enter</Button> <Button
isDisabled={!this.state.doPassphraseInputsMatch}
onClick={() => this.submitPassphrase()}
>Enter
</Button>
</Row> </Row>
<Footer> <Footer>
<P isSmaller>If you want to access your default account</P> <P isSmaller>If you want to access your default account</P>
<P isSmaller> <P isSmaller>
<Link isGreen onClick={() => this.submit(true)}>Leave passphrase blank</Link> <LinkButton
isGreen
onClick={() => this.submitPassphrase(true)}
>Leave passphrase blank
</LinkButton>
</P> </P>
</Footer> </Footer>
</Wrapper> </Wrapper>
); );
} }
} }
export default Passphrase;