diff --git a/ui/app/actions.js b/ui/app/actions.js index fe6048aa2..3a65155a6 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -95,6 +95,8 @@ var actions = { // account detail screen SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', showSendPage: showSendPage, + SHOW_SEND_TOKEN_PAGE: 'SHOW_SEND_TOKEN_PAGE', + showSendTokenPage, ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', addToAddressBook: addToAddressBook, REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', @@ -928,6 +930,12 @@ function showSendPage () { } } +function showSendTokenPage () { + return { + type: actions.SHOW_SEND_TOKEN_PAGE, + } +} + function buyEth (opts) { return (dispatch) => { const url = getBuyEthUrl(opts) diff --git a/ui/app/app.js b/ui/app/app.js index 9ad85d85d..1ca59e406 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -9,6 +9,7 @@ const NewKeyChainScreen = require('./new-keychain') // accounts const MainContainer = require('./main-container') const SendTransactionScreen = require('./send') +const SendTokenScreen = require('./components/send-token') const ConfirmTxScreen = require('./conf-tx') // notice const NoticeScreen = require('./components/notice') @@ -327,6 +328,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering send tx screen') return h(SendTransactionScreen, {key: 'send-transaction'}) + case 'sendToken': + log.debug('rendering send token screen') + return h(SendTokenScreen, {key: 'sendToken'}) + case 'newKeychain': log.debug('rendering new keychain screen') return h(NewKeyChainScreen, {key: 'new-keychain'}) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js new file mode 100644 index 000000000..a49e559dc --- /dev/null +++ b/ui/app/components/send-token/index.js @@ -0,0 +1,213 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const ethUtil = require('ethereumjs-util') +const inherits = require('util').inherits +const actions = require('../../actions') +const selectors = require('../../selectors') + +// const BalanceComponent = require('./balance-component') +const Identicon = require('../identicon') +const TokenBalance = require('../token-balance') +const CurrencyToggle = require('../send/currency-toggle') +const GasTooltip = require('../send/gas-tooltip') +const GasFeeDisplay = require('../send/gas-fee-display') + + +module.exports = connect(mapStateToProps, mapDispatchToProps)(SendTokenScreen) + +function mapStateToProps (state) { + // const sidebarOpen = state.appState.sidebarOpen + + const identities = state.metamask.identities + const addressBook = state.metamask.addressBook + const conversionRate = state.metamask.conversionRate + const currentBlockGasLimit = state.metamask.currentBlockGasLimit + // const accounts = state.metamask.accounts + // const network = state.metamask.network + const selectedTokenAddress = state.metamask.selectedTokenAddress + // const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + // const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress) + // const identity = identities[selectedAddress] + + return { + // sidebarOpen, + // selectedAddress, + // checksumAddress, + selectedTokenAddress, + identities, + addressBook, + conversionRate, + currentBlockGasLimit, + selectedToken: selectors.getSelectedToken(state), + // selectedToken: selectors.getSelectedToken(state), + // identity, + // network, + } +} + +function mapDispatchToProps (dispatch) { + return { + // showSidebar: () => { dispatch(actions.showSidebar()) }, + // hideSidebar: () => { dispatch(actions.hideSidebar()) }, + // showModal: (payload) => { dispatch(actions.showModal(payload)) }, + // showSendPage: () => { dispatch(actions.showSendPage()) }, + // showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) }, + } +} + +inherits(SendTokenScreen, Component) +function SendTokenScreen () { + Component.call(this) + this.state = { + to: '', + selectedCurrency: 'USD', + isGasTooltipOpen: false, + gasPrice: '0x5d21dba00', + gasLimit: '0x7b0d', + } +} + +SendTokenScreen.prototype.renderToAddressInput = function () { + const { + identities, + addressBook, + } = this.props + + const { + to, + } = this.state + + return h('div.send-screen-input-wrapper', {}, [ + h('div', ['To:']), + h('input.large-input.send-screen-input', { + name: 'address', + list: 'addresses', + placeholder: 'Address', + value: to, + onChange: e => this.setState({ to: e.target.value }), + }), + h('datalist#addresses', [ + // Corresponds to the addresses owned. + Object.entries(identities).map(([key, { address, name }]) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + addressBook.map(({ address, name }) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + ]), + ]) +} + +SendTokenScreen.prototype.renderAmountInput = function () { + const { + selectedCurrency, + } = this.state + + const { + selectedToken: {symbol}, + } = this.props + + return h('div.send-screen-input-wrapper', {}, [ + h('div.send-screen-amount-labels', [ + h('span', ['Amount']), + h(CurrencyToggle, { + selectedCurrency, + onClick: currency => this.setState({ selectedCurrency: currency }), + }), + ]), + h('input.large-input.send-screen-input', { + placeholder: `0 ${symbol}`, + type: 'number', + onChange: e => this.setState({ amount: e.target.value }), + }), + ]) +} + +SendTokenScreen.prototype.renderGasInput = function () { + const { + isGasTooltipOpen, + gasPrice, + gasLimit, + selectedCurrency, + } = this.state + + const { + conversionRate, + currentBlockGasLimit, + } = this.props + + return h('div.send-screen-input-wrapper', [ + isGasTooltipOpen && h(GasTooltip, { + className: 'send-tooltip', + gasPrice, + gasLimit, + onClose: () => this.setState({ isGasTooltipOpen: false }), + onFeeChange: ({ gasLimit, gasPrice }) => { + this.setState({ gasLimit, gasPrice }) + }, + }), + + h('div.send-screen-gas-labels', {}, [ + h('span', [ h('i.fa.fa-bolt'), 'Gas fee:']), + h('span', ['What\'s this?']), + ]), + h('div.large-input.send-screen-gas-input', [ + h(GasFeeDisplay, { + conversionRate, + gasPrice, + currentCurrency: selectedCurrency, + gas: gasLimit, + blockGasLimit: currentBlockGasLimit, + }), + h( + 'div.send-screen-gas-input-customize', + { onClick: () => this.setState({ isGasTooltipOpen: !isGasTooltipOpen }) }, + ['Customize'] + ), + ]), + ]) +} + +SendTokenScreen.prototype.renderMemoInput = function () { + return h('div.send-screen-input-wrapper', {}, [ + h('div', {}, ['Transaction memo (optional)']), + h( + 'input.large-input.send-screen-input', + { onChange: e => this.setState({ memo: e.target.value }) } + ), + ]) +} + +SendTokenScreen.prototype.render = function () { + const { + selectedTokenAddress, + selectedToken, + } = this.props + + return h('div.send-token', [ + h(Identicon, { + diameter: 75, + address: selectedTokenAddress, + }), + h('div.send-token__title', ['Send Tokens']), + h('div.send-token__description', ['Send Tokens to anyone with an Ethereum account']), + h('div.send-token__balance-text', ['Your Token Balance is:']), + h('div.send-token__token-balance', [ + h(TokenBalance, { token: selectedToken, balanceOnly: true }), + ]), + h('div.send-token__token-symbol', [selectedToken.symbol]), + this.renderToAddressInput(), + this.renderAmountInput(), + this.renderGasInput(), + this.renderMemoInput(), + ]) +} diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js index b4a249300..3a923eb9d 100644 --- a/ui/app/components/token-balance.js +++ b/ui/app/components/token-balance.js @@ -93,8 +93,10 @@ TokenBalance.prototype.componentDidUpdate = function (nextProps) { TokenBalance.prototype.updateBalance = function (tokens = []) { const [{ string, symbol }] = tokens + const { balanceOnly } = this.props + this.setState({ - balance: `${string} ${symbol}`, + balance: balanceOnly ? string : `${string} ${symbol}`, isLoading: false, }) } diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index c9be0d67d..f5ac6e2dc 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -40,6 +40,7 @@ function mapDispatchToProps (dispatch) { hideSidebar: () => { dispatch(actions.hideSidebar()) }, showModal: (payload) => { dispatch(actions.showModal(payload)) }, showSendPage: () => { dispatch(actions.showSendPage()) }, + showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) }, } } @@ -60,7 +61,7 @@ TxView.prototype.renderHeroBalance = function () { } TxView.prototype.renderButtons = function () { - const {selectedToken, showModal, showSendPage } = this.props + const {selectedToken, showModal, showSendPage, showSendTokenPage } = this.props return !selectedToken ? ( @@ -90,7 +91,7 @@ TxView.prototype.renderButtons = function () { textAlign: 'center', marginLeft: '0.8em', }, - onClick: showSendPage, + onClick: showSendTokenPage, }, 'SEND'), ]) ) diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 016833db9..87c4c0a1b 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -203,3 +203,44 @@ font-size: .8em; padding: 1px 4px; } + +.send-token { + width: 498px; + height: 605px; + background-color: #fff; + display: flex; + flex-flow: column nowrap; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); + padding: 46px 40.5px 26px; + position: relative; + top: -26px; + z-index: 25; + align-items: center; + font-family: "Montserrat Light"; + + .identicon { + position: absolute; + top: -35px; + z-index: 25; + } + + &__title { + color: $scorpion; + font-size: 20px; + line-height: 29px; + } + + &__description, + &__balance-text, + &__token-symbol { + margin-top: 10px; + font-size: 16px; + line-height: 24px; + } + + &__token-balance { + font-size: 43px; + line-height: 40px; + margin-top: 13px; + } +} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index ea7145f22..f444aaf38 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -219,6 +219,16 @@ function reduceApp (state, action) { warning: null, }) + case actions.SHOW_SEND_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'sendToken', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + case actions.SHOW_NEW_KEYCHAIN: return extend(appState, { currentView: {