diff --git a/.babelrc b/.babelrc index ca61982f..18127ec5 100644 --- a/.babelrc +++ b/.babelrc @@ -25,7 +25,7 @@ ], "env": { "test": { - "presets": ["jest"] + "presets": ["jest"] } - } + } } \ No newline at end of file diff --git a/package.json b/package.json index ef0acf8f..695f239d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/SignVerifyActions.js b/src/actions/SignVerifyActions.js new file mode 100644 index 00000000..021694f8 --- /dev/null +++ b/src/actions/SignVerifyActions.js @@ -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, + message: string, + hex: boolean = false, +): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + 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 => { + 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, +}; \ No newline at end of file diff --git a/src/actions/constants/signVerify.js b/src/actions/constants/signVerify.js new file mode 100644 index 00000000..e48d615b --- /dev/null +++ b/src/actions/constants/signVerify.js @@ -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'; \ No newline at end of file diff --git a/src/components/Textarea/index.js b/src/components/Textarea/index.js index d67ba8d7..736158e2 100644 --- a/src/components/Textarea/index.js +++ b/src/components/Textarea/index.js @@ -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, }) => ( - + {topLabel && ( {topLabel} )} event.target.select() : null} placeholder={placeholder} onChange={onChange} - borderColor={getColor(state)} /> + + {trezorAction} + {bottomText && ( ); -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; diff --git a/src/components/inputs/Input/index.js b/src/components/inputs/Input/index.js index cf296bf2..6645da8d 100644 --- a/src/components/inputs/Input/index.js +++ b/src/components/inputs/Input/index.js @@ -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 { {this.props.icon} 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; diff --git a/src/components/modals/confirm/Action/index.js b/src/components/modals/confirm/Action/index.js new file mode 100644 index 00000000..f08427ad --- /dev/null +++ b/src/components/modals/confirm/Action/index.js @@ -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 = () => ( + +
+ +

Confirm action on your Trezor

+
+
+); + +export default ConfirmAction; \ No newline at end of file diff --git a/src/components/modals/confirm/Address/index.js b/src/components/modals/confirm/Address/index.js index 05314500..f47c18e9 100644 --- a/src/components/modals/confirm/Address/index.js +++ b/src/components/modals/confirm/Address/index.js @@ -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'; diff --git a/src/components/modals/index.js b/src/components/modals/index.js index 54e37ae8..9e0a6330 100644 --- a/src/components/modals/index.js +++ b/src/components/modals/index.js @@ -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 ; + case 'ButtonRequest_ProtectCall': + return ; + + case 'ButtonRequest_Other': + return ; + case RECEIVE.REQUEST_UNVERIFIED: return ( , + errors: Array +} + +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; + } +}; \ No newline at end of file diff --git a/src/reducers/index.js b/src/reducers/index.js index a4955da5..a6406053 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -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; diff --git a/src/views/Wallet/components/Content/index.js b/src/views/Wallet/components/Content/index.js index 3f1958db..701f19eb 100644 --- a/src/views/Wallet/components/Content/index.js +++ b/src/views/Wallet/components/Content/index.js @@ -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, diff --git a/src/views/Wallet/components/Title/index.js b/src/views/Wallet/components/Title/index.js new file mode 100644 index 00000000..9745e1dc --- /dev/null +++ b/src/views/Wallet/components/Title/index.js @@ -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, +}) => ( + + {children} + +); + +Title.propTypes = { + children: PropTypes.string, +}; + +export default Title; diff --git a/src/views/Wallet/components/TopNavigationAccount/index.js b/src/views/Wallet/components/TopNavigationAccount/index.js index 05d52489..9f5fe363 100644 --- a/src/views/Wallet/components/TopNavigationAccount/index.js +++ b/src/views/Wallet/components/TopNavigationAccount/index.js @@ -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 { Summary Receive Send - {/* Sign & Verify */} + Sign & Verify this.wrapper} />
); diff --git a/src/views/Wallet/views/Account/Send/Container.js b/src/views/Wallet/views/Account/Send/Container.js index e282b52b..eac00b9f 100644 --- a/src/views/Wallet/views/Account/Send/Container.js +++ b/src/views/Wallet/views/Account/Send/Container.js @@ -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, diff --git a/src/views/Wallet/views/Account/Send/index.js b/src/views/Wallet/views/Account/Send/index.js index 587d4279..d4e3ff5a 100644 --- a/src/views/Wallet/views/Account/Send/index.js +++ b/src/views/Wallet/views/Account/Send/index.js @@ -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 ( - -

Send Ethereum or tokens

- - onAddressChange(event.target.value)} - /> - - - - - Amount - {(isCurrentCurrencyToken && selectedToken) && ( - You have: {selectedTokenBalance} {selectedToken.symbol} - )} - - )} - value={amount} - onChange={event => onAmountChange(event.target.value)} - bottomText={errors.amount || warnings.amount || infos.amount} - sideAddons={[ - ( - onSetMax()} - isActive={setMax} - > - {!setMax && ( - - )} - {setMax && ( - - )} - Set max - - ), - ( - - ), - ]} - /> - - - - - Fee - {gasPriceNeedsUpdate && ( - - - Recommended fees updated. Click here to use them - - )} - - onAddressChange(event.target.value)} + /> + + + + Amount + {(isCurrentCurrencyToken && selectedToken) && ( + You have: {selectedTokenBalance} {selectedToken.symbol} + )} + )} - + value={amount} + onChange={event => onAmountChange(event.target.value)} + bottomText={errors.amount || warnings.amount || infos.amount} + sideAddons={[ + ( + onSetMax()} + isActive={setMax} + > + {!setMax && ( + + )} + {setMax && ( + + )} + Set max + + ), + ( + + ), + ]} + /> + - {advanced && ( - - onSend()} - > - {sendButtonText} - - - )} + + + Fee + {gasPriceNeedsUpdate && ( + + + Recommended fees updated. Click here to use them + + )} + +