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

Merge branch 'sign/verify'

This commit is contained in:
Szymon Lesisz 2018-12-05 09:20:39 +01:00
commit 2cdd32c503
25 changed files with 775 additions and 212 deletions

View File

@ -25,7 +25,7 @@
], ],
"env": { "env": {
"test": { "test": {
"presets": ["jest"] "presets": ["jest"]
} }
} }
} }

View File

@ -59,6 +59,7 @@
"react-router-redux": "next", "react-router-redux": "next",
"react-scale-text": "^1.2.2", "react-scale-text": "^1.2.2",
"react-select": "2.0.0", "react-select": "2.0.0",
"react-textarea-autosize": "^7.0.4",
"react-transition-group": "^2.4.0", "react-transition-group": "^2.4.0",
"redbox-react": "^1.6.0", "redbox-react": "^1.6.0",
"redux": "4.0.0", "redux": "4.0.0",

View File

@ -0,0 +1,165 @@
/* @flow */
import TrezorConnect from 'trezor-connect';
import type {
GetState, Dispatch, ThunkAction, AsyncAction,
} from 'flowtype';
import { validateAddress } from 'utils/ethUtils';
import * as NOTIFICATION from 'actions/constants/notification';
import * as SIGN_VERIFY from './constants/signVerify';
export type SignVerifyAction = {
type: typeof SIGN_VERIFY.SIGN_SUCCESS,
signSignature: string
} | {
type: typeof SIGN_VERIFY.CLEAR_SIGN,
} | {
type: typeof SIGN_VERIFY.CLEAR_VERIFY,
} | {
type: typeof SIGN_VERIFY.INPUT_CHANGE,
inputName: string,
value: string
} | {
type: typeof SIGN_VERIFY.TOUCH,
inputName: string,
} | {
type: typeof SIGN_VERIFY.ERROR,
inputName: string,
message: ?string
} | {
type: typeof SIGN_VERIFY.ERROR,
inputName: string,
message: ?string
}
const sign = (
path: Array<number>,
message: string,
hex: boolean = false,
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected = getState().wallet.selectedDevice;
if (!selected) return;
const response = await TrezorConnect.ethereumSignMessage({
device: {
path: selected.path,
instance: selected.instance,
state: selected.state,
},
path,
hex,
message,
useEmptyPassphrase: selected.useEmptyPassphrase,
});
if (response && response.success) {
dispatch({
type: SIGN_VERIFY.SIGN_SUCCESS,
signSignature: response.payload.signature,
});
} else {
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'error',
title: 'Sign error',
message: response.payload.error,
cancelable: true,
},
});
}
};
const verify = (
address: string,
message: string,
signature: string,
hex: boolean = false,
): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const selected = getState().wallet.selectedDevice;
if (!selected) return;
const hasError = validateAddress(address);
if (hasError) {
dispatch({
type: SIGN_VERIFY.ERROR,
inputName: 'verifyAddress',
message: validateAddress(address),
});
}
if (!hasError) {
const response = await TrezorConnect.ethereumVerifyMessage({
device: {
path: selected.path,
instance: selected.instance,
state: selected.state,
},
address,
message,
signature,
hex,
useEmptyPassphrase: selected.useEmptyPassphrase,
});
if (response && response.success) {
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'success',
title: 'Verify success',
message: 'signature is valid',
cancelable: true,
},
});
} else {
dispatch({
type: NOTIFICATION.ADD,
payload: {
type: 'error',
title: 'Verify error',
message: response.payload.error,
cancelable: true,
},
});
}
}
};
const inputChange = (inputName: string, value: string): ThunkAction => (dispatch: Dispatch): void => {
dispatch({
type: SIGN_VERIFY.INPUT_CHANGE,
inputName,
value,
});
dispatch({
type: SIGN_VERIFY.TOUCH,
inputName,
});
if (inputName === 'verifyAddress' && validateAddress(value) !== null) {
dispatch({
type: SIGN_VERIFY.ERROR,
inputName,
message: validateAddress(value),
});
}
};
const clearSign = (): ThunkAction => (dispatch: Dispatch): void => {
dispatch({
type: SIGN_VERIFY.CLEAR_SIGN,
});
};
const clearVerify = (): ThunkAction => (dispatch: Dispatch): void => {
dispatch({
type: SIGN_VERIFY.CLEAR_VERIFY,
});
};
export default {
sign,
verify,
clearSign,
clearVerify,
inputChange,
};

View File

@ -0,0 +1,7 @@
/* @flow */
export const SIGN_SUCCESS: 'sign__verify__sign__success' = 'sign__verify__sign__success';
export const INPUT_CHANGE: 'sign__verify__input__change' = 'sign__verify__input__change';
export const TOUCH: 'sign__verify__input__touch' = 'sign__verify__input__touch';
export const CLEAR_SIGN: 'sign__verify__sign__clear' = 'sign__verify__sign__clear';
export const CLEAR_VERIFY: 'sign__verify__verify__clear' = 'sign__verify__verify__clear';
export const ERROR: 'sign__verify__error' = 'sign__verify__error';

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import Textarea from 'react-textarea-autosize';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE, FONT_WEIGHT, FONT_FAMILY } from 'config/variables'; import { FONT_SIZE, FONT_WEIGHT, FONT_FAMILY } from 'config/variables';
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
@ -13,13 +15,12 @@ const Wrapper = styled.div`
const disabledColor = colors.TEXT_PRIMARY; const disabledColor = colors.TEXT_PRIMARY;
const StyledTextarea = styled.textarea` const StyledTextarea = styled(Textarea)`
width: 100%; width: 100%;
min-height: 85px; min-height: 85px;
margin-bottom: 10px; padding: 10px 12px;
padding: 6px 12px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid ${props => (props.borderColor ? props.borderColor : colors.DIVIDER)}; border: 1px solid ${props => (props.colorBorder ? props.colorBorder : colors.DIVIDER)};
border-radius: 2px; border-radius: 2px;
resize: none; resize: none;
outline: none; outline: none;
@ -27,7 +28,7 @@ const StyledTextarea = styled.textarea`
color: ${colors.TEXT_PRIMARY}; color: ${colors.TEXT_PRIMARY};
background: ${colors.WHITE}; background: ${colors.WHITE};
font-weight: ${FONT_WEIGHT.BASE}; font-weight: ${FONT_WEIGHT.BASE};
font-size: ${FONT_SIZE.BASE}; font-size: ${FONT_SIZE.SMALL};
white-space: pre-wrap; /* css-3 */ white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */ white-space: -pre-wrap; /* Opera 4-6 */
@ -81,10 +82,19 @@ const StyledTextarea = styled.textarea`
opacity: 1; opacity: 1;
} }
} }
${props => props.trezorAction && css`
z-index: 10001; /* bigger than modal container */
border-color: ${colors.WHITE};
border-width: 2px;
transform: translate(-1px, -1px);
background: ${colors.DIVIDER};
pointer-events: none;
`}
`; `;
const TopLabel = styled.span` const TopLabel = styled.span`
padding-bottom: 4px; padding-bottom: 8px;
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
`; `;
@ -105,7 +115,34 @@ const getColor = (inputState) => {
return color; return color;
}; };
const Textarea = ({ const TrezorAction = styled.div`
display: ${props => (props.action ? 'flex' : 'none')};
align-items: center;
margin: 0px 10px;
padding: 0 14px 0 5px;
position: absolute;
background: black;
bottom: -25px;
color: ${colors.WHITE};
border-radius: 5px;
line-height: 37px;
z-index: 10002;
transform: translate(-1px, -1px);
`;
const ArrowUp = styled.div`
position: absolute;
top: -9px;
left: 12px;
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-bottom: 9px solid black;
z-index: 10001;
`;
const TextArea = ({
className, className,
placeholder = '', placeholder = '',
value, value,
@ -113,28 +150,40 @@ const Textarea = ({
onFocus, onFocus,
onBlur, onBlur,
isDisabled, isDisabled,
name,
onChange, onChange,
topLabel, topLabel,
rows,
maxRows,
autoSelect,
state = '', state = '',
bottomText = '', bottomText = '',
trezorAction = null,
}) => ( }) => (
<Wrapper <Wrapper className={className}>
className={className}
>
{topLabel && ( {topLabel && (
<TopLabel>{topLabel}</TopLabel> <TopLabel>{topLabel}</TopLabel>
)} )}
<StyledTextarea <StyledTextarea
spellCheck="false"
autoCorrect="off"
autoCapitalize="off"
maxRows={maxRows}
rows={rows}
className={className} className={className}
disabled={isDisabled} disabled={isDisabled}
name={name}
style={customStyle} style={customStyle}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
value={value} value={value}
onClick={autoSelect ? event => event.target.select() : null}
placeholder={placeholder} placeholder={placeholder}
onChange={onChange} onChange={onChange}
borderColor={getColor(state)}
/> />
<TrezorAction action={trezorAction}>
<ArrowUp />{trezorAction}
</TrezorAction>
{bottomText && ( {bottomText && (
<BottomText <BottomText
color={getColor(state)} color={getColor(state)}
@ -145,7 +194,7 @@ const Textarea = ({
</Wrapper> </Wrapper>
); );
Textarea.propTypes = { TextArea.propTypes = {
className: PropTypes.string, className: PropTypes.string,
onFocus: PropTypes.func, onFocus: PropTypes.func,
onBlur: PropTypes.func, onBlur: PropTypes.func,
@ -153,10 +202,15 @@ Textarea.propTypes = {
customStyle: PropTypes.string, customStyle: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
maxRows: PropTypes.number,
rows: PropTypes.number,
name: PropTypes.string,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
topLabel: PropTypes.node, topLabel: PropTypes.node,
state: PropTypes.string, state: PropTypes.string,
autoSelect: PropTypes.bool,
bottomText: PropTypes.string, bottomText: PropTypes.string,
trezorAction: PropTypes.node,
}; };
export default Textarea; export default TextArea;

View File

@ -29,19 +29,19 @@ const InputIconWrapper = styled.div`
`; `;
const TopLabel = styled.span` const TopLabel = styled.span`
padding-bottom: 10px; padding-bottom: 8px;
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
`; `;
const StyledInput = styled.input` const StyledInput = styled.input`
width: 100%; width: 100%;
height: 40px; height: ${props => (props.height ? `${props.height}px` : '40px')};
padding: 5px ${props => (props.hasIcon ? '40px' : '12px')} 6px 12px; padding: 5px ${props => (props.hasIcon ? '40px' : '12px')} 6px 12px;
line-height: 1.42857143; line-height: 1.42857143;
font-size: ${FONT_SIZE.SMALL}; font-size: ${props => (props.isSmallText ? `${FONT_SIZE.SMALLER}` : `${FONT_SIZE.SMALL}`)};
font-weight: ${FONT_WEIGHT.BASE}; font-weight: ${FONT_WEIGHT.BASE};
color: ${props => (props.color ? props.color : colors.TEXT_SECONDARY)}; color: ${props => (props.color ? props.color : colors.TEXT)};
border-radius: 2px; border-radius: 2px;
@ -55,6 +55,7 @@ const StyledInput = styled.input`
background-color: ${colors.WHITE}; background-color: ${colors.WHITE};
transition: ${TRANSITION.HOVER}; transition: ${TRANSITION.HOVER};
&:disabled { &:disabled {
pointer-events: none; pointer-events: none;
background: ${colors.GRAY_LIGHT}; background: ${colors.GRAY_LIGHT};
@ -111,7 +112,7 @@ const TrezorAction = styled.div`
color: ${colors.WHITE}; color: ${colors.WHITE};
border-radius: 5px; border-radius: 5px;
line-height: 37px; line-height: 37px;
z-index: 10001; z-index: 10002;
transform: translate(-1px, -1px); transform: translate(-1px, -1px);
`; `;
@ -171,6 +172,8 @@ class Input extends PureComponent {
<Overlay isPartiallyHidden={this.props.isPartiallyHidden} /> <Overlay isPartiallyHidden={this.props.isPartiallyHidden} />
{this.props.icon} {this.props.icon}
<StyledInput <StyledInput
autoComplete="off"
height={this.props.height}
trezorAction={this.props.trezorAction} trezorAction={this.props.trezorAction}
hasIcon={this.getIcon(this.props.state).length > 0} hasIcon={this.getIcon(this.props.state).length > 0}
innerRef={this.props.innerRef} innerRef={this.props.innerRef}
@ -178,10 +181,10 @@ class Input extends PureComponent {
type={this.props.type} type={this.props.type}
color={this.getColor(this.props.state)} color={this.getColor(this.props.state)}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
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}
isSmallText={this.props.isSmallText}
value={this.props.value} value={this.props.value}
readOnly={this.props.readOnly} readOnly={this.props.readOnly}
onChange={this.props.onChange} onChange={this.props.onChange}
@ -214,7 +217,7 @@ Input.propTypes = {
innerRef: PropTypes.func, innerRef: PropTypes.func,
placeholder: PropTypes.string, placeholder: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
autocomplete: PropTypes.string, height: PropTypes.number,
autocorrect: PropTypes.string, autocorrect: PropTypes.string,
autocapitalize: PropTypes.string, autocapitalize: PropTypes.string,
icon: PropTypes.node, icon: PropTypes.node,
@ -230,12 +233,14 @@ Input.propTypes = {
sideAddons: PropTypes.arrayOf(PropTypes.node), sideAddons: PropTypes.arrayOf(PropTypes.node),
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
name: PropTypes.string, name: PropTypes.string,
isSmallText: PropTypes.bool,
isPartiallyHidden: PropTypes.bool, isPartiallyHidden: PropTypes.bool,
}; };
Input.defaultProps = { Input.defaultProps = {
type: 'text', type: 'text',
autoSelect: false, autoSelect: false,
height: 40,
}; };
export default Input; export default Input;

View File

@ -0,0 +1,24 @@
/* @flow */
import React from 'react';
import styled from 'styled-components';
import { H3 } from 'components/Heading';
import ICONS from 'config/icons';
import Icon from 'components/Icon';
const Wrapper = styled.div``;
const Header = styled.div`
padding: 48px;
`;
const ConfirmAction = () => (
<Wrapper>
<Header>
<Icon icon={ICONS.T1} size={100} />
<H3>Confirm action on your Trezor</H3>
</Header>
</Wrapper>
);
export default ConfirmAction;

View File

@ -7,7 +7,7 @@ import styled from 'styled-components';
import colors from 'config/colors'; import colors from 'config/colors';
import { FONT_SIZE } from 'config/variables'; import { FONT_SIZE } from 'config/variables';
import H3 from 'components/Heading'; import { H3 } from 'components/Heading';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import type { Props } from '../../Container'; import type { Props } from '../../Container';

View File

@ -17,6 +17,7 @@ import InvalidPin from 'components/modals/pin/Invalid';
import Passphrase from 'components/modals/passphrase/Passphrase'; import Passphrase from 'components/modals/passphrase/Passphrase';
import PassphraseType from 'components/modals/passphrase/Type'; import PassphraseType from 'components/modals/passphrase/Type';
import ConfirmSignTx from 'components/modals/confirm/SignTx'; import ConfirmSignTx from 'components/modals/confirm/SignTx';
import ConfirmAction from 'components/modals/confirm/Action';
import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress'; import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress';
import ForgetDevice from 'components/modals/device/Forget'; import ForgetDevice from 'components/modals/device/Forget';
import RememberDevice from 'components/modals/device/Remember'; import RememberDevice from 'components/modals/device/Remember';
@ -90,6 +91,12 @@ const getDeviceContextModal = (props: Props) => {
case 'ButtonRequest_SignTx': case 'ButtonRequest_SignTx':
return <ConfirmSignTx device={modal.device} sendForm={props.sendForm} />; return <ConfirmSignTx device={modal.device} sendForm={props.sendForm} />;
case 'ButtonRequest_ProtectCall':
return <ConfirmAction />;
case 'ButtonRequest_Other':
return <ConfirmAction />;
case RECEIVE.REQUEST_UNVERIFIED: case RECEIVE.REQUEST_UNVERIFIED:
return ( return (
<ConfirmUnverifiedAddress <ConfirmUnverifiedAddress

View File

@ -3,6 +3,7 @@ export default {
BACKGROUND: '#EBEBEB', BACKGROUND: '#EBEBEB',
TEXT: '#333333', TEXT: '#333333',
WALLET_VIEW_TITLE: '#505050',
HEADER: '#1A1A1A', HEADER: '#1A1A1A',
BODY: '#E3E3E3', BODY: '#E3E3E3',

View File

@ -3,8 +3,8 @@ export const FONT_SIZE = {
SMALLER: '12px', SMALLER: '12px',
SMALL: '14px', SMALL: '14px',
BASE: '16px', BASE: '16px',
WALLET_TITLE: '18px',
TOP_MENU: '17px', TOP_MENU: '17px',
WALLET_TITLE: '18px',
BIG: '21px', BIG: '21px',
BIGGER: '32px', BIGGER: '32px',
BIGGEST: '36px', BIGGEST: '36px',

View File

@ -25,6 +25,7 @@ import type { ModalAction } from 'actions/ModalActions';
import type { NotificationAction } from 'actions/NotificationActions'; import type { NotificationAction } from 'actions/NotificationActions';
import type { PendingTxAction } from 'actions/PendingTxActions'; import type { PendingTxAction } from 'actions/PendingTxActions';
import type { ReceiveAction } from 'actions/ReceiveActions'; import type { ReceiveAction } from 'actions/ReceiveActions';
import type { SignVerifyAction } from 'actions/SignVerifyActions';
import type { SendFormAction } from 'actions/SendFormActions'; import type { SendFormAction } from 'actions/SendFormActions';
import type { SummaryAction } from 'actions/SummaryActions'; import type { SummaryAction } from 'actions/SummaryActions';
import type { TokenAction } from 'actions/TokenActions'; import type { TokenAction } from 'actions/TokenActions';
@ -135,6 +136,7 @@ export type Action =
| DiscoveryAction | DiscoveryAction
| StorageAction | StorageAction
| LogAction | LogAction
| SignVerifyAction
| ModalAction | ModalAction
| NotificationAction | NotificationAction
| PendingTxAction | PendingTxAction

View File

@ -6,7 +6,6 @@ import * as RECEIVE from 'actions/constants/receive';
import * as ACCOUNT from 'actions/constants/account'; import * as ACCOUNT from 'actions/constants/account';
import type { Action } from 'flowtype'; import type { Action } from 'flowtype';
export type State = { export type State = {
addressVerified: boolean; addressVerified: boolean;
addressUnverified: boolean; addressUnverified: boolean;

View File

@ -0,0 +1,95 @@
/* @flow */
import type { Action } from 'flowtype';
import * as ACCOUNT from 'actions/constants/account';
import * as ACTION from 'actions/constants/signVerify';
export type Error = {
inputName: string,
message: ?string,
};
export type State = {
signAddress: string,
signMessage: string,
signSignature: string,
verifyAddress: string,
verifyMessage: string,
verifySignature: string,
touched: Array<string>,
errors: Array<Error>
}
export const initialState: State = {
signAddress: '',
signMessage: '',
signSignature: '',
verifyAddress: '',
verifyMessage: '',
verifySignature: '',
touched: [],
errors: [],
};
export default (state: State = initialState, action: Action): State => {
switch (action.type) {
case ACTION.SIGN_SUCCESS:
return {
...state,
signSignature: action.signSignature,
};
case ACTION.ERROR: {
const { inputName } = action;
if (!state.errors.some(e => e.inputName === inputName)) {
const error = { inputName, message: action.message };
return {
...state,
errors: [...state.errors, error],
};
}
return state;
}
case ACTION.TOUCH: {
const { inputName } = action;
if (!state.touched.includes(inputName)) {
return {
...state,
touched: [...state.touched, action.inputName],
};
}
return {
...state,
errors: state.errors.filter(error => error.inputName !== inputName),
};
}
case ACTION.INPUT_CHANGE: {
const change = { [action.inputName]: action.value };
return { ...state, ...change };
}
case ACCOUNT.DISPOSE:
return initialState;
case ACTION.CLEAR_SIGN:
return {
...state,
signAddress: '',
signMessage: '',
signSignature: '',
};
case ACTION.CLEAR_VERIFY:
return {
...state,
verifyAddress: '',
verifyMessage: '',
verifySignature: '',
};
default:
return state;
}
};

View File

@ -20,6 +20,7 @@ import fiat from 'reducers/FiatRateReducer';
import wallet from 'reducers/WalletReducer'; import wallet from 'reducers/WalletReducer';
import devices from 'reducers/DevicesReducer'; import devices from 'reducers/DevicesReducer';
import blockchain from 'reducers/BlockchainReducer'; import blockchain from 'reducers/BlockchainReducer';
import signVerify from 'reducers/SignVerifyReducer';
const reducers = { const reducers = {
router: routerReducer, router: routerReducer,
@ -41,6 +42,7 @@ const reducers = {
wallet, wallet,
devices, devices,
blockchain, blockchain,
signVerify,
}; };
export type Reducers = typeof reducers; export type Reducers = typeof reducers;

View File

@ -58,7 +58,7 @@ const Content = ({
); );
Content.propTypes = { Content.propTypes = {
children: PropTypes.element, children: PropTypes.oneOfType([PropTypes.element, PropTypes.array]),
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
title: PropTypes.string, title: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import colors from 'config/colors';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
const Wrapper = styled.div`
font-size: ${FONT_SIZE.WALLET_TITLE};
font-weight: ${FONT_WEIGHT.BASE};
color: ${colors.WALLET_TITLE};
padding-bottom: 35px;
`;
const Title = ({
children,
}) => (
<Wrapper>
{children}
</Wrapper>
);
Title.propTypes = {
children: PropTypes.string,
};
export default Title;

View File

@ -31,6 +31,7 @@ const StyledNavLink = styled(NavLink)`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
margin: 0px 4px; margin: 0px 4px;
padding: 20px; padding: 20px;
white-space: nowrap;
&.active, &.active,
&:hover { &:hover {
@ -64,7 +65,7 @@ class TopNavigationAccount extends React.PureComponent<Props> {
<StyledNavLink exact to={`${basePath}`}>Summary</StyledNavLink> <StyledNavLink exact to={`${basePath}`}>Summary</StyledNavLink>
<StyledNavLink to={`${basePath}/receive`}>Receive</StyledNavLink> <StyledNavLink to={`${basePath}/receive`}>Receive</StyledNavLink>
<StyledNavLink to={`${basePath}/send`}>Send</StyledNavLink> <StyledNavLink to={`${basePath}/send`}>Send</StyledNavLink>
{/* <StyledNavLink to={`${basePath}/signverify`}>Sign & Verify</StyledNavLink> */} <StyledNavLink to={`${basePath}/signverify`}>Sign &amp; Verify</StyledNavLink>
<Indicator pathname={pathname} wrapper={() => this.wrapper} /> <Indicator pathname={pathname} wrapper={() => this.wrapper} />
</Wrapper> </Wrapper>
); );

View File

@ -8,7 +8,7 @@ import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype'; import type { State, Dispatch } from 'flowtype';
import AccountSend from './index'; import AccountSend from './index';
type OwnProps = { } type OwnProps = {}
export type StateProps = { export type StateProps = {
selectedAccount: $ElementType<State, 'selectedAccount'>, selectedAccount: $ElementType<State, 'selectedAccount'>,

View File

@ -10,8 +10,8 @@ import Link from 'components/Link';
import ICONS from 'config/icons'; import ICONS from 'config/icons';
import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables'; import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables';
import colors from 'config/colors'; import colors from 'config/colors';
import Title from 'views/Wallet/components/Title';
import P from 'components/Paragraph'; import P from 'components/Paragraph';
import { H2 } from 'components/Heading';
import Content from 'views/Wallet/components/Content'; import Content from 'views/Wallet/components/Content';
import type { Token } from 'flowtype'; import type { Token } from 'flowtype';
import AdvancedForm from './components/AdvancedForm'; import AdvancedForm from './components/AdvancedForm';
@ -34,7 +34,7 @@ const AmountInputLabel = styled.span`
`; `;
const InputRow = styled.div` const InputRow = styled.div`
margin: 20px 0; padding: 0 0 15px 0;
`; `;
const SetMaxAmountButton = styled(Button)` const SetMaxAmountButton = styled(Button)`
@ -250,155 +250,152 @@ const AccountSend = (props: Props) => {
return ( return (
<Content> <Content>
<React.Fragment> <Title>Send Ethereum or tokens</Title>
<H2>Send Ethereum or tokens</H2> <InputRow>
<InputRow> <Input
<Input state={getAddressInputState(address, errors.address, warnings.address)}
state={getAddressInputState(address, errors.address, warnings.address)} autoComplete="off"
autoComplete="off" autoCorrect="off"
autoCorrect="off" autoCapitalize="off"
autoCapitalize="off" spellCheck="false"
spellCheck="false" topLabel="Address"
topLabel="Address" bottomText={errors.address || warnings.address || infos.address}
bottomText={errors.address || warnings.address || infos.address} value={address}
value={address} onChange={event => onAddressChange(event.target.value)}
onChange={event => onAddressChange(event.target.value)} />
/> </InputRow>
</InputRow> <InputRow>
<Input
<InputRow> state={getAmountInputState(errors.amount, warnings.amount)}
<Input autoComplete="off"
state={getAmountInputState(errors.amount, warnings.amount)} autoCorrect="off"
autoComplete="off" autoCapitalize="off"
autoCorrect="off" spellCheck="false"
autoCapitalize="off" topLabel={(
spellCheck="false" <AmountInputLabelWrapper>
topLabel={( <AmountInputLabel>Amount</AmountInputLabel>
<AmountInputLabelWrapper> {(isCurrentCurrencyToken && selectedToken) && (
<AmountInputLabel>Amount</AmountInputLabel> <AmountInputLabel>You have: {selectedTokenBalance} {selectedToken.symbol}</AmountInputLabel>
{(isCurrentCurrencyToken && selectedToken) && ( )}
<AmountInputLabel>You have: {selectedTokenBalance} {selectedToken.symbol}</AmountInputLabel> </AmountInputLabelWrapper>
)}
</AmountInputLabelWrapper>
)}
value={amount}
onChange={event => onAmountChange(event.target.value)}
bottomText={errors.amount || warnings.amount || infos.amount}
sideAddons={[
(
<SetMaxAmountButton
key="icon"
onClick={() => onSetMax()}
isActive={setMax}
>
{!setMax && (
<Icon
icon={ICONS.TOP}
size={25}
color={colors.TEXT_SECONDARY}
/>
)}
{setMax && (
<Icon
icon={ICONS.CHECKED}
size={25}
color={colors.WHITE}
/>
)}
Set max
</SetMaxAmountButton>
),
(
<CurrencySelect
key="currency"
isSearchable={false}
isClearable={false}
value={tokensSelectValue}
isDisabled={tokensSelectData.length < 2}
onChange={onCurrencyChange}
options={tokensSelectData}
/>
),
]}
/>
</InputRow>
<InputRow>
<FeeLabelWrapper>
<FeeLabel>Fee</FeeLabel>
{gasPriceNeedsUpdate && (
<UpdateFeeWrapper>
<Icon
icon={ICONS.WARNING}
color={colors.WARNING_PRIMARY}
size={20}
/>
Recommended fees updated. <StyledLink onClick={updateFeeLevels} isGreen>Click here to use them</StyledLink>
</UpdateFeeWrapper>
)}
</FeeLabelWrapper>
<Select
isSearchable={false}
isClearable={false}
value={selectedFeeLevel}
onChange={onFeeLevelChange}
options={feeLevels}
formatOptionLabel={option => (
<FeeOptionWrapper>
<P>{option.value}</P>
<P>{option.label}</P>
</FeeOptionWrapper>
)}
/>
</InputRow>
<ToggleAdvancedSettingsWrapper
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
>
<ToggleAdvancedSettingsButton
isTransparent
onClick={toggleAdvanced}
>
Advanced settings
<AdvancedSettingsIcon
icon={ICONS.ARROW_DOWN}
color={colors.TEXT_SECONDARY}
size={24}
isActive={advanced}
canAnimate
/>
</ToggleAdvancedSettingsButton>
{isAdvancedSettingsHidden && (
<SendButton
isDisabled={isSendButtonDisabled}
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
)} )}
</ToggleAdvancedSettingsWrapper> value={amount}
onChange={event => onAmountChange(event.target.value)}
bottomText={errors.amount || warnings.amount || infos.amount}
sideAddons={[
(
<SetMaxAmountButton
key="icon"
onClick={() => onSetMax()}
isActive={setMax}
>
{!setMax && (
<Icon
icon={ICONS.TOP}
size={25}
color={colors.TEXT_SECONDARY}
/>
)}
{setMax && (
<Icon
icon={ICONS.CHECKED}
size={25}
color={colors.WHITE}
/>
)}
Set max
</SetMaxAmountButton>
),
(
<CurrencySelect
key="currency"
isSearchable={false}
isClearable={false}
value={tokensSelectValue}
isDisabled={tokensSelectData.length < 2}
onChange={onCurrencyChange}
options={tokensSelectData}
/>
),
]}
/>
</InputRow>
{advanced && ( <InputRow>
<AdvancedForm {...props}> <FeeLabelWrapper>
<SendButton <FeeLabel>Fee</FeeLabel>
isDisabled={isSendButtonDisabled} {gasPriceNeedsUpdate && (
onClick={() => onSend()} <UpdateFeeWrapper>
> <Icon
{sendButtonText} icon={ICONS.WARNING}
</SendButton> color={colors.WARNING_PRIMARY}
</AdvancedForm> size={20}
)} />
Recommended fees updated. <StyledLink onClick={updateFeeLevels} isGreen>Click here to use them</StyledLink>
</UpdateFeeWrapper>
)}
</FeeLabelWrapper>
<Select
isSearchable={false}
isClearable={false}
value={selectedFeeLevel}
onChange={onFeeLevelChange}
options={feeLevels}
formatOptionLabel={option => (
<FeeOptionWrapper>
<P>{option.value}</P>
<P>{option.label}</P>
</FeeOptionWrapper>
)}
/>
</InputRow>
{props.selectedAccount.pending.length > 0 && ( <ToggleAdvancedSettingsWrapper
<PendingTransactions isAdvancedSettingsHidden={isAdvancedSettingsHidden}
pending={props.selectedAccount.pending} >
tokens={props.selectedAccount.tokens} <ToggleAdvancedSettingsButton
network={network} isTransparent
onClick={toggleAdvanced}
>
Advanced settings
<AdvancedSettingsIcon
icon={ICONS.ARROW_DOWN}
color={colors.TEXT_SECONDARY}
size={24}
isActive={advanced}
canAnimate
/> />
</ToggleAdvancedSettingsButton>
{isAdvancedSettingsHidden && (
<SendButton
isDisabled={isSendButtonDisabled}
isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
)} )}
</React.Fragment> </ToggleAdvancedSettingsWrapper>
{advanced && (
<AdvancedForm {...props}>
<SendButton
isDisabled={isSendButtonDisabled}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
</AdvancedForm>
)}
{props.selectedAccount.pending.length > 0 && (
<PendingTransactions
pending={props.selectedAccount.pending}
tokens={props.selectedAccount.tokens}
network={network}
/>
)}
</Content> </Content>
); );
}; };

View File

@ -0,0 +1,40 @@
/* @flow */
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import SignVerifyActions from 'actions/SignVerifyActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype';
import Component from './index';
type OwnProps = {}
export type Error = {
inputName: string,
message: ?string,
};
export type StateProps = {
wallet: $ElementType<State, 'wallet'>,
selectedAccount: $ElementType<State, 'selectedAccount'>,
signVerify: $ElementType<State, 'signVerify'>,
}
export type DispatchProps = {
signVerifyActions: typeof SignVerifyActions,
}
export type Props = StateProps & DispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
wallet: state.wallet,
selectedAccount: state.selectedAccount,
signVerify: state.signVerify,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
signVerifyActions: bindActionCreators(SignVerifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Component);

View File

@ -1,21 +1,36 @@
import React from 'react'; /* @flow */
import React, { Component } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import Input from 'components/inputs/Input'; import Input from 'components/inputs/Input';
import Textarea from 'components/Textarea'; import Textarea from 'components/Textarea';
import Title from 'views/Wallet/components/Title';
import Button from 'components/Button';
import Content from 'views/Wallet/components/Content'; import Content from 'views/Wallet/components/Content';
import { H2 } from 'components/Heading';
import colors from 'config/colors'; import colors from 'config/colors';
import type { Props } from './Container';
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
margin-top: -5px;
flex-direction: row; flex-direction: row;
background: ${colors.WHITE}; background: ${colors.WHITE};
`; `;
const StyledH2 = styled(H2)` const Row = styled.div`
padding-bottom: 10px; padding: 0 0 25px 0;
`;
const RowButtons = styled(Row)`
display: flex;
align-items: center;
justify-content: flex-end;
`;
const StyledButton = styled(Button)`
margin-left: 10px;
width: 110px;
`; `;
const Column = styled.div` const Column = styled.div`
@ -30,34 +45,149 @@ const Verify = styled(Column)`
padding-left: 20px; padding-left: 20px;
`; `;
const Label = styled.div` class SignVerify extends Component <Props> {
color: ${colors.LABEL}; getError(inputName: string) {
padding: 5px 0px; if (!this.props.signVerify) return null;
`; return this.props.signVerify.errors.find(e => e.inputName === inputName);
}
const AccountSignVerify = () => ( handleInputChange = (event: SyntheticInputEvent<Text>) => {
<Content> this.props.signVerifyActions.inputChange(event.target.name, event.target.value);
<Wrapper> }
<Sign>
<StyledH2>Sign message</StyledH2>
<Label>Message</Label>
<Textarea rows="4" maxLength="255" />
<Label>Address</Label>
<Input type="text" />
<Label>Signature</Label>
<Textarea rows="4" maxLength="255" readOnly="readonly" />
</Sign>
<Verify>
<StyledH2>Verify message</StyledH2>
<Label>Message</Label>
<Textarea rows="4" maxLength="255" />
<Label>Address</Label>
<Input type="text" />
<Label>Signature</Label>
<Textarea rows="4" maxLength="255" />
</Verify>
</Wrapper>
</Content>
);
export default AccountSignVerify; render() {
const device = this.props.wallet.selectedDevice;
const {
account, discovery, shouldRender, notification,
} = this.props.selectedAccount;
const { type, title, message } = notification;
if (!device || !account || !discovery || !shouldRender) return <Content type={type} title={title} message={message} isLoading />;
const {
signVerifyActions,
signVerify: {
signMessage,
signSignature,
verifyAddress,
verifyMessage,
verifySignature,
errors,
},
} = this.props;
const verifyAddressError = this.getError('verifyAddress');
return (
<Content>
<Title>Sign & Verify</Title>
<Wrapper>
<Sign>
<Row>
<Input
topLabel="Address"
name="signAddress"
value={account.address}
type="text"
isSmallText
autoSelect
isDisabled
/>
</Row>
<Row>
<Textarea
topLabel="Message"
name="signMessage"
value={signMessage}
onChange={this.handleInputChange}
rows={4}
maxRows={4}
maxLength="255"
/>
</Row>
<Row>
<Textarea
topLabel="Signature"
name="signSignature"
value={signSignature}
rows={4}
autoSelect
maxRows={4}
maxLength="255"
isDisabled
/>
</Row>
<RowButtons>
<Button
onClick={this.props.signVerifyActions.clearSign}
isWhite
>Clear
</Button>
<StyledButton
onClick={() => signVerifyActions.sign(account.addressPath, signMessage)}
>Sign
</StyledButton>
</RowButtons>
</Sign>
<Verify>
<Row>
<Input
topLabel="Address"
autoSelect
name="verifyAddress"
value={verifyAddress}
onChange={this.handleInputChange}
type="text"
state={verifyAddressError ? 'error' : null}
bottomText={verifyAddressError ? verifyAddressError.message : null}
isSmallText
/>
</Row>
<Row>
<Textarea
topLabel="Message"
name="verifyMessage"
value={verifyMessage}
onChange={this.handleInputChange}
rows={4}
maxRows={4}
maxLength="255"
/>
</Row>
<Row>
<Textarea
topLabel="Signature"
autoSelect
name="verifySignature"
value={verifySignature}
onChange={this.handleInputChange}
rows={4}
maxRows={4}
maxLength="255"
/>
</Row>
<RowButtons>
<Button
onClick={signVerifyActions.clearVerify}
isWhite
>Clear
</Button>
<StyledButton
onClick={
() => {
if (errors.length <= 0) {
signVerifyActions.verify(
verifyAddress,
verifyMessage,
verifySignature,
);
}
}
}
>Verify
</StyledButton>
</RowButtons>
</Verify>
</Wrapper>
</Content>
);
}
}
export default SignVerify;

View File

@ -57,6 +57,7 @@ const StyledCoinLogo = styled(CoinLogo)`
const StyledIcon = styled(Icon)` const StyledIcon = styled(Icon)`
position: relative; position: relative;
top: -7px; top: -7px;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }

View File

@ -19,7 +19,7 @@ import WalletContainer from 'views/Wallet';
import AccountSummary from 'views/Wallet/views/Account/Summary/Container'; import AccountSummary from 'views/Wallet/views/Account/Summary/Container';
import AccountSend from 'views/Wallet/views/Account/Send/Container'; import AccountSend from 'views/Wallet/views/Account/Send/Container';
import AccountReceive from 'views/Wallet/views/Account/Receive/Container'; import AccountReceive from 'views/Wallet/views/Account/Receive/Container';
import AccountSignVerify from 'views/Wallet/views/Account/SignVerify'; import AccountSignVerify from 'views/Wallet/views/Account/SignVerify/Container';
import WalletDashboard from 'views/Wallet/views/Dashboard'; import WalletDashboard from 'views/Wallet/views/Dashboard';
import WalletDeviceSettings from 'views/Wallet/views/DeviceSettings'; import WalletDeviceSettings from 'views/Wallet/views/DeviceSettings';

View File

@ -8500,9 +8500,15 @@ react-select@2.0.0:
react-input-autosize "^2.2.1" react-input-autosize "^2.2.1"
react-transition-group "^2.2.1" react-transition-group "^2.2.1"
<<<<<<< HEAD
react-textarea-autosize@^7.0.4:
version "7.0.4"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-7.0.4.tgz#4e4be649b544a88713e7b5043f76950f35d3d503"
=======
react-textarea-autosize@^6.1.0: react-textarea-autosize@^6.1.0:
version "6.1.0" version "6.1.0"
resolved "http://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-6.1.0.tgz#df91387f8a8f22020b77e3833c09829d706a09a5" resolved "http://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-6.1.0.tgz#df91387f8a8f22020b77e3833c09829d706a09a5"
>>>>>>> master
dependencies: dependencies:
prop-types "^15.6.0" prop-types "^15.6.0"