diff --git a/old-ui/app/add-token.js b/old-ui/app/add-token.js index 9d6f2167e..03c11f1f6 100644 --- a/old-ui/app/add-token.js +++ b/old-ui/app/add-token.js @@ -1,244 +1,509 @@ -const inherits = require('util').inherits -const Component = require('react').Component +const React = require('react') +const { Component } = React const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('../../ui/app/actions') +const { setPendingTokens, clearPendingTokens, displayWarning, goHome, addToken } = actions const Tooltip = require('./components/tooltip.js') - - +const TabBar = require('./components/tab-bar') +// const { CONFIRM_ADD_TOKEN_ROUTE } = require('../../ui/app/routes') +const { checkExistingAddresses } = require('../../ui/app/components/pages/add-token/util') +const TokenList = require('../../ui/app/components/pages/add-token/token-list') +const TokenSearch = require('../../ui/app/components/pages/add-token/token-search') +const { tokenInfoGetter } = require('../../ui/app/token-util') const ethUtil = require('ethereumjs-util') const abi = require('human-standard-token-abi') const Eth = require('ethjs-query') const EthContract = require('ethjs-contract') +const PropTypes = require('prop-types') const emptyAddr = '0x0000000000000000000000000000000000000000' +const SEARCH_TAB = 'SEARCH' +const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN' -module.exports = connect(mapStateToProps)(AddTokenScreen) - -function mapStateToProps (state) { +const mapStateToProps = ({metamask}) => { + const { identities, tokens, pendingTokens, network } = metamask return { - identities: state.metamask.identities, - network: state.metamask.network, + identities, + tokens, + network, + pendingTokens, } } -inherits(AddTokenScreen, Component) -function AddTokenScreen () { - this.state = { - warning: null, - address: '', - symbol: 'TOKEN', - decimals: 18, +const mapDispatchToProps = dispatch => { + return { + setPendingTokens: tokens => dispatch(setPendingTokens(tokens)), + clearPendingTokens: () => dispatch(clearPendingTokens()), + displayWarning: (warn) => dispatch(displayWarning(warn)), + goHome: () => dispatch(goHome()), + addToken: (address, symbol, decimals, token) => dispatch(addToken(address, symbol, decimals, token)), } - Component.call(this) } -AddTokenScreen.prototype.render = function () { - const state = this.state - const props = this.props - const { warning, symbol, decimals } = state - const { network } = props +class AddTokenScreen extends Component { - return ( - h('.flex-column.flex-grow', [ + static contextTypes = { + t: PropTypes.func, + } - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - style: { - position: 'absolute', - left: '30px', - }, - }), - h('h2.page-subtitle', 'Add Token'), - ]), + static propTypes = { + // history: PropTypes.object, + setPendingTokens: PropTypes.func, + showConfirmAddTokenPage: PropTypes.func, + pendingTokens: PropTypes.object, + clearPendingTokens: PropTypes.func, + displayWarning: PropTypes.func, + tokens: PropTypes.array, + identities: PropTypes.object, + address: PropTypes.string, + dispatch: PropTypes.func, + } - h('div', { + constructor (props) { + super(props) + this.state = { + warning: null, + customAddress: '', + customSymbol: '', + customDecimals: 18, + searchResults: [], + selectedTokens: {}, + tokenSelectorError: null, + customAddressError: null, + customSymbolError: null, + customDecimalsError: null, + autoFilled: false, + displayedTab: SEARCH_TAB, + } + Component.call(this) + } + + componentDidMount () { + this.tokenInfoGetter = tokenInfoGetter() + const { pendingTokens = {} } = this.props + const pendingTokenKeys = Object.keys(pendingTokens) + + if (pendingTokenKeys.length > 0) { + let selectedTokens = {} + let customToken = {} + + pendingTokenKeys.forEach(tokenAddress => { + const token = pendingTokens[tokenAddress] + const { isCustom } = token + + if (isCustom) { + customToken = { ...token } + } else { + selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } } + } + }) + + const { + address: customAddress = '', + symbol: customSymbol = '', + decimals: customDecimals = 0, + } = customToken + + const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB + this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab }) + } + } + + render () { + return ( + h('.flex-column.flex-grow', { style: { - margin: '0 30px', + width: '100%', }, }, [ - h('.error', { + // subtitle and nav + h('.section-title.flex-row.flex-center', { style: { - display: warning ? 'block' : 'none', - }, - }, warning), - ]), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '30px', + background: '#60269c', }, }, [ + h('h2.page-subtitle', { + style: { + color: '#ffffff', + }, + }, 'Add Token'), + ]), - h('div', [ - h(Tooltip, { - position: 'top', - title: 'The contract of the actual token contract.', + h(TabBar, { + tabs: [ + { content: 'Search', key: SEARCH_TAB }, + { content: 'Custom', key: CUSTOM_TOKEN_TAB }, + ], + defaultTab: this.state.displayedTab || CUSTOM_TOKEN_TAB, + tabSelected: (key) => this.setCurrentAddTokenTab(key), + }), + + this.tabSwitchView(), + ]) + ) + } + + setCurrentAddTokenTab (key) { + this.setState({displayedTab: key}) + } + + tabSwitchView () { + const props = this.props + const state = this.state + const { warning, customSymbol, customDecimals, tokenSelectorError, selectedTokens, searchResults, displayedTab } = state + const { network, clearPendingTokens, goHome, addToken } = props + + switch (displayedTab) { + case CUSTOM_TOKEN_TAB: + return h('.flex-column.flex-justify-center.flex-grow.select-none', [ + warning ? h('div', { + style: { + margin: '20px 30px 0 30px', + }, + }, [ + h('.error', { + style: { + display: warning ? 'block' : 'none', + }, + }, warning), + ]) : null, + h('.flex-space-around', { + style: { + padding: '30px', + }, }, [ - h('span', 'Token Contract Address '), + + h('div', [ + h(Tooltip, { + position: 'top', + title: 'The contract of the actual token contract.', + }, [ + h('span', { + style: { fontWeight: 'bold'}, + }, 'Token Address' /* this.context.t('tokenAddress')*/), + ]), + ]), + + h('section.flex-row.flex-center', [ + h('input.large-input#token-address', { + name: 'address', + placeholder: 'Token Contract Address', + style: { + width: '100%', + margin: '10px 0', + }, + onChange: e => this.handleCustomAddressChange(e.target.value), + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Symbol' /* this.context.t('tokenSymbol')*/), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input.large-input#token_symbol', { + placeholder: `Like "ETH"`, + value: customSymbol, + style: { + width: '100%', + margin: '10px 0', + }, + onChange: e => this.handleCustomSymbolChange(e.target.value), + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision' /* this.context.t('decimal')*/), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input.large-input#token_decimals', { + value: customDecimals, + type: 'number', + min: 0, + max: 36, + style: { + width: '100%', + margin: '10px 0', + }, + onChange: e => this.handleCustomDecimalsChange(e.target.value), + }), + ]), + + h('div', { + key: 'buttons', + style: { + alignSelf: 'center', + float: 'right', + marginTop: '10px', + }, + }, [ + h('button.btn-violet', { + onClick: () => { + goHome() + }, + }, 'Cancel' /* this.context.t('cancel')*/), + h('button', { + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { customAddress, customSymbol, customDecimals } = this.state + addToken(customAddress.trim(), customSymbol.trim(), customDecimals, network) + .then(() => { + // this.props.dispatch(actions.goHome()) + goHome() + }) + }, + }, 'Add' /* this.context.t('addToken')*/), + ]), + ]), + ]) + default: + return h('div', [ + h('.add-token__search-token', [ + h(TokenSearch, { + onSearch: ({ results = [] }) => this.setState({ searchResults: results }), + error: tokenSelectorError, + }), + h('.add-token__token-list', { + style: { + marginTop: '20px', + height: '250px', + overflow: 'auto', + }, + }, [ + h(TokenList, { + results: searchResults, + selectedTokens: selectedTokens, + onToggleToken: token => this.handleToggleToken(token), + }), ]), ]), - - h('section.flex-row.flex-center', [ - h('input.large-input#token-address', { - name: 'address', - placeholder: 'Token Contract Address', - onChange: this.tokenAddressDidChange.bind(this), - style: { - width: '100%', - margin: '10px 0', - }, - }), + h('.page-container__footer', [ + h('.page-container__footer-container', [ + h('button.btn-violet', { + onClick: () => { + clearPendingTokens() + goHome() + }, + }, 'Cancel' /* this.context.t('cancel')*/), + h('button', { + onClick: () => this.handleNext(), + disabled: this.hasError() || !this.hasSelected(), + }, 'Next' /* this.context.t('next')*/), + ]), ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Symbol'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input.large-input#token_symbol', { - placeholder: `Like "ETH"`, - value: symbol, - style: { - width: '100%', - margin: '10px 0', - }, - onChange: (event) => { - var element = event.target - var symbol = element.value - this.setState({ symbol }) - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input.large-input#token_decimals', { - value: decimals, - type: 'number', - min: 0, - max: 36, - style: { - width: '100%', - margin: '10px 0', - }, - onChange: (event) => { - var element = event.target - var decimals = element.value.trim() - this.setState({ decimals }) - }, - }), - ]), - - h('button', { - style: { - alignSelf: 'center', - float: 'right', - marginTop: '10px', - }, - onClick: (event) => { - const valid = this.validateInputs() - if (!valid) return - - const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals, network)) - .then(() => { - this.props.dispatch(actions.goHome()) - }) - }, - }, 'Add'), - ]), - ]), - ]) - ) -} - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.componentWillUnmount = function () { - this.props.dispatch(actions.displayWarning('')) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const identitiesList = Object.keys(this.props.identities) - const { address, symbol, decimals } = state - const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid.' + ]) + } } - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' + componentWillMount () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) } - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' + componentWillUnmount () { + const { displayWarning } = this.props + displayWarning('') } - const ownAddress = identitiesList.includes(standardAddress) - if (ownAddress) { - msg = 'Personal address detected. Input the token contract address.' + // tokenAddressDidChange (event) { + // const el = event.target + // const address = el.value.trim() + // if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + // this.setState({ address }) + // this.attemptToAutoFillTokenParams(address) + // } + // } + + validateInputs () { + let msg = '' + const state = this.state + const identitiesList = Object.keys(this.props.identities) + const { customAddress: address, customSymbol: symbol, customDecimals: decimals } = state + const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid.' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const ownAddress = identitiesList.includes(standardAddress) + if (ownAddress) { + msg = 'Personal address detected. Input the token contract address.' + } + + const isValid = validAddress && validDecimals && !ownAddress + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid } - const isValid = validAddress && validDecimals && !ownAddress + handleToggleToken = (token) => { + const { address } = token + const { selectedTokens = {} } = this.state + const selectedTokensCopy = { ...selectedTokens } + + if (address in selectedTokensCopy) { + delete selectedTokensCopy[address] + } else { + selectedTokensCopy[address] = token + } - if (!isValid) { this.setState({ - warning: msg, + selectedTokens: selectedTokensCopy, + tokenSelectorError: null, }) - } else { - this.setState({ warning: null }) } - return isValid -} + hasError = () => { + const { + tokenSelectorError, + customAddressError, + customSymbolError, + customDecimalsError, + } = this.state -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) + return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError + } - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) + hasSelected = () => { + const { customAddress = '', selectedTokens = {} } = this.state + return customAddress || Object.keys(selectedTokens).length > 0 + } - const [ symbol, decimals ] = results - if (symbol && decimals) { - console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + handleNext = () => { + if (this.hasError()) { + return + } + + if (!this.hasSelected()) { + this.setState({ tokenSelectorError: 'Must select at least 1 token.' /* this.context.t('mustSelectOne')*/ }) + return + } + + const { setPendingTokens/* , history*/ } = this.props + const { + customAddress: address, + customSymbol: symbol, + customDecimals: decimals, + selectedTokens, + } = this.state + + const customToken = { + address, + symbol, + decimals, + } + + setPendingTokens({ customToken, selectedTokens }) + // history.push(CONFIRM_ADD_TOKEN_ROUTE) + } + + attemptToAutoFillTokenParams = async (address) => { + const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address) + + const autoFilled = Boolean(symbol && decimals) + this.setState({ autoFilled }) + this.handleCustomSymbolChange(symbol || '') + this.handleCustomDecimalsChange(decimals) + } + + handleCustomAddressChange = (value) => { + const customAddress = value.trim() + this.setState({ + customAddress, + customAddressError: null, + tokenSelectorError: null, + autoFilled: false, + }) + + const isValidAddress = ethUtil.isValidAddress(customAddress) + const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() + + switch (true) { + case !isValidAddress: + this.setState({ + customAddressError: 'Invalid address' /* this.context.t('invalidAddress')*/, + customSymbol: '', + customDecimals: 0, + customSymbolError: null, + customDecimalsError: null, + }) + + break + case Boolean(this.props.identities[standardAddress]): + this.setState({ + customAddressError: 'Personal address detected. Input the token contract address.' /* this.context.t('personalAddressDetected')*/, + }) + + break + case checkExistingAddresses(customAddress, this.props.tokens): + this.setState({ + customAddressError: 'Token has already been added.' /* this.context.t('tokenAlreadyAdded')*/, + }) + + break + default: + if (customAddress !== emptyAddr) { + this.attemptToAutoFillTokenParams(customAddress) + } + } + } + + handleCustomSymbolChange = (value) => { + const customSymbol = value.trim() + const symbolLength = customSymbol.length + let customSymbolError = null + + if (symbolLength <= 0 || symbolLength >= 10) { + customSymbolError = 'Symbol must be between 0 and 10 characters.' /* this.context.t('symbolBetweenZeroTen')*/ + } + + this.setState({ customSymbol, customSymbolError }) + } + + handleCustomDecimalsChange = (value) => { + const customDecimals = value.trim() + const validDecimals = customDecimals !== null && + customDecimals !== '' && + customDecimals >= 0 && + customDecimals < 36 + let customDecimalsError = null + + if (!validDecimals) { + customDecimalsError = 'Decimals must be at least 0, and not over 36.' /* this.context.t('decimalsMustZerotoTen')*/ + } + + this.setState({ customDecimals, customDecimalsError }) } } + +module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen) diff --git a/old-ui/app/app.js b/old-ui/app/app.js index 8b3ab403f..dc2d759a9 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -21,7 +21,9 @@ const NoticeScreen = require('./components/notice') const generateLostAccountsNotice = require('../lib/lost-accounts-notice') // other views const ConfigScreen = require('./config') +// const AddTokenScreen = require('../../ui/app/components/pages/add-token') const AddTokenScreen = require('./add-token') +const ConfirmAddTokenScreen = require('../../ui/app/components/pages/confirm-add-token') const RemoveTokenScreen = require('./remove-token') const Import = require('./accounts/import') const InfoScreen = require('./info') @@ -588,6 +590,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering add-token screen from unlock screen.') return h(AddTokenScreen, {key: 'add-token'}) + case 'confirm-add-token': + log.debug('rendering confirm-add-token screen from unlock screen.') + return h(ConfirmAddTokenScreen, {key: 'confirm-add-token'}) + case 'remove-token': log.debug('rendering remove-token screen from unlock screen.') return h(RemoveTokenScreen, {key: 'remove-token', ...props.currentView.context }) diff --git a/old-ui/app/components/account-export.js b/old-ui/app/components/account-export.js index e01ade414..606b3c1c2 100644 --- a/old-ui/app/components/account-export.js +++ b/old-ui/app/components/account-export.js @@ -80,9 +80,6 @@ ExportAccountView.prototype.render = function () { [ h('button.btn-violet', { onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - style: { - marginRight: '10px', - }, }, 'Cancel'), h('button', { onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), diff --git a/old-ui/app/components/confirm-change-password.js b/old-ui/app/components/confirm-change-password.js index cd5607d3f..8237d23be 100644 --- a/old-ui/app/components/confirm-change-password.js +++ b/old-ui/app/components/confirm-change-password.js @@ -91,9 +91,6 @@ ConfirmChangePassword.prototype.render = function () { }, [ h('button.btn-violet', { - style: { - marginRight: '10px', - }, onClick: () => { this.props.dispatch(actions.showConfigPage()) }, diff --git a/old-ui/app/components/delete-imported-account.js b/old-ui/app/components/delete-imported-account.js index 625288f9c..85a2d0879 100644 --- a/old-ui/app/components/delete-imported-account.js +++ b/old-ui/app/components/delete-imported-account.js @@ -20,7 +20,6 @@ function DeleteImportedAccount () { } DeleteImportedAccount.prototype.render = function () { - console.log('this.props:', this.props) return h('.flex-column.flex-grow', { style: { overflowX: 'auto', @@ -61,9 +60,6 @@ DeleteImportedAccount.prototype.render = function () { }, [ h('button.btn-violet', { - style: { - marginRight: '10px', - }, onClick: () => { this.props.dispatch(actions.showConfigPage()) }, diff --git a/old-ui/app/components/delete-rpc.js b/old-ui/app/components/delete-rpc.js index 9f40d8049..b9a9dd1dc 100644 --- a/old-ui/app/components/delete-rpc.js +++ b/old-ui/app/components/delete-rpc.js @@ -53,9 +53,6 @@ DeleteRpc.prototype.render = function () { }, [ h('button.btn-violet', { - style: { - marginRight: '10px', - }, onClick: () => { this.props.dispatch(actions.showConfigPage()) }, diff --git a/old-ui/app/components/tab-bar.js b/old-ui/app/components/tab-bar.js index 2e9ebd653..583be27dc 100644 --- a/old-ui/app/components/tab-bar.js +++ b/old-ui/app/components/tab-bar.js @@ -24,9 +24,9 @@ TabBar.prototype.render = function () { minHeight: '45px', lineHeight: '45px', }, - }, tabs.map((tab) => { + }, tabs.map((tab, ind) => { const { key, content } = tab - return h(subview === key ? subview === 'history' ? '.activeForm.left' : '.activeForm.right' : '.inactiveForm.pointer', { + return h(subview === key ? ind === 0 ? '.activeForm.left' : '.activeForm.right' : '.inactiveForm.pointer', { onClick: () => { this.setState({ subview: key }) tabSelected(key) diff --git a/old-ui/app/css/confirm-add-token.css b/old-ui/app/css/confirm-add-token.css new file mode 100644 index 000000000..16a0f174a --- /dev/null +++ b/old-ui/app/css/confirm-add-token.css @@ -0,0 +1,46 @@ +.confirm-add-token { + padding: 20px; } + .confirm-add-token__header { + font-size: .75rem; + display: flex; } + .confirm-add-token__token { + flex: 1; + min-width: 0; } + .confirm-add-token__balance { + flex: 0 0 30%; + min-width: 0; } + .confirm-add-token__token-list { + display: flex; + flex-flow: column nowrap; } + .confirm-add-token__token-list .token-balance { + display: flex; + flex-flow: row nowrap; + align-items: flex-start; } + .confirm-add-token__token-list .token-balance__amount { + color: #5d5d5d; + font-size: 43px; + line-height: 43px; + margin-right: 8px; } + .confirm-add-token__token-list .token-balance__symbol { + color: #5d5d5d; + font-size: 16px; + font-weight: 400; + line-height: 24px; } + .confirm-add-token__token-list-item { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-top: 8px; + box-sizing: border-box; } + .confirm-add-token__data { + display: flex; + align-items: center; + padding: 8px; } + .confirm-add-token__name { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } + .confirm-add-token__token-icon { + margin-right: 12px; + flex: 0 0 auto; } \ No newline at end of file diff --git a/old-ui/app/css/index.css b/old-ui/app/css/index.css index 446a0efa6..3e38c3e71 100644 --- a/old-ui/app/css/index.css +++ b/old-ui/app/css/index.css @@ -112,6 +112,7 @@ button, input[type="submit"] { .btn-violet { background: #6729a8; + margin-right: 10px; } button[disabled], input[type="submit"][disabled] { diff --git a/old-ui/app/css/page-container.css b/old-ui/app/css/page-container.css new file mode 100644 index 000000000..16d992e10 --- /dev/null +++ b/old-ui/app/css/page-container.css @@ -0,0 +1,130 @@ +.page-container { + width: 408px; + background-color: #fff; + box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08); + z-index: 25; + display: flex; + flex-flow: column; + border-radius: 8px; } + .page-container__header { + text-align: center; + display: flex; + flex-flow: column; + border-bottom: 1px solid #d2d8dd; + padding: 20px; + flex: 0 0 auto; + position: relative; } + .page-container__header--no-padding-bottom { + padding-bottom: 0; } + .page-container__header-close { + color: #4d4d4d; + position: absolute; + top: 16px; + right: 16px; + cursor: pointer; + overflow: hidden; } + .page-container__header-close::after { + content: '\00D7'; + font-size: 40px; + line-height: 20px; } + .page-container__header-row { + padding-bottom: 10px; + display: flex; + justify-content: space-between; } + .page-container__footer { + flex-flow: row; + justify-content: center; + border-top: 1px solid #d2d8dd; + padding: 16px; + flex: 0 0 auto; } + .page-container__footer .btn-default, + .page-container__footer .btn-confirm { + font-size: 1rem; } + .page-container__footer-container { + float: right; + } + .page-container__footer-button { + height: 55px; + font-size: 1rem; + text-transform: uppercase; + margin-right: 16px; } + .page-container__footer-button:last-of-type { + margin-right: 0; } + .page-container__back-button { + color: #2f9ae0; + font-size: 1rem; + cursor: pointer; + font-weight: 400; } + .page-container__title { + color: #000; + font-size: 2rem; + font-weight: 500; + line-height: 2rem; } + .page-container__subtitle { + padding-top: .5rem; + line-height: initial; + font-size: .9rem; + color: #808080; } + .page-container__tabs { + display: flex; + margin-top: 16px; } + .page-container__tab { + min-width: 5rem; + padding: 8px; + color: #9b9b9b; + font-family: Nunito Regular; + font-size: 1rem; + text-align: center; + cursor: pointer; + border-bottom: none; + margin-right: 16px; } + .page-container__tab:last-of-type { + margin-right: 0; } + .page-container__tab--selected { + color: #2f9ae0; + border-bottom: 3px solid #2f9ae0; } + .page-container--full-width { + width: 100% !important; } + .page-container--full-height { + height: 100% !important; + max-height: initial !important; + min-height: initial !important; } + .page-container__content { + overflow-y: auto; + flex: 1; } + .page-container__warning-container { + background: #fdf4f4; + padding: 20px; + display: flex; + align-items: start; } + .page-container__warning-message { + padding-left: 15px; } + .page-container__warning-title { + font-weight: 500; } + .page-container__warning-icon { + padding-top: 5px; } + +@media screen and (max-width: 250px) { + .page-container__footer { + flex-flow: column-reverse; } + .page-container__footer-button { + width: 100%; + margin-bottom: 1rem; + margin-right: 0; } + .page-container__footer-button:first-of-type { + margin-bottom: 0; } } + +@media screen and (max-width: 575px) { + .page-container { + height: 100%; + width: 100%; + overflow-y: auto; + background-color: #fff; + border-radius: 0; + flex: 1; } } + +@media screen and (min-width: 576px) { + .page-container { + max-height: 82vh; + min-height: 570px; + flex: 0 0 auto; } } \ No newline at end of file diff --git a/old-ui/app/css/search-token.css b/old-ui/app/css/search-token.css new file mode 100644 index 000000000..53ffa9956 --- /dev/null +++ b/old-ui/app/css/search-token.css @@ -0,0 +1,129 @@ +.token-list-placeholder { + display: flex; + align-items: center; + padding-top: 36px; + flex-direction: column; + line-height: 22px; + opacity: .5; } + .token-list-placeholder__text { + color: #aeaeae; + width: 50%; + text-align: center; + margin-top: 8px; } + @media screen and (max-width: 575px) { + .token-list-placeholder__text { + width: 60%; } } + .token-list-placeholder__link { + color: #2f9ae0; } + +.token-list__title { + font-size: .75rem; } + +.token-list__tokens-container { + display: flex; + flex-direction: column; } + +.token-list__token { + transition: 200ms ease-in-out; + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: 8px; + margin-top: 8px; + box-sizing: border-box; + border-radius: 10px; + cursor: pointer; + border: 2px solid transparent; + position: relative; } + .token-list__token:hover { + border: 2px solid rgba(122, 201, 253, 0.5); } + .token-list__token--selected { + border: 2px solid #7ac9fd !important; } + .token-list__token--disabled { + opacity: .4; + pointer-events: none; } + +.token-list__token-icon { + width: 48px; + height: 48px; + background-repeat: no-repeat; + background-size: contain; + background-position: center; + border-radius: 50%; + background-color: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.24); + margin-right: 12px; + flex: 0 0 auto; } + +.token-list__token-data { + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; } + +.token-list__token-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + +.add-token__custom-token-form { + padding: 8px 16px 16px; } + .add-token__custom-token-form input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; } + .add-token__custom-token-form input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; } + +.add-token__search-token { + padding: 20px; } + +.add-token__token-list { + margin-top: 16px; } + +.confirm-add-token { + padding: 16px; } + .confirm-add-token__header { + font-size: .75rem; + display: flex; } + .confirm-add-token__token { + flex: 1; + min-width: 0; } + .confirm-add-token__balance { + flex: 0 0 30%; + min-width: 0; } + .confirm-add-token__token-list { + display: flex; + flex-flow: column nowrap; } + .confirm-add-token__token-list .token-balance { + display: flex; + flex-flow: row nowrap; + align-items: flex-start; } + .confirm-add-token__token-list .token-balance__amount { + color: #5d5d5d; + font-size: 43px; + line-height: 43px; + margin-right: 8px; } + .confirm-add-token__token-list .token-balance__symbol { + color: #5d5d5d; + font-size: 16px; + font-weight: 400; + line-height: 24px; } + .confirm-add-token__token-list-item { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-top: 8px; + box-sizing: border-box; } + .confirm-add-token__data { + display: flex; + align-items: center; + padding: 8px; } + .confirm-add-token__name { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } + .confirm-add-token__token-icon { + margin-right: 12px; + flex: 0 0 auto; } \ No newline at end of file diff --git a/old-ui/app/keychains/hd/restore-vault.js b/old-ui/app/keychains/hd/restore-vault.js index 2344ee034..4bdfb602b 100644 --- a/old-ui/app/keychains/hd/restore-vault.js +++ b/old-ui/app/keychains/hd/restore-vault.js @@ -118,9 +118,6 @@ RestoreVaultScreen.prototype.render = function () { // cancel h('button.btn-violet', { onClick: this.showInitializeMenu.bind(this), - style: { - marginRight: '10px', - }, }, 'Cancel'), // submit diff --git a/old-ui/app/remove-token.js b/old-ui/app/remove-token.js index fa3842414..d64e5a234 100644 --- a/old-ui/app/remove-token.js +++ b/old-ui/app/remove-token.js @@ -56,9 +56,6 @@ RemoveTokenScreen.prototype.render = function () { }, [ h('button.btn-violet', { - style: { - marginRight: '10px', - }, onClick: () => { this.props.dispatch(actions.goHome()) }, diff --git a/old-ui/css.js b/old-ui/css.js index 21b311c28..826032dc7 100644 --- a/old-ui/css.js +++ b/old-ui/css.js @@ -7,6 +7,9 @@ var cssFiles = { 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), + 'search-token.css': fs.readFileSync(path.join(__dirname, '/app/css/search-token.css'), 'utf8'), + 'confirm-add-token.css': fs.readFileSync(path.join(__dirname, '/app/css/confirm-add-token.css'), 'utf8'), + 'page-container.css': fs.readFileSync(path.join(__dirname, '/app/css/page-container.css'), 'utf8'), 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), 'first-time.css': fs.readFileSync(path.join(__dirname, '../mascara/src/app/first-time/index.css'), 'utf8'), diff --git a/ui/app/actions.js b/ui/app/actions.js index 94f8400fd..b89e2a1e7 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -221,8 +221,10 @@ var actions = { SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', showConfigPage, SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + SHOW_CONFIRM_ADD_TOKEN_PAGE: 'SHOW_CONFIRM_ADD_TOKEN_PAGE', SHOW_REMOVE_TOKEN_PAGE: 'SHOW_REMOVE_TOKEN_PAGE', showAddTokenPage, + showConfirmAddTokenPage, showRemoveTokenPage, addToken, addTokens, @@ -1528,6 +1530,7 @@ function showAccountDetail (address) { } function backToAccountDetail (address) { + console.log('### backToAccountDetail ###') return { type: actions.BACK_TO_ACCOUNT_DETAIL, value: address, @@ -1588,6 +1591,13 @@ function showAddTokenPage (transitionForward = true) { } } +function showConfirmAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_CONFIRM_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + function showRemoveTokenPage (token, transitionForward = true) { return { type: actions.SHOW_REMOVE_TOKEN_PAGE, diff --git a/ui/app/components/pages/add-token/add-token.container.js b/ui/app/components/pages/add-token/add-token.container.js index 87671b156..d7ad694cb 100644 --- a/ui/app/components/pages/add-token/add-token.container.js +++ b/ui/app/components/pages/add-token/add-token.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import AddToken from './add-token.component' -const { setPendingTokens, clearPendingTokens } = require('../../../actions') +const { setPendingTokens, clearPendingTokens, showConfirmAddTokenPage } = require('../../../actions') const mapStateToProps = ({ metamask }) => { const { identities, tokens, pendingTokens } = metamask @@ -16,6 +16,7 @@ const mapDispatchToProps = dispatch => { return { setPendingTokens: tokens => dispatch(setPendingTokens(tokens)), clearPendingTokens: () => dispatch(clearPendingTokens()), + showConfirmAddTokenPage: () => dispatch(showConfirmAddTokenPage()), } } diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js index 1611f817b..59badf028 100644 --- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js +++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js @@ -11,16 +11,8 @@ export default class TokenListPlaceholder extends Component {
- { this.context.t('addAcquiredTokens') } + {`Add the tokens you've acquired using Nifty Wallet` /* this.context.t('addAcquiredTokens')*/}
- - { this.context.t('learnMore') } -
) } diff --git a/ui/app/components/pages/add-token/token-search/token-search.component.js b/ui/app/components/pages/add-token/token-search/token-search.component.js index 036b2db1e..a24c01c52 100644 --- a/ui/app/components/pages/add-token/token-search/token-search.component.js +++ b/ui/app/components/pages/add-token/token-search/token-search.component.js @@ -72,7 +72,7 @@ export default class TokenSearch extends Component { return ( this.handleSearch(e.target.value)} diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js index 65d654b92..a928b382b 100644 --- a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js +++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' +// import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' import Button from '../../button' import Identicon from '../../../components/identicon' import TokenBalance from './token-balance' @@ -15,13 +15,15 @@ export default class ConfirmAddToken extends Component { clearPendingTokens: PropTypes.func, addTokens: PropTypes.func, pendingTokens: PropTypes.object, + goHome: PropTypes.func, } componentDidMount () { - const { pendingTokens = {}, history } = this.props + const { pendingTokens = {}, goHome /*, history*/ } = this.props if (Object.keys(pendingTokens).length === 0) { - history.push(DEFAULT_ROUTE) + goHome() + // history.push(DEFAULT_ROUTE) } } @@ -32,26 +34,26 @@ export default class ConfirmAddToken extends Component { } render () { - const { history, addTokens, clearPendingTokens, pendingTokens } = this.props + const { /* history,*/ addTokens, clearPendingTokens, pendingTokens, goHome } = this.props return (
-
- { this.context.t('addTokens') } -
-
- { this.context.t('likeToAddTokens') } -
+

+ { 'Add Tokens' /* this.context.t('addTokens')*/ } +

+

+ { 'Would you like to add these tokens?' /* this.context.t('likeToAddTokens')*/ } +

- { this.context.t('token') } + { 'Token' /* this.context.t('token')*/ }
- { this.context.t('balance') } + { 'Balance' /* this.context.t('balance')*/ }
@@ -86,28 +88,28 @@ export default class ConfirmAddToken extends Component {
- - +
+ + +
) diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js index 0190024d9..542203068 100644 --- a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js +++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import ConfirmAddToken from './confirm-add-token.component' -const { addTokens, clearPendingTokens } = require('../../../actions') +const { addTokens, clearPendingTokens, goHome } = require('../../../actions') const mapStateToProps = ({ metamask }) => { const { pendingTokens } = metamask @@ -14,6 +14,7 @@ const mapDispatchToProps = dispatch => { return { addTokens: tokens => dispatch(addTokens(tokens)), clearPendingTokens: () => dispatch(clearPendingTokens()), + goHome: () => dispatch(goHome()), } } diff --git a/ui/app/i18n-provider.js b/ui/app/i18n-provider.js index d46911f7c..c0f6d00ec 100644 --- a/ui/app/i18n-provider.js +++ b/ui/app/i18n-provider.js @@ -1,7 +1,6 @@ const { Component } = require('react') const connect = require('react-redux').connect const PropTypes = require('prop-types') -const { withRouter } = require('react-router-dom') const { compose } = require('recompose') const t = require('../i18n-helper').getMessage @@ -38,7 +37,6 @@ const mapStateToProps = state => { } module.exports = compose( - withRouter, connect(mapStateToProps) )(I18nProvider) diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 99f5530be..a11a49bc5 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -69,6 +69,9 @@ function reduceApp (state, action) { let curPendingTxIndex = appState.currentView.pendingTxIndex || 0 + console.log(`action.type: ${action.type}`) + console.log(`appState.currentView: ${appState.currentView.name}`) + switch (action.type) { // dropdown methods case actions.NETWORK_DROPDOWN_OPEN: @@ -199,6 +202,16 @@ function reduceApp (state, action) { transForward: action.value, }) + // case actions.SHOW_CONFIRM_ADD_TOKEN_PAGE: + case actions.SET_PENDING_TOKENS: + return extend(appState, { + currentView: { + name: 'confirm-add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + case actions.SHOW_REMOVE_TOKEN_PAGE: return extend(appState, { currentView: { @@ -387,6 +400,7 @@ function reduceApp (state, action) { }) case actions.BACK_TO_ACCOUNT_DETAIL: + console.log('BACK_TO_ACCOUNT_DETAIL') return extend(appState, { currentView: { name: 'accountDetail', diff --git a/ui/app/select-app.js b/ui/app/select-app.js index f2e8e8d10..1b8bf5886 100644 --- a/ui/app/select-app.js +++ b/ui/app/select-app.js @@ -68,5 +68,5 @@ SelectedApp.prototype.render = function () { }, [ h(I18nProvider, [ h(App) ]), ]) - : h(OldApp) + : h(I18nProvider, [ h(OldApp) ]) }