diff options
Diffstat (limited to 'ui/app/components')
33 files changed, 1267 insertions, 42 deletions
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index f34631ca8..9c063d31e 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -9,11 +9,17 @@ const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') const Identicon = require('../identicon') const { formatBalance } = require('../../util') +const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') +const { getEnvironmentType } = require('../../../../app/scripts/lib/util') +const Tooltip = require('../tooltip') + + const { SETTINGS_ROUTE, INFO_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, DEFAULT_ROUTE, } = require('../../routes') @@ -63,6 +69,9 @@ function mapDispatchToProps (dispatch) { dispatch(actions.hideSidebar()) dispatch(actions.toggleAccountMenu()) }, + showRemoveAccountConfirmationModal: (identity) => { + return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) + }, } } @@ -106,6 +115,18 @@ AccountMenu.prototype.render = function () { icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }), text: this.context.t('importAccount'), }), + h(Item, { + onClick: () => { + toggleAccountMenu() + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE) + } else { + history.push(CONNECT_HARDWARE_ROUTE) + } + }, + icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }), + text: this.context.t('connectHardwareWallet'), + }), h(Divider), h(Item, { onClick: () => { @@ -136,7 +157,8 @@ AccountMenu.prototype.renderAccounts = function () { } = this.props const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) - return accountOrder.map((address) => { + return accountOrder.filter(address => !!identities[address]).map((address) => { + const identity = identities[address] const isSelected = identity.address === selectedAddress @@ -170,16 +192,53 @@ AccountMenu.prototype.renderAccounts = function () { h('div.account-menu__balance', formattedBalance), ]), - this.indicateIfLoose(keyring), + this.renderKeyringType(keyring), + this.renderRemoveAccount(keyring, identity), ], ) }) } -AccountMenu.prototype.indicateIfLoose = function (keyring) { +AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) { + // Any account that's not from the HD wallet Keyring can be removed + const type = keyring.type + const isRemovable = type !== 'HD Key Tree' + if (isRemovable) { + return h(Tooltip, { + title: this.context.t('removeAccount'), + position: 'bottom', + }, [ + h('a.remove-account-icon', { + onClick: (e) => this.removeAccount(e, identity), + }, ''), + ]) + } + return null +} + +AccountMenu.prototype.removeAccount = function (e, identity) { + e.preventDefault() + e.stopPropagation() + const { showRemoveAccountConfirmationModal } = this.props + showRemoveAccountConfirmationModal(identity) +} + +AccountMenu.prototype.renderKeyringType = function (keyring) { try { // Sometimes keyrings aren't loaded yet: const type = keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label.allcaps', this.context.t('imported')) : null + let label + switch (type) { + case 'Trezor Hardware': + label = this.context.t('hardware') + break + case 'Simple Key Pair': + label = this.context.t('imported') + break + default: + label = '' + } + + return label !== '' ? h('.keyring-label.allcaps', label) : null + } catch (e) { return } } diff --git a/ui/app/components/alert/index.js b/ui/app/components/alert/index.js new file mode 100644 index 000000000..5620d847a --- /dev/null +++ b/ui/app/components/alert/index.js @@ -0,0 +1,62 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') + +class Alert extends Component { + + constructor (props) { + super(props) + + this.state = { + visble: false, + msg: false, + className: '', + } + } + + componentWillReceiveProps (nextProps) { + if (!this.props.visible && nextProps.visible) { + this.animateIn(nextProps) + } else if (this.props.visible && !nextProps.visible) { + this.animateOut(nextProps) + } + } + + animateIn (props) { + this.setState({ + msg: props.msg, + visible: true, + className: '.visible', + }) + } + + animateOut (props) { + this.setState({ + msg: null, + className: '.hidden', + }) + + setTimeout(_ => { + this.setState({visible: false}) + }, 500) + + } + + render () { + if (this.state.visible) { + return ( + h(`div.global-alert${this.state.className}`, {}, + h('a.msg', {}, this.state.msg) + ) + ) + } + return null + } +} + +Alert.propTypes = { + visible: PropTypes.bool.isRequired, + msg: PropTypes.string, +} +module.exports = Alert + diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js new file mode 100644 index 000000000..f99f710ce --- /dev/null +++ b/ui/app/components/button-group/button-group.component.js @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class ButtonGroup extends PureComponent { + static propTypes = { + defaultActiveButtonIndex: PropTypes.number, + disabled: PropTypes.bool, + children: PropTypes.array, + className: PropTypes.string, + style: PropTypes.object, + } + + static defaultProps = { + className: 'button-group', + } + + state = { + activeButtonIndex: this.props.defaultActiveButtonIndex || 0, + } + + handleButtonClick (activeButtonIndex) { + this.setState({ activeButtonIndex }) + } + + renderButtons () { + const { children, disabled } = this.props + + return React.Children.map(children, (child, index) => { + return child && ( + <button + className={classnames( + 'button-group__button', + { 'button-group__button--active': index === this.state.activeButtonIndex }, + )} + onClick={() => { + this.handleButtonClick(index) + child.props.onClick && child.props.onClick() + }} + disabled={disabled || child.props.disabled} + key={index} + > + { child.props.children } + </button> + ) + }) + } + + render () { + const { className, style } = this.props + + return ( + <div + className={className} + style={style} + > + { this.renderButtons() } + </div> + ) + } +} diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/button-group/button-group.stories.js new file mode 100644 index 000000000..14e1a7e49 --- /dev/null +++ b/ui/app/components/button-group/button-group.stories.js @@ -0,0 +1,49 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import ButtonGroup from './' +import Button from '../button' +import { text, boolean } from '@storybook/addon-knobs/react' + +storiesOf('ButtonGroup', module) + .add('with Buttons', () => + <ButtonGroup + style={{ width: '300px' }} + disabled={boolean('Disabled', false)} + defaultActiveButtonIndex={1} + > + <Button + onClick={action('cheap')} + > + {text('Button1', 'Cheap')} + </Button> + <Button + onClick={action('average')} + > + {text('Button2', 'Average')} + </Button> + <Button + onClick={action('fast')} + > + {text('Button3', 'Fast')} + </Button> + </ButtonGroup> + ) + .add('with a disabled Button', () => + <ButtonGroup + style={{ width: '300px' }} + disabled={boolean('Disabled', false)} + > + <Button + onClick={action('enabled')} + > + {text('Button1', 'Enabled')} + </Button> + <Button + onClick={action('disabled')} + disabled + > + {text('Button2', 'Disabled')} + </Button> + </ButtonGroup> + ) diff --git a/ui/app/components/button-group/index.js b/ui/app/components/button-group/index.js new file mode 100644 index 000000000..df470bd57 --- /dev/null +++ b/ui/app/components/button-group/index.js @@ -0,0 +1 @@ +export { default } from './button-group.component' diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/button-group/index.scss new file mode 100644 index 000000000..29713c75b --- /dev/null +++ b/ui/app/components/button-group/index.scss @@ -0,0 +1,38 @@ +.button-group { + display: flex; + justify-content: center; + align-items: center; + + &__button { + font-family: Roboto; + font-size: 1rem; + color: $tundora; + border-style: solid; + border-color: $alto; + border-width: 1px 1px 1px; + border-left: 0; + flex: 1; + padding: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:first-child { + border-left: 1px solid $alto; + border-radius: 4px 0 0 4px; + } + + &:last-child { + border-radius: 0 4px 4px 0; + } + + &--active { + background-color: $dodger-blue; + color: $white; + } + + &:disabled { + opacity: .5; + } + } +}
\ No newline at end of file diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js new file mode 100644 index 000000000..f07bb97c8 --- /dev/null +++ b/ui/app/components/button-group/tests/button-group-component.test.js @@ -0,0 +1,97 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import ButtonGroup from '../button-group.component.js' + +const childButtonSpies = { + onClick: sinon.spy(), +} + +sinon.spy(ButtonGroup.prototype, 'handleButtonClick') +sinon.spy(ButtonGroup.prototype, 'renderButtons') + +const mockButtons = [ + <button onClick={childButtonSpies.onClick} key={'a'}><div className="mockClass" /></button>, + <button onClick={childButtonSpies.onClick} key={'b'}></button>, + <button onClick={childButtonSpies.onClick} key={'c'}></button>, +] + +describe('ButtonGroup Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<ButtonGroup + defaultActiveButtonIndex={1} + disabled={false} + className="someClassName" + style={ { color: 'red' } } + >{mockButtons}</ButtonGroup>) + }) + + afterEach(() => { + childButtonSpies.onClick.resetHistory() + ButtonGroup.prototype.handleButtonClick.resetHistory() + ButtonGroup.prototype.renderButtons.resetHistory() + }) + + describe('handleButtonClick', () => { + it('should set the activeButtonIndex', () => { + assert.equal(wrapper.state('activeButtonIndex'), 1) + wrapper.instance().handleButtonClick(2) + assert.equal(wrapper.state('activeButtonIndex'), 2) + }) + }) + + describe('renderButtons', () => { + it('should render a button for each child', () => { + const childButtons = wrapper.find('.button-group__button') + assert.equal(childButtons.length, 3) + }) + + it('should render the correct button with an active state', () => { + const childButtons = wrapper.find('.button-group__button') + const activeChildButton = wrapper.find('.button-group__button--active') + assert.deepEqual(childButtons.get(1), activeChildButton.get(0)) + }) + + it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => { + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0) + assert.equal(childButtonSpies.onClick.callCount, 0) + const childButtons = wrapper.find('.button-group__button') + childButtons.at(0).props().onClick() + childButtons.at(1).props().onClick() + childButtons.at(2).props().onClick() + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3) + assert.equal(childButtonSpies.onClick.callCount, 3) + }) + + it('should render all child buttons as disabled if props.disabled is true', () => { + const childButtons = wrapper.find('.button-group__button') + childButtons.forEach(button => { + assert.equal(button.props().disabled, undefined) + }) + wrapper.setProps({ disabled: true }) + const disabledChildButtons = wrapper.find('[disabled=true]') + assert.equal(disabledChildButtons.length, 3) + }) + + it('should render the children of the button', () => { + const mockClass = wrapper.find('.mockClass') + assert.equal(mockClass.length, 1) + }) + }) + + describe('render', () => { + it('should render a div with the expected class and style', () => { + assert.equal(wrapper.find('div').at(0).props().className, 'someClassName') + assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' }) + }) + + it('should call renderButtons when rendering', () => { + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1) + wrapper.instance().render() + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2) + }) + }) +}) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 32f0e90e4..b3e14ce23 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,3 +1,5 @@ +@import './button-group/index'; + @import './export-text-container/index'; @import './selected-account/index'; diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js new file mode 100644 index 000000000..5a9f0f289 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js @@ -0,0 +1,93 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../button' +import { addressSummary } from '../../../util' +import Identicon from '../../identicon' +import genAccountLink from '../../../../lib/account-link' + +class ConfirmRemoveAccount extends Component { + static propTypes = { + hideModal: PropTypes.func.isRequired, + removeAccount: PropTypes.func.isRequired, + identity: PropTypes.object.isRequired, + network: PropTypes.string.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleRemove () { + this.props.removeAccount(this.props.identity.address) + .then(() => this.props.hideModal()) + } + + renderSelectedAccount () { + const { identity } = this.props + return ( + <div className="modal-container__account"> + <div className="modal-container__account__identicon"> + <Identicon + address={identity.address} + diameter={32} + /> + </div> + <div className="modal-container__account__name"> + <span className="modal-container__account__label">Name</span> + <span className="account_value">{identity.name}</span> + </div> + <div className="modal-container__account__address"> + <span className="modal-container__account__label">Public Address</span> + <span className="account_value">{ addressSummary(identity.address, 4, 4) }</span> + </div> + <div className="modal-container__account__link"> + <a + className="" + href={genAccountLink(identity.address, this.props.network)} + target={'_blank'} + title={this.context.t('etherscanView')} + > + <img src="images/popout.svg" /> + </a> + </div> + </div> + ) + } + + render () { + const { t } = this.context + + return ( + <div className="modal-container"> + <div className="modal-container__content"> + <div className="modal-container__title"> + { `${t('removeAccount')}` }? + </div> + { this.renderSelectedAccount() } + <div className="modal-container__description"> + { t('removeAccountDescription') } + <a className="modal-container__link" rel="noopener noreferrer" target="_blank" href="https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI-">{ t('learnMore') }</a> + </div> + </div> + <div className="modal-container__footer"> + <Button + type="default" + className="modal-container__footer-button" + onClick={() => this.props.hideModal()} + > + { t('nevermind') } + </Button> + <Button + type="secondary" + className="modal-container__footer-button" + onClick={() => this.handleRemove()} + > + { t('remove') } + </Button> + </div> + </div> + ) + } +} + +export default ConfirmRemoveAccount diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js new file mode 100644 index 000000000..4b194c995 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import ConfirmRemoveAccount from './confirm-remove-account.component' + +const { hideModal, removeAccount } = require('../../../actions') + +const mapStateToProps = state => { + return { + identity: state.appState.modal.modalState.props.identity, + network: state.metamask.network, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + removeAccount: (address) => dispatch(removeAccount(address)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount) diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/modals/confirm-remove-account/index.js new file mode 100644 index 000000000..9763fbe05 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/index.js @@ -0,0 +1,2 @@ +import ConfirmRemoveAccount from './confirm-remove-account.container' +module.exports = ConfirmRemoveAccount diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss index 160911c10..e198cca44 100644 --- a/ui/app/components/modals/index.scss +++ b/ui/app/components/modals/index.scss @@ -20,6 +20,58 @@ font-size: .875rem; } + &__account { + border: 1px solid #b7b7b7; + border-radius: 4px; + padding: 10px; + display: flex; + margin-top: 10px; + margin-bottom: 20px; + width: 100%; + + &__identicon { + margin-right: 10px; + } + + &__name, + &__address { + margin-right: 10px; + font-size: 14px; + } + + &__name { + width: 100px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__label { + font-size: 11px; + display: block; + color: #9b9b9b; + } + + &__link { + margin-top: 14px; + + img { + width: 15px; + height: 15px; + } + } + + @media screen and (max-width: 575px) { + &__name { + width: 90px; + } + } + } + + &__link { + color: #2f9ae0; + } + &__content { overflow-y: auto; flex: 1; diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 973438b6b..f59825ed1 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -20,6 +20,7 @@ const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') const ConfirmResetAccount = require('./confirm-reset-account') +const ConfirmRemoveAccount = require('./confirm-remove-account') const TransactionConfirmed = require('./transaction-confirmed') const WelcomeBeta = require('./welcome-beta') const Notification = require('./notification') @@ -243,6 +244,19 @@ const MODALS = { }, }, + CONFIRM_REMOVE_ACCOUNT: { + contents: h(ConfirmRemoveAccount), + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + NEW_ACCOUNT: { contents: [ h(NewAccountModal, {}, []), diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index 7db39adec..0c0deff18 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux' import { compose } from 'recompose' import { withRouter } from 'react-router-dom' import R from 'ramda' +import contractMap from 'eth-contract-metadata' import ConfirmTransactionBase from './confirm-transaction-base.component' import { clearConfirmTransaction, @@ -16,6 +17,14 @@ import { getHexGasTotal } from '../../../helpers/confirm-transaction/util' import { isBalanceSufficient } from '../../send/send.utils' import { conversionGreaterThan } from '../../../conversion-util' import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants' +import { addressSlicer } from '../../../util' + +const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { + return { + ...acc, + [base.toLowerCase()]: contractMap[base], + } +}, {}) const mapStateToProps = (state, props) => { const { toAddress: propsToAddress } = props @@ -48,7 +57,10 @@ const mapStateToProps = (state, props) => { const { balance } = accounts[selectedAddress] const { name: fromName } = identities[selectedAddress] const toAddress = propsToAddress || txParamsToAddress - const toName = identities[toAddress] && identities[toAddress].name + const toName = identities[toAddress] + ? identities[toAddress].name + : casedContractMap[toAddress] ? casedContractMap[toAddress].name : addressSlicer(toAddress) + const isTxReprice = Boolean(lastGasPrice) const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList) diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js new file mode 100644 index 000000000..c722d1f55 --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -0,0 +1,143 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const genAccountLink = require('../../../../../lib/account-link.js') + +class AccountList extends Component { + constructor (props, context) { + super(props) + } + + renderHeader () { + return ( + h('div.hw-connect', [ + h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')), + h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')), + ]) + ) + } + + renderAccounts () { + return h('div.hw-account-list', [ + this.props.accounts.map((a, i) => { + + return h('div.hw-account-list__item', { key: a.address }, [ + h('div.hw-account-list__item__radio', [ + h('input', { + type: 'radio', + name: 'selectedAccount', + id: `address-${i}`, + value: a.index, + onChange: (e) => this.props.onAccountChange(e.target.value), + checked: this.props.selectedAccount === a.index.toString(), + }), + h( + 'label.hw-account-list__item__label', + { + htmlFor: `address-${i}`, + }, + [ + h('span.hw-account-list__item__index', a.index + 1), + `${a.address.slice(0, 4)}...${a.address.slice(-4)}`, + h('span.hw-account-list__item__balance', `${a.balance}`), + ]), + ]), + h( + 'a.hw-account-list__item__link', + { + href: genAccountLink(a.address, this.props.network), + target: '_blank', + title: this.context.t('etherscanView'), + }, + h('img', { src: 'images/popout.svg' }) + ), + ]) + }), + ]) + } + + renderPagination () { + return h('div.hw-list-pagination', [ + h( + 'button.hw-list-pagination__button', + { + onClick: () => this.props.getPage(-1), + }, + `< ${this.context.t('prev')}` + ), + + h( + 'button.hw-list-pagination__button', + { + onClick: () => this.props.getPage(1), + }, + `${this.context.t('next')} >` + ), + ]) + } + + renderButtons () { + const disabled = this.props.selectedAccount === null + const buttonProps = {} + if (disabled) { + buttonProps.disabled = true + } + + return h('div.new-account-connect-form__buttons', {}, [ + h( + 'button.btn-default.btn--large.new-account-connect-form__button', + { + onClick: this.props.onCancel.bind(this), + }, + [this.context.t('cancel')] + ), + + h( + `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`, + { + onClick: this.props.onUnlockAccount.bind(this), + ...buttonProps, + }, + [this.context.t('unlock')] + ), + ]) + } + + renderForgetDevice () { + return h('div.hw-forget-device-container', {}, [ + h('a', { + onClick: this.props.onForgetDevice.bind(this), + }, this.context.t('forgetDevice')), + ]) + } + + render () { + return h('div.new-account-connect-form.account-list', {}, [ + this.renderHeader(), + this.renderAccounts(), + this.renderPagination(), + this.renderButtons(), + this.renderForgetDevice(), + ]) + } + +} + + +AccountList.propTypes = { + accounts: PropTypes.array.isRequired, + onAccountChange: PropTypes.func.isRequired, + onForgetDevice: PropTypes.func.isRequired, + getPage: PropTypes.func.isRequired, + network: PropTypes.string, + selectedAccount: PropTypes.string, + history: PropTypes.object, + onUnlockAccount: PropTypes.func, + onCancel: PropTypes.func, +} + +AccountList.contextTypes = { + t: PropTypes.func, +} + +module.exports = AccountList diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js new file mode 100644 index 000000000..cb2b86595 --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -0,0 +1,149 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') + +class ConnectScreen extends Component { + constructor (props, context) { + super(props) + } + + renderUnsupportedBrowser () { + return ( + h('div.new-account-connect-form.unsupported-browser', {}, [ + h('div.hw-connect', [ + h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')), + h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')), + ]), + h( + 'button.btn-primary.btn--large', + { + onClick: () => global.platform.openWindow({ + url: 'https://google.com/chrome', + }), + }, + this.context.t('downloadGoogleChrome') + ), + ]) + ) + } + + renderHeader () { + return ( + h('div.hw-connect__header', {}, [ + h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)), + h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)), + ]) + ) + } + + renderTrezorAffiliateLink () { + return h('div.hw-connect__get-trezor', {}, [ + h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)), + h('a.hw-connect__get-trezor__link', { + href: 'https://shop.trezor.io/?a=metamask', + target: '_blank', + }, this.context.t('orderOneHere')), + ]) + } + + renderConnectToTrezorButton () { + return h( + 'button.btn-primary.btn--large', + { onClick: this.props.connectToTrezor.bind(this) }, + this.props.btnText + ) + } + + scrollToTutorial = (e) => { + if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) + } + + renderLearnMore () { + return ( + h('p.hw-connect__learn-more', { + onClick: this.scrollToTutorial, + }, [ + this.context.t('learnMore'), + h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}), + ]) + ) + } + + renderTutorialSteps () { + const steps = [ + { + asset: 'hardware-wallet-step-1', + dimensions: {width: '225px', height: '75px'}, + }, + { + asset: 'hardware-wallet-step-2', + dimensions: {width: '300px', height: '100px'}, + }, + { + asset: 'hardware-wallet-step-3', + dimensions: {width: '120px', height: '90px'}, + }, + ] + + return h('.hw-tutorial', { + ref: node => { this.referenceNode = node }, + }, + steps.map((step, i) => ( + h('div.hw-connect', {}, [ + h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)), + h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)), + h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }), + ]) + )) + ) + } + + renderFooter () { + return ( + h('div.hw-connect__footer', {}, [ + h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)), + this.renderConnectToTrezorButton(), + h('p.hw-connect__footer__msg', {}, [ + this.context.t(`havingTroubleConnecting`), + h('a.hw-connect__footer__link', { + href: 'https://support.metamask.io/', + target: '_blank', + }, this.context.t('getHelp')), + ]), + ]) + ) + } + + renderConnectScreen () { + return ( + h('div.new-account-connect-form', {}, [ + this.renderHeader(), + this.renderTrezorAffiliateLink(), + this.renderConnectToTrezorButton(), + this.renderLearnMore(), + this.renderTutorialSteps(), + this.renderFooter(), + ]) + ) + } + + render () { + if (this.props.browserSupported) { + return this.renderConnectScreen() + } + return this.renderUnsupportedBrowser() + } +} + +ConnectScreen.propTypes = { + connectToTrezor: PropTypes.func.isRequired, + btnText: PropTypes.string.isRequired, + browserSupported: PropTypes.bool.isRequired, +} + +ConnectScreen.contextTypes = { + t: PropTypes.func, +} + +module.exports = ConnectScreen + diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js new file mode 100644 index 000000000..3f66e7098 --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -0,0 +1,241 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../../../actions') +const ConnectScreen = require('./connect-screen') +const AccountList = require('./account-list') +const { DEFAULT_ROUTE } = require('../../../../routes') +const { formatBalance } = require('../../../../util') + +class ConnectHardwareForm extends Component { + constructor (props, context) { + super(props) + this.state = { + error: null, + btnText: context.t('connectToTrezor'), + selectedAccount: null, + accounts: [], + browserSupported: true, + unlocked: false, + } + } + + componentWillReceiveProps (nextProps) { + const { accounts } = nextProps + const newAccounts = this.state.accounts.map(a => { + const normalizedAddress = a.address.toLowerCase() + const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null + a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' + return a + }) + this.setState({accounts: newAccounts}) + } + + + componentDidMount () { + this.checkIfUnlocked() + } + + async checkIfUnlocked () { + const unlocked = await this.props.checkHardwareStatus('trezor') + if (unlocked) { + this.setState({unlocked: true}) + this.getPage(0) + } + } + + connectToTrezor = () => { + if (this.state.accounts.length) { + return null + } + this.setState({ btnText: this.context.t('connecting')}) + this.getPage(0) + } + + onAccountChange = (account) => { + this.setState({selectedAccount: account.toString(), error: null}) + } + + showTemporaryAlert () { + this.props.showAlert(this.context.t('hardwareWalletConnected')) + // Autohide the alert after 5 seconds + setTimeout(_ => { + this.props.hideAlert() + }, 5000) + } + + getPage = (page) => { + this.props + .connectHardware('trezor', page) + .then(accounts => { + if (accounts.length) { + + // If we just loaded the accounts for the first time + // (device previously locked) show the global alert + if (this.state.accounts.length === 0 && !this.state.unlocked) { + this.showTemporaryAlert() + } + + const newState = { unlocked: true } + // Default to the first account + if (this.state.selectedAccount === null) { + accounts.forEach((a, i) => { + if (a.address.toLowerCase() === this.props.address) { + newState.selectedAccount = a.index.toString() + } + }) + // If the page doesn't contain the selected account, let's deselect it + } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) { + newState.selectedAccount = null + } + + + // Map accounts with balances + newState.accounts = accounts.map(account => { + const normalizedAddress = account.address.toLowerCase() + const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null + account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' + return account + }) + + this.setState(newState) + } + }) + .catch(e => { + if (e === 'Window blocked') { + this.setState({ browserSupported: false }) + } + this.setState({ btnText: this.context.t('connectToTrezor') }) + }) + } + + onForgetDevice = () => { + this.props.forgetDevice('trezor') + .then(_ => { + this.setState({ + error: null, + btnText: this.context.t('connectToTrezor'), + selectedAccount: null, + accounts: [], + unlocked: false, + }) + }).catch(e => { + this.setState({ error: e.toString() }) + }) + } + + onUnlockAccount = () => { + + if (this.state.selectedAccount === null) { + this.setState({ error: this.context.t('accountSelectionRequired') }) + } + + this.props.unlockTrezorAccount(this.state.selectedAccount) + .then(_ => { + this.props.history.push(DEFAULT_ROUTE) + }).catch(e => { + this.setState({ error: e.toString() }) + }) + } + + onCancel = () => { + this.props.history.push(DEFAULT_ROUTE) + } + + renderError () { + return this.state.error + ? h('span.error', { style: { marginBottom: 40 } }, this.state.error) + : null + } + + renderContent () { + if (!this.state.accounts.length) { + return h(ConnectScreen, { + connectToTrezor: this.connectToTrezor, + btnText: this.state.btnText, + browserSupported: this.state.browserSupported, + }) + } + + return h(AccountList, { + accounts: this.state.accounts, + selectedAccount: this.state.selectedAccount, + onAccountChange: this.onAccountChange, + network: this.props.network, + getPage: this.getPage, + history: this.props.history, + onUnlockAccount: this.onUnlockAccount, + onForgetDevice: this.onForgetDevice, + onCancel: this.onCancel, + }) + } + + render () { + return h('div', [ + this.renderError(), + this.renderContent(), + ]) + } +} + +ConnectHardwareForm.propTypes = { + hideModal: PropTypes.func, + showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, + connectHardware: PropTypes.func, + checkHardwareStatus: PropTypes.func, + forgetDevice: PropTypes.func, + showAlert: PropTypes.func, + hideAlert: PropTypes.func, + unlockTrezorAccount: PropTypes.func, + numberOfExistingAccounts: PropTypes.number, + history: PropTypes.object, + t: PropTypes.func, + network: PropTypes.string, + accounts: PropTypes.object, + address: PropTypes.string, +} + +const mapStateToProps = state => { + const { + metamask: { network, selectedAddress, identities = {}, accounts = [] }, + } = state + const numberOfExistingAccounts = Object.keys(identities).length + + return { + network, + accounts, + address: selectedAddress, + numberOfExistingAccounts, + } +} + +const mapDispatchToProps = dispatch => { + return { + connectHardware: (deviceName, page) => { + return dispatch(actions.connectHardware(deviceName, page)) + }, + checkHardwareStatus: (deviceName) => { + return dispatch(actions.checkHardwareStatus(deviceName)) + }, + forgetDevice: (deviceName) => { + return dispatch(actions.forgetDevice(deviceName)) + }, + unlockTrezorAccount: index => { + return dispatch(actions.unlockTrezorAccount(index)) + }, + showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), + showAlert: (msg) => dispatch(actions.showAlert(msg)), + hideAlert: () => dispatch(actions.hideAlert()), + } +} + +ConnectHardwareForm.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)( + ConnectHardwareForm +) diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js index 5681e43a9..d3de1ea01 100644 --- a/ui/app/components/pages/create-account/index.js +++ b/ui/app/components/pages/create-account/index.js @@ -8,7 +8,12 @@ const { getCurrentViewContext } = require('../../../selectors') const classnames = require('classnames') const NewAccountCreateForm = require('./new-account') const NewAccountImportForm = require('./import-account') -const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes') +const ConnectHardwareForm = require('./connect-hardware') +const { + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, +} = require('../../../routes') class CreateAccountPage extends Component { renderTabs () { @@ -36,6 +41,19 @@ class CreateAccountPage extends Component { }, [ this.context.t('import'), ]), + h( + 'div.new-account__tabs__tab', + { + className: classnames('new-account__tabs__tab', { + 'new-account__tabs__selected': matchPath(location.pathname, { + path: CONNECT_HARDWARE_ROUTE, + exact: true, + }), + }), + onClick: () => history.push(CONNECT_HARDWARE_ROUTE), + }, + this.context.t('connect') + ), ]) } @@ -57,6 +75,11 @@ class CreateAccountPage extends Component { path: IMPORT_ACCOUNT_ROUTE, component: NewAccountImportForm, }), + h(Route, { + exact: true, + path: CONNECT_HARDWARE_ROUTE, + component: ConnectHardwareForm, + }), ]), ]), ]) diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js index 9c94990e0..402b8f03b 100644 --- a/ui/app/components/pages/create-account/new-account.js +++ b/ui/app/components/pages/create-account/new-account.js @@ -62,6 +62,7 @@ class NewAccountCreateForm extends Component { NewAccountCreateForm.propTypes = { hideModal: PropTypes.func, showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, createAccount: PropTypes.func, numberOfExistingAccounts: PropTypes.number, history: PropTypes.object, @@ -92,6 +93,7 @@ const mapDispatchToProps = dispatch => { }) }, showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), } } diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js index 38aa02dae..5e3fdc9af 100644 --- a/ui/app/components/pages/home.js +++ b/ui/app/components/pages/home.js @@ -27,19 +27,17 @@ const { NOTICE_ROUTE, } = require('../../routes') +const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction') + class Home extends Component { componentDidMount () { const { history, - unapprovedTxs = {}, - unapprovedMsgCount = 0, - unapprovedPersonalMsgCount = 0, - unapprovedTypedMessagesCount = 0, + unconfirmedTransactionsCount = 0, } = this.props // unapprovedTxs and unapproved messages - if (Object.keys(unapprovedTxs).length || - unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) { + if (unconfirmedTransactionsCount > 0) { history.push(CONFIRM_TRANSACTION_ROUTE) } } @@ -167,6 +165,7 @@ Home.propTypes = { isPopup: PropTypes.bool, isMouseUser: PropTypes.bool, t: PropTypes.func, + unconfirmedTransactionsCount: PropTypes.number, } function mapStateToProps (state) { @@ -230,6 +229,7 @@ function mapStateToProps (state) { // state needed to get account dropdown temporarily rendering from app bar selected, + unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), } } diff --git a/ui/app/components/selected-account/selected-account.component.js b/ui/app/components/selected-account/selected-account.component.js index 3386a4196..6c202141e 100644 --- a/ui/app/components/selected-account/selected-account.component.js +++ b/ui/app/components/selected-account/selected-account.component.js @@ -1,17 +1,10 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import copyToClipboard from 'copy-to-clipboard' +import { addressSlicer } from '../../util' const Tooltip = require('../tooltip-v2.js') -const addressStripper = (address = '') => { - if (address.length < 4) { - return address - } - - return `${address.slice(0, 4)}...${address.slice(-4)}` -} - class SelectedAccount extends Component { state = { copied: false, @@ -48,7 +41,7 @@ class SelectedAccount extends Component { { selectedIdentity.name } </div> <div className="selected-account__address"> - { addressStripper(selectedAddress) } + { addressSlicer(selectedAddress) } </div> </div> </Tooltip> diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js index adc114c0e..7a0b1a18e 100644 --- a/ui/app/components/send/send-content/send-content.component.js +++ b/ui/app/components/send/send-content/send-content.component.js @@ -4,6 +4,7 @@ import PageContainerContent from '../../page-container/page-container-content.co import SendAmountRow from './send-amount-row/' import SendFromRow from './send-from-row/' import SendGasRow from './send-gas-row/' +import SendHexDataRow from './send-hex-data-row' import SendToRow from './send-to-row/' export default class SendContent extends Component { @@ -20,6 +21,7 @@ export default class SendContent extends Component { <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendGasRow /> + <SendHexDataRow /> </div> </PageContainerContent> ) diff --git a/ui/app/components/send/send-content/send-hex-data-row/index.js b/ui/app/components/send/send-content/send-hex-data-row/index.js new file mode 100644 index 000000000..08c341067 --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/index.js @@ -0,0 +1 @@ +export { default } from './send-hex-data-row.container' diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js new file mode 100644 index 000000000..063930db3 --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' + +export default class SendHexDataRow extends Component { + static propTypes = { + data: PropTypes.string, + inError: PropTypes.bool, + updateSendHexData: PropTypes.func.isRequired, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onInput = (event) => { + const {updateSendHexData} = this.props + event.target.value = event.target.value.replace(/\n/g, '') + updateSendHexData(event.target.value || null) + } + + render () { + const {inError} = this.props + const {t} = this.context + + return ( + <SendRowWrapper + label={`${t('hexData')}:`} + showError={inError} + errorType={'amount'} + > + <textarea + onInput={this.onInput} + placeholder="Optional" + className="send-v2__hex-data__input" + /> + </SendRowWrapper> + ) + } +} diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js new file mode 100644 index 000000000..df554ca5f --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { + updateSendHexData, +} from '../../../../actions' +import SendHexDataRow from './send-hex-data-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow) + +function mapStateToProps (state) { + return { + data: state.metamask.send.data, + } +} + +function mapDispatchToProps (dispatch) { + return { + updateSendHexData (data) { + return dispatch(updateSendHexData(data)) + }, + } +} diff --git a/ui/app/components/send/send-footer/send-footer.component.js b/ui/app/components/send/send-footer/send-footer.component.js index 2085f1dce..518cff06e 100644 --- a/ui/app/components/send/send-footer/send-footer.component.js +++ b/ui/app/components/send/send-footer/send-footer.component.js @@ -8,6 +8,7 @@ export default class SendFooter extends Component { static propTypes = { addToAddressBookIfNew: PropTypes.func, amount: PropTypes.string, + data: PropTypes.string, clearSend: PropTypes.func, disabled: PropTypes.bool, editingTransactionId: PropTypes.string, @@ -41,6 +42,7 @@ export default class SendFooter extends Component { const { addToAddressBookIfNew, amount, + data, editingTransactionId, from: {address: from}, gasLimit: gas, @@ -68,6 +70,7 @@ export default class SendFooter extends Component { const promise = editingTransactionId ? update({ amount, + data, editingTransactionId, from, gas, @@ -76,7 +79,7 @@ export default class SendFooter extends Component { to, unapprovedTxs, }) - : sign({ selectedToken, to, amount, from, gas, gasPrice }) + : sign({ data, selectedToken, to, amount, from, gas, gasPrice }) Promise.resolve(promise) .then(() => history.push(CONFIRM_TRANSACTION_ROUTE)) diff --git a/ui/app/components/send/send-footer/send-footer.container.js b/ui/app/components/send/send-footer/send-footer.container.js index 0af6fcfa1..60de4d030 100644 --- a/ui/app/components/send/send-footer/send-footer.container.js +++ b/ui/app/components/send/send-footer/send-footer.container.js @@ -18,6 +18,7 @@ import { getSendFromObject, getSendTo, getSendToAccounts, + getSendHexData, getTokenBalance, getUnapprovedTxs, } from '../send.selectors' @@ -35,6 +36,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendFooter) function mapStateToProps (state) { return { amount: getSendAmount(state), + data: getSendHexData(state), editingTransactionId: getSendEditingTransactionId(state), from: getSendFromObject(state), gasLimit: getGasLimit(state), @@ -52,9 +54,10 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { clearSend: () => dispatch(clearSend()), - sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => { + sign: ({ selectedToken, to, amount, from, gas, gasPrice, data }) => { const txParams = constructTxParams({ amount, + data, from, gas, gasPrice, @@ -68,6 +71,7 @@ function mapDispatchToProps (dispatch) { }, update: ({ amount, + data, editingTransactionId, from, gas, @@ -78,6 +82,7 @@ function mapDispatchToProps (dispatch) { }) => { const editingTx = constructUpdatedTx({ amount, + data, editingTransactionId, from, gas, diff --git a/ui/app/components/send/send-footer/send-footer.utils.js b/ui/app/components/send/send-footer/send-footer.utils.js index 875e7d948..f82ff1e9b 100644 --- a/ui/app/components/send/send-footer/send-footer.utils.js +++ b/ui/app/components/send/send-footer/send-footer.utils.js @@ -8,8 +8,9 @@ function addHexPrefixToObjectValues (obj) { }, {}) } -function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) { +function constructTxParams ({ selectedToken, data, to, amount, from, gas, gasPrice }) { const txParams = { + data, from, value: '0', gas, @@ -21,13 +22,12 @@ function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) txParams.to = to } - const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams) - - return hexPrefixedTxParams + return addHexPrefixToObjectValues(txParams) } function constructUpdatedTx ({ amount, + data, editingTransactionId, from, gas, @@ -36,9 +36,21 @@ function constructUpdatedTx ({ to, unapprovedTxs, }) { + const unapprovedTx = unapprovedTxs[editingTransactionId] + const txParamsData = unapprovedTx.txParams.data ? unapprovedTx.txParams.data : data const editingTx = { - ...unapprovedTxs[editingTransactionId], - txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }), + ...unapprovedTx, + txParams: Object.assign( + unapprovedTx.txParams, + addHexPrefixToObjectValues({ + data: txParamsData, + to, + from, + gas, + gasPrice, + value: amount, + }) + ), } if (selectedToken) { @@ -52,18 +64,10 @@ function constructUpdatedTx ({ to: selectedToken.address, data, })) - } else { - const { data } = unapprovedTxs[editingTransactionId].txParams - - Object.assign(editingTx.txParams, addHexPrefixToObjectValues({ - value: amount, - to, - data, - })) + } - if (typeof editingTx.txParams.data === 'undefined') { - delete editingTx.txParams.data - } + if (typeof editingTx.txParams.data === 'undefined') { + delete editingTx.txParams.data } return editingTx diff --git a/ui/app/components/send/send-footer/tests/send-footer-component.test.js b/ui/app/components/send/send-footer/tests/send-footer-component.test.js index 4b2cd327d..65e4bb654 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-component.test.js +++ b/ui/app/components/send/send-footer/tests/send-footer-component.test.js @@ -129,6 +129,7 @@ describe('SendFooter Component', function () { assert.deepEqual( propsMethodSpies.update.getCall(0).args[0], { + data: undefined, amount: 'mockAmount', editingTransactionId: 'mockEditingTransactionId', from: 'mockAddress', @@ -152,6 +153,7 @@ describe('SendFooter Component', function () { assert.deepEqual( propsMethodSpies.sign.getCall(0).args[0], { + data: undefined, amount: 'mockAmount', from: 'mockAddress', gas: 'mockGasLimit', diff --git a/ui/app/components/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js index 39d6a7686..cf4c893ee 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-container.test.js +++ b/ui/app/components/send/send-footer/tests/send-footer-container.test.js @@ -38,6 +38,7 @@ proxyquire('../send-footer.container.js', { getSendTo: (s) => `mockTo:${s}`, getSendToAccounts: (s) => `mockToAccounts:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`, + getSendHexData: (s) => `mockHexData:${s}`, getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`, }, './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` }, @@ -51,6 +52,7 @@ describe('send-footer container', () => { it('should map the correct properties to props', () => { assert.deepEqual(mapStateToProps('mockState'), { amount: 'mockAmount:mockState', + data: 'mockHexData:mockState', selectedToken: 'mockSelectedToken:mockState', editingTransactionId: 'mockEditingTransactionId:mockState', from: 'mockFromObject:mockState', @@ -100,6 +102,7 @@ describe('send-footer container', () => { assert.deepEqual( utilsStubs.constructTxParams.getCall(0).args[0], { + data: undefined, selectedToken: { address: '0xabc', }, @@ -129,6 +132,7 @@ describe('send-footer container', () => { assert.deepEqual( utilsStubs.constructTxParams.getCall(0).args[0], { + data: undefined, selectedToken: undefined, to: 'mockTo', amount: 'mockAmount', @@ -160,6 +164,7 @@ describe('send-footer container', () => { assert.deepEqual( utilsStubs.constructUpdatedTx.getCall(0).args[0], { + data: undefined, to: 'mockTo', amount: 'mockAmount', from: 'mockFrom', diff --git a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js index 2d3135995..28ff0c891 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js +++ b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js @@ -65,6 +65,28 @@ describe('send-footer utils', () => { }) describe('constructTxParams()', () => { + it('should return a new txParams object with data if there data is given', () => { + assert.deepEqual( + constructTxParams({ + data: 'someData', + selectedToken: false, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }), + { + data: '0xsomeData', + to: '0xmockTo', + value: '0xmockAmount', + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + } + ) + }) + it('should return a new txParams object with value and to properties if there is no selectedToken', () => { assert.deepEqual( constructTxParams({ @@ -76,6 +98,7 @@ describe('send-footer utils', () => { gasPrice: 'mockGasPrice', }), { + data: undefined, to: '0xmockTo', value: '0xmockAmount', from: '0xmockFrom', @@ -96,6 +119,7 @@ describe('send-footer utils', () => { gasPrice: 'mockGasPrice', }), { + data: undefined, value: '0x0', from: '0xmockFrom', gas: '0xmockGas', diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index f910f7caf..cf07eafe1 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -33,6 +33,7 @@ const selectors = { getSelectedTokenExchangeRate, getSelectedTokenToFiatRate, getSendAmount, + getSendHexData, getSendEditingTransactionId, getSendErrors, getSendFrom, @@ -210,6 +211,10 @@ function getSendAmount (state) { return state.metamask.send.amount } +function getSendHexData (state) { + return state.metamask.send.data +} + function getSendEditingTransactionId (state) { return state.metamask.send.editingTransactionId } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index da142fad8..20c2be0f1 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -175,7 +175,7 @@ WalletView.prototype.render = function () { this.setState({ copyToClipboardPressed: false }) }, }, [ - `${checksummedAddress.slice(0, 4)}...${checksummedAddress.slice(-4)}`, + `${checksummedAddress.slice(0, 6)}...${checksummedAddress.slice(-4)}`, h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }), ]), ]), |