Search tokens

This commit is contained in:
Victor Baranov 2018-09-14 16:57:16 +03:00
parent 09e3d99c17
commit cd1af0fadc
23 changed files with 843 additions and 264 deletions

View File

@ -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)

View File

@ -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 })

View File

@ -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: () => {} }),

View File

@ -91,9 +91,6 @@ ConfirmChangePassword.prototype.render = function () {
}, [
h('button.btn-violet',
{
style: {
marginRight: '10px',
},
onClick: () => {
this.props.dispatch(actions.showConfigPage())
},

View File

@ -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())
},

View File

@ -53,9 +53,6 @@ DeleteRpc.prototype.render = function () {
}, [
h('button.btn-violet',
{
style: {
marginRight: '10px',
},
onClick: () => {
this.props.dispatch(actions.showConfigPage())
},

View File

@ -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)

View File

@ -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; }

View File

@ -112,6 +112,7 @@ button, input[type="submit"] {
.btn-violet {
background: #6729a8;
margin-right: 10px;
}
button[disabled], input[type="submit"][disabled] {

View File

@ -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; } }

View File

@ -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; }

View File

@ -118,9 +118,6 @@ RestoreVaultScreen.prototype.render = function () {
// cancel
h('button.btn-violet', {
onClick: this.showInitializeMenu.bind(this),
style: {
marginRight: '10px',
},
}, 'Cancel'),
// submit

View File

@ -56,9 +56,6 @@ RemoveTokenScreen.prototype.render = function () {
}, [
h('button.btn-violet',
{
style: {
marginRight: '10px',
},
onClick: () => {
this.props.dispatch(actions.goHome())
},

View File

@ -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'),

View File

@ -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,

View File

@ -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()),
}
}

View File

@ -11,16 +11,8 @@ export default class TokenListPlaceholder extends Component {
<div className="token-list-placeholder">
<img src="images/tokensearch.svg" />
<div className="token-list-placeholder__text">
{ this.context.t('addAcquiredTokens') }
{`Add the tokens you've acquired using Nifty Wallet` /* this.context.t('addAcquiredTokens')*/}
</div>
<a
className="token-list-placeholder__link"
href="https://consensys.zendesk.com/hc/en-us/articles/360004135092"
target="_blank"
rel="noopener noreferrer"
>
{ this.context.t('learnMore') }
</a>
</div>
)
}

View File

@ -72,7 +72,7 @@ export default class TokenSearch extends Component {
return (
<TextField
id="search-tokens"
placeholder={this.context.t('searchTokens')}
placeholder={'Search Tokens' /* this.context.t('searchTokens')*/}
type="text"
value={searchQuery}
onChange={e => this.handleSearch(e.target.value)}

View File

@ -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 (
<div className="page-container">
<div className="page-container__header">
<div className="page-container__title">
{ this.context.t('addTokens') }
</div>
<div className="page-container__subtitle">
{ this.context.t('likeToAddTokens') }
</div>
<h2 className="page-subtitle">
{ 'Add Tokens' /* this.context.t('addTokens')*/ }
</h2>
<p className="confirm-label">
{ 'Would you like to add these tokens?' /* this.context.t('likeToAddTokens')*/ }
</p>
</div>
<div className="page-container__content">
<div className="confirm-add-token">
<div className="confirm-add-token__header">
<div className="confirm-add-token__token">
{ this.context.t('token') }
{ 'Token' /* this.context.t('token')*/ }
</div>
<div className="confirm-add-token__balance">
{ this.context.t('balance') }
{ 'Balance' /* this.context.t('balance')*/ }
</div>
</div>
<div className="confirm-add-token__token-list">
@ -86,28 +88,28 @@ export default class ConfirmAddToken extends Component {
</div>
</div>
<div className="page-container__footer">
<Button
type="default"
large
className="page-container__footer-button"
onClick={() => history.push(ADD_TOKEN_ROUTE)}
>
{ this.context.t('back') }
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={() => {
addTokens(pendingTokens)
.then(() => {
clearPendingTokens()
history.push(DEFAULT_ROUTE)
})
}}
>
{ this.context.t('addTokens') }
</Button>
<div className="page-container__footer-container">
<Button
type="default"
className="btn-violet"
onClick={() => goHome()}// history.push(ADD_TOKEN_ROUTE)}
>
{ 'Back' /* this.context.t('back')*/ }
</Button>
<Button
type="primary"
onClick={() => {
addTokens(pendingTokens)
.then(() => {
clearPendingTokens()
goHome()
// history.push(DEFAULT_ROUTE)
})
}}
>
{ 'Add Tokens' /* this.context.t('addTokens')*/ }
</Button>
</div>
</div>
</div>
)

View File

@ -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()),
}
}

View File

@ -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)

View File

@ -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',

View File

@ -68,5 +68,5 @@ SelectedApp.prototype.render = function () {
}, [
h(I18nProvider, [ h(App) ]),
])
: h(OldApp)
: h(I18nProvider, [ h(OldApp) ])
}