1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-11-28 03:08: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": {
"test": {
"presets": ["jest"]
"presets": ["jest"]
}
}
}
}

View File

@ -59,6 +59,7 @@
"react-router-redux": "next",
"react-scale-text": "^1.2.2",
"react-select": "2.0.0",
"react-textarea-autosize": "^7.0.4",
"react-transition-group": "^2.4.0",
"redbox-react": "^1.6.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 Textarea from 'react-textarea-autosize';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import colors from 'config/colors';
import { FONT_SIZE, FONT_WEIGHT, FONT_FAMILY } from 'config/variables';
const Wrapper = styled.div`
width: 100%;
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-start;
@ -13,13 +15,12 @@ const Wrapper = styled.div`
const disabledColor = colors.TEXT_PRIMARY;
const StyledTextarea = styled.textarea`
const StyledTextarea = styled(Textarea)`
width: 100%;
min-height: 85px;
margin-bottom: 10px;
padding: 6px 12px;
padding: 10px 12px;
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;
resize: none;
outline: none;
@ -27,7 +28,7 @@ const StyledTextarea = styled.textarea`
color: ${colors.TEXT_PRIMARY};
background: ${colors.WHITE};
font-weight: ${FONT_WEIGHT.BASE};
font-size: ${FONT_SIZE.BASE};
font-size: ${FONT_SIZE.SMALL};
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
@ -81,10 +82,19 @@ const StyledTextarea = styled.textarea`
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`
padding-bottom: 4px;
padding-bottom: 8px;
color: ${colors.TEXT_SECONDARY};
`;
@ -105,7 +115,34 @@ const getColor = (inputState) => {
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,
placeholder = '',
value,
@ -113,28 +150,40 @@ const Textarea = ({
onFocus,
onBlur,
isDisabled,
name,
onChange,
topLabel,
rows,
maxRows,
autoSelect,
state = '',
bottomText = '',
trezorAction = null,
}) => (
<Wrapper
className={className}
>
<Wrapper className={className}>
{topLabel && (
<TopLabel>{topLabel}</TopLabel>
)}
<StyledTextarea
spellCheck="false"
autoCorrect="off"
autoCapitalize="off"
maxRows={maxRows}
rows={rows}
className={className}
disabled={isDisabled}
name={name}
style={customStyle}
onFocus={onFocus}
onBlur={onBlur}
value={value}
onClick={autoSelect ? event => event.target.select() : null}
placeholder={placeholder}
onChange={onChange}
borderColor={getColor(state)}
/>
<TrezorAction action={trezorAction}>
<ArrowUp />{trezorAction}
</TrezorAction>
{bottomText && (
<BottomText
color={getColor(state)}
@ -145,7 +194,7 @@ const Textarea = ({
</Wrapper>
);
Textarea.propTypes = {
TextArea.propTypes = {
className: PropTypes.string,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
@ -153,10 +202,15 @@ Textarea.propTypes = {
customStyle: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.string,
maxRows: PropTypes.number,
rows: PropTypes.number,
name: PropTypes.string,
isDisabled: PropTypes.bool,
topLabel: PropTypes.node,
state: PropTypes.string,
autoSelect: PropTypes.bool,
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`
padding-bottom: 10px;
padding-bottom: 8px;
color: ${colors.TEXT_SECONDARY};
`;
const StyledInput = styled.input`
width: 100%;
height: 40px;
height: ${props => (props.height ? `${props.height}px` : '40px')};
padding: 5px ${props => (props.hasIcon ? '40px' : '12px')} 6px 12px;
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};
color: ${props => (props.color ? props.color : colors.TEXT_SECONDARY)};
color: ${props => (props.color ? props.color : colors.TEXT)};
border-radius: 2px;
@ -55,6 +55,7 @@ const StyledInput = styled.input`
background-color: ${colors.WHITE};
transition: ${TRANSITION.HOVER};
&:disabled {
pointer-events: none;
background: ${colors.GRAY_LIGHT};
@ -111,7 +112,7 @@ const TrezorAction = styled.div`
color: ${colors.WHITE};
border-radius: 5px;
line-height: 37px;
z-index: 10001;
z-index: 10002;
transform: translate(-1px, -1px);
`;
@ -171,6 +172,8 @@ class Input extends PureComponent {
<Overlay isPartiallyHidden={this.props.isPartiallyHidden} />
{this.props.icon}
<StyledInput
autoComplete="off"
height={this.props.height}
trezorAction={this.props.trezorAction}
hasIcon={this.getIcon(this.props.state).length > 0}
innerRef={this.props.innerRef}
@ -178,10 +181,10 @@ class Input extends PureComponent {
type={this.props.type}
color={this.getColor(this.props.state)}
placeholder={this.props.placeholder}
autocomplete={this.props.autocomplete}
autocorrect={this.props.autocorrect}
autocapitalize={this.props.autocapitalize}
autoCorrect={this.props.autocorrect}
autoCapitalize={this.props.autocapitalize}
spellCheck={this.props.spellCheck}
isSmallText={this.props.isSmallText}
value={this.props.value}
readOnly={this.props.readOnly}
onChange={this.props.onChange}
@ -214,7 +217,7 @@ Input.propTypes = {
innerRef: PropTypes.func,
placeholder: PropTypes.string,
type: PropTypes.string,
autocomplete: PropTypes.string,
height: PropTypes.number,
autocorrect: PropTypes.string,
autocapitalize: PropTypes.string,
icon: PropTypes.node,
@ -230,12 +233,14 @@ Input.propTypes = {
sideAddons: PropTypes.arrayOf(PropTypes.node),
isDisabled: PropTypes.bool,
name: PropTypes.string,
isSmallText: PropTypes.bool,
isPartiallyHidden: PropTypes.bool,
};
Input.defaultProps = {
type: 'text',
autoSelect: false,
height: 40,
};
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 { FONT_SIZE } from 'config/variables';
import H3 from 'components/Heading';
import { H3 } from 'components/Heading';
import P from 'components/Paragraph';
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 PassphraseType from 'components/modals/passphrase/Type';
import ConfirmSignTx from 'components/modals/confirm/SignTx';
import ConfirmAction from 'components/modals/confirm/Action';
import ConfirmUnverifiedAddress from 'components/modals/confirm/UnverifiedAddress';
import ForgetDevice from 'components/modals/device/Forget';
import RememberDevice from 'components/modals/device/Remember';
@ -90,6 +91,12 @@ const getDeviceContextModal = (props: Props) => {
case 'ButtonRequest_SignTx':
return <ConfirmSignTx device={modal.device} sendForm={props.sendForm} />;
case 'ButtonRequest_ProtectCall':
return <ConfirmAction />;
case 'ButtonRequest_Other':
return <ConfirmAction />;
case RECEIVE.REQUEST_UNVERIFIED:
return (
<ConfirmUnverifiedAddress

View File

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

View File

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

View File

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

View File

@ -6,7 +6,6 @@ import * as RECEIVE from 'actions/constants/receive';
import * as ACCOUNT from 'actions/constants/account';
import type { Action } from 'flowtype';
export type State = {
addressVerified: 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 devices from 'reducers/DevicesReducer';
import blockchain from 'reducers/BlockchainReducer';
import signVerify from 'reducers/SignVerifyReducer';
const reducers = {
router: routerReducer,
@ -41,6 +42,7 @@ const reducers = {
wallet,
devices,
blockchain,
signVerify,
};
export type Reducers = typeof reducers;

View File

@ -58,7 +58,7 @@ const Content = ({
);
Content.propTypes = {
children: PropTypes.element,
children: PropTypes.oneOfType([PropTypes.element, PropTypes.array]),
isLoading: PropTypes.bool,
title: 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};
margin: 0px 4px;
padding: 20px;
white-space: nowrap;
&.active,
&:hover {
@ -64,7 +65,7 @@ class TopNavigationAccount extends React.PureComponent<Props> {
<StyledNavLink exact to={`${basePath}`}>Summary</StyledNavLink>
<StyledNavLink to={`${basePath}/receive`}>Receive</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} />
</Wrapper>
);

View File

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

View File

@ -10,8 +10,8 @@ import Link from 'components/Link';
import ICONS from 'config/icons';
import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables';
import colors from 'config/colors';
import Title from 'views/Wallet/components/Title';
import P from 'components/Paragraph';
import { H2 } from 'components/Heading';
import Content from 'views/Wallet/components/Content';
import type { Token } from 'flowtype';
import AdvancedForm from './components/AdvancedForm';
@ -34,7 +34,7 @@ const AmountInputLabel = styled.span`
`;
const InputRow = styled.div`
margin: 20px 0;
padding: 0 0 15px 0;
`;
const SetMaxAmountButton = styled(Button)`
@ -250,155 +250,152 @@ const AccountSend = (props: Props) => {
return (
<Content>
<React.Fragment>
<H2>Send Ethereum or tokens</H2>
<InputRow>
<Input
state={getAddressInputState(address, errors.address, warnings.address)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
topLabel="Address"
bottomText={errors.address || warnings.address || infos.address}
value={address}
onChange={event => onAddressChange(event.target.value)}
/>
</InputRow>
<InputRow>
<Input
state={getAmountInputState(errors.amount, warnings.amount)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
topLabel={(
<AmountInputLabelWrapper>
<AmountInputLabel>Amount</AmountInputLabel>
{(isCurrentCurrencyToken && selectedToken) && (
<AmountInputLabel>You have: {selectedTokenBalance} {selectedToken.symbol}</AmountInputLabel>
)}
</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>
<Title>Send Ethereum or tokens</Title>
<InputRow>
<Input
state={getAddressInputState(address, errors.address, warnings.address)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
topLabel="Address"
bottomText={errors.address || warnings.address || infos.address}
value={address}
onChange={event => onAddressChange(event.target.value)}
/>
</InputRow>
<InputRow>
<Input
state={getAmountInputState(errors.amount, warnings.amount)}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
topLabel={(
<AmountInputLabelWrapper>
<AmountInputLabel>Amount</AmountInputLabel>
{(isCurrentCurrencyToken && selectedToken) && (
<AmountInputLabel>You have: {selectedTokenBalance} {selectedToken.symbol}</AmountInputLabel>
)}
</AmountInputLabelWrapper>
)}
</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 && (
<AdvancedForm {...props}>
<SendButton
isDisabled={isSendButtonDisabled}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
</AdvancedForm>
)}
<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>
{props.selectedAccount.pending.length > 0 && (
<PendingTransactions
pending={props.selectedAccount.pending}
tokens={props.selectedAccount.tokens}
network={network}
<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>
)}
</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>
);
};

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 Input from 'components/inputs/Input';
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 { H2 } from 'components/Heading';
import colors from 'config/colors';
import type { Props } from './Container';
const Wrapper = styled.div`
display: flex;
flex: 1;
margin-top: -5px;
flex-direction: row;
background: ${colors.WHITE};
`;
const StyledH2 = styled(H2)`
padding-bottom: 10px;
const Row = styled.div`
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`
@ -30,34 +45,149 @@ const Verify = styled(Column)`
padding-left: 20px;
`;
const Label = styled.div`
color: ${colors.LABEL};
padding: 5px 0px;
`;
class SignVerify extends Component <Props> {
getError(inputName: string) {
if (!this.props.signVerify) return null;
return this.props.signVerify.errors.find(e => e.inputName === inputName);
}
const AccountSignVerify = () => (
<Content>
<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>
);
handleInputChange = (event: SyntheticInputEvent<Text>) => {
this.props.signVerifyActions.inputChange(event.target.name, event.target.value);
}
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)`
position: relative;
top: -7px;
&:hover {
cursor: pointer;
}

View File

@ -19,7 +19,7 @@ import WalletContainer from 'views/Wallet';
import AccountSummary from 'views/Wallet/views/Account/Summary/Container';
import AccountSend from 'views/Wallet/views/Account/Send/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 WalletDeviceSettings from 'views/Wallet/views/DeviceSettings';

View File

@ -8500,9 +8500,15 @@ react-select@2.0.0:
react-input-autosize "^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:
version "6.1.0"
resolved "http://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-6.1.0.tgz#df91387f8a8f22020b77e3833c09829d706a09a5"
>>>>>>> master
dependencies:
prop-types "^15.6.0"