aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app')
-rw-r--r--ui/app/accounts/new-account/index.js2
-rw-r--r--ui/app/actions.js53
-rw-r--r--ui/app/app.js122
-rw-r--r--ui/app/components/app-header/app-header.component.js140
-rw-r--r--ui/app/components/app-header/app-header.container.js38
-rw-r--r--ui/app/components/app-header/index.js2
-rw-r--r--ui/app/components/button/button.component.js19
-rw-r--r--ui/app/components/button/index.js2
-rw-r--r--ui/app/components/export-text-container/index.scss (renamed from ui/app/components/export-text-container/export-text-container.scss)2
-rw-r--r--ui/app/components/index.scss5
-rw-r--r--ui/app/components/info-box/index.js2
-rw-r--r--ui/app/components/info-box/index.scss24
-rw-r--r--ui/app/components/info-box/info-box.component.js49
-rw-r--r--ui/app/components/modals/account-details-modal.js6
-rw-r--r--ui/app/components/modals/edit-account-name-modal.js8
-rw-r--r--ui/app/components/modals/new-account-modal.js2
-rw-r--r--ui/app/components/pages/add-token.js431
-rw-r--r--ui/app/components/pages/add-token/add-token.component.js351
-rw-r--r--ui/app/components/pages/add-token/add-token.container.js22
-rw-r--r--ui/app/components/pages/add-token/index.js2
-rw-r--r--ui/app/components/pages/add-token/index.scss25
-rw-r--r--ui/app/components/pages/add-token/token-list/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-list/index.scss65
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss19
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js27
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list.component.js60
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list.container.js11
-rw-r--r--ui/app/components/pages/add-token/token-search/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-search/token-search.component.js85
-rw-r--r--ui/app/components/pages/add-token/util.js13
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.component.js115
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.container.js20
-rw-r--r--ui/app/components/pages/confirm-add-token/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/index.scss69
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js16
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js16
-rw-r--r--ui/app/components/pages/create-account/index.js2
-rw-r--r--ui/app/components/pages/create-account/new-account.js2
-rw-r--r--ui/app/components/pages/index.scss5
-rw-r--r--ui/app/components/pages/settings/settings.js2
-rw-r--r--ui/app/components/pages/unlock-page/index.js2
-rw-r--r--ui/app/components/pages/unlock-page/index.scss51
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js181
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.container.js31
-rw-r--r--ui/app/components/pages/unlock.js194
-rw-r--r--ui/app/components/pending-tx/confirm-send-ether.js14
-rw-r--r--ui/app/components/signature-request.js2
-rw-r--r--ui/app/components/text-field/index.js2
-rw-r--r--ui/app/components/text-field/text-field.component.js102
-rw-r--r--ui/app/components/text-field/text-field.stories.js24
-rw-r--r--ui/app/components/wallet-view.js6
-rw-r--r--ui/app/css/itcss/components/account-menu.scss4
-rw-r--r--ui/app/css/itcss/components/add-token.scss461
-rw-r--r--ui/app/css/itcss/components/buttons.scss5
-rw-r--r--ui/app/css/itcss/components/header.scss114
-rw-r--r--ui/app/css/itcss/components/index.scss4
-rw-r--r--ui/app/css/itcss/components/modal.scss2
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss2
-rw-r--r--ui/app/css/itcss/components/pages/index.scss2
-rw-r--r--ui/app/css/itcss/components/pages/unlock.scss9
-rw-r--r--ui/app/css/itcss/components/request-signature.scss2
-rw-r--r--ui/app/css/itcss/components/sections.scss41
-rw-r--r--ui/app/css/itcss/components/settings.scss4
-rw-r--r--ui/app/css/itcss/components/welcome-screen.scss97
-rw-r--r--ui/app/css/itcss/generic/index.scss56
-rw-r--r--ui/app/first-time/init-menu.js2
-rw-r--r--ui/app/helpers/with-token-tracker.js105
-rw-r--r--ui/app/main-container.js2
-rw-r--r--ui/app/reducers/metamask.js14
-rw-r--r--ui/app/routes.js2
-rw-r--r--ui/app/select-app.js5
-rw-r--r--ui/app/token-util.js3
-rw-r--r--ui/app/unlock.js141
75 files changed, 1933 insertions, 1597 deletions
diff --git a/ui/app/accounts/new-account/index.js b/ui/app/accounts/new-account/index.js
index 207cf7760..795bd7ce6 100644
--- a/ui/app/accounts/new-account/index.js
+++ b/ui/app/accounts/new-account/index.js
@@ -24,7 +24,7 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
- saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
+ setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
}
}
diff --git a/ui/app/actions.js b/ui/app/actions.js
index f060e40bd..894e31fde 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -124,8 +124,8 @@ var actions = {
SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
showPrivateKey: showPrivateKey,
exportAccountComplete,
- SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL',
- saveAccountLabel: saveAccountLabel,
+ SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
+ setAccountLabel,
// tx conf screen
COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
@@ -271,11 +271,14 @@ var actions = {
SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
// Network
- setNetworkEndpoints,
updateNetworkEndpointType,
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
retryTransaction,
+ SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
+ CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
+ setPendingTokens,
+ clearPendingTokens,
}
module.exports = actions
@@ -317,6 +320,7 @@ function tryUnlockMetamask (password) {
background.verifySeedPhrase(err => {
if (err) {
dispatch(actions.displayWarning(err.message))
+ return reject(err)
}
resolve()
@@ -330,6 +334,7 @@ function tryUnlockMetamask (password) {
.catch(err => {
dispatch(actions.unlockFailed(err.message))
dispatch(actions.hideLoadingIndication())
+ return Promise.reject(err)
})
}
}
@@ -1597,13 +1602,13 @@ function showPrivateKey (key) {
}
}
-function saveAccountLabel (account, label) {
+function setAccountLabel (account, label) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
- log.debug(`background.saveAccountLabel`)
+ log.debug(`background.setAccountLabel`)
return new Promise((resolve, reject) => {
- background.saveAccountLabel(account, label, (err) => {
+ background.setAccountLabel(account, label, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) {
@@ -1612,7 +1617,7 @@ function saveAccountLabel (account, label) {
}
dispatch({
- type: actions.SAVE_ACCOUNT_LABEL,
+ type: actions.SET_ACCOUNT_LABEL,
value: { account, label },
})
@@ -1922,26 +1927,28 @@ function setLocaleMessages (localeMessages) {
}
}
-function setNetworkEndpoints (networkEndpointType) {
- return dispatch => {
- log.debug('background.setNetworkEndpoints')
- return new Promise((resolve, reject) => {
- background.setNetworkEndpoints(networkEndpointType, err => {
- if (err) {
- dispatch(actions.displayWarning(err.message))
- return reject(err)
- }
+function updateNetworkEndpointType (networkEndpointType) {
+ return {
+ type: actions.UPDATE_NETWORK_ENDPOINT_TYPE,
+ value: networkEndpointType,
+ }
+}
- dispatch(actions.updateNetworkEndpointType(networkEndpointType))
- resolve(networkEndpointType)
- })
- })
+function setPendingTokens (pendingTokens) {
+ const { customToken = {}, selectedTokens = {} } = pendingTokens
+ const { address, symbol, decimals } = customToken
+ const tokens = address && symbol && decimals
+ ? { ...selectedTokens, [address]: { ...customToken, isCustom: true } }
+ : selectedTokens
+
+ return {
+ type: actions.SET_PENDING_TOKENS,
+ payload: tokens,
}
}
-function updateNetworkEndpointType (networkEndpointType) {
+function clearPendingTokens () {
return {
- type: actions.UPDATE_NETWORK_ENDPOINT_TYPE,
- value: networkEndpointType,
+ type: actions.CLEAR_PENDING_TOKENS,
}
}
diff --git a/ui/app/app.js b/ui/app/app.js
index c93a6314c..3d2961340 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -22,16 +22,15 @@ const Home = require('./components/pages/home')
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings')
-const UnlockPage = require('./components/pages/unlock')
+const UnlockPage = require('./components/pages/unlock-page')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
+const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading-screen')
-const NetworkIndicator = require('./components/network')
-const Identicon = require('./components/identicon')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const NetworkDropdown = require('./components/dropdowns/network-dropdown')
const AccountMenu = require('./components/account-menu')
@@ -39,6 +38,8 @@ const AccountMenu = require('./components/account-menu')
// Global Modals
const Modal = require('./components/modals/index').Modal
+const AppHeader = require('./components/app-header')
+
// Routes
const {
DEFAULT_ROUTE,
@@ -47,6 +48,7 @@ const {
REVEAL_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
@@ -69,14 +71,15 @@ class App extends Component {
return (
h(Switch, [
h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }),
- h(Initialized, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }),
- h(Initialized, { path: SETTINGS_ROUTE, component: Settings }),
h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }),
- h(Initialized, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
+ h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
+ h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
+ h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
+ h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])
@@ -119,8 +122,7 @@ class App extends Component {
// global modal
h(Modal, {}, []),
- // app bar
- this.renderAppBar(),
+ h(AppHeader),
// sidebar
this.renderSidebar(),
@@ -197,110 +199,6 @@ class App extends Component {
])
}
- renderAppBar () {
- const {
- isUnlocked,
- network,
- provider,
- networkDropdownOpen,
- showNetworkDropdown,
- hideNetworkDropdown,
- isInitialized,
- welcomeScreenSeen,
- isPopup,
- betaUI,
- } = this.props
-
- if (window.METAMASK_UI_TYPE === 'notification') {
- return null
- }
-
- const props = this.props
- const {isMascara, isOnboarding} = props
-
- // Do not render header if user is in mascara onboarding
- if (isMascara && isOnboarding) {
- return null
- }
-
- // Do not render header if user is in mascara buy ether
- if (isMascara && props.currentView.name === 'buyEth') {
- return null
- }
-
- return (
-
- h('.full-width', {
- style: {},
- }, [
-
- (isInitialized || welcomeScreenSeen || isPopup || !betaUI) && h('.app-header.flex-row.flex-space-between', {
- className: classnames({
- 'app-header--initialized': !isOnboarding,
- }),
- }, [
- h('div.app-header-contents', {}, [
- h('div.left-menu-wrapper', {
- onClick: () => props.history.push(DEFAULT_ROUTE),
- }, [
- // mini logo
- h('img.metafox-icon', {
- height: 42,
- width: 42,
- src: '/images/metamask-fox.svg',
- }),
-
- // metamask name
- h('.flex-row', [
- h('h1', this.context.t('appName')),
- h('div.beta-label', this.context.t('beta')),
- ]),
-
- ]),
-
- betaUI && isInitialized && h('div.header__right-actions', [
- h('div.network-component-wrapper', {
- style: {},
- }, [
- // Network Indicator
- h(NetworkIndicator, {
- network,
- provider,
- disabled: this.props.location.pathname === CONFIRM_TRANSACTION_ROUTE,
- onClick: (event) => {
- event.preventDefault()
- event.stopPropagation()
- return networkDropdownOpen === false
- ? showNetworkDropdown()
- : hideNetworkDropdown()
- },
- }),
-
- ]),
-
- isUnlocked && h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [
- h(Identicon, {
- address: this.props.selectedAddress,
- diameter: 32,
- }),
- ]),
- ]),
- ]),
- ]),
-
- !isInitialized && !isPopup && betaUI && h('.alpha-warning__container', {}, [
- h('h2', {
- className: classnames({
- 'alpha-warning': welcomeScreenSeen,
- 'alpha-warning-welcome-screen': !welcomeScreenSeen,
- }),
- }, 'Please be aware that this version is still under development'),
- ]),
-
- ])
- )
- }
-
toggleMetamaskActive () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js
new file mode 100644
index 000000000..62b04562a
--- /dev/null
+++ b/ui/app/components/app-header/app-header.component.js
@@ -0,0 +1,140 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { matchPath } from 'react-router-dom'
+
+const {
+ ENVIRONMENT_TYPE_NOTIFICATION,
+ ENVIRONMENT_TYPE_POPUP,
+} = require('../../../../app/scripts/lib/enums')
+const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes')
+const Identicon = require('../identicon')
+const NetworkIndicator = require('../network')
+
+class AppHeader extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ location: PropTypes.object,
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ networkDropdownOpen: PropTypes.bool,
+ showNetworkDropdown: PropTypes.func,
+ hideNetworkDropdown: PropTypes.func,
+ toggleAccountMenu: PropTypes.func,
+ selectedAddress: PropTypes.string,
+ isUnlocked: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ handleNetworkIndicatorClick (event) {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props
+
+ return networkDropdownOpen === false
+ ? showNetworkDropdown()
+ : hideNetworkDropdown()
+ }
+
+ isConfirming () {
+ const { location } = this.props
+
+ return Boolean(matchPath(location.pathname, {
+ path: CONFIRM_TRANSACTION_ROUTE, exact: false,
+ }))
+ }
+
+ renderAccountMenu () {
+ const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props
+
+ return isUnlocked && (
+ <div
+ className={classnames('account-menu__icon', {
+ 'account-menu__icon--disabled': this.isConfirming(),
+ })}
+ onClick={() => this.isConfirming() || toggleAccountMenu()}
+ >
+ <Identicon
+ address={selectedAddress}
+ diameter={32}
+ />
+ </div>
+ )
+ }
+
+ hideAppHeader () {
+ const { location } = this.props
+
+ const isInitializing = Boolean(matchPath(location.pathname, {
+ path: INITIALIZE_ROUTE, exact: false,
+ }))
+
+ if (isInitializing) {
+ return true
+ }
+
+ if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
+ return true
+ }
+
+ if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) {
+ return true
+ }
+ }
+
+ render () {
+ const {
+ network,
+ provider,
+ history,
+ location,
+ isUnlocked,
+ } = this.props
+
+ if (this.hideAppHeader()) {
+ return null
+ }
+
+ return (
+ <div
+ className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}>
+ <div className="app-header__contents">
+ <div
+ className="app-header__logo-container"
+ onClick={() => history.push(DEFAULT_ROUTE)}
+ >
+ <img
+ className="app-header__metafox"
+ src="/images/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ <div className="flex-row">
+ <h1>{ this.context.t('appName') }</h1>
+ <div className="app-header__beta-label">
+ { this.context.t('beta') }
+ </div>
+ </div>
+ </div>
+ <div className="app-header__account-menu-container">
+ <div className="network-component-wrapper">
+ <NetworkIndicator
+ network={network}
+ provider={provider}
+ onClick={event => this.handleNetworkIndicatorClick(event)}
+ disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE}
+ />
+ </div>
+ { this.renderAccountMenu() }
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default AppHeader
diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app-header/app-header.container.js
new file mode 100644
index 000000000..30d3f8cc4
--- /dev/null
+++ b/ui/app/components/app-header/app-header.container.js
@@ -0,0 +1,38 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+
+import AppHeader from './app-header.component'
+const actions = require('../../actions')
+
+const mapStateToProps = state => {
+ const { appState, metamask } = state
+ const { networkDropdownOpen } = appState
+ const {
+ network,
+ provider,
+ selectedAddress,
+ isUnlocked,
+ } = metamask
+
+ return {
+ networkDropdownOpen,
+ network,
+ provider,
+ selectedAddress,
+ isUnlocked,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
+ hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
+ toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(AppHeader)
diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app-header/index.js
new file mode 100644
index 000000000..daa31f621
--- /dev/null
+++ b/ui/app/components/app-header/index.js
@@ -0,0 +1,2 @@
+import AppHeader from './app-header.container'
+module.exports = AppHeader
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
index 7769e4875..fe3bf363c 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/button/button.component.js
@@ -1,7 +1,6 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const classnames = require('classnames')
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
const SECONDARY = 'secondary'
const CLASSNAME_PRIMARY = 'btn-primary'
@@ -24,10 +23,12 @@ class Button extends Component {
const { type, large, className, ...buttonProps } = this.props
return (
- h('button', {
- className: classnames(getClassName(type, large), className),
- ...buttonProps,
- }, this.props.children)
+ <button
+ className={classnames(getClassName(type, large), className)}
+ { ...buttonProps }
+ >
+ { this.props.children }
+ </button>
)
}
}
@@ -39,5 +40,5 @@ Button.propTypes = {
children: PropTypes.string,
}
-module.exports = Button
+export default Button
diff --git a/ui/app/components/button/index.js b/ui/app/components/button/index.js
index 3dc7d1eea..33ae95ae2 100644
--- a/ui/app/components/button/index.js
+++ b/ui/app/components/button/index.js
@@ -1,2 +1,2 @@
-const Button = require('./button.component')
+import Button from './button.component'
module.exports = Button
diff --git a/ui/app/components/export-text-container/export-text-container.scss b/ui/app/components/export-text-container/index.scss
index a42de8233..975d62f70 100644
--- a/ui/app/components/export-text-container/export-text-container.scss
+++ b/ui/app/components/export-text-container/index.scss
@@ -37,7 +37,7 @@
display: flex;
justify-content: center;
align-items: center;
- font-size: 14px;
+ font-size: 12px;
cursor: pointer;
color: $curious-blue;
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
new file mode 100644
index 000000000..f3fe823f8
--- /dev/null
+++ b/ui/app/components/index.scss
@@ -0,0 +1,5 @@
+@import './export-text-container/index';
+
+@import './info-box/index';
+
+@import './pages/index';
diff --git a/ui/app/components/info-box/index.js b/ui/app/components/info-box/index.js
new file mode 100644
index 000000000..6110422ed
--- /dev/null
+++ b/ui/app/components/info-box/index.js
@@ -0,0 +1,2 @@
+import InfoBox from './info-box.component'
+module.exports = InfoBox
diff --git a/ui/app/components/info-box/index.scss b/ui/app/components/info-box/index.scss
new file mode 100644
index 000000000..8b5626d79
--- /dev/null
+++ b/ui/app/components/info-box/index.scss
@@ -0,0 +1,24 @@
+.info-box {
+ border-radius: 4px;
+ background-color: $alabaster;
+ position: relative;
+ padding: 16px;
+ display: flex;
+ flex-flow: column;
+ color: $mid-gray;
+
+ &__close::after {
+ content: '\00D7';
+ font-size: 29px;
+ font-weight: 200;
+ color: $dusty-gray;
+ position: absolute;
+ right: 12px;
+ top: 0;
+ cursor: pointer;
+ }
+
+ &__description {
+ font-size: .75rem;
+ }
+}
diff --git a/ui/app/components/info-box/info-box.component.js b/ui/app/components/info-box/info-box.component.js
new file mode 100644
index 000000000..8688b8e8f
--- /dev/null
+++ b/ui/app/components/info-box/info-box.component.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class InfoBox extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ onClose: PropTypes.func,
+ title: PropTypes.string,
+ description: PropTypes.string,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ isShowing: true,
+ }
+ }
+
+ handleClose () {
+ const { onClose } = this.props
+
+ if (onClose) {
+ onClose()
+ } else {
+ this.setState({ isShowing: false })
+ }
+ }
+
+ render () {
+ const { title, description } = this.props
+
+ return !this.state.isShowing
+ ? null
+ : (
+ <div className="info-box">
+ <div
+ className="info-box__close"
+ onClick={() => this.handleClose()}
+ />
+ <div className="info-box__title">{ title }</div>
+ <div className="info-box__description">{ description }</div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
index d9885daf5..5607cf051 100644
--- a/ui/app/components/modals/account-details-modal.js
+++ b/ui/app/components/modals/account-details-modal.js
@@ -25,7 +25,7 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
- saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
+ setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
}
}
@@ -49,7 +49,7 @@ AccountDetailsModal.prototype.render = function () {
selectedIdentity,
network,
showExportPrivateKeyModal,
- saveAccountLabel,
+ setAccountLabel,
} = this.props
const { name, address } = selectedIdentity
@@ -57,7 +57,7 @@ AccountDetailsModal.prototype.render = function () {
h(EditableLabel, {
className: 'account-modal__name',
defaultValue: name,
- onSubmit: label => saveAccountLabel(address, label),
+ onSubmit: label => setAccountLabel(address, label),
}),
h(QrView, {
diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/modals/edit-account-name-modal.js
index c79645dbf..5681a3cad 100644
--- a/ui/app/components/modals/edit-account-name-modal.js
+++ b/ui/app/components/modals/edit-account-name-modal.js
@@ -18,8 +18,8 @@ function mapDispatchToProps (dispatch) {
hideModal: () => {
dispatch(actions.hideModal())
},
- saveAccountLabel: (account, label) => {
- dispatch(actions.saveAccountLabel(account, label))
+ setAccountLabel: (account, label) => {
+ dispatch(actions.setAccountLabel(account, label))
},
}
}
@@ -41,7 +41,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(EditAccountNameMod
EditAccountNameModal.prototype.render = function () {
- const { hideModal, saveAccountLabel, identity } = this.props
+ const { hideModal, setAccountLabel, identity } = this.props
return h('div', {}, [
h('div.flex-column.edit-account-name-modal-content', {
@@ -69,7 +69,7 @@ EditAccountNameModal.prototype.render = function () {
h('button.btn-clear.edit-account-name-modal-save-button.allcaps', {
onClick: () => {
if (this.state.inputText.length !== 0) {
- saveAccountLabel(identity.address, this.state.inputText)
+ setAccountLabel(identity.address, this.state.inputText)
hideModal()
}
},
diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js
index 0635b3f72..a66a3ed4a 100644
--- a/ui/app/components/modals/new-account-modal.js
+++ b/ui/app/components/modals/new-account-modal.js
@@ -95,7 +95,7 @@ const mapDispatchToProps = dispatch => {
dispatch(actions.addNewAccount())
.then((newAccountAddress) => {
if (newAccountName) {
- dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName))
+ dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
}
dispatch(actions.hideModal())
})
diff --git a/ui/app/components/pages/add-token.js b/ui/app/components/pages/add-token.js
deleted file mode 100644
index 8d52571d0..000000000
--- a/ui/app/components/pages/add-token.js
+++ /dev/null
@@ -1,431 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const classnames = require('classnames')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const R = require('ramda')
-const Fuse = require('fuse.js')
-const contractMap = require('eth-contract-metadata')
-const TokenBalance = require('../../components/token-balance')
-const Identicon = require('../../components/identicon')
-const contractList = Object.entries(contractMap)
- .map(([ _, tokenData]) => tokenData)
- .filter(tokenData => Boolean(tokenData.erc20))
-const fuse = new Fuse(contractList, {
- shouldSort: true,
- threshold: 0.45,
- location: 0,
- distance: 100,
- maxPatternLength: 32,
- minMatchCharLength: 1,
- keys: [
- { name: 'name', weight: 0.5 },
- { name: 'symbol', weight: 0.5 },
- ],
-})
-const actions = require('../../actions')
-const ethUtil = require('ethereumjs-util')
-const { tokenInfoGetter } = require('../../token-util')
-const { DEFAULT_ROUTE } = require('../../routes')
-
-const emptyAddr = '0x0000000000000000000000000000000000000000'
-
-AddTokenScreen.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
-
-
-function mapStateToProps (state) {
- const { identities, tokens } = state.metamask
- return {
- identities,
- tokens,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- addTokens: tokens => dispatch(actions.addTokens(tokens)),
- }
-}
-
-inherits(AddTokenScreen, Component)
-function AddTokenScreen () {
- this.state = {
- isShowingConfirmation: false,
- isShowingInfoBox: true,
- customAddress: '',
- customSymbol: '',
- customDecimals: '',
- searchQuery: '',
- selectedTokens: {},
- errors: {},
- autoFilled: false,
- displayedTab: 'SEARCH',
- }
- this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
- this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this)
- this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this)
- this.onNext = this.onNext.bind(this)
- Component.call(this)
-}
-
-AddTokenScreen.prototype.componentWillMount = function () {
- this.tokenInfoGetter = tokenInfoGetter()
-}
-
-AddTokenScreen.prototype.toggleToken = function (address, token) {
- const { selectedTokens = {}, errors } = this.state
- const selectedTokensCopy = { ...selectedTokens }
-
- if (address in selectedTokensCopy) {
- delete selectedTokensCopy[address]
- } else {
- selectedTokensCopy[address] = token
- }
-
- this.setState({
- selectedTokens: selectedTokensCopy,
- errors: {
- ...errors,
- tokenSelector: null,
- },
- })
-}
-
-AddTokenScreen.prototype.onNext = function () {
- const { isValid, errors } = this.validate()
-
- return !isValid
- ? this.setState({ errors })
- : this.setState({ isShowingConfirmation: true })
-}
-
-AddTokenScreen.prototype.tokenAddressDidChange = function (e) {
- const customAddress = e.target.value.trim()
- this.setState({ customAddress })
- if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) {
- this.attemptToAutoFillTokenParams(customAddress)
- } else {
- this.setState({
- customSymbol: '',
- customDecimals: 0,
- })
- }
-}
-
-AddTokenScreen.prototype.tokenSymbolDidChange = function (e) {
- const customSymbol = e.target.value.trim()
- this.setState({ customSymbol })
-}
-
-AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) {
- const customDecimals = e.target.value.trim()
- this.setState({ customDecimals })
-}
-
-AddTokenScreen.prototype.checkExistingAddresses = function (address) {
- if (!address) return false
- const tokensList = this.props.tokens
- const matchesAddress = existingToken => {
- return existingToken.address.toLowerCase() === address.toLowerCase()
- }
-
- return R.any(matchesAddress)(tokensList)
-}
-
-AddTokenScreen.prototype.validate = function () {
- const errors = {}
- const identitiesList = Object.keys(this.props.identities)
- const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
- const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
-
- if (customAddress) {
- const validAddress = ethUtil.isValidAddress(customAddress)
- if (!validAddress) {
- errors.customAddress = this.context.t('invalidAddress')
- }
-
- const validDecimals = customDecimals !== null
- && customDecimals !== ''
- && customDecimals >= 0
- && customDecimals < 36
- if (!validDecimals) {
- errors.customDecimals = this.context.t('decimalsMustZerotoTen')
- }
-
- const symbolLen = customSymbol.trim().length
- const validSymbol = symbolLen > 0 && symbolLen < 10
- if (!validSymbol) {
- errors.customSymbol = this.context.t('symbolBetweenZeroTen')
- }
-
- const ownAddress = identitiesList.includes(standardAddress)
- if (ownAddress) {
- errors.customAddress = this.context.t('personalAddressDetected')
- }
-
- const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
- if (tokenAlreadyAdded) {
- errors.customAddress = this.context.t('tokenAlreadyAdded')
- }
- } else if (
- Object.entries(selectedTokens)
- .reduce((isEmpty, [ symbol, isSelected ]) => (
- isEmpty && !isSelected
- ), true)
- ) {
- errors.tokenSelector = this.context.t('mustSelectOne')
- }
-
- return {
- isValid: !Object.keys(errors).length,
- errors,
- }
-}
-
-AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
- const { symbol, decimals } = await this.tokenInfoGetter(address)
- if (symbol && decimals) {
- this.setState({
- customSymbol: symbol,
- customDecimals: decimals,
- autoFilled: true,
- })
- }
-}
-
-AddTokenScreen.prototype.renderCustomForm = function () {
- const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state
-
- return (
- h('div.add-token__add-custom-form', [
- h('div', {
- className: classnames('add-token__add-custom-field', {
- 'add-token__add-custom-field--error': errors.customAddress,
- }),
- }, [
- h('div.add-token__add-custom-label', this.context.t('tokenAddress')),
- h('input.add-token__add-custom-input', {
- type: 'text',
- onChange: this.tokenAddressDidChange,
- value: customAddress,
- }),
- h('div.add-token__add-custom-error-message', errors.customAddress),
- ]),
- h('div', {
- className: classnames('add-token__add-custom-field', {
- 'add-token__add-custom-field--error': errors.customSymbol,
- }),
- }, [
- h('div.add-token__add-custom-label', this.context.t('tokenSymbol')),
- h('input.add-token__add-custom-input', {
- type: 'text',
- onChange: this.tokenSymbolDidChange,
- value: customSymbol,
- disabled: autoFilled,
- }),
- h('div.add-token__add-custom-error-message', errors.customSymbol),
- ]),
- h('div', {
- className: classnames('add-token__add-custom-field', {
- 'add-token__add-custom-field--error': errors.customDecimals,
- }),
- }, [
- h('div.add-token__add-custom-label', this.context.t('decimal')),
- h('input.add-token__add-custom-input', {
- type: 'number',
- onChange: this.tokenDecimalsDidChange,
- value: customDecimals,
- disabled: autoFilled,
- }),
- h('div.add-token__add-custom-error-message', errors.customDecimals),
- ]),
- ])
- )
-}
-
-AddTokenScreen.prototype.renderTokenList = function () {
- const { searchQuery = '', selectedTokens } = this.state
- const fuseSearchResult = fuse.search(searchQuery)
- const addressSearchResult = contractList.filter(token => {
- return token.address.toLowerCase() === searchQuery.toLowerCase()
- })
- const results = [...addressSearchResult, ...fuseSearchResult]
-
- return h('div', [
- results.length > 0 && h('div.add-token__token-icons-title', this.context.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 () {
- const {
- customAddress: address,
- customSymbol: symbol,
- customDecimals: decimals,
- selectedTokens,
- } = this.state
-
- const { addTokens, history } = this.props
-
- const customToken = {
- address,
- symbol,
- decimals,
- }
-
- const tokens = address && symbol && decimals
- ? { ...selectedTokens, [address]: customToken }
- : selectedTokens
-
- return (
- h('div.add-token', [
- h('div.add-token__wrapper', [
- h('div.add-token__content-container.add-token__confirmation-content', [
- h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')),
- h('div.add-token__confirmation-token-list',
- Object.entries(tokens)
- .map(([ address, token ]) => (
- h('span.add-token__confirmation-token-list-item', [
- h(Identicon, {
- className: 'add-token__confirmation-token-icon',
- diameter: 75,
- address,
- }),
- h(TokenBalance, { token }),
- ])
- ))
- ),
- ]),
- ]),
- h('div.add-token__buttons', [
- h('button.btn-secondary--lg.add-token__cancel-button', {
- onClick: () => this.setState({ isShowingConfirmation: false }),
- }, this.context.t('back')),
- h('button.btn-primary--lg', {
- onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
- }, this.context.t('addTokens')),
- ]),
- ])
- )
-}
-
-AddTokenScreen.prototype.displayTab = function (selectedTab) {
- this.setState({ displayedTab: selectedTab })
-}
-
-AddTokenScreen.prototype.renderTabs = function () {
- const { isShowingInfoBox, displayedTab, errors } = this.state
-
- return displayedTab === 'CUSTOM_TOKEN'
- ? this.renderCustomForm()
- : h('div', [
- h('div.add-token__wrapper', [
- h('div.add-token__content-container', [
- isShowingInfoBox && h('div.add-token__info-box', [
- h('div.add-token__info-box__close', {
- onClick: () => this.setState({ isShowingInfoBox: false }),
- }),
- h('div.add-token__info-box__title', this.context.t('whatsThis')),
- h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
- h('a.add-token__info-box__copy--blue', {
- href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
- target: '_blank',
- }, this.context.t('learnMore')),
- ]),
- h('div.add-token__input-container', [
- h('input.add-token__input', {
- type: 'text',
- placeholder: this.context.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 {
- isShowingConfirmation,
- displayedTab,
- } = this.state
- const { history } = this.props
-
- return h('div.add-token', [
- h('div.add-token__header', [
- h('div.add-token__header__cancel', {
- onClick: () => history.push(DEFAULT_ROUTE),
- }, [
- h('i.fa.fa-angle-left.fa-lg'),
- h('span', this.context.t('cancel')),
- ]),
- h('div.add-token__header__title', this.context.t('addTokens')),
- isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
- !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': displayedTab !== 'SEARCH',
- }),
- onClick: () => this.displayTab('SEARCH'),
- }, this.context.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': displayedTab !== 'CUSTOM_TOKEN',
- }),
- onClick: () => this.displayTab('CUSTOM_TOKEN'),
- }, this.context.t('customToken')),
-
- ]),
- ]),
-
- isShowingConfirmation
- ? this.renderConfirmation()
- : this.renderTabs(),
-
- !isShowingConfirmation && h('div.add-token__buttons', [
- h('button.btn-secondary--lg.add-token__cancel-button', {
- onClick: () => history.push(DEFAULT_ROUTE),
- }, this.context.t('cancel')),
- h('button.btn-primary--lg.add-token__confirm-button', {
- onClick: this.onNext,
- }, this.context.t('next')),
- ]),
- ])
-}
diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js
new file mode 100644
index 000000000..0677b4317
--- /dev/null
+++ b/ui/app/components/pages/add-token/add-token.component.js
@@ -0,0 +1,351 @@
+import React, { Component } from 'react'
+import classnames from 'classnames'
+import PropTypes from 'prop-types'
+import ethUtil from 'ethereumjs-util'
+import { checkExistingAddresses } from './util'
+import { tokenInfoGetter } from '../../../token-util'
+import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes'
+import Button from '../../button'
+import TextField from '../../text-field'
+import TokenList from './token-list'
+import TokenSearch from './token-search'
+
+const emptyAddr = '0x0000000000000000000000000000000000000000'
+const SEARCH_TAB = 'SEARCH'
+const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN'
+
+class AddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ setPendingTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ tokens: PropTypes.array,
+ identities: PropTypes.object,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ customAddress: '',
+ customSymbol: '',
+ customDecimals: 0,
+ searchResults: [],
+ selectedTokens: {},
+ tokenSelectorError: null,
+ customAddressError: null,
+ customSymbolError: null,
+ customDecimalsError: null,
+ autoFilled: false,
+ displayedTab: SEARCH_TAB,
+ }
+ }
+
+ 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 })
+ }
+ }
+
+ handleToggleToken (token) {
+ const { address } = token
+ const { selectedTokens = {} } = this.state
+ const selectedTokensCopy = { ...selectedTokens }
+
+ if (address in selectedTokensCopy) {
+ delete selectedTokensCopy[address]
+ } else {
+ selectedTokensCopy[address] = token
+ }
+
+ this.setState({
+ selectedTokens: selectedTokensCopy,
+ tokenSelectorError: null,
+ })
+ }
+
+ hasError () {
+ const {
+ tokenSelectorError,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ } = this.state
+
+ return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError
+ }
+
+ hasSelected () {
+ const { customAddress = '', selectedTokens = {} } = this.state
+ return customAddress || Object.keys(selectedTokens).length > 0
+ }
+
+ handleNext () {
+ if (this.hasError()) {
+ return
+ }
+
+ if (!this.hasSelected()) {
+ this.setState({ tokenSelectorError: 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)
+ }
+
+ async attemptToAutoFillTokenParams (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: this.context.t('invalidAddress'),
+ customSymbol: '',
+ customDecimals: 0,
+ customSymbolError: null,
+ customDecimalsError: null,
+ })
+
+ break
+ case Boolean(this.props.identities[standardAddress]):
+ this.setState({
+ customAddressError: this.context.t('personalAddressDetected'),
+ })
+
+ break
+ case checkExistingAddresses(customAddress, this.props.tokens):
+ this.setState({
+ customAddressError: 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 = 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 = this.context.t('decimalsMustZerotoTen')
+ }
+
+ this.setState({ customDecimals, customDecimalsError })
+ }
+
+ renderCustomTokenForm () {
+ const {
+ customAddress,
+ customSymbol,
+ customDecimals,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ autoFilled,
+ } = this.state
+
+ return (
+ <div className="add-token__custom-token-form">
+ <TextField
+ id="custom-address"
+ label="Token Address"
+ type="text"
+ value={customAddress}
+ onChange={e => this.handleCustomAddressChange(e.target.value)}
+ error={customAddressError}
+ fullWidth
+ margin="normal"
+ />
+ <TextField
+ id="custom-symbol"
+ label="Token Symbol"
+ type="text"
+ value={customSymbol}
+ onChange={e => this.handleCustomSymbolChange(e.target.value)}
+ error={customSymbolError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled}
+ />
+ <TextField
+ id="custom-decimals"
+ label="Decimals of Precision"
+ type="number"
+ value={customDecimals}
+ onChange={e => this.handleCustomDecimalsChange(e.target.value)}
+ error={customDecimalsError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled}
+ />
+ </div>
+ )
+ }
+
+ renderSearchToken () {
+ const { tokenSelectorError, selectedTokens, searchResults } = this.state
+
+ return (
+ <div className="add-token__search-token">
+ <TokenSearch
+ onSearch={({ results = [] }) => this.setState({ searchResults: results })}
+ error={tokenSelectorError}
+ />
+ <div className="add-token__token-list">
+ <TokenList
+ results={searchResults}
+ selectedTokens={selectedTokens}
+ onToggleToken={token => this.handleToggleToken(token)}
+ />
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { displayedTab } = this.state
+ const { history, clearPendingTokens } = this.props
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header page-container__header--no-padding-bottom">
+ <div className="page-container__title">
+ { this.context.t('addTokens') }
+ </div>
+ <div className="page-container__tabs">
+ <div
+ className={classnames('page-container__tab', {
+ 'page-container__tab--selected': displayedTab === SEARCH_TAB,
+ })}
+ onClick={() => this.setState({ displayedTab: SEARCH_TAB })}
+ >
+ { this.context.t('search') }
+ </div>
+ <div
+ className={classnames('page-container__tab', {
+ 'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB,
+ })}
+ onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })}
+ >
+ { this.context.t('customToken') }
+ </div>
+ </div>
+ </div>
+ <div className="page-container__content">
+ {
+ displayedTab === CUSTOM_TOKEN_TAB
+ ? this.renderCustomTokenForm()
+ : this.renderSearchToken()
+ }
+ </div>
+ <div className="page-container__footer">
+ <Button
+ type="secondary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ }}
+ >
+ { this.context.t('cancel') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => this.handleNext()}
+ disabled={this.hasError() || !this.hasSelected()}
+ >
+ { this.context.t('next') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default AddToken
diff --git a/ui/app/components/pages/add-token/add-token.container.js b/ui/app/components/pages/add-token/add-token.container.js
new file mode 100644
index 000000000..87671b156
--- /dev/null
+++ b/ui/app/components/pages/add-token/add-token.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import AddToken from './add-token.component'
+
+const { setPendingTokens, clearPendingTokens } = require('../../../actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { identities, tokens, pendingTokens } = metamask
+ return {
+ identities,
+ tokens,
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setPendingTokens: tokens => dispatch(setPendingTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddToken)
diff --git a/ui/app/components/pages/add-token/index.js b/ui/app/components/pages/add-token/index.js
new file mode 100644
index 000000000..3666cae82
--- /dev/null
+++ b/ui/app/components/pages/add-token/index.js
@@ -0,0 +1,2 @@
+import AddToken from './add-token.container'
+module.exports = AddToken
diff --git a/ui/app/components/pages/add-token/index.scss b/ui/app/components/pages/add-token/index.scss
new file mode 100644
index 000000000..39e86b97b
--- /dev/null
+++ b/ui/app/components/pages/add-token/index.scss
@@ -0,0 +1,25 @@
+@import './token-list/index';
+
+.add-token {
+ &__custom-token-form {
+ padding: 8px 16px 16px;
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+ }
+
+ &__search-token {
+ padding: 16px;
+ }
+
+ &__token-list {
+ margin-top: 16px;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/index.js b/ui/app/components/pages/add-token/token-list/index.js
new file mode 100644
index 000000000..21dd5ac72
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/index.js
@@ -0,0 +1,2 @@
+import TokenList from './token-list.container'
+module.exports = TokenList
diff --git a/ui/app/components/pages/add-token/token-list/index.scss b/ui/app/components/pages/add-token/token-list/index.scss
new file mode 100644
index 000000000..e32739d59
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/index.scss
@@ -0,0 +1,65 @@
+@import './token-list-placeholder/index';
+
+.token-list {
+ &__title {
+ font-size: .75rem;
+ }
+
+ &__tokens-container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__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;
+
+ &:hover {
+ border: 2px solid rgba($malibu-blue, .5);
+ }
+
+ &--selected {
+ border: 2px solid $malibu-blue !important;
+ }
+
+ &--disabled {
+ opacity: .4;
+ pointer-events: none;
+ }
+ }
+
+ &__token-icon {
+ width: 48px;
+ height: 48px;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ border-radius: 50%;
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba($black, .24);
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+
+ &__token-data {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ min-width: 0;
+ }
+
+ &__token-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js
new file mode 100644
index 000000000..b82f45e93
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js
@@ -0,0 +1,2 @@
+import TokenListPlaceholder from './token-list-placeholder.component'
+module.exports = TokenListPlaceholder
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
new file mode 100644
index 000000000..9d0f4be32
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
@@ -0,0 +1,19 @@
+.token-list-placeholder {
+ display: flex;
+ align-items: center;
+ padding-top: 36px;
+ flex-direction: column;
+ line-height: 22px;
+ opacity: .5;
+
+ &__text {
+ color: $silver-chalice;
+ width: 50%;
+ text-align: center;
+ margin-top: 8px;
+ }
+
+ &__link {
+ color: $curious-blue;
+ }
+}
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
new file mode 100644
index 000000000..abd599b26
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class TokenListPlaceholder extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ return (
+ <div className="token-list-placeholder">
+ <img src="images/tokensearch.svg" />
+ <div className="token-list-placeholder__text">
+ { this.context.t('addAcquiredTokens') }
+ </div>
+ <a
+ className="token-list-placeholder__link"
+ href="http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ { this.context.t('learnMore') }
+ </a>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list.component.js b/ui/app/components/pages/add-token/token-list/token-list.component.js
new file mode 100644
index 000000000..724a68d6e
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list.component.js
@@ -0,0 +1,60 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { checkExistingAddresses } from '../util'
+import TokenListPlaceholder from './token-list-placeholder'
+
+export default class InfoBox extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokens: PropTypes.array,
+ results: PropTypes.array,
+ selectedTokens: PropTypes.object,
+ onToggleToken: PropTypes.func,
+ }
+
+ render () {
+ const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props
+
+ return results.length === 0
+ ? <TokenListPlaceholder />
+ : (
+ <div className="token-list">
+ <div className="token-list__title">
+ { this.context.t('searchResults') }
+ </div>
+ <div className="token-list__tokens-container">
+ {
+ Array(6).fill(undefined)
+ .map((_, i) => {
+ const { logo, symbol, name, address } = results[i] || {}
+ const tokenAlreadyAdded = checkExistingAddresses(address, tokens)
+
+ return Boolean(logo || symbol || name) && (
+ <div
+ className={classnames('token-list__token', {
+ 'token-list__token--selected': selectedTokens[address],
+ 'token-list__token--disabled': tokenAlreadyAdded,
+ })}
+ onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])}
+ key={i}
+ >
+ <div
+ className="token-list__token-icon"
+ style={{ backgroundImage: logo && `url(images/contract/${logo})` }}>
+ </div>
+ <div className="token-list__token-data">
+ <span className="token-list__token-name">{ `${name} (${symbol})` }</span>
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list.container.js b/ui/app/components/pages/add-token/token-list/token-list.container.js
new file mode 100644
index 000000000..cd7b07a37
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import TokenList from './token-list.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const { tokens } = metamask
+ return {
+ tokens,
+ }
+}
+
+export default connect(mapStateToProps)(TokenList)
diff --git a/ui/app/components/pages/add-token/token-search/index.js b/ui/app/components/pages/add-token/token-search/index.js
new file mode 100644
index 000000000..acaa6b084
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-search/index.js
@@ -0,0 +1,2 @@
+import TokenSearch from './token-search.component'
+module.exports = TokenSearch
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
new file mode 100644
index 000000000..036b2db1e
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-search/token-search.component.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import contractMap from 'eth-contract-metadata'
+import Fuse from 'fuse.js'
+import InputAdornment from '@material-ui/core/InputAdornment'
+import TextField from '../../../text-field'
+
+const contractList = Object.entries(contractMap)
+ .map(([ _, tokenData]) => tokenData)
+ .filter(tokenData => Boolean(tokenData.erc20))
+
+const fuse = new Fuse(contractList, {
+ shouldSort: true,
+ threshold: 0.45,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: [
+ { name: 'name', weight: 0.5 },
+ { name: 'symbol', weight: 0.5 },
+ ],
+})
+
+export default class TokenSearch extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ error: null,
+ }
+
+ static propTypes = {
+ onSearch: PropTypes.func,
+ error: PropTypes.string,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ searchQuery: '',
+ }
+ }
+
+ handleSearch (searchQuery) {
+ this.setState({ searchQuery })
+ const fuseSearchResult = fuse.search(searchQuery)
+ const addressSearchResult = contractList.filter(token => {
+ return token.address.toLowerCase() === searchQuery.toLowerCase()
+ })
+ const results = [...addressSearchResult, ...fuseSearchResult]
+ this.props.onSearch({ searchQuery, results })
+ }
+
+ renderAdornment () {
+ return (
+ <InputAdornment
+ position="start"
+ style={{ marginRight: '12px' }}
+ >
+ <img src="images/search.svg" />
+ </InputAdornment>
+ )
+ }
+
+ render () {
+ const { error } = this.props
+ const { searchQuery } = this.state
+
+ return (
+ <TextField
+ id="search-tokens"
+ placeholder={this.context.t('searchTokens')}
+ type="text"
+ value={searchQuery}
+ onChange={e => this.handleSearch(e.target.value)}
+ error={error}
+ fullWidth
+ startAdornment={this.renderAdornment()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/util.js b/ui/app/components/pages/add-token/util.js
new file mode 100644
index 000000000..579c56cc0
--- /dev/null
+++ b/ui/app/components/pages/add-token/util.js
@@ -0,0 +1,13 @@
+import R from 'ramda'
+
+export function checkExistingAddresses (address, tokenList = []) {
+ if (!address) {
+ return false
+ }
+
+ const matchesAddress = existingToken => {
+ return existingToken.address.toLowerCase() === address.toLowerCase()
+ }
+
+ return R.any(matchesAddress)(tokenList)
+}
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
new file mode 100644
index 000000000..9db9efc37
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
@@ -0,0 +1,115 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
+import Button from '../../button'
+import Identicon from '../../../components/identicon'
+import TokenBalance from './token-balance'
+
+export default class ConfirmAddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ addTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ }
+
+ componentDidMount () {
+ const { pendingTokens = {}, history } = this.props
+
+ if (Object.keys(pendingTokens).length === 0) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ getTokenName (name, symbol) {
+ return typeof name === 'undefined'
+ ? symbol
+ : `${name} (${symbol})`
+ }
+
+ render () {
+ const { history, addTokens, clearPendingTokens, pendingTokens } = 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>
+ </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') }
+ </div>
+ <div className="confirm-add-token__balance">
+ { this.context.t('balance') }
+ </div>
+ </div>
+ <div className="confirm-add-token__token-list">
+ {
+ Object.entries(pendingTokens)
+ .map(([ address, token ]) => {
+ const { name, symbol } = token
+
+ return (
+ <div
+ className="confirm-add-token__token-list-item"
+ key={address}
+ >
+ <div className="confirm-add-token__token confirm-add-token__data">
+ <Identicon
+ className="confirm-add-token__token-icon"
+ diameter={48}
+ address={address}
+ />
+ <div className="confirm-add-token__name">
+ { this.getTokenName(name, symbol) }
+ </div>
+ </div>
+ <div className="confirm-add-token__balance">
+ <TokenBalance token={token} />
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ </div>
+ <div className="page-container__footer">
+ <Button
+ type="secondary"
+ 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>
+ </div>
+ )
+ }
+}
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
new file mode 100644
index 000000000..0190024d9
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmAddToken from './confirm-add-token.component'
+
+const { addTokens, clearPendingTokens } = require('../../../actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { pendingTokens } = metamask
+ return {
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addTokens: tokens => dispatch(addTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken)
diff --git a/ui/app/components/pages/confirm-add-token/index.js b/ui/app/components/pages/confirm-add-token/index.js
new file mode 100644
index 000000000..b7decabec
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/index.js
@@ -0,0 +1,2 @@
+import ConfirmAddToken from './confirm-add-token.container'
+module.exports = ConfirmAddToken
diff --git a/ui/app/components/pages/confirm-add-token/index.scss b/ui/app/components/pages/confirm-add-token/index.scss
new file mode 100644
index 000000000..66146cf78
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/index.scss
@@ -0,0 +1,69 @@
+.confirm-add-token {
+ padding: 16px;
+
+ &__header {
+ font-size: .75rem;
+ display: flex;
+ }
+
+ &__token {
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__balance {
+ flex: 0 0 30%;
+ min-width: 0;
+ }
+
+ &__token-list {
+ display: flex;
+ flex-flow: column nowrap;
+
+ .token-balance {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: flex-start;
+
+ &__amount {
+ color: $scorpion;
+ font-size: 43px;
+ line-height: 43px;
+ margin-right: 8px;
+ }
+
+ &__symbol {
+ color: $scorpion;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 24px;
+ }
+ }
+ }
+
+ &__token-list-item {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ margin-top: 8px;
+ box-sizing: border-box;
+ }
+
+ &__data {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ }
+
+ &__name {
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__token-icon {
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/index.js b/ui/app/components/pages/confirm-add-token/token-balance/index.js
new file mode 100644
index 000000000..6fb5c8223
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/index.js
@@ -0,0 +1,2 @@
+import TokenBalance from './token-balance.container'
+module.exports = TokenBalance
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
new file mode 100644
index 000000000..976788d4c
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class TokenBalance extends Component {
+ static propTypes = {
+ string: PropTypes.string,
+ symbol: PropTypes.string,
+ error: PropTypes.string,
+ }
+
+ render () {
+ return (
+ <div className="hide-text-overflow">{ this.props.string }</div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
new file mode 100644
index 000000000..bc1289ce1
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import withTokenTracker from '../../../../helpers/with-token-tracker'
+import TokenBalance from './token-balance.component'
+import selectors from '../../../../selectors'
+
+const mapStateToProps = state => {
+ return {
+ userAddress: selectors.getSelectedAddress(state),
+ }
+}
+
+export default compose(
+ connect(mapStateToProps),
+ withTokenTracker
+)(TokenBalance)
diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js
index 0962477d8..475261253 100644
--- a/ui/app/components/pages/create-account/index.js
+++ b/ui/app/components/pages/create-account/index.js
@@ -75,7 +75,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
- saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
+ setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
})
module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 40fa584be..03a5ee72d 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -87,7 +87,7 @@ const mapDispatchToProps = dispatch => {
return dispatch(actions.addNewAccount())
.then(newAccountAddress => {
if (newAccountName) {
- dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName))
+ dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
}
})
},
diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss
new file mode 100644
index 000000000..b15c59863
--- /dev/null
+++ b/ui/app/components/pages/index.scss
@@ -0,0 +1,5 @@
+@import './unlock-page/index';
+
+@import './add-token/index';
+
+@import './confirm-add-token/index';
diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js
index bdefe56f8..f58ac7ddf 100644
--- a/ui/app/components/pages/settings/settings.js
+++ b/ui/app/components/pages/settings/settings.js
@@ -12,7 +12,6 @@ const SimpleDropdown = require('../../dropdowns/simple-dropdown')
const ToggleButton = require('react-toggle-button')
const { REVEAL_SEED_ROUTE } = require('../../../routes')
const locales = require('../../../../../app/_locales/index.json')
-const { OLD_UI_NETWORK_TYPE } = require('../../../../../app/scripts/controllers/network/enums')
const getInfuraCurrencyOptions = () => {
const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
@@ -349,7 +348,6 @@ const mapDispatchToProps = dispatch => {
updateCurrentLocale: key => dispatch(actions.updateCurrentLocale(key)),
setFeatureFlagToBeta: () => {
return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
- .then(() => dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)))
},
showResetAccountConfirmationModal: () => {
return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' }))
diff --git a/ui/app/components/pages/unlock-page/index.js b/ui/app/components/pages/unlock-page/index.js
new file mode 100644
index 000000000..be80cde4f
--- /dev/null
+++ b/ui/app/components/pages/unlock-page/index.js
@@ -0,0 +1,2 @@
+import UnlockPage from './unlock-page.container'
+module.exports = UnlockPage
diff --git a/ui/app/components/pages/unlock-page/index.scss b/ui/app/components/pages/unlock-page/index.scss
new file mode 100644
index 000000000..3d44bd037
--- /dev/null
+++ b/ui/app/components/pages/unlock-page/index.scss
@@ -0,0 +1,51 @@
+.unlock-page {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ width: 357px;
+ padding: 30px;
+ font-weight: 400;
+ color: $silver-chalice;
+
+ &__container {
+ background: $white;
+ display: flex;
+ align-self: stretch;
+ justify-content: center;
+ flex: 1 0 auto;
+ }
+
+ &__mascot-container {
+ margin-top: 24px;
+ }
+
+ &__title {
+ margin-top: 5px;
+ font-size: 2rem;
+ font-weight: 800;
+ color: $tundora;
+ }
+
+ &__form {
+ width: 100%;
+ margin: 56px 0 8px;
+ }
+
+ &__links {
+ margin-top: 25px;
+ width: 100%;
+ }
+
+ &__link {
+ cursor: pointer;
+
+ &--import {
+ color: $ecstasy;
+ }
+
+ &--use-classic {
+ margin-top: 10px;
+ }
+ }
+}
diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js
new file mode 100644
index 000000000..a2f009d8f
--- /dev/null
+++ b/ui/app/components/pages/unlock-page/unlock-page.component.js
@@ -0,0 +1,181 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '@material-ui/core/Button'
+import TextField from '../../text-field'
+
+const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
+const { getEnvironmentType } = require('../../../../../app/scripts/lib/util')
+const getCaretCoordinates = require('textarea-caret')
+const EventEmitter = require('events').EventEmitter
+const Mascot = require('../../mascot')
+const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes')
+
+class UnlockPage extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ password: '',
+ error: null,
+ }
+
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ componentWillMount () {
+ const { isUnlocked, history } = this.props
+
+ if (isUnlocked) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ tryUnlockMetamask (password) {
+ const { tryUnlockMetamask, history } = this.props
+ tryUnlockMetamask(password)
+ .then(() => history.push(DEFAULT_ROUTE))
+ .catch(({ message }) => this.setState({ error: message }))
+ }
+
+ handleSubmit (event) {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const { password } = this.state
+ const { tryUnlockMetamask, history } = this.props
+
+ if (password === '') {
+ return
+ }
+
+ this.setState({ error: null })
+
+ tryUnlockMetamask(password)
+ .then(() => history.push(DEFAULT_ROUTE))
+ .catch(({ message }) => this.setState({ error: message }))
+ }
+
+ handleInputChange ({ target }) {
+ this.setState({ password: target.value, error: null })
+
+ // tell mascot to look at page action
+ const element = target
+ const boundingRect = element.getBoundingClientRect()
+ const coordinates = getCaretCoordinates(element, element.selectionEnd)
+ this.animationEventEmitter.emit('point', {
+ x: boundingRect.left + coordinates.left - element.scrollLeft,
+ y: boundingRect.top + coordinates.top - element.scrollTop,
+ })
+ }
+
+ renderSubmitButton () {
+ const style = {
+ backgroundColor: '#f7861c',
+ color: 'white',
+ marginTop: '20px',
+ height: '60px',
+ fontWeight: '400',
+ boxShadow: 'none',
+ borderRadius: '4px',
+ }
+
+ return (
+ <Button
+ type="submit"
+ style={style}
+ disabled={!this.state.password}
+ fullWidth
+ variant="raised"
+ size="large"
+ onClick={event => this.handleSubmit(event)}
+ disableRipple
+ >
+ { this.context.t('login') }
+ </Button>
+ )
+ }
+
+ render () {
+ const { error } = this.state
+
+ return (
+ <div className="unlock-page__container">
+ <div className="unlock-page">
+ <div className="unlock-page__mascot-container">
+ <Mascot
+ animationEventEmitter={this.animationEventEmitter}
+ width="120"
+ height="120"
+ />
+ </div>
+ <h1 className="unlock-page__title">
+ { this.context.t('welcomeBack') }
+ </h1>
+ <div>{ this.context.t('unlockMessage') }</div>
+ <form
+ className="unlock-page__form"
+ onSubmit={event => this.handleSubmit(event)}
+ >
+ <TextField
+ id="password"
+ label="Password"
+ type="password"
+ value={this.state.password}
+ onChange={event => this.handleInputChange(event)}
+ error={error}
+ autoFocus
+ autoComplete="current-password"
+ material
+ fullWidth
+ />
+ </form>
+ { this.renderSubmitButton() }
+ <div className="unlock-page__links">
+ <div
+ className="unlock-page__link"
+ onClick={() => {
+ this.props.markPasswordForgotten()
+ this.props.history.push(RESTORE_VAULT_ROUTE)
+
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser()
+ }
+ }}
+ >
+ { this.context.t('restoreFromSeed') }
+ </div>
+ <div
+ className="unlock-page__link unlock-page__link--import"
+ onClick={() => {
+ this.props.markPasswordForgotten()
+ this.props.history.push(RESTORE_VAULT_ROUTE)
+
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser()
+ }
+ }}
+ >
+ { this.context.t('importUsingSeed') }
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
+
+UnlockPage.propTypes = {
+ forgotPassword: PropTypes.func,
+ tryUnlockMetamask: PropTypes.func,
+ markPasswordForgotten: PropTypes.func,
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+ t: PropTypes.func,
+ useOldInterface: PropTypes.func,
+}
+
+export default UnlockPage
diff --git a/ui/app/components/pages/unlock-page/unlock-page.container.js b/ui/app/components/pages/unlock-page/unlock-page.container.js
new file mode 100644
index 000000000..18fed9b2e
--- /dev/null
+++ b/ui/app/components/pages/unlock-page/unlock-page.container.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+
+const {
+ tryUnlockMetamask,
+ forgotPassword,
+ markPasswordForgotten,
+} = require('../../../actions')
+
+import UnlockPage from './unlock-page.component'
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked } } = state
+ return {
+ isUnlocked,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ forgotPassword: () => dispatch(forgotPassword()),
+ tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
+ markPasswordForgotten: () => dispatch(markPasswordForgotten()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(UnlockPage)
diff --git a/ui/app/components/pages/unlock.js b/ui/app/components/pages/unlock.js
deleted file mode 100644
index 30144b978..000000000
--- a/ui/app/components/pages/unlock.js
+++ /dev/null
@@ -1,194 +0,0 @@
-const { Component } = require('react')
-const PropTypes = require('prop-types')
-const connect = require('../../metamask-connect')
-const h = require('react-hyperscript')
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const {
- tryUnlockMetamask,
- forgotPassword,
- markPasswordForgotten,
- setNetworkEndpoints,
- setFeatureFlag,
-} = require('../../actions')
-const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
-const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
-const getCaretCoordinates = require('textarea-caret')
-const EventEmitter = require('events').EventEmitter
-const Mascot = require('../mascot')
-const { OLD_UI_NETWORK_TYPE } = require('../../../../app/scripts/controllers/network/enums')
-const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../routes')
-
-class UnlockScreen extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- error: null,
- }
-
- this.animationEventEmitter = new EventEmitter()
- }
-
- componentWillMount () {
- const { isUnlocked, history } = this.props
-
- if (isUnlocked) {
- history.push(DEFAULT_ROUTE)
- }
- }
-
- componentDidMount () {
- const passwordBox = document.getElementById('password-box')
-
- if (passwordBox) {
- passwordBox.focus()
- }
- }
-
- tryUnlockMetamask (password) {
- const { tryUnlockMetamask, history } = this.props
- tryUnlockMetamask(password)
- .then(() => history.push(DEFAULT_ROUTE))
- .catch(({ message }) => this.setState({ error: message }))
- }
-
- onSubmit (event) {
- const input = document.getElementById('password-box')
- const password = input.value
- this.tryUnlockMetamask(password)
- }
-
- onKeyPress (event) {
- if (event.key === 'Enter') {
- this.submitPassword(event)
- }
- }
-
- submitPassword (event) {
- var element = event.target
- var password = element.value
- // reset input
- element.value = ''
- this.tryUnlockMetamask(password)
- }
-
- inputChanged (event) {
- // tell mascot to look at page action
- var element = event.target
- var boundingRect = element.getBoundingClientRect()
- var coordinates = getCaretCoordinates(element, element.selectionEnd)
- this.animationEventEmitter.emit('point', {
- x: boundingRect.left + coordinates.left - element.scrollLeft,
- y: boundingRect.top + coordinates.top - element.scrollTop,
- })
- }
-
- render () {
- const { error } = this.state
- return (
- h('.unlock-screen', [
-
- h(Mascot, {
- animationEventEmitter: this.animationEventEmitter,
- }),
-
- h('h1', {
- style: {
- fontSize: '1.4em',
- textTransform: 'uppercase',
- color: '#7F8082',
- },
- }, this.props.t('appName')),
-
- h('input.large-input', {
- type: 'password',
- id: 'password-box',
- placeholder: 'enter password',
- style: {
- background: 'white',
- },
- onKeyPress: this.onKeyPress.bind(this),
- onInput: this.inputChanged.bind(this),
- }),
-
- h('.error', {
- style: {
- display: error ? 'block' : 'none',
- padding: '0 20px',
- textAlign: 'center',
- },
- }, error),
-
- h('button.primary.cursor-pointer', {
- onClick: this.onSubmit.bind(this),
- style: {
- margin: 10,
- },
- }, this.props.t('login')),
-
- h('p.pointer', {
- onClick: () => {
- this.props.markPasswordForgotten()
- this.props.history.push(RESTORE_VAULT_ROUTE)
-
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser()
- }
- },
- style: {
- fontSize: '0.8em',
- color: 'rgb(247, 134, 28)',
- textDecoration: 'underline',
- },
- }, this.props.t('restoreFromSeed')),
-
- h('p.pointer', {
- onClick: () => {
- this.props.useOldInterface()
- .then(() => this.props.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))
- },
- style: {
- fontSize: '0.8em',
- color: '#aeaeae',
- textDecoration: 'underline',
- marginTop: '32px',
- },
- }, this.props.t('classicInterface')),
- ])
- )
- }
-}
-
-UnlockScreen.propTypes = {
- forgotPassword: PropTypes.func,
- tryUnlockMetamask: PropTypes.func,
- markPasswordForgotten: PropTypes.func,
- history: PropTypes.object,
- isUnlocked: PropTypes.bool,
- t: PropTypes.func,
- useOldInterface: PropTypes.func,
- setNetworkEndpoints: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- const { metamask: { isUnlocked } } = state
- return {
- isUnlocked,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- forgotPassword: () => dispatch(forgotPassword()),
- tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
- markPasswordForgotten: () => dispatch(markPasswordForgotten()),
- useOldInterface: () => dispatch(setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')),
- setNetworkEndpoints: type => dispatch(setNetworkEndpoints(type)),
- }
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(UnlockScreen)
diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js
index 16dbd273b..c07c96ccc 100644
--- a/ui/app/components/pending-tx/confirm-send-ether.js
+++ b/ui/app/components/pending-tx/confirm-send-ether.js
@@ -28,6 +28,10 @@ const currencies = require('currency-formatter/currencies')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
+const {
+ ENVIRONMENT_TYPE_POPUP,
+ ENVIRONMENT_TYPE_NOTIFICATION,
+} = require('../../../../app/scripts/lib/enums')
ConfirmSendEther.contextTypes = {
t: PropTypes.func,
@@ -293,6 +297,14 @@ ConfirmSendEther.prototype.editTransaction = function (txMeta) {
history.push(SEND_ROUTE)
}
+ConfirmSendEther.prototype.renderNetworkDisplay = function () {
+ const windowType = window.METAMASK_UI_TYPE
+
+ return (windowType === ENVIRONMENT_TYPE_NOTIFICATION || windowType === ENVIRONMENT_TYPE_POPUP)
+ ? h(NetworkDisplay)
+ : null
+}
+
ConfirmSendEther.prototype.render = function () {
const {
currentCurrency,
@@ -358,7 +370,7 @@ ConfirmSendEther.prototype.render = function () {
visibility: !txMeta.lastGasPrice ? 'initial' : 'hidden',
},
}, 'Edit'),
- window.METAMASK_UI_TYPE === 'notification' && h(NetworkDisplay),
+ this.renderNetworkDisplay(),
]),
h('.page-container__title', title),
h('.page-container__subtitle', subtitle),
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index b958a2d2d..474fcf439 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -115,7 +115,7 @@ SignatureRequest.prototype.renderBalance = function () {
return h('div.request-signature__balance', [
- h('div.request-signature__balance-text', [this.context.t('balance')]),
+ h('div.request-signature__balance-text', `${this.context.t('balance')}:`),
h('div.request-signature__balance-value', `${balanceInEther} ETH`),
diff --git a/ui/app/components/text-field/index.js b/ui/app/components/text-field/index.js
new file mode 100644
index 000000000..171caf7a4
--- /dev/null
+++ b/ui/app/components/text-field/index.js
@@ -0,0 +1,2 @@
+import TextField from './text-field.component'
+module.exports = TextField
diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/text-field/text-field.component.js
new file mode 100644
index 000000000..b695a449a
--- /dev/null
+++ b/ui/app/components/text-field/text-field.component.js
@@ -0,0 +1,102 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { withStyles } from '@material-ui/core/styles'
+import { default as MaterialTextField } from '@material-ui/core/TextField'
+
+const styles = {
+ materialLabel: {
+ '&$materialFocused': {
+ color: '#aeaeae',
+ },
+ '&$materialError': {
+ color: '#aeaeae',
+ },
+ fontWeight: '400',
+ color: '#aeaeae',
+ },
+ materialFocused: {},
+ materialUnderline: {
+ '&:after': {
+ borderBottom: '2px solid #f7861c',
+ },
+ },
+ materialError: {},
+ // Non-material styles
+ formLabel: {
+ '&$formLabelFocused': {
+ color: '#5b5b5b',
+ },
+ '&$materialError': {
+ color: '#5b5b5b',
+ },
+ },
+ formLabelFocused: {},
+ inputFocused: {},
+ inputRoot: {
+ 'label + &': {
+ marginTop: '8px',
+ },
+ border: '1px solid #d2d8dd',
+ height: '48px',
+ borderRadius: '4px',
+ padding: '0 16px',
+ display: 'flex',
+ alignItems: 'center',
+ '&$inputFocused': {
+ border: '1px solid #2f9ae0',
+ },
+ },
+ inputLabel: {
+ fontSize: '.75rem',
+ transform: 'none',
+ transition: 'none',
+ position: 'initial',
+ color: '#5b5b5b',
+ },
+}
+
+class TextField extends Component {
+ static defaultProps = {
+ error: null,
+ }
+
+ static propTypes = {
+ error: PropTypes.string,
+ classes: PropTypes.object,
+ material: PropTypes.bool,
+ startAdornment: PropTypes.element,
+ }
+
+ render () {
+ const { error, classes, material, startAdornment, ...textFieldProps } = this.props
+
+ return (
+ <MaterialTextField
+ error={Boolean(error)}
+ helperText={error}
+ InputLabelProps={{
+ shrink: material ? undefined : true,
+ className: material ? '' : classes.inputLabel,
+ FormLabelClasses: {
+ root: material ? classes.materialLabel : classes.formLabel,
+ focused: material ? classes.materialFocused : classes.formLabelFocused,
+ error: classes.materialError,
+ },
+ }}
+ InputProps={{
+ startAdornment: startAdornment || undefined,
+ disableUnderline: !material,
+ classes: {
+ root: material ? '' : classes.inputRoot,
+ input: material ? '' : classes.input,
+ underline: material ? classes.materialUnderline : '',
+ focused: material ? '' : classes.inputFocused,
+ },
+ }}
+ {...textFieldProps}
+ />
+ )
+ }
+}
+
+export default withStyles(styles)(TextField)
diff --git a/ui/app/components/text-field/text-field.stories.js b/ui/app/components/text-field/text-field.stories.js
new file mode 100644
index 000000000..ee3e5faaf
--- /dev/null
+++ b/ui/app/components/text-field/text-field.stories.js
@@ -0,0 +1,24 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import TextField from './'
+
+storiesOf('TextField', module)
+ .add('text', () =>
+ <TextField
+ label="Text"
+ type="text"
+ />
+ )
+ .add('password', () =>
+ <TextField
+ label="Password"
+ type="password"
+ />
+ )
+ .add('error', () =>
+ <TextField
+ type="text"
+ label="Name"
+ error="Invalid value"
+ />
+ )
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index 9e430f87b..3b29dacac 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -102,6 +102,7 @@ WalletView.prototype.render = function () {
selectedIdentity,
keyrings,
showAccountDetailModal,
+ sidebarOpen,
hideSidebar,
history,
} = this.props
@@ -182,7 +183,10 @@ WalletView.prototype.render = function () {
h(TokenList),
h('button.btn-primary.wallet-view__add-token-button', {
- onClick: () => history.push(ADD_TOKEN_ROUTE),
+ onClick: () => {
+ history.push(ADD_TOKEN_ROUTE)
+ sidebarOpen && hideSidebar()
+ },
}, this.context.t('addToken')),
])
}
diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss
index 824b2ddb6..657760ab5 100644
--- a/ui/app/css/itcss/components/account-menu.scss
+++ b/ui/app/css/itcss/components/account-menu.scss
@@ -23,6 +23,10 @@
&__icon {
margin-left: 20px;
cursor: pointer;
+
+ &--disabled {
+ cursor: initial;
+ }
}
&__header {
diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss
deleted file mode 100644
index a3ea0d85b..000000000
--- a/ui/app/css/itcss/components/add-token.scss
+++ /dev/null
@@ -1,461 +0,0 @@
-.add-token {
- width: 498px;
- max-height: 805px;
- display: flex;
- flex-flow: column nowrap;
- position: relative;
- z-index: 12;
- font-family: 'Roboto';
- background: white;
- border-radius: 8px;
- box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08);
-
- &__wrapper {
- background-color: $white;
- display: flex;
- flex-flow: column nowrap;
- align-items: center;
- flex: 0 0 auto;
- }
-
- &__header {
- display: flex;
- flex-flow: column nowrap;
- padding: 20px 20px 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;
- font-weight: 400;
- line-height: 21px;
- margin-left: 8px;
- cursor:pointer;
- }
- }
-
- &__title {
- color: $tundora;
- font-size: 32px;
- font-weight: 500;
- margin-top: 4px;
- }
-
- &__subtitle {
- font-weight: 400;
- margin-top: 15px;
- margin-bottom: 21px;
- }
-
- &__tabs {
- display: flex;
-
- &__tab {
- height: 54px;
- padding: 15px 10px;
- color: $dusty-gray;
- font-family: Roboto;
- font-size: 18px;
- font-weight: 400;
- line-height: 24px;
- text-align: center;
- }
-
- &__tab:first-of-type {
- margin-right: 20px;
- }
-
- &__unselected:hover {
- color: $black;
- border-bottom: none;
- cursor: pointer;
- }
-
- &__selected {
- color: $curious-blue;
- border-bottom: 3px solid $curious-blue;
- }
- }
- }
-
- &__info-box {
- height: 96px;
- margin: 20px 20px 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;
- font-weight: 400;
- margin-top: 15px;
- margin-bottom: 9px;
- }
-
- &__copy,
- &__copy--blue {
- color: $mid-gray;
- font-family: Roboto;
- font-size: 12px;
- font-weight: 400;
- line-height: 18px;
- }
-
- &__copy--blue {
- color: $curious-blue;
- }
- }
-
- &__description {
- text-align: center;
- }
-
- &__description + &__description {
- margin-top: 24px;
- }
-
- &__confirmation-description {
- font-weight: 400;
- margin: 20px 0 40px 0;
- }
-
- &__content-container {
- width: 100%;
- }
-
- &__input-container {
- display: flex;
- position: relative;
- }
-
- &__search-input-error-message {
- position: absolute;
- bottom: -10px;
- left: 22px;
- font-size: 12px;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- color: $red;
- }
-
- &__input,
- &__add-custom-input {
- height: 54px;
- padding: 0px 20px;
- border: 1px solid $geyser;
- border-radius: 4px;
- margin: 22px 24px;
- position: relative;
- flex: 1 0 auto;
- color: $scorpion;
- font-family: Roboto;
- font-size: 16px;
-
- &::placeholder {
- color: $scorpion;
- font-family: Roboto;
- font-size: 16px;
- line-height: 21px;
- }
- }
-
- &__footers {
- width: 100%;
- }
-
- &__add-custom {
- color: $scorpion;
- font-size: 18px;
- line-height: 24px;
- text-align: center;
- padding: 12px 0;
- font-weight: 600;
- cursor: pointer;
- position: relative;
-
- &:hover {
- background-color: rgba(0, 0, 0, .05);
- }
-
- &:active {
- background-color: rgba(0, 0, 0, .1);
- }
-
- .fa {
- position: absolute;
- right: 24px;
- font-size: 24px;
- line-height: 24px;
- }
- }
-
- &__add-custom-form {
- display: flex;
- flex-flow: column nowrap;
- margin: 40px 0 30px;
- }
-
- &__add-custom-field {
- position: relative;
- display: flex;
- flex-flow: column;
- flex: 1 0 auto;
-
- &--error {
- .add-token__add-custom-input {
- border-color: $red;
- }
- }
- }
-
- &__add-custom-error-message {
- position: absolute;
- bottom: 1px;
- left: 22px;
- font-size: 12px;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- color: $red;
- }
-
- &__add-custom-label {
- font-size: 16px;
- font-weight: 400;
- line-height: 21px;
- margin-left: 22px;
- color: $scorpion;
- }
-
- &__add-custom-input {
- margin-top: 6px;
- font-size: 16px;
-
- &::placeholder {
- color: $silver;
- font-size: 16px;
- }
- }
-
- &__add-custom-field + &__add-custom-field {
- margin-top: 6px;
- }
-
- &__buttons {
- display: flex;
- flex-flow: row nowrap;
- 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;
- font-weight: 400;
- line-height: 24px;
- margin-left: 24px;
- margin-top: 8px;
- margin-bottom: 20px;
- }
-
- &__token-icons-container {
- display: flex;
- flex-flow: row wrap;
- }
-
- &__token-wrapper {
- transition: 200ms ease-in-out;
- display: flex;
- flex-flow: row nowrap;
- flex: 0 0 42.5%;
- align-items: center;
- padding: 12px;
- margin: 0% 2.5% 1.5%;
- box-sizing: border-box;
- border-radius: 10px;
- cursor: pointer;
- border: 2px solid transparent;
- position: relative;
-
- &:hover {
- border: 2px solid rgba($malibu-blue, .5);
- }
-
- &--selected {
- border: 2px solid $malibu-blue !important;
- }
-
- &--disabled {
- opacity: .4;
- pointer-events: none;
- }
- }
-
- &__token-data {
- align-self: flex-start;
- }
-
- &__token-name {
- font-weight: 400;
- font-size: 14px;
- line-height: 19px;
- }
-
- &__token-symbol {
- font-size: 22px;
- line-height: 29px;
- font-weight: 600;
- }
-
- &__token-icon {
- width: 60px;
- height: 60px;
- background-repeat: no-repeat;
- background-size: contain;
- background-position: center;
- border-radius: 50%;
- background-color: $white;
- box-shadow: 0 2px 4px 0 rgba($black, .24);
- margin-right: 12px;
- flex: 0 0 auto;
- }
-
- &__token-message {
- position: absolute;
- color: $caribbean-green;
- font-size: 11px;
- bottom: 0;
- left: 85px;
- }
-
- &__confirmation-token-list {
- display: flex;
- flex-flow: column nowrap;
-
- .token-balance {
- display: flex;
- flex-flow: row nowrap;
- align-items: flex-start;
-
- &__amount {
- color: $scorpion;
- font-size: 43px;
- line-height: 43px;
- margin-right: 8px;
- }
-
- &__symbol {
- color: $scorpion;
- font-size: 16px;
- font-weight: 400;
- line-height: 24px;
- }
- }
- }
-
- &__confirmation-title {
- padding: 30px 120px 12px;
-
- @media screen and (max-width: $break-small) {
- padding: 20px 0;
- width: 100%;
- }
- }
-
- &__confirmation-content {
- padding-bottom: 60px;
- }
-
- &__confirmation-token-list-item {
- display: flex;
- flex-flow: row nowrap;
- margin: 0 auto;
- align-items: center;
- }
-
- &__confirmation-token-list-item + &__confirmation-token-list-item {
- margin-top: 30px;
- }
-
- &__confirmation-token-icon {
- margin-right: 18px;
- }
-
- @media screen and (max-width: $break-small) {
- top: 0;
- width: 100%;
- overflow: hidden;
- flex: 1 0 auto;
-
- &__wrapper {
- box-shadow: none !important;
- flex: 1 1 auto;
- width: 100%;
- overflow-y: scroll;
- height: 400px;
- }
-
- &__footers {
- border-bottom: 1px solid $gallery;
- }
-
- &__token-icon {
- width: 50px;
- height: 50px;
- }
-
- &__token-symbol {
- font-size: 18px;
- line-height: 24px;
- }
-
- &__token-name {
- font-size: 12px;
- line-height: 16px;
- }
-
- &__buttons {
- padding: 1rem;
- margin: 0;
- border-top: 1px solid $gallery;
- width: 100%;
- }
- }
-}
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index 86daf60d8..4cbed6093 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -15,8 +15,9 @@
font-size: 14px;
font-weight: 500;
transition: border-color .3s ease;
- padding: 0 20px;
+ padding: 0 16px;
min-width: 140px;
+ width: 100%;
text-transform: uppercase;
outline: none;
}
@@ -110,6 +111,7 @@
font-size: .85rem;
font-weight: 400;
transition: border-color .3s ease;
+ width: 100%;
&:hover {
border-color: $scorpion;
@@ -126,6 +128,7 @@
font-size: .85rem;
font-weight: 400;
transition: border-color .3s ease;
+ width: 100%;
}
// No longer used in flat design, remove when modal buttons done
diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss
index eeed9ee06..3ccfd5c15 100644
--- a/ui/app/css/itcss/components/header.scss
+++ b/ui/app/css/itcss/components/header.scss
@@ -1,15 +1,15 @@
.app-header {
align-items: center;
- visibility: visible;
background: $gallery;
position: relative;
z-index: $header-z-index;
display: flex;
flex-flow: column nowrap;
+ width: 100%;
+ flex: 0 0 auto;
@media screen and (max-width: 575px) {
padding: 12px;
- width: 100%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .08);
z-index: $mobile-header-z-index;
}
@@ -17,48 +17,71 @@
@media screen and (min-width: 576px) {
height: 75px;
justify-content: center;
+
+ &--back-drop {
+ &::after {
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: 32px;
+ background: $gallery;
+ bottom: -32px;
+ }
+ }
}
- .metafox-icon {
+ &__metafox {
cursor: pointer;
}
-}
-
-.app-header--initialized {
- @media screen and (min-width: 576px) {
- &::after {
- content: '';
- position: absolute;
- width: 100%;
- height: 32px;
- background: $gallery;
- bottom: -32px;
+ &__beta-label {
+ font-family: Roboto;
+ text-transform: uppercase;
+ font-weight: 500;
+ font-size: .8rem;
+ color: $buttercup;
+ margin-left: 5px;
+ line-height: initial;
+
+ @media screen and (max-width: 575px) {
+ display: none;
}
}
-}
-.app-header-contents {
- display: flex;
- justify-content: space-between;
- flex-flow: row nowrap;
- width: 100%;
- height: 6.9vh;
+ &__contents {
+ display: flex;
+ justify-content: space-between;
+ flex-flow: row nowrap;
+ width: 100%;
- @media screen and (max-width: 575px) {
- height: 100%;
- }
+ @media screen and (max-width: 575px) {
+ height: 100%;
+ }
- @media screen and (min-width: 576px) {
- width: 85vw;
+ @media screen and (min-width: 576px) {
+ width: 85vw;
+ }
+
+ @media screen and (min-width: 769px) {
+ width: 80vw;
+ }
+
+ @media screen and (min-width: 1281px) {
+ width: 62vw;
+ }
}
- @media screen and (min-width: 769px) {
- width: 80vw;
+ &__logo-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ cursor: pointer;
}
- @media screen and (min-width: 1281px) {
- width: 62vw;
+ &__account-menu-container {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
}
}
@@ -76,20 +99,6 @@
}
}
-.beta-label {
- font-family: Roboto;
- text-transform: uppercase;
- font-weight: 500;
- font-size: .8rem;
- color: $buttercup;
- margin-left: 5px;
- line-height: initial;
-
- @media screen and (max-width: 575px) {
- display: none;
- }
-}
-
h2.page-subtitle {
text-transform: uppercase;
color: #aeaeae;
@@ -102,20 +111,3 @@ h2.page-subtitle {
flex-direction: row;
align-items: center;
}
-
-.left-menu-wrapper {
- display: flex;
- flex-direction: row;
- align-items: center;
- cursor: pointer;
-}
-
-.header__right-actions {
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
-
- .identicon {
- cursor: pointer;
- }
-}
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 1c544e162..1d87b8004 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -30,8 +30,6 @@
@import './token-list.scss';
-@import './add-token.scss';
-
@import './currency-display.scss';
@import './account-menu.scss';
@@ -62,4 +60,4 @@
@import './sender-to-recipient.scss';
-@import '../../../components/export-text-container/export-text-container.scss';
+@import '../../../components/index';
diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss
index f972c0f7a..74658f656 100644
--- a/ui/app/css/itcss/components/modal.scss
+++ b/ui/app/css/itcss/components/modal.scss
@@ -566,7 +566,6 @@
padding: 30px;
font-size: 22px;
color: $nile-blue;
- height: 79px;
}
&__message {
@@ -832,7 +831,6 @@
padding: 30px;
font-size: 22px;
color: $nile-blue;
- height: 79px;
}
.notification-modal-message {
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index 2903e07b4..bbe0ee661 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -144,8 +144,8 @@ $wallet-view-bg: $alabaster;
flex: 0 0 auto;
margin: 36px auto;
background: none;
- padding: .7rem 2rem;
transition: border-color .3s ease;
+ width: 150px;
&:hover {
border-color: $curious-blue;
diff --git a/ui/app/css/itcss/components/pages/index.scss b/ui/app/css/itcss/components/pages/index.scss
index d0b59da53..709f8baf6 100644
--- a/ui/app/css/itcss/components/pages/index.scss
+++ b/ui/app/css/itcss/components/pages/index.scss
@@ -1,3 +1 @@
-@import './unlock.scss';
-
@import './reveal-seed.scss';
diff --git a/ui/app/css/itcss/components/pages/unlock.scss b/ui/app/css/itcss/components/pages/unlock.scss
deleted file mode 100644
index 5d438377b..000000000
--- a/ui/app/css/itcss/components/pages/unlock.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.unlock-page {
- box-shadow: none;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: rgb(247, 247, 247);
- width: 100%;
-}
diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss
index 8bba6c98e..e6916c418 100644
--- a/ui/app/css/itcss/components/request-signature.scss
+++ b/ui/app/css/itcss/components/request-signature.scss
@@ -43,8 +43,6 @@
}
&__header__text {
- height: 29px;
- width: 179px;
color: #5B5D67;
font-family: Roboto;
font-size: 22px;
diff --git a/ui/app/css/itcss/components/sections.scss b/ui/app/css/itcss/components/sections.scss
index ace46bd8a..feec71c89 100644
--- a/ui/app/css/itcss/components/sections.scss
+++ b/ui/app/css/itcss/components/sections.scss
@@ -95,19 +95,6 @@ textarea.twelve-word-phrase {
margin: -2px 8px 0px -8px;
}
-.unlock-screen #metamask-mascot-container {
- margin-top: 24px;
-}
-
-.unlock-screen h1 {
- margin-top: -28px;
- margin-bottom: 42px;
-}
-
-.unlock-screen input[type=password] {
- width: 260px;
-}
-
.sizing-input {
font-size: 14px;
height: 30px;
@@ -118,34 +105,6 @@ textarea.twelve-word-phrase {
display: flex;
}
-/* Webkit */
-
-.unlock-screen input::-webkit-input-placeholder {
- text-align: center;
- font-size: 1.2em;
-}
-
-/* Firefox 18- */
-
-.unlock-screen input:-moz-placeholder {
- text-align: center;
- font-size: 1.2em;
-}
-
-/* Firefox 19+ */
-
-.unlock-screen input::-moz-placeholder {
- text-align: center;
- font-size: 1.2em;
-}
-
-/* IE */
-
-.unlock-screen input:-ms-input-placeholder {
- text-align: center;
- font-size: 1.2em;
-}
-
/* accounts */
.accounts-section {
diff --git a/ui/app/css/itcss/components/settings.scss b/ui/app/css/itcss/components/settings.scss
index dcc9b98d5..0dd61ac5e 100644
--- a/ui/app/css/itcss/components/settings.scss
+++ b/ui/app/css/itcss/components/settings.scss
@@ -3,8 +3,6 @@
background: $white;
display: flex;
flex-flow: column nowrap;
- height: auto;
- overflow: auto;
}
.settings__header {
@@ -29,6 +27,8 @@
.settings__content {
padding: 0 25px;
+ height: auto;
+ overflow: auto;
}
.settings__content-row {
diff --git a/ui/app/css/itcss/components/welcome-screen.scss b/ui/app/css/itcss/components/welcome-screen.scss
index bfd174ad9..af1d67398 100644
--- a/ui/app/css/itcss/components/welcome-screen.scss
+++ b/ui/app/css/itcss/components/welcome-screen.scss
@@ -1,59 +1,60 @@
.welcome-screen {
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ align-items: center;
+ font-family: Roboto;
+ font-weight: 400;
+ width: 100%;
+ flex: 1 0 auto;
+ padding: 70px 0;
+ background: $white;
+
+ @media screen and (max-width: 575px) {
+ padding: 0;
+ }
+
+ &__info {
display: flex;
flex-flow: column;
- justify-content: center;
- align-items: center;
- font-family: Roboto;
- font-weight: 400;
width: 100%;
- flex: 1 0 auto;
- padding: 70px 0;
- background: $white;
-
- @media screen and (max-width: 575px) {
- padding: 0;
- }
-
- &__info {
- display: flex;
- flex-flow: column;
- width: 100%;
- height: 100%;
- align-items: center;
-
- &__header {
- font-size: 1.65em;
- margin-bottom: 14px;
-
- @media screen and (max-width: 575px) {
- font-size: 1.5em;
- }
- }
+ height: 100%;
+ align-items: center;
+ justify-content: center;
- &__copy {
- font-size: 1em;
- width: 400px;
- max-width: 90vw;
- text-align: center;
+ &__header {
+ font-size: 1.65em;
+ margin-bottom: 14px;
- @media screen and (max-width: 575px) {
- font-size: 0.9em;
- }
- }
+ @media screen and (max-width: 575px) {
+ font-size: 1.5em;
+ }
}
- &__button {
- height: 54px;
- width: 198px;
- box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14);
- color: #FFFFFF;
- font-size: 20px;
- font-weight: 500;
- line-height: 26px;
+ &__copy {
+ font-size: 1em;
+ width: 400px;
+ max-width: 90vw;
text-align: center;
- text-transform: uppercase;
- margin: 35px 0 14px;
- transition: 200ms ease-in-out;
- background-color: rgba(247, 134, 28, 0.9);
+
+ @media screen and (max-width: 575px) {
+ font-size: .9em;
+ }
}
+ }
+
+ &__button {
+ height: 54px;
+ width: 198px;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14);
+ color: #fff;
+ font-size: 20px;
+ font-weight: 500;
+ line-height: 26px;
+ text-align: center;
+ text-transform: uppercase;
+ margin: 35px 0 14px;
+ transition: 200ms ease-in-out;
+ background-color: rgba(247, 134, 28, .9);
+ }
}
diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss
index b484d5a91..3525d2003 100644
--- a/ui/app/css/itcss/generic/index.scss
+++ b/ui/app/css/itcss/generic/index.scss
@@ -74,28 +74,32 @@ input.large-input {
}
.page-container {
- width: 400px;
+ width: 408px;
background-color: $white;
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
z-index: 25;
display: flex;
flex-flow: column;
- border-radius: 7px;
+ border-radius: 8px;
&__header {
display: flex;
flex-flow: column;
border-bottom: 1px solid $geyser;
- padding: 20px;
+ padding: 16px;
flex: 0 0 auto;
position: relative;
+
+ &--no-padding-bottom {
+ padding-bottom: 0;
+ }
}
&__header-close {
color: $tundora;
position: absolute;
- top: 20px;
- right: 20px;
+ top: 16px;
+ right: 16px;
cursor: pointer;
overflow: hidden;
@@ -117,7 +121,7 @@ input.large-input {
flex-flow: row;
justify-content: center;
border-top: 1px solid $geyser;
- padding: 1.6rem;
+ padding: 16px;
flex: 0 0 auto;
.btn-clear,
@@ -128,11 +132,10 @@ input.large-input {
}
&__footer-button {
- width: 165px;
height: 55px;
font-size: 1rem;
text-transform: uppercase;
- margin-right: 1.2rem;
+ margin-right: 16px;
border-radius: 2px;
&:last-of-type {
@@ -162,25 +165,20 @@ input.large-input {
}
&__tabs {
- padding: 0 1.3rem;
display: flex;
+ margin-top: 16px;
}
&__tab {
min-width: 5rem;
- padding: .2rem .8rem .9rem;
+ padding: 8px;
color: $dusty-gray;
font-family: Roboto;
- font-size: 1.1rem;
- line-height: initial;
+ font-size: 1rem;
text-align: center;
cursor: pointer;
border-bottom: none;
- margin-right: 1rem;
-
- &:hover {
- color: $black;
- }
+ margin-right: 16px;
&:last-of-type {
margin-right: 0;
@@ -189,26 +187,22 @@ input.large-input {
&--selected {
color: $curious-blue;
border-bottom: 3px solid $curious-blue;
-
- &:hover {
- color: $curious-blue;
- }
}
}
&--full-width {
- width: initial;
+ width: 100% !important;
}
&--full-height {
- height: 100%;
+ height: 100% !important;
+ max-height: initial !important;
+ min-height: initial !important;
}
&__content {
- height: 100%;
overflow-y: auto;
- min-height: 250px;
- max-height: 400px;
+ flex: 1;
}
&__warning-container {
@@ -256,12 +250,14 @@ input.large-input {
overflow-y: auto;
background-color: $white;
border-radius: 0;
+ flex: 1;
}
}
@media screen and (min-width: 576px) {
.page-container {
- height: 600px;
+ max-height: 82vh;
+ min-height: 570px;
flex: 0 0 auto;
}
}
@@ -304,3 +300,9 @@ input.form-control {
border: 1px solid $monzo;
}
}
+
+.hide-text-overflow {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js
index 6cb548bb9..c20ba2d77 100644
--- a/ui/app/first-time/init-menu.js
+++ b/ui/app/first-time/init-menu.js
@@ -10,7 +10,6 @@ const getCaretCoordinates = require('textarea-caret')
const { RESTORE_VAULT_ROUTE, DEFAULT_ROUTE } = require('../routes')
const { getEnvironmentType } = require('../../../app/scripts/lib/util')
const { ENVIRONMENT_TYPE_POPUP } = require('../../../app/scripts/lib/enums')
-const { OLD_UI_NETWORK_TYPE } = require('../../../app/scripts/controllers/network/enums')
class InitializeMenuScreen extends Component {
constructor (props) {
@@ -190,7 +189,6 @@ class InitializeMenuScreen extends Component {
showOldUI () {
this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
- .then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)))
}
}
diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/helpers/with-token-tracker.js
new file mode 100644
index 000000000..e24517c18
--- /dev/null
+++ b/ui/app/helpers/with-token-tracker.js
@@ -0,0 +1,105 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import TokenTracker from 'eth-token-tracker'
+
+const withTokenTracker = WrappedComponent => {
+ return class TokenTrackerWrappedComponent extends Component {
+ static propTypes = {
+ userAddress: PropTypes.string.isRequired,
+ token: PropTypes.object.isRequired,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ string: '',
+ symbol: '',
+ error: null,
+ }
+
+ this.tracker = null
+ this.updateBalance = this.updateBalance.bind(this)
+ this.setError = this.setError.bind(this)
+ }
+
+ componentDidMount () {
+ this.createFreshTokenTracker()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props
+ const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps
+
+ if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) {
+ return
+ }
+
+ if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) {
+ return
+ }
+
+ this.createFreshTokenTracker()
+ }
+
+ componentWillUnmount () {
+ this.removeListeners()
+ }
+
+ createFreshTokenTracker () {
+ this.removeListeners()
+
+ if (!global.ethereumProvider) {
+ return
+ }
+
+ const { userAddress, token } = this.props
+
+ this.tracker = new TokenTracker({
+ userAddress,
+ provider: global.ethereumProvider,
+ tokens: [token],
+ pollingInterval: 8000,
+ })
+
+ this.tracker.on('update', this.updateBalance)
+ this.tracker.on('error', this.setError)
+
+ this.tracker.updateBalances()
+ .then(() => this.updateBalance(this.tracker.serialize()))
+ .catch(error => this.setState({ error: error.message }))
+ }
+
+ setError (error) {
+ this.setState({ error })
+ }
+
+ updateBalance (tokens = []) {
+ const [{ string, symbol }] = tokens
+ this.setState({ string, symbol, error: null })
+ }
+
+ removeListeners () {
+ if (this.tracker) {
+ this.tracker.stop()
+ this.tracker.removeListener('update', this.updateBalance)
+ this.tracker.removeListener('error', this.setError)
+ }
+ }
+
+ render () {
+ const { string, symbol, error } = this.state
+
+ return (
+ <WrappedComponent
+ { ...this.props }
+ string={string}
+ symbol={symbol}
+ error={error}
+ />
+ )
+ }
+ }
+}
+
+module.exports = withTokenTracker
diff --git a/ui/app/main-container.js b/ui/app/main-container.js
index c305687ea..c9b05db3b 100644
--- a/ui/app/main-container.js
+++ b/ui/app/main-container.js
@@ -3,7 +3,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountAndTransactionDetails = require('./account-and-transaction-details')
const Settings = require('./components/pages/settings')
-const UnlockScreen = require('./components/pages/unlock')
+const UnlockScreen = require('./components/pages/unlock-page')
const log = require('loglevel')
module.exports = MainContainer
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index bb35cf990..732fa6dea 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -28,6 +28,7 @@ function reduceMetamask (state, action) {
contractExchangeRates: {},
tokenExchangeRates: {},
tokens: [],
+ pendingTokens: {},
send: {
gasLimit: null,
gasPrice: null,
@@ -163,7 +164,7 @@ function reduceMetamask (state, action) {
selectedTokenAddress: action.value,
})
- case actions.SAVE_ACCOUNT_LABEL:
+ case actions.SET_ACCOUNT_LABEL:
const account = action.value.account
const name = action.value.label
const id = {}
@@ -356,6 +357,17 @@ function reduceMetamask (state, action) {
currentLocale: action.value,
})
+ case actions.SET_PENDING_TOKENS:
+ return extend(metamaskState, {
+ pendingTokens: { ...action.payload },
+ })
+
+ case actions.CLEAR_PENDING_TOKENS: {
+ return extend(metamaskState, {
+ pendingTokens: {},
+ })
+ }
+
default:
return metamaskState
diff --git a/ui/app/routes.js b/ui/app/routes.js
index 4b3f8f4d8..0ff3f644d 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -6,6 +6,7 @@ const REVEAL_SEED_ROUTE = '/seed'
const CONFIRM_SEED_ROUTE = '/confirm-seed'
const RESTORE_VAULT_ROUTE = '/restore-vault'
const ADD_TOKEN_ROUTE = '/add-token'
+const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
const SEND_ROUTE = '/send'
@@ -31,6 +32,7 @@ module.exports = {
CONFIRM_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
SEND_ROUTE,
diff --git a/ui/app/select-app.js b/ui/app/select-app.js
index 808f9ba56..f2e8e8d10 100644
--- a/ui/app/select-app.js
+++ b/ui/app/select-app.js
@@ -6,8 +6,7 @@ const { HashRouter } = require('react-router-dom')
const App = require('./app')
const OldApp = require('../../old-ui/app/app')
const { autoAddToBetaUI } = require('./selectors')
-const { setFeatureFlag, setNetworkEndpoints } = require('./actions')
-const { BETA_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums')
+const { setFeatureFlag } = require('./actions')
const I18nProvider = require('./i18n-provider')
function mapStateToProps (state) {
@@ -24,11 +23,9 @@ function mapDispatchToProps (dispatch) {
return {
setFeatureFlagWithModal: () => {
return dispatch(setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
- .then(() => dispatch(setNetworkEndpoints(BETA_UI_NETWORK_TYPE)))
},
setFeatureFlagWithoutModal: () => {
return dispatch(setFeatureFlag('betaUI', true))
- .then(() => dispatch(setNetworkEndpoints(BETA_UI_NETWORK_TYPE)))
},
}
}
diff --git a/ui/app/token-util.js b/ui/app/token-util.js
index 920442bfc..8c5b37d7b 100644
--- a/ui/app/token-util.js
+++ b/ui/app/token-util.js
@@ -1,3 +1,4 @@
+const log = require('loglevel')
const util = require('./util')
function tokenInfoGetter () {
@@ -29,7 +30,7 @@ async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
token.decimals(),
])
} catch (err) {
- console.log(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err)
+ log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err)
}
const [ symbol = [], decimals = [] ] = result
diff --git a/ui/app/unlock.js b/ui/app/unlock.js
deleted file mode 100644
index 099e5f9c6..000000000
--- a/ui/app/unlock.js
+++ /dev/null
@@ -1,141 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const PropTypes = require('prop-types')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const actions = require('./actions')
-const getCaretCoordinates = require('textarea-caret')
-const EventEmitter = require('events').EventEmitter
-const { OLD_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums')
-const { getEnvironmentType } = require('../../app/scripts/lib/util')
-const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums')
-
-const Mascot = require('./components/mascot')
-
-UnlockScreen.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(UnlockScreen)
-
-
-inherits(UnlockScreen, Component)
-function UnlockScreen () {
- Component.call(this)
- this.animationEventEmitter = new EventEmitter()
-}
-
-function mapStateToProps (state) {
- return {
- warning: state.appState.warning,
- }
-}
-
-UnlockScreen.prototype.render = function () {
- const state = this.props
- const warning = state.warning
- return (
- h('.unlock-screen', [
-
- h(Mascot, {
- animationEventEmitter: this.animationEventEmitter,
- }),
-
- h('h1', {
- style: {
- fontSize: '1.4em',
- textTransform: 'uppercase',
- color: '#7F8082',
- },
- }, this.context.t('appName')),
-
- h('input.large-input', {
- type: 'password',
- id: 'password-box',
- placeholder: 'enter password',
- style: {
- background: 'white',
- },
- onKeyPress: this.onKeyPress.bind(this),
- onInput: this.inputChanged.bind(this),
- }),
-
- h('.error', {
- style: {
- display: warning ? 'block' : 'none',
- padding: '0 20px',
- textAlign: 'center',
- },
- }, warning),
-
- h('button.primary.cursor-pointer', {
- onClick: this.onSubmit.bind(this),
- style: {
- margin: 10,
- },
- }, this.context.t('login')),
-
- h('p.pointer', {
- onClick: () => {
- this.props.dispatch(actions.markPasswordForgotten())
- if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
- global.platform.openExtensionInBrowser()
- }
- },
- style: {
- fontSize: '0.8em',
- color: 'rgb(247, 134, 28)',
- textDecoration: 'underline',
- },
- }, this.context.t('restoreFromSeed')),
-
- h('p.pointer', {
- onClick: () => {
- this.props.dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
- .then(() => this.props.dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)))
- },
- style: {
- fontSize: '0.8em',
- color: '#aeaeae',
- textDecoration: 'underline',
- marginTop: '32px',
- },
- }, this.context.t('classicInterface')),
- ])
- )
-}
-
-UnlockScreen.prototype.componentDidMount = function () {
- document.getElementById('password-box').focus()
-}
-
-UnlockScreen.prototype.onSubmit = function (event) {
- const input = document.getElementById('password-box')
- const password = input.value
- this.props.dispatch(actions.tryUnlockMetamask(password))
-}
-
-UnlockScreen.prototype.onKeyPress = function (event) {
- if (event.key === 'Enter') {
- this.submitPassword(event)
- }
-}
-
-UnlockScreen.prototype.submitPassword = function (event) {
- var element = event.target
- var password = element.value
- // reset input
- element.value = ''
- this.props.dispatch(actions.tryUnlockMetamask(password))
-}
-
-UnlockScreen.prototype.inputChanged = function (event) {
- // tell mascot to look at page action
- var element = event.target
- var boundingRect = element.getBoundingClientRect()
- var coordinates = getCaretCoordinates(element, element.selectionEnd)
- this.animationEventEmitter.emit('point', {
- x: boundingRect.left + coordinates.left - element.scrollLeft,
- y: boundingRect.top + coordinates.top - element.scrollTop,
- })
-}