diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c64b7248b..0bfa992b4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -171,6 +171,9 @@ "customGas": { "message": "Customize Gas" }, + "customToken": { + "message": "Custom Token" + }, "customize": { "message": "Customize" }, @@ -415,6 +418,9 @@ "message": "JSON File", "description": "format for importing an account" }, + "keepTrackTokens": { + "message": "Keep track of the tokens you’ve bought with your MetaMask account." + }, "kovan": { "message": "Kovan Test Network" }, @@ -424,6 +430,9 @@ "max": { "message": "Max" }, + "learnMore": { + "message": "Learn more." + }, "lessThanMax": { "message": "must be less than or equal to $1.", "description": "helper for inputting hex as decimal input" @@ -564,6 +573,9 @@ "pleaseReviewTransaction": { "message": "Please review your transaction." }, + "popularTokens": { + "message": "Popular Tokens" + }, "privacyMsg": { "message": "Privacy Policy" }, @@ -702,6 +714,9 @@ "onlySendToEtherAddress": { "message": "Only send ETH to an Ethereum address." }, + "searchTokens": { + "message": "Search Tokens" + }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" }, diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js index 228192e7b..cc04beb21 100644 --- a/test/integration/lib/add-token.js +++ b/test/integration/lib/add-token.js @@ -34,8 +34,8 @@ async function runAddTokenFlowTest (assert, done) { let addTokenWrapper = await queryAsync($, '.add-token__wrapper') assert.ok(addTokenWrapper[0], 'add token wrapper renders') - let addTokenTitle = await queryAsync($, '.add-token__title') - assert.equal(addTokenTitle[0].textContent, 'Add Token', 'add token title is correct') + let addTokenTitle = await queryAsync($, '.add-token__header__title') + assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') // Cancel Add Token const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.add-token__cancel-button') @@ -51,9 +51,9 @@ async function runAddTokenFlowTest (assert, done) { // Verify Add Token Screen addTokenWrapper = await queryAsync($, '.add-token__wrapper') - addTokenTitle = await queryAsync($, '.add-token__title') + addTokenTitle = await queryAsync($, '.add-token__header__title') assert.ok(addTokenWrapper[0], 'add token wrapper renders') - assert.equal(addTokenTitle[0].textContent, 'Add Token', 'add token title is correct') + assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') // Search for token const searchInput = await queryAsync($, 'input.add-token__input') @@ -91,9 +91,11 @@ async function runAddTokenFlowTest (assert, done) { assert.ok(addTokenButton[0], 'add token button present') addTokenButton[0].click() - const addCustom = await queryAsync($, '.add-token__add-custom') - assert.ok(addCustom[0], 'add custom token button present') - addCustom[0].click() + const addTokenTabs = await queryAsync($, '.add-token__header__tabs__tab') + assert.equal(addTokenTabs.length, 2, 'expected number of tabs') + assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present') + assert.ok(addTokenTabs[1], 'add custom token tab present') + addTokenTabs[1].click() // Input token contract address const customInput = await queryAsync($, 'input.add-token__add-custom-input') @@ -108,6 +110,7 @@ async function runAddTokenFlowTest (assert, done) { // Verify symbol length error since contract address won't return symbol const errorMessage = await queryAsync($, '.add-token__add-custom-error-message') assert.ok(errorMessage[0], 'error rendered') + $('button.btn-secondary--lg')[0].click() // // Confirm Add token diff --git a/ui/app/add-token.js b/ui/app/add-token.js index edeea11d8..b4ea4a532 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -55,10 +55,10 @@ function AddTokenScreen () { customSymbol: '', customDecimals: '', searchQuery: '', - isCollapsed: true, selectedTokens: {}, errors: {}, autoFilled: false, + displayedTab: 'SEARCH', } this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this) this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this) @@ -192,7 +192,7 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) AddTokenScreen.prototype.renderCustomForm = function () { const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state - return !this.state.isCollapsed && ( + return ( h('div.add-token__add-custom-form', [ h('div', { className: classnames('add-token__add-custom-field', { @@ -247,33 +247,36 @@ AddTokenScreen.prototype.renderTokenList = function () { }) const results = [...addressSearchResult, ...fuseSearchResult] - return Array(6).fill(undefined) - .map((_, i) => { - const { logo, symbol, name, address } = results[i] || {} - const tokenAlreadyAdded = this.checkExistingAddresses(address) - return Boolean(logo || symbol || name) && ( - h('div.add-token__token-wrapper', { - className: classnames({ - 'add-token__token-wrapper--selected': selectedTokens[address], - 'add-token__token-wrapper--disabled': tokenAlreadyAdded, - }), - onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]), - }, [ - h('div.add-token__token-icon', { - style: { - backgroundImage: logo && `url(images/contract/${logo})`, - }, - }), - h('div.add-token__token-data', [ - h('div.add-token__token-symbol', symbol), - h('div.add-token__token-name', name), - ]), - // tokenAlreadyAdded && ( - // h('div.add-token__token-message', 'Already added') - // ), - ]) - ) - }) + return h('div', [ + results.length > 0 && h('div.add-token__token-icons-title', t('popularTokens')), + h('div.add-token__token-icons-container', Array(6).fill(undefined) + .map((_, i) => { + const { logo, symbol, name, address } = results[i] || {} + const tokenAlreadyAdded = this.checkExistingAddresses(address) + return Boolean(logo || symbol || name) && ( + h('div.add-token__token-wrapper', { + className: classnames({ + 'add-token__token-wrapper--selected': selectedTokens[address], + 'add-token__token-wrapper--disabled': tokenAlreadyAdded, + }), + onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]), + }, [ + h('div.add-token__token-icon', { + style: { + backgroundImage: logo && `url(images/contract/${logo})`, + }, + }), + h('div.add-token__token-data', [ + h('div.add-token__token-symbol', symbol), + h('div.add-token__token-name', name), + ]), + // tokenAlreadyAdded && ( + // h('div.add-token__token-message', 'Already added') + // ), + ]) + ) + })), + ]) } AddTokenScreen.prototype.renderConfirmation = function () { @@ -300,7 +303,6 @@ AddTokenScreen.prototype.renderConfirmation = function () { h('div.add-token', [ h('div.add-token__wrapper', [ h('div.add-token__title-container.add-token__confirmation-title', [ - h('div.add-token__title', t('addToken')), h('div.add-token__description', t('likeToAddTokens')), ]), h('div.add-token__content-container.add-token__confirmation-content', [ @@ -332,52 +334,86 @@ AddTokenScreen.prototype.renderConfirmation = function () { ) } +AddTokenScreen.prototype.displayTab = function (selectedTab) { + this.setState({ displayedTab: selectedTab }) +} + +AddTokenScreen.prototype.renderTabs = function () { + const { displayedTab, errors } = this.state + + return displayedTab === 'CUSTOM_TOKEN' + ? this.renderCustomForm() + : h('div', [ + h('div.add-token__wrapper', [ + h('div.add-token__content-container', [ + h('div.add-token__info-box', [ + h('div.add-token__info-box__close'), + h('div.add-token__info-box__title', t('whatsThis')), + h('div.add-token__info-box__copy', t('keepTrackTokens')), + h('div.add-token__info-box__copy--blue', t('learnMore')), + ]), + h('div.add-token__input-container', [ + h('input.add-token__input', { + type: 'text', + placeholder: t('searchTokens'), + onChange: e => this.setState({ searchQuery: e.target.value }), + }), + h('div.add-token__search-input-error-message', errors.tokenSelector), + ]), + this.renderTokenList(), + ]), + ]), + ]) +} + AddTokenScreen.prototype.render = function () { - const { isCollapsed, errors, isShowingConfirmation } = this.state + const { + isShowingConfirmation, + displayedTab, + } = this.state const { goHome } = this.props - return isShowingConfirmation - ? this.renderConfirmation() - : ( - h('div.add-token', [ - h('div.add-token__wrapper', [ - h('div.add-token__title-container', [ - h('div.add-token__title', t('addToken')), - h('div.add-token__description', t('tokenWarning1')), - h('div.add-token__description', t('tokenSelection')), - ]), - h('div.add-token__content-container', [ - h('div.add-token__input-container', [ - h('input.add-token__input', { - type: 'text', - placeholder: t('search'), - onChange: e => this.setState({ searchQuery: e.target.value }), - }), - h('div.add-token__search-input-error-message', errors.tokenSelector), - ]), - h( - 'div.add-token__token-icons-container', - this.renderTokenList(), - ), - ]), - h('div.add-token__footers', [ - h('div.add-token__add-custom', { - onClick: () => this.setState({ isCollapsed: !isCollapsed }), - }, [ - t('addCustomToken'), - h(`i.fa.fa-angle-${isCollapsed ? 'down' : 'up'}`), - ]), - this.renderCustomForm(), - ]), + return h('div.add-token', [ + h('div.add-token__header', [ + h('div.add-token__header__cancel', { + onClick: () => goHome(), + }, [ + h('i.fa.fa-angle-left.fa-lg'), + h('span', t('cancel')), ]), - h('div.add-token__buttons', [ - h('button.btn-secondary--lg.add-token__cancel-button', { - onClick: goHome, - }, t('cancel')), - h('button.btn-primary--lg', { - onClick: this.onNext, - }, t('next')), + h('div.add-token__header__title', t('addTokens')), + !isShowingConfirmation && h('div.add-token__header__tabs', [ + + h('div.add-token__header__tabs__tab', { + className: classnames('add-token__header__tabs__tab', { + 'add-token__header__tabs__selected': displayedTab === 'SEARCH', + 'add-token__header__tabs__unselected cursor-pointer': displayedTab !== 'SEARCH', + }), + onClick: () => this.displayTab('SEARCH'), + }, t('search')), + + h('div.add-token__header__tabs__tab', { + className: classnames('add-token__header__tabs__tab', { + 'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN', + 'add-token__header__tabs__unselected cursor-pointer': displayedTab !== 'CUSTOM_TOKEN', + }), + onClick: () => this.displayTab('CUSTOM_TOKEN'), + }, t('customToken')), + ]), - ]) - ) + ]), +// + isShowingConfirmation + ? this.renderConfirmation() + : this.renderTabs(), + + !isShowingConfirmation && h('div.add-token__buttons', [ + h('button.btn-secondary--lg.add-token__cancel-button', { + onClick: goHome, + }, t('cancel')), + h('button.btn-primary--lg.add-token__confirm-button', { + onClick: this.onNext, + }, t('next')), + ]), + ]) } diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss index bdf9da385..f5c1de67c 100644 --- a/ui/app/css/itcss/components/add-token.scss +++ b/ui/app/css/itcss/components/add-token.scss @@ -1,37 +1,118 @@ .add-token { width: 498px; + max-height: 805px; display: flex; flex-flow: column nowrap; - align-items: center; position: relative; z-index: 12; - font-family: 'DIN Next Light'; + font-family: 'Roboto'; + background: white; + border-radius: 8px; &__wrapper { background-color: $white; - box-shadow: 0 2px 4px 0 rgba($black, .08); display: flex; flex-flow: column nowrap; align-items: center; flex: 0 0 auto; } - &__title-container { + &__header { display: flex; flex-flow: column nowrap; - align-items: center; - padding: 30px 60px 12px; - border-bottom: 1px solid $gallery; + padding: 16px 16px 0px; + border-bottom: 1px solid $geyser; flex: 0 0 auto; + + &__cancel { + color: $dodger-blue; + display: flex; + align-items: center; + + span { + font-family: Roboto; + font-size: 16px; + line-height: 21px; + margin-left: 8px; + } + } + + &__title { + color: $tundora; + font-size: 32px; + font-weight: 500; + margin-top: 4px; + } + + &__tabs { + margin-left: 22px; + display: flex; + + &__tab { + height: 54px; + padding: 15px 10px; + color: $dusty-gray; + font-family: Roboto; + font-size: 18px; + line-height: 24px; + text-align: center; + } + + &__tab:first-of-type { + margin-right: 20px; + } + + &__unselected:hover { + color: $black; + border-bottom: none; + } + + &__selected { + color: $curious-blue; + border-bottom: 3px solid $curious-blue; + } + } } - &__title { - color: $scorpion; - font-size: 20px; - line-height: 26px; - text-align: center; - font-weight: 600; - margin-bottom: 12px; + &__info-box { + height: 96px; + margin: 20px 24px 0px; + border-radius: 4px; + background-color: $alabaster; + position: relative; + padding-left: 18px; + display: flex; + flex-flow: column; + + &__close::after { + content: '\00D7'; + font-size: 29px; + font-weight: 200; + color: $dusty-gray; + position: absolute; + right: 17px; + cursor: pointer; + } + + &__title { + color: $mid-gray; + font-family: Roboto; + font-size: 14px; + margin-top: 15px; + margin-bottom: 9px; + } + + &__copy, + &__copy--blue { + color: $mid-gray; + font-family: Roboto; + font-size: 12px; + line-height: 18px; + } + + &__copy--blue { + color: $curious-blue; + } } &__description { @@ -48,19 +129,17 @@ &__content-container { width: 100%; - border-bottom: 1px solid $gallery; } &__input-container { - padding: 11px 0; - width: 263px; - margin: 0 auto; + display: flex; position: relative; } &__search-input-error-message { position: absolute; bottom: -10px; + left: 22px; font-size: 12px; width: 100%; text-overflow: ellipsis; @@ -69,16 +148,24 @@ color: $red; } - &__input { - width: 100%; - border: 2px solid $gallery; + &__input, + &__add-custom-input { + height: 54px; + padding: 21px 6px; + border: 1px solid $geyser; border-radius: 4px; - padding: 5px 15px; - font-size: 14px; - line-height: 19px; + margin: 22px 24px; + position: relative; + flex: 1 0 auto; + color: $scorpion; + font-family: Roboto; + font-size: 16px; &::placeholder { - color: $silver; + color: $scorpion; + font-family: Roboto; + font-size: 16px; + line-height: 21px; } } @@ -115,13 +202,14 @@ &__add-custom-form { display: flex; flex-flow: column nowrap; - margin: 8px 0 51px; + margin: 40px 0 30px; } &__add-custom-field { - width: 290px; - margin: 0 auto; position: relative; + display: flex; + flex-flow: column; + flex: 1 0 auto; &--error { .add-token__add-custom-input { @@ -132,7 +220,8 @@ &__add-custom-error-message { position: absolute; - bottom: -21px; + bottom: 1px; + left: 22px; font-size: 12px; width: 100%; text-overflow: ellipsis; @@ -144,38 +233,52 @@ &__add-custom-label { font-size: 16px; line-height: 21px; - margin-bottom: 8px; + margin-left: 22px; + color: $scorpion; } &__add-custom-input { - width: 100%; - border: 1px solid $silver; - padding: 5px 15px; - font-size: 14px; - line-height: 19px; + margin-top: 6px; + font-size: 16px; &::placeholder { color: $silver; + font-size: 16px; } } &__add-custom-field + &__add-custom-field { - margin-top: 21px; + margin-top: 6px; } &__buttons { display: flex; flex-flow: row nowrap; - margin: 30px 0 51px; flex: 0 0 auto; align-items: center; justify-content: center; + padding-bottom: 30px; + padding-top: 20px; } + &__confirm-button, &__cancel-button { + margin: 0 12px; + padding: 10px 13px; + height: 54px; + width: 133px; margin-right: 1.2rem; } + &__token-icons-title { + color: #5B5D67; + font-family: Roboto; + font-size: 18px; + line-height: 24px; + margin-left: 24px; + margin-top: 8px; + } + &__token-icons-container { display: flex; flex-flow: row wrap; @@ -188,7 +291,7 @@ flex: 0 0 42.5%; align-items: center; padding: 12px; - margin: 2.5%; + margin: 0% 2.5% 1.5%; box-sizing: border-box; border-radius: 10px; cursor: pointer; @@ -302,13 +405,14 @@ top: 0; width: 100%; overflow: hidden; - height: 100%; + flex: 1 0 auto; &__wrapper { box-shadow: none !important; flex: 1 1 auto; width: 100%; - overflow-y: auto; + overflow-y: scroll; + height: 400px; } &__footers { diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 0031238a8..51548306f 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -51,6 +51,7 @@ $java: #29b6af; $wild-strawberry: #ff4a8d; $cornflower-blue: #7057ff; $saffron: #f6c343; +$dodger-blue: #3099f2; $zumthor: #edf7ff; $ecstasy: #f7861c; diff --git a/yarn.lock b/yarn.lock index 7512e10db..8ccdc7efd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,6 +72,15 @@ normalize-path "^2.0.1" through2 "^2.0.3" +"@sentry/cli@^1.30.3": + version "1.30.3" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.30.3.tgz#02d0f7781c1ee5e1be5a4312a325fbe8b1b37231" + dependencies: + https-proxy-agent "^2.1.1" + node-fetch "^1.7.3" + progress "2.0.0" + proxy-from-env "^1.0.0" + "@types/node@*": version "8.5.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.2.tgz#83b8103fa9a2c2e83d78f701a9aa7c9539739aa5" @@ -201,6 +210,12 @@ agent-base@2: extend "~3.0.0" semver "~5.0.1" +agent-base@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" + dependencies: + es6-promisify "^5.0.0" + ajv-keywords@^1.1.1: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -3711,6 +3726,16 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" +es6-promise@^4.0.3: + version "4.2.4" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + dependencies: + es6-promise "^4.0.3" + es6-set@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" @@ -5919,6 +5944,13 @@ https-proxy-agent@1: debug "2" extend "3" +https-proxy-agent@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.0.tgz#7fbba856be8cd677986f42ebd3664f6317257887" + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + human-standard-token-abi@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/human-standard-token-abi/-/human-standard-token-abi-1.0.2.tgz#207d7846796ee5bb85fdd336e769cb38045b2ae0" @@ -7984,7 +8016,7 @@ nock@^9.0.14: qs "^6.5.1" semver "^5.3.0" -node-fetch@^1.0.1, node-fetch@~1.7.1: +node-fetch@^1.0.1, node-fetch@^1.7.3, node-fetch@~1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" dependencies: @@ -8991,7 +9023,7 @@ process@~0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" -progress@^2.0.0: +progress@2.0.0, progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" @@ -9067,6 +9099,10 @@ proxy-agent@~2.0.0: pac-proxy-agent "1" socks-proxy-agent "2" +proxy-from-env@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -9245,6 +9281,10 @@ raphael@^2.2.0: dependencies: eve-raphael "0.5.0" +raven-js@^3.24.0: + version "3.24.0" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.24.0.tgz#59464d8bc4b3812ae87a282e9bb98ecad5b4b047" + raw-body@2, raw-body@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"