diff options
Diffstat (limited to 'ui/app/components')
94 files changed, 1673 insertions, 1019 deletions
diff --git a/ui/app/components/app/account-details/account-details.component.js b/ui/app/components/app/account-details/account-details.component.js new file mode 100644 index 000000000..ecf2f9428 --- /dev/null +++ b/ui/app/components/app/account-details/account-details.component.js @@ -0,0 +1,99 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Identicon from '../../ui/identicon' +import Tooltip from '../../ui/tooltip-v2' +import copyToClipboard from 'copy-to-clipboard' + +export default class AccountDetails extends Component { + static contextTypes = { + t: PropTypes.func.isRequired, + metricsEvent: PropTypes.func, + } + + static defaultProps = { + hideSidebar: () => {}, + showAccountDetailModal: () => {}, + } + + static propTypes = { + hideSidebar: PropTypes.func, + showAccountDetailModal: PropTypes.func, + label: PropTypes.string.isRequired, + checksummedAddress: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + } + + state = { + hasCopied: false, + copyToClipboardPressed: false, + } + + copyAddress () { + copyToClipboard(this.props.checksummedAddress) + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Copied Address', + }, + }) + this.setState({ hasCopied: true }) + setTimeout(() => this.setState({ hasCopied: false }), 3000) + } + + render () { + const { t } = this.context + + const { + hideSidebar, + showAccountDetailModal, + label, + checksummedAddress, + name, + } = this.props + + const { + hasCopied, + copyToClipboardPressed, + } = this.state + + return ( + <div> + <div className="flex-column account-details"> + <div className="account-details__sidebar-close" onClick={hideSidebar} /> + <div className="account-details__keyring-label allcaps"> + {label} + </div> + <div className="flex-column flex-center account-details__name-container" onClick={showAccountDetailModal}> + <Identicon diameter={54} address={checksummedAddress} /> + <span className="account-details__account-name"> + {name} + </span> + <button className="btn-secondary account-details__details-button"> + {t('details')} + </button> + </div> + </div> + <Tooltip + position={'bottom'} + title={hasCopied ? t('copiedExclamation') : t('copyToClipboard')} + wrapperClassName="account-details__tooltip" + > + <button + className={classnames({ + 'account-details__address': true, + 'account-details__address__pressed': copyToClipboardPressed, + })} + onClick={() => this.copyAddress()} + onMouseDown={() => this.setState({ copyToClipboardPressed: true })} + onMouseUp={() => this.setState({ copyToClipboardPressed: false })} + > + {checksummedAddress.slice(0, 6)}...{checksummedAddress.slice(-4)} + <i className="fa fa-clipboard" style={{ marginLeft: '8px' }} /> + </button> + </Tooltip> + </div> + ) + } +} diff --git a/ui/app/components/app/account-details/account-details.container.js b/ui/app/components/app/account-details/account-details.container.js new file mode 100644 index 000000000..581ff1e2f --- /dev/null +++ b/ui/app/components/app/account-details/account-details.container.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux' +import { hideSidebar, showModal } from '../../../store/actions' +import AccountDetails from './account-details.component' + +function mapDispatchToProps (dispatch) { + return { + hideSidebar: () => dispatch(hideSidebar()), + showAccountDetailModal: () => { + dispatch(showModal({ name: 'ACCOUNT_DETAILS' })) + }, + } +} + +export default connect(null, mapDispatchToProps)(AccountDetails) diff --git a/ui/app/components/app/account-details/index.js b/ui/app/components/app/account-details/index.js new file mode 100644 index 000000000..dca244fee --- /dev/null +++ b/ui/app/components/app/account-details/index.js @@ -0,0 +1 @@ +export { default } from './account-details.container' diff --git a/ui/app/components/app/account-details/index.scss b/ui/app/components/app/account-details/index.scss new file mode 100644 index 000000000..b0a921df3 --- /dev/null +++ b/ui/app/components/app/account-details/index.scss @@ -0,0 +1,79 @@ +.account-details { + flex: 0 0 auto; + + &__keyring-label { + height: 50px; + color: $dusty-gray; + font-family: Roboto; + font-size: 10px; + text-align: right; + padding: 17px 20px 0; + box-sizing: border-box; + } + + &__name-container { + flex: 0 0 auto; + cursor: pointer; + width: 100%; + margin: 0 auto; + } + + &__account-name { + font-size: 24px; + color: $black; + margin-top: 8px; + margin-bottom: .9rem; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + padding: 0 8px; + text-align: center; + } + + &__details-button { + font-size: 10px; + border-radius: 17px; + background-color: transparent; + margin: 0 auto; + padding: 4px 12px; + flex: 0 0 auto; + } + + &__tooltip { + display: flex; + justify-content: center; + align-items: center; + padding: 24px; + } + + &__address { + border-radius: 3px; + background-color: $alto; + color: $scorpion; + font-size: 14px; + line-height: 12px; + padding: 4px 12px; + cursor: pointer; + flex: 0 0 auto; + + &__pressed { + background-color: $manatee, + } + } + + &__sidebar-close { + + @media screen and (max-width: 575px) { + &::after { + content: '\00D7'; + font-size: 40px; + color: $tundora; + position: absolute; + top: 12px; + left: 12px; + cursor: pointer; + } + } + } +} diff --git a/ui/app/components/app/app-header/app-header.component.js b/ui/app/components/app/app-header/app-header.component.js index 171a3499f..7bf7a39bd 100644 --- a/ui/app/components/app/app-header/app-header.component.js +++ b/ui/app/components/app/app-header/app-header.component.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import Identicon from '../../ui/identicon' +import MetaFoxLogo from '../../ui/metafox-logo' import { DEFAULT_ROUTE } from '../../../helpers/constants/routes' const NetworkIndicator = require('../network') @@ -70,6 +71,7 @@ export default class AppHeader extends PureComponent { <Identicon address={selectedAddress} diameter={32} + addBorder={true} /> </div> ) @@ -89,20 +91,10 @@ export default class AppHeader extends PureComponent { <div className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}> <div className="app-header__contents"> - <div - className="app-header__logo-container" + <MetaFoxLogo + unsetIconHeight={true} onClick={() => history.push(DEFAULT_ROUTE)} - > - <img - className="app-header__metafox-logo app-header__metafox-logo--horizontal" - src="/images/logo/metamask-logo-horizontal.svg" - height={30} - /> - <img - className="app-header__metafox-logo app-header__metafox-logo--icon" - src="/images/logo/metamask-fox.svg" - /> - </div> + /> <div className="app-header__account-menu-container"> { !hideNetworkIndicator && ( diff --git a/ui/app/components/app/app-header/index.scss b/ui/app/components/app/app-header/index.scss index d3f37b7a2..0ea1793ca 100644 --- a/ui/app/components/app/app-header/index.scss +++ b/ui/app/components/app/app-header/index.scss @@ -10,7 +10,6 @@ @media screen and (max-width: 575px) { padding: 1rem; - box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); z-index: $mobile-header-z-index; } @@ -24,7 +23,7 @@ position: absolute; width: 100%; height: 32px; - background: $gallery; + background: $Grey-000; bottom: -32px; } } diff --git a/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js index c8507985d..95ca8144a 100644 --- a/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js @@ -27,7 +27,7 @@ describe('Confirm Detail Row Component', function () { ) }) - describe('render', () => { + describe('render', () => { it('should render a div with a confirm-detail-row class', () => { assert.equal(wrapper.find('div.confirm-detail-row').length, 1) }) @@ -60,5 +60,5 @@ describe('Confirm Detail Row Component', function () { wrapper.find('.confirm-detail-row__header-text').props().onClick() assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1)) }) - }) + }) }) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js index 8327f997b..c24d24b17 100755 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js @@ -6,9 +6,9 @@ const ConfirmPageContainerNavigation = props => { return ( <div className="confirm-page-container-navigation" - style={{ - display: showNavigation ? 'flex' : 'none', - }} + style={{ + display: showNavigation ? 'flex' : 'none', + }} > <div className="confirm-page-container-navigation__container" style={{ diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index 326e4f83e..1ff797fa1 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -106,16 +106,16 @@ export default class ConfirmPageContainer extends Component { return ( <div className="page-container"> <ConfirmPageContainerNavigation - totalTx={totalTx} - positionOfCurrentTx={positionOfCurrentTx} - nextTxId={nextTxId} - prevTxId={prevTxId} - showNavigation={showNavigation} - onNextTx={(txId) => onNextTx(txId)} - firstTx={firstTx} - lastTx={lastTx} - ofText={ofText} - requestsWaitingText={requestsWaitingText} + totalTx={totalTx} + positionOfCurrentTx={positionOfCurrentTx} + nextTxId={nextTxId} + prevTxId={prevTxId} + showNavigation={showNavigation} + onNextTx={(txId) => onNextTx(txId)} + firstTx={firstTx} + lastTx={lastTx} + ofText={ofText} + requestsWaitingText={requestsWaitingText} /> <ConfirmPageContainerHeader showEdit={showEdit} diff --git a/ui/app/components/app/contact-list/contact-list.component.js b/ui/app/components/app/contact-list/contact-list.component.js new file mode 100644 index 000000000..ec9b5f8eb --- /dev/null +++ b/ui/app/components/app/contact-list/contact-list.component.js @@ -0,0 +1,114 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import RecipientGroup from './recipient-group/recipient-group.component' + +export default class ContactList extends PureComponent { + static propTypes = { + searchForContacts: PropTypes.func, + searchForRecents: PropTypes.func, + searchForMyAccounts: PropTypes.func, + selectRecipient: PropTypes.func, + children: PropTypes.node, + selectedAddress: PropTypes.string, + } + + static contextTypes = { + t: PropTypes.func, + } + + state = { + isShowingAllRecent: false, + } + + renderRecents () { + const { t } = this.context + const { isShowingAllRecent } = this.state + const nonContacts = this.props.searchForRecents() + + const showLoadMore = !isShowingAllRecent && nonContacts.length > 2 + + return ( + <div className="send__select-recipient-wrapper__recent-group-wrapper"> + <RecipientGroup + label={t('recents')} + items={showLoadMore ? nonContacts.slice(0, 2) : nonContacts} + onSelect={this.props.selectRecipient} + selectedAddress={this.props.selectedAddress} + /> + { + showLoadMore && ( + <div + className="send__select-recipient-wrapper__recent-group-wrapper__load-more" + onClick={() => this.setState({ isShowingAllRecent: true })} + > + {t('loadMore')} + </div> + ) + } + </div> + ) + } + + renderAddressBook () { + const contacts = this.props.searchForContacts() + + const contactGroups = contacts.reduce((acc, contact) => { + const firstLetter = contact.name.slice(0, 1).toUpperCase() + acc[firstLetter] = acc[firstLetter] || [] + const bucket = acc[firstLetter] + bucket.push(contact) + return acc + }, {}) + + return Object + .entries(contactGroups) + .sort(([letter1], [letter2]) => { + if (letter1 > letter2) { + return 1 + } else if (letter1 === letter2) { + return 0 + } else if (letter1 < letter2) { + return -1 + } + }) + .map(([letter, groupItems]) => ( + <RecipientGroup + key={`${letter}-contract-group`} + label={letter} + items={groupItems} + onSelect={this.props.selectRecipient} + selectedAddress={this.props.selectedAddress} + /> + )) + } + + renderMyAccounts () { + const myAccounts = this.props.searchForMyAccounts() + + return ( + <RecipientGroup + items={myAccounts} + onSelect={this.props.selectRecipient} + selectedAddress={this.props.selectedAddress} + /> + ) + } + + render () { + const { + children, + searchForRecents, + searchForContacts, + searchForMyAccounts, + } = this.props + + return ( + <div className="send__select-recipient-wrapper__list"> + { children || null } + { searchForRecents && this.renderRecents() } + { searchForContacts && this.renderAddressBook() } + { searchForMyAccounts && this.renderMyAccounts() } + </div> + ) + } +} diff --git a/ui/app/components/app/contact-list/index.js b/ui/app/components/app/contact-list/index.js new file mode 100644 index 000000000..d90c29b2b --- /dev/null +++ b/ui/app/components/app/contact-list/index.js @@ -0,0 +1 @@ +export { default } from './contact-list.component' diff --git a/ui/app/components/app/contact-list/recipient-group/index.js b/ui/app/components/app/contact-list/recipient-group/index.js new file mode 100644 index 000000000..7d827523f --- /dev/null +++ b/ui/app/components/app/contact-list/recipient-group/index.js @@ -0,0 +1 @@ +export { default } from './recipient-group.component' diff --git a/ui/app/components/app/contact-list/recipient-group/recipient-group.component.js b/ui/app/components/app/contact-list/recipient-group/recipient-group.component.js new file mode 100644 index 000000000..a2248326e --- /dev/null +++ b/ui/app/components/app/contact-list/recipient-group/recipient-group.component.js @@ -0,0 +1,59 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Identicon from '../../../ui/identicon' +import classnames from 'classnames' +import { ellipsify } from '../../../../pages/send/send.utils' + +function addressesEqual (address1, address2) { + return String(address1).toLowerCase() === String(address2).toLowerCase() +} + +export default function RecipientGroup ({ label, items, onSelect, selectedAddress }) { + if (!items || !items.length) { + return null + } + + return ( + <div className="send__select-recipient-wrapper__group"> + {label && <div className="send__select-recipient-wrapper__group-label"> + {label} + </div>} + { + items.map(({ address, name }) => ( + <div + key={address} + onClick={() => onSelect(address, name)} + className={classnames({ + 'send__select-recipient-wrapper__group-item': !addressesEqual(address, selectedAddress), + 'send__select-recipient-wrapper__group-item--selected': addressesEqual(address, selectedAddress), + })} + > + <Identicon address={address} diameter={28} /> + <div className="send__select-recipient-wrapper__group-item__content"> + <div className="send__select-recipient-wrapper__group-item__title"> + {name || ellipsify(address)} + </div> + { + name && ( + <div className="send__select-recipient-wrapper__group-item__subtitle"> + {ellipsify(address)} + </div> + ) + } + </div> + </div> + )) + } + </div> + ) +} + +RecipientGroup.propTypes = { + label: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.shape({ + address: PropTypes.string, + name: PropTypes.string, + })), + onSelect: PropTypes.func.isRequired, + selectedAddress: PropTypes.string, +} diff --git a/ui/app/components/app/dropdowns/account-details-dropdown.js b/ui/app/components/app/dropdowns/account-details-dropdown.js index a4c33620a..cf2aa8ae8 100644 --- a/ui/app/components/app/dropdowns/account-details-dropdown.js +++ b/ui/app/components/app/dropdowns/account-details-dropdown.js @@ -59,7 +59,7 @@ AccountDetailsDropdown.prototype.render = function () { viewOnEtherscan, showRemoveAccountConfirmationModal, rpcPrefs, - } = this.props + } = this.props const address = selectedIdentity.address diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index 378ad3ba6..e6a24ef11 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -32,9 +32,6 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - hideModal: () => { - dispatch(actions.hideModal()) - }, setProviderType: (type) => { dispatch(actions.setProviderType(type)) }, @@ -47,7 +44,6 @@ function mapDispatchToProps (dispatch) { delRpcTarget: (target) => { dispatch(actions.delRpcTarget(target)) }, - showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), setNetworksTabAddMode: isInAddMode => dispatch(actions.setNetworksTabAddMode(isInAddMode)), } @@ -357,12 +353,12 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) { }, }, nickname || rpc), h('i.fa.fa-times.delete', - { - onClick: (e) => { - e.stopPropagation() - props.delRpcTarget(rpc) - }, - }), + { + onClick: (e) => { + e.stopPropagation() + props.delRpcTarget(rpc) + }, + }), ] ) } diff --git a/ui/app/components/app/ens-input.js b/ui/app/components/app/ens-input.js deleted file mode 100644 index 5eea0dd90..000000000 --- a/ui/app/components/app/ens-input.js +++ /dev/null @@ -1,181 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const extend = require('xtend') -const debounce = require('debounce') -const copyToClipboard = require('copy-to-clipboard') -const ENS = require('ethjs-ens') -const networkMap = require('ethjs-ens/lib/network-map.json') -const ensRE = /.+\..+$/ -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -const connect = require('react-redux').connect -const ToAutoComplete = require('../../pages/send/to-autocomplete').default -const log = require('loglevel') -const { isValidENSAddress } = require('../../helpers/utils/util') - -EnsInput.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect()(EnsInput) - - -inherits(EnsInput, Component) -function EnsInput () { - Component.call(this) -} - -EnsInput.prototype.onChange = function (recipient) { - - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - - this.props.onChange({ toAddress: recipient }) - - if (!networkHasEnsSupport) return - - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - toError: null, - }) - } - - this.setState({ - loadingEns: true, - }) - this.checkName(recipient) -} - -EnsInput.prototype.render = function () { - const props = this.props - const opts = extend(props, { - list: 'addresses', - onChange: this.onChange.bind(this), - qrScanner: true, - }) - return h('div', { - style: { width: '100%', position: 'relative' }, - }, [ - h(ToAutoComplete, { ...opts }), - this.ensIcon(), - ]) -} - -EnsInput.prototype.componentDidMount = function () { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - this.setState({ ensResolution: ZERO_ADDRESS }) - - if (networkHasEnsSupport) { - const provider = global.ethereumProvider - this.ens = new ENS({ provider, network }) - this.checkName = debounce(this.lookupEnsName.bind(this), 200) - } -} - -EnsInput.prototype.lookupEnsName = function (recipient) { - const { ensResolution } = this.state - - log.info(`ENS attempting to resolve name: ${recipient}`) - this.ens.lookup(recipient.trim()) - .then((address) => { - if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName')) - if (address !== ensResolution) { - this.setState({ - loadingEns: false, - ensResolution: address, - nickname: recipient.trim(), - hoverText: address + '\n' + this.context.t('clickCopy'), - ensFailure: false, - toError: null, - }) - } - }) - .catch((reason) => { - const setStateObj = { - loadingEns: false, - ensResolution: recipient, - ensFailure: true, - toError: null, - } - if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') { - setStateObj.hoverText = this.context.t('ensNameNotFound') - setStateObj.toError = 'ensNameNotFound' - setStateObj.ensFailure = false - } else { - log.error(reason) - setStateObj.hoverText = reason.message - } - - return this.setState(setStateObj) - }) -} - -EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { - const state = this.state || {} - const ensResolution = state.ensResolution - // If an address is sent without a nickname, meaning not from ENS or from - // the user's own accounts, a default of a one-space string is used. - const nickname = state.nickname || ' ' - if (prevProps.network !== this.props.network) { - const provider = global.ethereumProvider - this.ens = new ENS({ provider, network: this.props.network }) - this.onChange(ensResolution) - } - if (prevState && ensResolution && this.props.onChange && - ensResolution !== prevState.ensResolution) { - this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning }) - } -} - -EnsInput.prototype.ensIcon = function (recipient) { - const { hoverText } = this.state || {} - return h('span.#ensIcon', { - title: hoverText, - style: { - position: 'absolute', - top: '16px', - left: '-25px', - }, - }, this.ensIconContents(recipient)) -} - -EnsInput.prototype.ensIconContents = function () { - const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } - - if (toError) return - - if (loadingEns) { - return h('img', { - src: 'images/loading.svg', - style: { - width: '30px', - height: '30px', - transform: 'translateY(-6px)', - }, - }) - } - - if (ensFailure) { - return h('i.fa.fa-warning.fa-lg.warning') - } - - if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { - return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { - style: { color: 'green' }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ensResolution) - }, - }) - } -} - -function getNetworkEnsSupport (network) { - return Boolean(networkMap[network]) -} diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js index d942fd150..7b87b3033 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -131,8 +131,8 @@ export default class AdvancedTabContent extends Component { </div> { isInError ? <div className={`advanced-gas-inputs__gas-edit-row__${errorType}-text`}> - { errorText } - </div> + { errorText } + </div> : null } </div> ) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index eab3434df..88d28b9ed 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -104,8 +104,8 @@ export default class AdvancedTabContent extends Component { </div> { isInError ? <div className={`advanced-tab__gas-edit-row__${errorType}-text`}> - { errorText } - </div> + { errorText } + </div> : null } </div> ) @@ -126,7 +126,7 @@ export default class AdvancedTabContent extends Component { <div className="advanced-tab__transaction-data-summary__fee"> {transactionFee} </div> - <div className="time-remaining">{timeRemaining}</div> + <div className="advanced-tab__transaction-data-summary__time-remaining">{timeRemaining}</div> </div> </div> ) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index 20a503018..e35b6d594 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -1,5 +1,3 @@ -@import './time-remaining/index'; - .advanced-tab { display: flex; flex-flow: column; @@ -36,6 +34,24 @@ font-size: 16px; color: #313A5E; } + + &__time-remaining { + color: #313A5E; + font-size: 16px; + + .minutes-num, .seconds-num { + font-size: 16px; + } + + .seconds-num { + margin-left: 7px; + font-size: 16px; + } + + .minutes-label, .seconds-label { + font-size: 16px; + } + } } &__fee-chart { @@ -94,7 +110,7 @@ display: flex; justify-content: space-between; align-items: center; - + .fa-info-circle { color: $silver; margin-left: 10px; @@ -204,4 +220,4 @@ color: $dusty-gray; } } -}
\ No newline at end of file +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index 5f7d90922..683eeda9b 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -124,7 +124,7 @@ describe('AdvancedTabContent Component', function () { const dataNode = dataSummary.children().at(1) assert(dataNode.hasClass('advanced-tab__transaction-data-summary__container')) assert.equal(dataNode.children().at(0).text(), 'mockTotalFee') - assert(dataNode.children().at(1).hasClass('time-remaining')) + assert(dataNode.children().at(1).hasClass('advanced-tab__transaction-data-summary__time-remaining')) assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining') }) }) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js deleted file mode 100644 index 61b681e1a..000000000 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './time-remaining.component' diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss deleted file mode 100644 index e2115af7f..000000000 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss +++ /dev/null @@ -1,17 +0,0 @@ -.time-remaining { - color: #313A5E; - font-size: 16px; - - .minutes-num, .seconds-num { - font-size: 16px; - } - - .seconds-num { - margin-left: 7px; - font-size: 16px; - } - - .minutes-label, .seconds-label { - font-size: 16px; - } -}
\ No newline at end of file diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js deleted file mode 100644 index 17f0345d5..000000000 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import assert from 'assert' -import shallow from '../../../../../../../../lib/shallow-with-context' -import TimeRemaining from '../time-remaining.component.js' - -describe('TimeRemaining Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(<TimeRemaining - milliseconds={495000} - />) - }) - - describe('render()', () => { - it('should render the time-remaining root node', () => { - assert(wrapper.hasClass('time-remaining')) - }) - - it('should render minutes and seconds numbers and labels', () => { - const timeRemainingChildren = wrapper.children() - assert.equal(timeRemainingChildren.length, 4) - assert.equal(timeRemainingChildren.at(0).text(), 8) - assert.equal(timeRemainingChildren.at(1).text(), 'minutesShorthand') - assert.equal(timeRemainingChildren.at(2).text(), 15) - assert.equal(timeRemainingChildren.at(3).text(), 'secondsShorthand') - }) - }) - -}) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js deleted file mode 100644 index 826d41f9c..000000000 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { getTimeBreakdown } from './time-remaining.utils' - -export default class TimeRemaining extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - milliseconds: PropTypes.number, - } - - render () { - const { - milliseconds, - } = this.props - - const { - minutes, - seconds, - } = getTimeBreakdown(milliseconds) - - return ( - <div className="time-remaining"> - <span className="minutes-num">{minutes}</span> - <span className="minutes-label">{this.context.t('minutesShorthand')}</span> - <span className="seconds-num">{seconds}</span> - <span className="seconds-label">{this.context.t('secondsShorthand')}</span> - </div> - ) - } -} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js deleted file mode 100644 index cf43e0acb..000000000 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js +++ /dev/null @@ -1,11 +0,0 @@ -function getTimeBreakdown (milliseconds) { - return { - hours: Math.floor(milliseconds / 3600000), - minutes: Math.floor((milliseconds % 3600000) / 60000), - seconds: Math.floor((milliseconds % 60000) / 1000), - } -} - -module.exports = { - getTimeBreakdown, -} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js index 5f3925fa5..931611460 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -21,12 +21,12 @@ export default class BasicTabContent extends Component { <div className="basic-tab-content__title">{ t('estimatedProcessingTimes') }</div> <div className="basic-tab-content__blurb">{ t('selectAHigherGasFee') }</div> {!gasPriceButtonGroupProps.loading - ? <GasPriceButtonGroup - className="gas-price-button-group--alt" - showCheck={true} - {...gasPriceButtonGroupProps} - /> - : <Loading /> + ? <GasPriceButtonGroup + className="gas-price-button-group--alt" + showCheck={true} + {...gasPriceButtonGroupProps} + /> + : <Loading /> } <div className="basic-tab-content__footer-blurb">{ t('acceleratingATransaction') }</div> </div> diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index e18c1067e..5e557f660 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -47,7 +47,7 @@ export default class GasModalPageContainer extends Component { const promise = this.props.hideBasic ? Promise.resolve(this.props.blockTime) : this.props.fetchBasicGasAndTimeEstimates() - .then(basicEstimates => basicEstimates.blockTime) + .then(basicEstimates => basicEstimates.blockTime) promise .then(blockTime => { @@ -144,11 +144,11 @@ export default class GasModalPageContainer extends Component { return ( <Tabs> {tabsToRender.map(({ name, content }, i) => <Tab name={this.context.t(name)} key={`gas-modal-tab-${i}`}> - <div className="gas-modal-content"> - { content } - { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) } - </div> - </Tab> + <div className="gas-modal-content"> + { content } + { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) } + </div> + </Tab> )} </Tabs> ) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 03d254eee..d5f3837a9 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -341,44 +341,44 @@ describe('gas-modal-page-container container', () => { }) describe('mergeProps', () => { - let stateProps - let dispatchProps - let ownProps - - beforeEach(() => { - stateProps = { - gasPriceButtonGroupProps: { - someGasPriceButtonGroupProp: 'foo', - anotherGasPriceButtonGroupProp: 'bar', - }, - isConfirm: true, - someOtherStateProp: 'baz', - transaction: {}, - } - dispatchProps = { - updateCustomGasPrice: sinon.spy(), - hideGasButtonGroup: sinon.spy(), - setGasData: sinon.spy(), - updateConfirmTxGasAndCalculate: sinon.spy(), - someOtherDispatchProp: sinon.spy(), - createSpeedUpTransaction: sinon.spy(), - hideSidebar: sinon.spy(), - hideModal: sinon.spy(), - cancelAndClose: sinon.spy(), - } - ownProps = { someOwnProp: 123 } - }) + let stateProps + let dispatchProps + let ownProps - afterEach(() => { - dispatchProps.updateCustomGasPrice.resetHistory() - dispatchProps.hideGasButtonGroup.resetHistory() - dispatchProps.setGasData.resetHistory() - dispatchProps.updateConfirmTxGasAndCalculate.resetHistory() - dispatchProps.someOtherDispatchProp.resetHistory() - dispatchProps.createSpeedUpTransaction.resetHistory() - dispatchProps.hideSidebar.resetHistory() - dispatchProps.hideModal.resetHistory() - }) + beforeEach(() => { + stateProps = { + gasPriceButtonGroupProps: { + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }, + isConfirm: true, + someOtherStateProp: 'baz', + transaction: {}, + } + dispatchProps = { + updateCustomGasPrice: sinon.spy(), + hideGasButtonGroup: sinon.spy(), + setGasData: sinon.spy(), + updateConfirmTxGasAndCalculate: sinon.spy(), + someOtherDispatchProp: sinon.spy(), + createSpeedUpTransaction: sinon.spy(), + hideSidebar: sinon.spy(), + hideModal: sinon.spy(), + cancelAndClose: sinon.spy(), + } + ownProps = { someOwnProp: 123 } + }) + + afterEach(() => { + dispatchProps.updateCustomGasPrice.resetHistory() + dispatchProps.hideGasButtonGroup.resetHistory() + dispatchProps.setGasData.resetHistory() + dispatchProps.updateConfirmTxGasAndCalculate.resetHistory() + dispatchProps.someOtherDispatchProp.resetHistory() + dispatchProps.createSpeedUpTransaction.resetHistory() + dispatchProps.hideSidebar.resetHistory() + dispatchProps.hideModal.resetHistory() + }) it('should return the expected props when isConfirm is true', () => { const result = mergeProps(stateProps, dispatchProps, ownProps) diff --git a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js index 55512ce09..b941f1cf9 100644 --- a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js +++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -210,17 +210,17 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate }, padding: {left: 20, right: 15, top: 6, bottom: 10}, data: { - x: 'x', - columns: [ - ['x', ...gasPrices], - ['data1', ...estimatedTimes], - ], - types: { - data1: 'area', - }, - selection: { - enabled: false, - }, + x: 'x', + columns: [ + ['x', ...gasPrices], + ['data1', ...estimatedTimes], + ], + types: { + data1: 'area', + }, + selection: { + enabled: false, + }, }, color: { data1: '#259de5', @@ -254,13 +254,13 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate }, }, legend: { - show: false, + show: false, }, grid: { - x: {}, - lines: { - front: false, - }, + x: {}, + lines: { + front: false, + }, }, point: { focus: { @@ -296,8 +296,8 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate const flipTooltip = circleY - circleWidth < chartYStart + 5 d3 - .select('.tooltip-arrow') - .style('margin-top', flipTooltip ? '-16px' : '4px') + .select('.tooltip-arrow') + .style('margin-top', flipTooltip ? '-16px' : '4px') return { top: bigNumMinus(circleY, chartYStart).minus(19).plus(flipTooltip ? circleWidth + 38 : 0).toNumber(), diff --git a/ui/app/components/app/home-notification/home-notification.component.js b/ui/app/components/app/home-notification/home-notification.component.js new file mode 100644 index 000000000..cc86ef6d8 --- /dev/null +++ b/ui/app/components/app/home-notification/home-notification.component.js @@ -0,0 +1,112 @@ +import React, { PureComponent } from 'react' +import classnames from 'classnames' +import {Tooltip as ReactTippy} from 'react-tippy' +import PropTypes from 'prop-types' +import Button from '../../ui/button' + +export default class HomeNotification extends PureComponent { + static contextTypes = { + metricsEvent: PropTypes.func, + } + + static defaultProps = { + onAccept: null, + ignoreText: null, + onIgnore: null, + infoText: null, + } + + static propTypes = { + acceptText: PropTypes.string.isRequired, + onAccept: PropTypes.func, + ignoreText: PropTypes.string, + onIgnore: PropTypes.func, + descriptionText: PropTypes.string.isRequired, + infoText: PropTypes.string, + classNames: PropTypes.array, + } + + handleAccept = () => { + this.props.onAccept() + } + + handleIgnore = () => { + this.props.onIgnore() + } + + render () { + const { descriptionText, acceptText, onAccept, ignoreText, onIgnore, infoText, classNames = [] } = this.props + + return ( + <div className={classnames('home-notification', ...classNames)}> + <div className="home-notification__header"> + <div className="home-notification__header-container"> + <img + className="home-notification__icon" + alt="" + src="images/icons/connect.svg" + /> + <div className="home-notification__text"> + { descriptionText } + </div> + </div> + { + infoText ? ( + <ReactTippy + style={{ + display: 'flex', + }} + html={( + <p className="home-notification-tooltip__content"> + {infoText} + </p> + )} + offset={-36} + distance={36} + animation="none" + position="top" + arrow + theme="info" + > + <img + alt="" + src="images/icons/info.svg" + /> + </ReactTippy> + ) : ( + null + ) + } + </div> + <div className="home-notification__buttons"> + { + (onAccept && acceptText) ? ( + <Button + type="primary" + className="home-notification__accept-button" + onClick={this.handleAccept} + > + { acceptText } + </Button> + ) : ( + null + ) + } + { + (onIgnore && ignoreText) ? ( + <Button + type="secondary" + className="home-notification__ignore-button" + onClick={this.handleIgnore} + > + { ignoreText } + </Button> + ) : ( + null + ) + } + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/home-notification/index.js b/ui/app/components/app/home-notification/index.js new file mode 100644 index 000000000..918a35be2 --- /dev/null +++ b/ui/app/components/app/home-notification/index.js @@ -0,0 +1 @@ +export { default } from './home-notification.component' diff --git a/ui/app/components/app/home-notification/index.scss b/ui/app/components/app/home-notification/index.scss new file mode 100644 index 000000000..c855a0814 --- /dev/null +++ b/ui/app/components/app/home-notification/index.scss @@ -0,0 +1,118 @@ +.tippy-tooltip.info-theme { + background: rgba(36, 41, 46, 0.9); + color: $white; + border-radius: 8px; +} + +.home-notification { + background: rgba(36, 41, 46, 0.9); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); + border-radius: 8px; + height: 116px; + padding: 16px; + + @media screen and (min-width: 576px) { + min-width: 472px; + } + + display: flex; + flex-flow: column; + justify-content: space-between; + + &__header-container { + display: flex; + } + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__text { + font-family: Roboto, 'sans-serif'; + font-style: normal; + font-weight: normal; + font-size: 12px; + color: $white; + margin-left: 10px; + margin-right: 8px; + } + + .fa-info-circle { + color: #6A737D; + } + + &__ignore-button { + border: 2px solid #6A737D; + box-sizing: border-box; + border-radius: 6px; + color: $white; + background-color: inherit; + height: 34px; + width: 155px; + padding: 0; + + @media screen and (max-width: 575px) { + width: 135px; + } + + &:hover { + border-color: #6A737D; + background-color: #6A737D; + } + + &:active { + background-color: #141618; + } + } + + &__accept-button { + border: 2px solid #6A737D; + box-sizing: border-box; + border-radius: 6px; + color: $white; + background-color: inherit; + height: 34px; + width: 155px; + padding: 0; + margin-left: 4px; + + @media screen and (max-width: 575px) { + width: 135px; + } + + &:hover { + border-color: #6A737D; + background-color: #6A737D; + } + + &:active { + background-color: #141618; + } + } + + &__buttons { + display: flex; + width: 100%; + justify-content: flex-start; + flex-direction: row-reverse; + } +} + +.home-notification-tooltip { + &__tooltip-container { + display: flex; + } + + &__content { + font-family: Roboto, 'sans-serif'; + font-style: normal; + font-weight: normal; + font-size: 12px; + color: $white; + text-align: left; + display: inline-block; + width: 200px; + } +} diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index e9bb4ac9f..1ccb6a94a 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -1,3 +1,5 @@ +@import 'account-details/index'; + @import 'account-menu/index'; @import 'add-token-button/index'; @@ -78,4 +80,8 @@ @import 'gas-customization/gas-price-button-group/index'; -@import 'ui-migration-annoucement/index'; +@import '../ui/toggle-button/index'; + +@import 'home-notification/index'; + +@import 'multiple-notifications/index'; diff --git a/ui/app/components/app/menu-droppo.js b/ui/app/components/app/menu-droppo.js index c80bee2be..a88cad4b4 100644 --- a/ui/app/components/app/menu-droppo.js +++ b/ui/app/components/app/menu-droppo.js @@ -2,7 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const findDOMNode = require('react-dom').findDOMNode -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const ReactCSSTransitionGroup = require('react-transition-group/CSSTransitionGroup') module.exports = MenuDroppoComponent diff --git a/ui/app/components/app/modals/account-details-modal/account-details-modal.component.js b/ui/app/components/app/modals/account-details-modal/account-details-modal.component.js index e3919edcf..1b9a6a718 100644 --- a/ui/app/components/app/modals/account-details-modal/account-details-modal.component.js +++ b/ui/app/components/app/modals/account-details-modal/account-details-modal.component.js @@ -72,14 +72,14 @@ export default class AccountDetailsModal extends Component { </Button> {exportPrivateKeyFeatureEnabled - ? <Button - type="secondary" - className="account-modal__button" - onClick={() => showExportPrivateKeyModal()} - > - {this.context.t('exportPrivateKey')} - </Button> - : null + ? <Button + type="secondary" + className="account-modal__button" + onClick={() => showExportPrivateKeyModal()} + > + {this.context.t('exportPrivateKey')} + </Button> + : null } </AccountModalContainer> ) diff --git a/ui/app/components/app/modals/add-to-addressbook-modal/add-to-addressbook-modal.component.js b/ui/app/components/app/modals/add-to-addressbook-modal/add-to-addressbook-modal.component.js new file mode 100644 index 000000000..64161a632 --- /dev/null +++ b/ui/app/components/app/modals/add-to-addressbook-modal/add-to-addressbook-modal.component.js @@ -0,0 +1,79 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../../ui/button/button.component' + +export default class AddToAddressBookModal extends Component { + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + hideModal: PropTypes.func.isRequired, + addToAddressBook: PropTypes.func.isRequired, + recipient: PropTypes.string.isRequired, + } + + state = { + alias: '', + } + + onSave = () => { + const { recipient, addToAddressBook, hideModal } = this.props + addToAddressBook(recipient, this.state.alias) + hideModal() + } + + onChange = e => { + this.setState({ + alias: e.target.value, + }) + } + + onKeyPress = e => { + if (e.key === 'Enter' && this.state.alias) { + this.onSave() + } + } + + render () { + const { t } = this.context + + return ( + <div className="add-to-address-book-modal"> + <div className="add-to-address-book-modal__content"> + <div className="add-to-address-book-modal__content__header"> + {t('addToAddressBook')} + </div> + <div className="add-to-address-book-modal__input-label"> + {t('enterAnAlias')} + </div> + <input + type="text" + className="add-to-address-book-modal__input" + placeholder={t('addToAddressBookModalPlaceholder')} + onChange={this.onChange} + onKeyPress={this.onKeyPress} + value={this.state.alias} + autoFocus + /> + </div> + <div className="add-to-address-book-modal__footer"> + <Button + type="secondary" + onClick={this.props.hideModal} + > + {t('cancel')} + </Button> + <Button + type="primary" + onClick={this.onSave} + disabled={!this.state.alias} + > + {t('save')} + </Button> + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/modals/add-to-addressbook-modal/add-to-addressbook-modal.container.js b/ui/app/components/app/modals/add-to-addressbook-modal/add-to-addressbook-modal.container.js new file mode 100644 index 000000000..413d4aa4a --- /dev/null +++ b/ui/app/components/app/modals/add-to-addressbook-modal/add-to-addressbook-modal.container.js @@ -0,0 +1,18 @@ +import { connect } from 'react-redux' +import AddToAddressBookModal from './add-to-addressbook-modal.component' +import actions from '../../../../store/actions' + +function mapStateToProps (state) { + return { + ...state.appState.modal.modalState.props || {}, + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => dispatch(actions.hideModal()), + addToAddressBook: (recipient, nickname) => dispatch(actions.addToAddressBook(recipient, nickname)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(AddToAddressBookModal) diff --git a/ui/app/components/app/modals/add-to-addressbook-modal/index.js b/ui/app/components/app/modals/add-to-addressbook-modal/index.js new file mode 100644 index 000000000..9ed4f018f --- /dev/null +++ b/ui/app/components/app/modals/add-to-addressbook-modal/index.js @@ -0,0 +1 @@ +export { default } from './add-to-addressbook-modal.container' diff --git a/ui/app/components/app/modals/add-to-addressbook-modal/index.scss b/ui/app/components/app/modals/add-to-addressbook-modal/index.scss new file mode 100644 index 000000000..f6bf85a0a --- /dev/null +++ b/ui/app/components/app/modals/add-to-addressbook-modal/index.scss @@ -0,0 +1,37 @@ +.add-to-address-book-modal { + @extend %col-nowrap; + @extend %modal; + + &__content { + @extend %col-nowrap; + padding: 1.5rem; + border-bottom: 1px solid $Grey-100; + + &__header { + @extend %h3; + } + } + + &__input-label { + color: $Grey-600; + margin-top: 1.25rem; + } + + &__input { + @extend %input; + margin-top: 0.75rem; + + &::placeholder { + color: $Grey-300; + } + } + + &__footer { + @extend %row-nowrap; + padding: 1rem; + + button + button { + margin-left: 1rem; + } + } +} diff --git a/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.component.js b/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.component.js new file mode 100644 index 000000000..ea92e340c --- /dev/null +++ b/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.component.js @@ -0,0 +1,43 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal, { ModalContent } from '../../modal' + +export default class ConfirmDeleteNetwork extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + delRpcTarget: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + target: PropTypes.string.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleDelete = () => { + this.props.delRpcTarget(this.props.target) + .then(() => { + this.props.onConfirm() + this.props.hideModal() + }) + } + + render () { + const { t } = this.context + + return ( + <Modal + onSubmit={this.handleDelete} + onCancel={() => this.props.hideModal()} + submitText={t('delete')} + cancelText={t('cancel')} + submitType="danger" + > + <ModalContent + title={t('deleteNetwork')} + description={t('deleteNetworkDescription')} + /> + </Modal> + ) + } +} diff --git a/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.container.js b/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.container.js new file mode 100644 index 000000000..4c9bb279f --- /dev/null +++ b/ui/app/components/app/modals/confirm-delete-network/confirm-delete-network.container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' +import ConfirmDeleteNetwork from './confirm-delete-network.component' +import { delRpcTarget } from '../../../../store/actions' + +const mapDispatchToProps = dispatch => { + return { + delRpcTarget: (target) => dispatch(delRpcTarget(target)), + } +} + +export default compose( + withModalProps, + connect(null, mapDispatchToProps) +)(ConfirmDeleteNetwork) diff --git a/ui/app/components/app/modals/confirm-delete-network/index.js b/ui/app/components/app/modals/confirm-delete-network/index.js new file mode 100644 index 000000000..de9543eea --- /dev/null +++ b/ui/app/components/app/modals/confirm-delete-network/index.js @@ -0,0 +1 @@ +export { default } from './confirm-delete-network.container' diff --git a/ui/app/components/app/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js index 20c4d018c..ff2411209 100644 --- a/ui/app/components/app/modals/deposit-ether-modal.js +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -87,8 +87,8 @@ DepositEtherModal.prototype.renderRow = function ({ } return h('div', { - className: className || 'deposit-ether-modal__buy-row', - }, [ + className: className || 'deposit-ether-modal__buy-row', + }, [ onBackClick && showBackButton && h('div.deposit-ether-modal__buy-row__back', { onClick: onBackClick, @@ -100,22 +100,22 @@ DepositEtherModal.prototype.renderRow = function ({ h('div.deposit-ether-modal__buy-row__logo-container', [logo]), - h('div.deposit-ether-modal__buy-row__description', [ + h('div.deposit-ether-modal__buy-row__description', [ - !hideTitle && h('div.deposit-ether-modal__buy-row__description__title', [title]), + !hideTitle && h('div.deposit-ether-modal__buy-row__description__title', [title]), - h('div.deposit-ether-modal__buy-row__description__text', [text]), + h('div.deposit-ether-modal__buy-row__description__text', [text]), - ]), + ]), - !hideButton && h('div.deposit-ether-modal__buy-row__button', [ - h(Button, { - type: 'secondary', - className: 'deposit-ether-modal__deposit-button', - large: true, - onClick: onButtonClick, - }, [buttonLabel]), - ]), + !hideButton && h('div.deposit-ether-modal__buy-row__button', [ + h(Button, { + type: 'secondary', + className: 'deposit-ether-modal__deposit-button', + large: true, + onClick: onButtonClick, + }, [buttonLabel]), + ]), ]) } diff --git a/ui/app/components/app/modals/export-private-key-modal.js b/ui/app/components/app/modals/export-private-key-modal.js index c3098a16c..43d7bcd74 100644 --- a/ui/app/components/app/modals/export-private-key-modal.js +++ b/ui/app/components/app/modals/export-private-key-modal.js @@ -86,12 +86,12 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) { return privateKey ? h(ReadOnlyInput, { - wrapperClass: 'private-key-password-display-wrapper', - inputClass: 'private-key-password-display-textarea', - textarea: true, - value: plainKey, - onClick: () => copyToClipboard(plainKey), - }) + wrapperClass: 'private-key-password-display-wrapper', + inputClass: 'private-key-password-display-textarea', + textarea: true, + value: plainKey, + onClick: () => copyToClipboard(plainKey), + }) : h('input.private-key-password-input', { type: 'password', onChange: event => this.setState({ password: event.target.value }), @@ -109,14 +109,14 @@ ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, address, h (privateKey ? ( - h(Button, { + h(Button, { type: 'secondary', large: true, className: 'export-private-key__button', onClick: () => hideModal(), }, this.context.t('done')) ) : ( - h(Button, { + h(Button, { type: 'secondary', large: true, className: 'export-private-key__button', @@ -149,29 +149,29 @@ ExportPrivateKeyModal.prototype.render = function () { backButtonAction: () => showAccountDetailModal(), }, [ - h('span.account-name', name), + h('span.account-name', name), - h(ReadOnlyInput, { - wrapperClass: 'ellip-address-wrapper', - inputClass: 'qr-ellip-address ellip-address', - value: checksumAddress(address), - }), + h(ReadOnlyInput, { + wrapperClass: 'ellip-address-wrapper', + inputClass: 'qr-ellip-address ellip-address', + value: checksumAddress(address), + }), - h('div.account-modal-divider'), + h('div.account-modal-divider'), - h('span.modal-body-title', this.context.t('showPrivateKeys')), + h('span.modal-body-title', this.context.t('showPrivateKeys')), - h('div.private-key-password', {}, [ - this.renderPasswordLabel(privateKey), + h('div.private-key-password', {}, [ + this.renderPasswordLabel(privateKey), - this.renderPasswordInput(privateKey), + this.renderPasswordInput(privateKey), - showWarning && warning ? h('span.private-key-password-error', warning) : null, - ]), + showWarning && warning ? h('span.private-key-password-error', warning) : null, + ]), - h('div.private-key-password-warning', this.context.t('privateKeyWarning')), + h('div.private-key-password-warning', this.context.t('privateKeyWarning')), - this.renderButtons(privateKey, address, hideModal), + this.renderButtons(privateKey, address, hideModal), ]) } diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss index 09b0bb73c..1bbfd2d07 100644 --- a/ui/app/components/app/modals/index.scss +++ b/ui/app/components/app/modals/index.scss @@ -9,3 +9,5 @@ @import 'transaction-confirmed/index'; @import 'metametrics-opt-in-modal/index'; + +@import './add-to-addressbook-modal/index'; diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js index 0335991fc..1bf7c21b5 100644 --- a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js @@ -1,5 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' +import MetaFoxLogo from '../../../ui/metafox-logo' import PageContainerFooter from '../../../ui/page-container/page-container-footer' export default class MetaMetricsOptInModal extends Component { @@ -20,19 +21,7 @@ export default class MetaMetricsOptInModal extends Component { <div className="metametrics-opt-in metametrics-opt-in-modal"> <div className="metametrics-opt-in__main"> <div className="metametrics-opt-in__content"> - <div className="app-header__logo-container"> - <img - className="app-header__metafox-logo app-header__metafox-logo--horizontal" - src="/images/logo/metamask-logo-horizontal.svg" - height={30} - /> - <img - className="app-header__metafox-logo app-header__metafox-logo--icon" - src="/images/logo/metamask-fox.svg" - height={42} - width={42} - /> - </div> + <MetaFoxLogo /> <div className="metametrics-opt-in__body-graphic"> <img src="images/metrics-chart.svg" /> </div> diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js index 394367c46..4044ded8c 100644 --- a/ui/app/components/app/modals/modal.js +++ b/ui/app/components/app/modals/modal.js @@ -29,6 +29,8 @@ import MetaMetricsOptInModal from './metametrics-opt-in-modal' import RejectTransactions from './reject-transactions' import ClearApprovedOrigins from './clear-approved-origins' import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' +import ConfirmDeleteNetwork from './confirm-delete-network' +import AddToAddressBookModal from './add-to-addressbook-modal' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -166,6 +168,35 @@ const MODALS = { }, }, + ADD_TO_ADDRESSBOOK: { + contents: [ + h(AddToAddressBookModal, {}, []), + ], + mobileModalStyle: { + width: '95%', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + laptopModalStyle: { + width: '375px', + top: '10%', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + borderRadius: '10px', + }, + contentStyle: { + borderRadius: '10px', + }, + }, + ACCOUNT_DETAILS: { contents: [ h(AccountDetailsModal, {}, []), @@ -301,6 +332,19 @@ const MODALS = { }, }, + CONFIRM_DELETE_NETWORK: { + contents: h(ConfirmDeleteNetwork), + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + NEW_ACCOUNT: { contents: [ h(NewAccountModal, {}, []), @@ -452,7 +496,6 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(Modal) Modal.prototype.render = function () { const modal = MODALS[this.props.modalState.name || 'DEFAULT'] - const { contents: children, disableBackdropClick = false } = modal const modalStyle = modal[isMobileView() ? 'mobileModalStyle' : 'laptopModalStyle'] const contentStyle = modal.contentStyle || {} diff --git a/ui/app/components/app/modals/new-account-modal.js b/ui/app/components/app/modals/new-account-modal.js index 27c81a701..4b18c52ba 100644 --- a/ui/app/components/app/modals/new-account-modal.js +++ b/ui/app/components/app/modals/new-account-modal.js @@ -69,7 +69,7 @@ NewAccountModal.propTypes = { showImportPage: PropTypes.func, createAccount: PropTypes.func, numberOfExistingAccounts: PropTypes.number, - t: PropTypes.func, + t: PropTypes.func, } const mapStateToProps = state => { diff --git a/ui/app/components/app/modals/notification-modal.js b/ui/app/components/app/modals/notification-modal.js index b8503ec1a..84d9004b7 100644 --- a/ui/app/components/app/modals/notification-modal.js +++ b/ui/app/components/app/modals/notification-modal.js @@ -62,7 +62,7 @@ NotificationModal.propTypes = { showCancelButton: PropTypes.bool, showConfirmButton: PropTypes.bool, onConfirm: PropTypes.func, - t: PropTypes.func, + t: PropTypes.func, } const mapDispatchToProps = dispatch => { diff --git a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js index a83ba8f8e..afeaef0da 100644 --- a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js +++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { BrowserQRCodeReader } from '@zxing/library' -import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars +import 'webrtc-adapter' import Spinner from '../../../ui/spinner' import WebcamUtils from '../../../../../lib/webcam-utils' import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component' @@ -75,23 +75,23 @@ export default class QrScanner extends Component { clearTimeout(this.permissionChecker) this.checkPermisisions() this.codeReader.decodeFromInputVideoDevice(undefined, 'video') - .then(content => { - const result = this.parseContent(content.text) - if (result.type !== 'unknown') { - this.props.qrCodeDetected(result) - this.stopAndClose() - } else { - this.setState({msg: this.context.t('unknownQrCode')}) - } - }) - .catch(err => { - if (err && err.name === 'NotAllowedError') { - this.setState({msg: this.context.t('youNeedToAllowCameraAccess')}) - clearTimeout(this.permissionChecker) - this.needsToReinit = true - this.checkPermisisions() - } - }) + .then(content => { + const result = this.parseContent(content.text) + if (result.type !== 'unknown') { + this.props.qrCodeDetected(result) + this.stopAndClose() + } else { + this.setState({msg: this.context.t('unknownQrCode')}) + } + }) + .catch(err => { + if (err && err.name === 'NotAllowedError') { + this.setState({msg: this.context.t('youNeedToAllowCameraAccess')}) + clearTimeout(this.permissionChecker) + this.needsToReinit = true + this.checkPermisisions() + } + }) }).catch(err => { console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err) }) diff --git a/ui/app/components/app/multiple-notifications/index.js b/ui/app/components/app/multiple-notifications/index.js new file mode 100644 index 000000000..a27a65187 --- /dev/null +++ b/ui/app/components/app/multiple-notifications/index.js @@ -0,0 +1 @@ +export { default } from './multiple-notifications.component' diff --git a/ui/app/components/app/multiple-notifications/index.scss b/ui/app/components/app/multiple-notifications/index.scss new file mode 100644 index 000000000..e8d064bc0 --- /dev/null +++ b/ui/app/components/app/multiple-notifications/index.scss @@ -0,0 +1,78 @@ +.home-notification-wrapper--show-all, +.home-notification-wrapper--show-first { + display: flex; + flex-direction: column; + width: 472px; + position: absolute; + bottom: 0; + right: 0; + margin: 8px; + + @media screen and (max-width: 576px) { + width: 340px; + } + + .home-notification-wrapper__i-container { + position: relative; + width: 100%; + height: 100%; + visibility: none; + + .fa-sm { + display: initial; + position: absolute; + bottom: 14px; + left: 16px; + color: white; + cursor: pointer; + visibility: visible; + + &:hover { + color: #b0d7f2; + font-size: 1.1rem; + } + } + } +} + +.home-notification-wrapper--show-all { + justify-content: flex-end; + margin-bottom: 0; + + .home-notification-wrapper__i-container { + height: 0; + } + + > div { + position: relative; + margin-top: 8px; + } + + .fa-sm { + margin-bottom: 8px; + } + +} + +.home-notification-wrapper--show-first { + > div { + position: absolute; + bottom: 0; + right: 0; + visibility: hidden; + } + + > div:first-of-type { + visibility: visible; + + } + + .fa-sm { + position: relative; + display: initial; + } +} + +.flipped { + transform: rotate(180deg); +} diff --git a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js new file mode 100644 index 000000000..040890e18 --- /dev/null +++ b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js @@ -0,0 +1,44 @@ +import React, { PureComponent } from 'react' +import classnames from 'classnames' +import PropTypes from 'prop-types' + +export default class MultipleNotifications extends PureComponent { + static propTypes = { + notifications: PropTypes.array, + classNames: PropTypes.array, + } + + state = { + showAll: false, + } + + render () { + const { showAll } = this.state + const { notifications, classNames = [] } = this.props + + const notificationsToBeRendered = notifications.filter(notificationConfig => notificationConfig.shouldBeRendered) + + if (notificationsToBeRendered.length === 0) { + return null + } + + return ( + <div + className={classnames(...classNames, { + 'home-notification-wrapper--show-all': showAll, + 'home-notification-wrapper--show-first': !showAll, + })} + > + { notificationsToBeRendered.map(notificationConfig => notificationConfig.component) } + <div + className="home-notification-wrapper__i-container" + onClick={() => this.setState({ showAll: !showAll })} + > + {notificationsToBeRendered.length > 1 ? <i className={classnames('fa fa-sm fa-sort-amount-asc', { + 'flipped': !showAll, + })} /> : null} + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/network-display/network-display.component.js b/ui/app/components/app/network-display/network-display.component.js index 9ef5341b0..266476267 100644 --- a/ui/app/components/app/network-display/network-display.component.js +++ b/ui/app/components/app/network-display/network-display.component.js @@ -39,12 +39,12 @@ export default class NetworkDisplay extends Component { return networkClass ? <div className={`network-display__icon network-display__icon--${networkClass}`} /> : <div - className="i fa fa-question-circle fa-med" - style={{ - margin: '0 4px', - color: 'rgb(125, 128, 130)', - }} - /> + className="i fa fa-question-circle fa-med" + style={{ + margin: '0 4px', + color: 'rgb(125, 128, 130)', + }} + /> } render () { @@ -62,12 +62,12 @@ export default class NetworkDisplay extends Component { networkClass ? <div className={`network-display__icon network-display__icon--${networkClass}`} /> : <div - className="i fa fa-question-circle fa-med" - style={{ - margin: '0 4px', - color: 'rgb(125, 128, 130)', - }} - /> + className="i fa fa-question-circle fa-med" + style={{ + margin: '0 4px', + color: 'rgb(125, 128, 130)', + }} + /> } <div className="network-display__name"> { type === 'rpc' && nickname ? nickname : this.context.t(type) } diff --git a/ui/app/components/app/network.js b/ui/app/components/app/network.js index e778700cd..d46906a66 100644 --- a/ui/app/components/app/network.js +++ b/ui/app/components/app/network.js @@ -127,19 +127,19 @@ Network.prototype.render = function () { default: return h('.network-indicator', [ networkNumber === 'loading' - ? h('span.pointer.network-loading-spinner', { - onClick: (event) => this.props.onClick(event), - }, [ - h('img', { - title: context.t('attemptingConnect'), - src: 'images/loading.svg', + ? h('span.pointer.network-loading-spinner', { + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: context.t('attemptingConnect'), + src: 'images/loading.svg', + }), + ]) + : h('i.fa.fa-question-circle.fa-lg', { + style: { + color: 'rgb(125, 128, 130)', + }, }), - ]) - : h('i.fa.fa-question-circle.fa-lg', { - style: { - color: 'rgb(125, 128, 130)', - }, - }), h('.network-name', providerName === 'localhost' ? context.t('localhost') : providerNick || context.t('privateNetwork')), h('.network-indicator__down-arrow'), diff --git a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js index 0eb1d616a..7eda7f2b7 100644 --- a/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js @@ -5,7 +5,7 @@ import Identicon from '../../../ui/identicon' export default class ProviderPageContainerContent extends PureComponent { static propTypes = { origin: PropTypes.string.isRequired, - selectedIdentity: PropTypes.string.isRequired, + selectedIdentity: PropTypes.object.isRequired, siteImage: PropTypes.string, siteTitle: PropTypes.string.isRequired, } diff --git a/ui/app/components/app/sidebars/sidebar.component.js b/ui/app/components/app/sidebars/sidebar.component.js index b9e0f9e81..e532ba7e5 100644 --- a/ui/app/components/app/sidebars/sidebar.component.js +++ b/ui/app/components/app/sidebars/sidebar.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import ReactCSSTransitionGroup from 'react-addons-css-transition-group' +import ReactCSSTransitionGroup from 'react-transition-group/CSSTransitionGroup' import WalletView from '../wallet-view' import { WALLET_VIEW_SIDEBAR } from './sidebar.constants' import CustomizeGas from '../gas-customization/gas-modal-page-container/' @@ -26,7 +26,7 @@ export default class Sidebar extends Component { onOverlayClose && onOverlayClose() this.props.hideSidebar() } - } /> + } /> } renderSidebarContent () { diff --git a/ui/app/components/app/sidebars/tests/sidebars-component.test.js b/ui/app/components/app/sidebars/tests/sidebars-component.test.js index cee22aca8..e2daea9b6 100644 --- a/ui/app/components/app/sidebars/tests/sidebars-component.test.js +++ b/ui/app/components/app/sidebars/tests/sidebars-component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' -import ReactCSSTransitionGroup from 'react-addons-css-transition-group' +import ReactCSSTransitionGroup from 'react-transition-group/CSSTransitionGroup' import Sidebar from '../sidebar.component.js' import WalletView from '../../wallet-view' diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request.js index fa237f1d1..9c0f53f57 100644 --- a/ui/app/components/app/signature-request.js +++ b/ui/app/components/app/signature-request.js @@ -12,7 +12,7 @@ const { compose } = require('recompose') const { withRouter } = require('react-router-dom') const { ObjectInspector } = require('react-inspector') -import AccountDropdownMini from '../ui/account-dropdown-mini' +import AccountListItem from '../../pages/send/account-list-item/account-list-item.component' const actions = require('../../store/actions') const { conversionUtil } = require('../../helpers/utils/conversion-util') @@ -21,7 +21,6 @@ const { getSelectedAccount, getCurrentAccountWithSendEtherInfo, getSelectedAddress, - accountsWithSendEtherInfoSelector, conversionRateSelector, } = require('../../selectors/selectors.js') @@ -37,7 +36,6 @@ function mapStateToProps (state) { selectedAddress: getSelectedAddress(state), requester: null, requesterAddress: null, - accounts: accountsWithSendEtherInfoSelector(state), conversionRate: conversionRateSelector(state), } } @@ -76,9 +74,9 @@ function mergeProps (stateProps, dispatchProps, ownProps) { } return { + ...ownProps, ...stateProps, ...dispatchProps, - ...ownProps, txData, cancel, sign, @@ -137,23 +135,19 @@ SignatureRequest.prototype.renderHeader = function () { ]) } -SignatureRequest.prototype.renderAccountDropdown = function () { +SignatureRequest.prototype.renderAccount = function () { const { selectedAccount } = this.state - const { - accounts, - } = this.props - return h('div.request-signature__account', [ h('div.request-signature__account-text', [this.context.t('account') + ':']), - h(AccountDropdownMini, { - selectedAccount, - accounts, - disabled: true, - }), - + h('div.request-signature__account-item', [ + h(AccountListItem, { + account: selectedAccount, + displayBalance: false, + }), + ]), ]) } @@ -180,7 +174,7 @@ SignatureRequest.prototype.renderBalance = function () { SignatureRequest.prototype.renderAccountInfo = function () { return h('div.request-signature__account-info', [ - this.renderAccountDropdown(), + this.renderAccount(), this.renderRequestIcon(), @@ -257,7 +251,7 @@ SignatureRequest.prototype.renderBody = function () { url: 'https://metamask.zendesk.com/hc/en-us/articles/360015488751', }) }, - }, this.context.t('learnMore'))] + }, this.context.t('learnMore'))] } return h('div.request-signature__body', {}, [ diff --git a/ui/app/components/app/token-list.js b/ui/app/components/app/token-list.js index 2188e7020..000ca6b3f 100644 --- a/ui/app/components/app/token-list.js +++ b/ui/app/components/app/token-list.js @@ -67,8 +67,8 @@ TokenList.prototype.render = function () { }, onClick: () => { global.platform.openWindow({ - url: `https://ethplorer.io/address/${userAddress}`, - }) + url: `https://ethplorer.io/address/${userAddress}`, + }) }, }, this.context.t('here')), ]) @@ -125,13 +125,13 @@ TokenList.prototype.createFreshTokenTracker = function () { this.tracker.on('error', this.showError) this.tracker.updateBalances() - .then(() => { - this.updateBalances(this.tracker.serialize()) - }) - .catch((reason) => { - log.error(`Problem updating balances`, reason) - this.setState({ isLoading: false }) - }) + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) } TokenList.prototype.componentDidUpdate = function (prevProps) { diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js index 871716002..6124325be 100644 --- a/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js @@ -40,15 +40,15 @@ export default class TransactionActivityLogIcon extends PureComponent { return ( <div className={classnames('transaction-activity-log-icon', className)}> - { - imagePath && ( - <img - src={imagePath} - height={9} - width={9} - /> - ) - } + { + imagePath && ( + <img + src={imagePath} + height={9} + width={9} + /> + ) + } </div> ) } diff --git a/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js index c4e118b01..583980d26 100644 --- a/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js +++ b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js @@ -78,4 +78,73 @@ describe('TransactionListItemDetails Component', () => { assert.ok(wrapper.hasClass('transaction-list-item-details')) assert.equal(wrapper.find(Button).length, 3) }) + + it('should disable the Copy Tx ID and View In Etherscan buttons when tx hash is missing', () => { + const transaction = { + history: [], + id: 1, + status: 'confirmed', + txParams: { + from: '0x1', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0xa4', + to: '0x2', + value: '0x2386f26fc10000', + }, + } + + const transactionGroup = { + transactions: [transaction], + primaryTransaction: transaction, + initialTransaction: transaction, + } + + const wrapper = shallow( + <TransactionListItemDetails + transactionGroup={transactionGroup} + />, + { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } + ) + + assert.ok(wrapper.hasClass('transaction-list-item-details')) + const buttons = wrapper.find(Button) + assert.strictEqual(buttons.at(0).prop('disabled'), true) + assert.strictEqual(buttons.at(1).prop('disabled'), true) + }) + + it('should render functional Copy Tx ID and View In Etherscan buttons when tx hash exists', () => { + const transaction = { + history: [], + id: 1, + status: 'confirmed', + hash: '0xaa', + txParams: { + from: '0x1', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0xa4', + to: '0x2', + value: '0x2386f26fc10000', + }, + } + + const transactionGroup = { + transactions: [transaction], + primaryTransaction: transaction, + initialTransaction: transaction, + } + + const wrapper = shallow( + <TransactionListItemDetails + transactionGroup={transactionGroup} + />, + { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } + ) + + assert.ok(wrapper.hasClass('transaction-list-item-details')) + const buttons = wrapper.find(Button) + assert.strictEqual(buttons.at(0).prop('disabled'), false) + assert.strictEqual(buttons.at(1).prop('disabled'), false) + }) }) diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 72ca784e2..d8dd965fc 100644 --- a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -128,7 +128,7 @@ export default class TransactionListItemDetails extends PureComponent { rpcPrefs: { blockExplorerUrl } = {}, } = this.props const { primaryTransaction: transaction } = transactionGroup - const { txParams: { to, from } = {} } = transaction + const { hash, txParams: { to, from } = {} } = transaction return ( <div className="transaction-list-item-details"> @@ -152,6 +152,7 @@ export default class TransactionListItemDetails extends PureComponent { type="raised" onClick={this.handleCopyTxId} className="transaction-list-item-details__header-button" + disabled={!hash} > <img className="transaction-list-item-details__header-button__copy-icon" @@ -164,7 +165,8 @@ export default class TransactionListItemDetails extends PureComponent { type="raised" onClick={this.handleEtherscanClick} className="transaction-list-item-details__header-button" - > + disabled={!hash} + > <img src="/images/arrow-popout.svg" /> </Button> </Tooltip> diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index fc5488884..157e7200b 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -10,11 +10,13 @@ export default class TransactionList extends PureComponent { } static defaultProps = { + children: null, pendingTransactions: [], completedTransactions: [], } static propTypes = { + children: PropTypes.node, pendingTransactions: PropTypes.array, completedTransactions: PropTypes.array, selectedToken: PropTypes.object, @@ -39,7 +41,7 @@ export default class TransactionList extends PureComponent { const { transactions = [], hasRetried } = transactionGroup const [earliestTransaction = {}] = transactions const { submittedTime } = earliestTransaction - return Date.now() - submittedTime > 30000 && isEarliestNonce && !hasRetried + return Date.now() - submittedTime > 5000 && isEarliestNonce && !hasRetried } shouldShowCancel (transactionGroup) { @@ -75,8 +77,8 @@ export default class TransactionList extends PureComponent { { completedTransactions.length > 0 ? completedTransactions.map((transactionGroup, index) => ( - this.renderTransaction(transactionGroup, index) - )) + this.renderTransaction(transactionGroup, index) + )) : this.renderEmpty() } </div> @@ -120,6 +122,7 @@ export default class TransactionList extends PureComponent { return ( <div className="transaction-list"> { this.renderTransactions() } + { this.props.children } </div> ) } diff --git a/ui/app/components/app/transaction-status/index.scss b/ui/app/components/app/transaction-status/index.scss index 024cbf2a1..99884d28c 100644 --- a/ui/app/components/app/transaction-status/index.scss +++ b/ui/app/components/app/transaction-status/index.scss @@ -43,4 +43,10 @@ border: 1px solid $monzo; } } + + &__pending-spinner { + height: 16px; + width: 16px; + margin-right: 6px; + } } diff --git a/ui/app/components/app/transaction-status/transaction-status.component.js b/ui/app/components/app/transaction-status/transaction-status.component.js index d3a239539..a97b79bde 100644 --- a/ui/app/components/app/transaction-status/transaction-status.component.js +++ b/ui/app/components/app/transaction-status/transaction-status.component.js @@ -2,6 +2,8 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import Tooltip from '../../ui/tooltip-v2' +import Spinner from '../../ui/spinner' + import { UNAPPROVED_STATUS, REJECTED_STATUS, @@ -51,6 +53,7 @@ export default class TransactionStatus extends PureComponent { return ( <div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}> + { statusToTextHash[statusKey] === 'pending' ? <Spinner className="transaction-status__pending-spinner" /> : null } <Tooltip position="top" title={title} diff --git a/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js index 3f6abbb00..feb701dbe 100644 --- a/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js +++ b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js @@ -43,38 +43,38 @@ export default class TransactionViewBalance extends PureComponent { /> </div> ) : ( - <Tooltip position="top" title={this.context.t('balanceOutdated')} disabled={!balanceIsCached}> - <div className="transaction-view-balance__balance"> - <div className="transaction-view-balance__primary-container"> - <UserPreferencedCurrencyDisplay - className={classnames('transaction-view-balance__primary-balance', { - 'transaction-view-balance__cached-balance': balanceIsCached, - })} - value={balance} - type={PRIMARY} - ethNumberOfDecimals={4} - hideTitle={true} - /> - { - balanceIsCached ? <span className="transaction-view-balance__cached-star">*</span> : null - } - </div> - { - showFiat && ( - <UserPreferencedCurrencyDisplay - className={classnames({ - 'transaction-view-balance__cached-secondary-balance': balanceIsCached, - 'transaction-view-balance__secondary-balance': !balanceIsCached, - })} - value={balance} - type={SECONDARY} - ethNumberOfDecimals={4} - hideTitle={true} - /> - ) - } + <Tooltip position="top" title={this.context.t('balanceOutdated')} disabled={!balanceIsCached}> + <div className="transaction-view-balance__balance"> + <div className="transaction-view-balance__primary-container"> + <UserPreferencedCurrencyDisplay + className={classnames('transaction-view-balance__primary-balance', { + 'transaction-view-balance__cached-balance': balanceIsCached, + })} + value={balance} + type={PRIMARY} + ethNumberOfDecimals={4} + hideTitle={true} + /> + { + balanceIsCached ? <span className="transaction-view-balance__cached-star">*</span> : null + } </div> - </Tooltip> + { + showFiat && ( + <UserPreferencedCurrencyDisplay + className={classnames({ + 'transaction-view-balance__cached-secondary-balance': balanceIsCached, + 'transaction-view-balance__secondary-balance': !balanceIsCached, + })} + value={balance} + type={SECONDARY} + ethNumberOfDecimals={4} + hideTitle={true} + /> + ) + } + </div> + </Tooltip> ) } diff --git a/ui/app/components/app/transaction-view/transaction-view.component.js b/ui/app/components/app/transaction-view/transaction-view.component.js index 7014ca173..fb2c2145c 100644 --- a/ui/app/components/app/transaction-view/transaction-view.component.js +++ b/ui/app/components/app/transaction-view/transaction-view.component.js @@ -10,6 +10,14 @@ export default class TransactionView extends PureComponent { t: PropTypes.func, } + static propTypes = { + children: PropTypes.node, + } + + static defaultProps = { + children: null, + } + render () { return ( <div className="transaction-view"> @@ -20,7 +28,9 @@ export default class TransactionView extends PureComponent { <div className="transaction-view__balance-wrapper"> <TransactionViewBalance /> </div> - <TransactionList /> + <TransactionList> + { this.props.children } + </TransactionList> </div> ) } diff --git a/ui/app/components/app/ui-migration-annoucement/index.js b/ui/app/components/app/ui-migration-annoucement/index.js deleted file mode 100644 index c6c8cc619..000000000 --- a/ui/app/components/app/ui-migration-annoucement/index.js +++ /dev/null @@ -1 +0,0 @@ -export {default} from './ui-migration-announcement.container' diff --git a/ui/app/components/app/ui-migration-annoucement/index.scss b/ui/app/components/app/ui-migration-annoucement/index.scss deleted file mode 100644 index 6138a3079..000000000 --- a/ui/app/components/app/ui-migration-annoucement/index.scss +++ /dev/null @@ -1,22 +0,0 @@ -.ui-migration-announcement { - position: absolute; - z-index: 9999; - width: 100vw; - height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - background: $white; - - p { - box-sizing: border-box; - padding: 1em; - font-size: 12pt; - } - - p:last-of-type { - cursor: pointer; - text-decoration: underline; - font-weight: bold; - } -} diff --git a/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js b/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js deleted file mode 100644 index 7a4124972..000000000 --- a/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types' -import React, {PureComponent} from 'react' - -export default class UiMigrationAnnouncement extends PureComponent { - static contextTypes = { - t: PropTypes.func.isRequired, - } - - static defaultProps = { - shouldShowAnnouncement: true, - }; - - static propTypes = { - onClose: PropTypes.func.isRequired, - shouldShowAnnouncement: PropTypes.bool, - } - - render () { - const { t } = this.context - const { onClose, shouldShowAnnouncement } = this.props - - if (!shouldShowAnnouncement) { - return null - } - - return ( - <div className="ui-migration-announcement"> - <p>{t('uiMigrationAnnouncement')}</p> - <p onClick={onClose}>{t('close')}</p> - </div> - ) - } -} diff --git a/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js b/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js deleted file mode 100644 index 55efd5a44..000000000 --- a/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux' -import UiMigrationAnnouncement from './ui-migration-annoucement.component' -import { setCompletedUiMigration } from '../../../store/actions' - -const mapStateToProps = (state) => { - const shouldShowAnnouncement = !state.metamask.completedUiMigration - - return { - shouldShowAnnouncement, - } -} - -const mapDispatchToProps = dispatch => { - return { - onClose () { - dispatch(setCompletedUiMigration()) - }, - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(UiMigrationAnnouncement) diff --git a/ui/app/components/app/wallet-view.js b/ui/app/components/app/wallet-view.js index b8bae5421..55aeec333 100644 --- a/ui/app/components/app/wallet-view.js +++ b/ui/app/components/app/wallet-view.js @@ -5,12 +5,8 @@ const h = require('react-hyperscript') const { withRouter } = require('react-router-dom') const { compose } = require('recompose') const inherits = require('util').inherits -const classnames = require('classnames') const { checksumAddress } = require('../../helpers/utils/util') -import Identicon from '../ui/identicon' // const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns -const Tooltip = require('../ui/tooltip-v2.js').default -const copyToClipboard = require('copy-to-clipboard') const actions = require('../../store/actions') import BalanceComponent from '../ui/balance' const TokenList = require('./token-list') @@ -18,6 +14,7 @@ const selectors = require('../../selectors/selectors') const { ADD_TOKEN_ROUTE } = require('../../helpers/constants/routes') import AddTokenButton from './add-token-button' +import AccountDetails from './account-details' module.exports = compose( withRouter, @@ -52,9 +49,6 @@ function mapDispatchToProps (dispatch) { showSendPage: () => dispatch(actions.showSendPage()), hideSidebar: () => dispatch(actions.hideSidebar()), unsetSelectedToken: () => dispatch(actions.setSelectedToken()), - showAccountDetailModal: () => { - dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) - }, showAddTokenPage: () => dispatch(actions.showAddTokenPage()), } } @@ -62,10 +56,6 @@ function mapDispatchToProps (dispatch) { inherits(WalletView, Component) function WalletView () { Component.call(this) - this.state = { - hasCopied: false, - copyToClipboardPressed: false, - } } WalletView.prototype.renderWalletBalance = function () { @@ -130,8 +120,6 @@ WalletView.prototype.render = function () { responsiveDisplayClassname, selectedAddress, keyrings, - showAccountDetailModal, - hideSidebar, identities, network, } = this.props @@ -165,67 +153,11 @@ WalletView.prototype.render = function () { className: responsiveDisplayClassname, }, [ - // TODO: Separate component: wallet account details - h('div.flex-column.wallet-view-account-details', { - style: {}, - }, [ - h('div.wallet-view__sidebar-close', { - onClick: hideSidebar, - }), - - h('div.wallet-view__keyring-label.allcaps', label), - - h('div.flex-column.flex-center.wallet-view__name-container', { - style: { margin: '0 auto' }, - onClick: showAccountDetailModal, - }, [ - h(Identicon, { - diameter: 54, - address: checksummedAddress, - }), - - h('span.account-name', { - style: {}, - }, [ - identities[selectedAddress].name, - ]), - - h('button.btn-secondary.wallet-view__details-button', this.context.t('details')), - ]), - ]), - - h(Tooltip, { - position: 'bottom', - title: this.state.hasCopied ? this.context.t('copiedExclamation') : this.context.t('copyToClipboard'), - wrapperClassName: 'wallet-view__tooltip', - }, [ - h('button.wallet-view__address', { - className: classnames({ - 'wallet-view__address__pressed': this.state.copyToClipboardPressed, - }), - onClick: () => { - copyToClipboard(checksummedAddress) - this.context.metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Home', - name: 'Copied Address', - }, - }) - this.setState({ hasCopied: true }) - setTimeout(() => this.setState({ hasCopied: false }), 3000) - }, - onMouseDown: () => { - this.setState({ copyToClipboardPressed: true }) - }, - onMouseUp: () => { - this.setState({ copyToClipboardPressed: false }) - }, - }, [ - `${checksummedAddress.slice(0, 6)}...${checksummedAddress.slice(-4)}`, - h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }), - ]), - ]), + h(AccountDetails, { + label, + checksummedAddress, + name: identities[selectedAddress].name, + }), this.renderWalletBalance(), diff --git a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js deleted file mode 100644 index d9627e31b..000000000 --- a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js +++ /dev/null @@ -1,84 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import AccountListItem from '../../../pages/send/account-list-item/account-list-item.component' - -export default class AccountDropdownMini extends PureComponent { - static propTypes = { - accounts: PropTypes.array.isRequired, - closeDropdown: PropTypes.func, - disabled: PropTypes.bool, - dropdownOpen: PropTypes.bool, - onSelect: PropTypes.func, - openDropdown: PropTypes.func, - selectedAccount: PropTypes.object.isRequired, - } - - static defaultProps = { - closeDropdown: () => {}, - disabled: false, - dropdownOpen: false, - onSelect: () => {}, - openDropdown: () => {}, - } - - getListItemIcon (currentAccount, selectedAccount) { - return currentAccount.address === selectedAccount.address && ( - <i - className="fa fa-check fa-lg" - style={{ color: '#02c9b1' }} - /> - ) - } - - renderDropdown () { - const { accounts, selectedAccount, closeDropdown, onSelect } = this.props - - return ( - <div> - <div - className="account-dropdown-mini__close-area" - onClick={closeDropdown} - /> - <div className="account-dropdown-mini__list"> - { - accounts.map(account => ( - <AccountListItem - key={account.address} - account={account} - displayBalance={false} - displayAddress={false} - handleClick={() => { - onSelect(account) - closeDropdown() - }} - icon={this.getListItemIcon(account, selectedAccount)} - /> - )) - } - </div> - </div> - ) - } - - render () { - const { disabled, selectedAccount, openDropdown, dropdownOpen } = this.props - - return ( - <div className="account-dropdown-mini"> - <AccountListItem - account={selectedAccount} - handleClick={() => !disabled && openDropdown()} - displayBalance={false} - displayAddress={false} - icon={ - !disabled && <i - className="fa fa-caret-down fa-lg" - style={{ color: '#dedede' }} - /> - } - /> - { !disabled && dropdownOpen && this.renderDropdown() } - </div> - ) - } -} diff --git a/ui/app/components/ui/account-dropdown-mini/index.js b/ui/app/components/ui/account-dropdown-mini/index.js deleted file mode 100644 index cb0839e72..000000000 --- a/ui/app/components/ui/account-dropdown-mini/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './account-dropdown-mini.component' diff --git a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js deleted file mode 100644 index 9691f38aa..000000000 --- a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import AccountDropdownMini from '../account-dropdown-mini.component' -import AccountListItem from '../../../../pages/send/account-list-item/account-list-item.component' - -describe('AccountDropdownMini', () => { - it('should render an account with an icon', () => { - const accounts = [ - { - address: '0x1', - name: 'account1', - balance: '0x1', - }, - { - address: '0x2', - name: 'account2', - balance: '0x2', - }, - { - address: '0x3', - name: 'account3', - balance: '0x3', - }, - ] - - const wrapper = shallow( - <AccountDropdownMini - selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }} - accounts={accounts} - /> - ) - - assert.ok(wrapper) - assert.equal(wrapper.find(AccountListItem).length, 1) - const accountListItemProps = wrapper.find(AccountListItem).at(0).props() - assert.equal(accountListItemProps.account.address, '0x1') - const iconProps = accountListItemProps.icon.props - assert.equal(iconProps.className, 'fa fa-caret-down fa-lg') - }) - - it('should render a list of accounts', () => { - const accounts = [ - { - address: '0x1', - name: 'account1', - balance: '0x1', - }, - { - address: '0x2', - name: 'account2', - balance: '0x2', - }, - { - address: '0x3', - name: 'account3', - balance: '0x3', - }, - ] - - const wrapper = shallow( - <AccountDropdownMini - selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }} - accounts={accounts} - dropdownOpen={true} - /> - ) - - assert.ok(wrapper) - assert.equal(wrapper.find(AccountListItem).length, 4) - }) - - it('should render a single account when disabled', () => { - const accounts = [ - { - address: '0x1', - name: 'account1', - balance: '0x1', - }, - { - address: '0x2', - name: 'account2', - balance: '0x2', - }, - { - address: '0x3', - name: 'account3', - balance: '0x3', - }, - ] - - const wrapper = shallow( - <AccountDropdownMini - selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }} - accounts={accounts} - dropdownOpen={false} - disabled={true} - /> - ) - - assert.ok(wrapper) - assert.equal(wrapper.find(AccountListItem).length, 1) - const accountListItemProps = wrapper.find(AccountListItem).at(0).props() - assert.equal(accountListItemProps.account.address, '0x1') - assert.equal(accountListItemProps.icon, false) - }) -}) diff --git a/ui/app/components/ui/alert/index.js b/ui/app/components/ui/alert/index.js index b1229f502..da2ca4b66 100644 --- a/ui/app/components/ui/alert/index.js +++ b/ui/app/components/ui/alert/index.js @@ -4,59 +4,59 @@ const h = require('react-hyperscript') class Alert extends Component { - constructor (props) { - super(props) - - this.state = { - visble: false, - msg: false, - className: '', - } - } - - componentWillReceiveProps (nextProps) { - if (!this.props.visible && nextProps.visible) { - this.animateIn(nextProps) - } else if (this.props.visible && !nextProps.visible) { - this.animateOut() - } - } + constructor (props) { + super(props) - animateIn (props) { - this.setState({ - msg: props.msg, - visible: true, - className: '.visible', - }) + this.state = { + visble: false, + msg: false, + className: '', } + } - animateOut () { - this.setState({ - msg: null, - className: '.hidden', - }) - - setTimeout(_ => { - this.setState({visible: false}) - }, 500) - + componentWillReceiveProps (nextProps) { + if (!this.props.visible && nextProps.visible) { + this.animateIn(nextProps) + } else if (this.props.visible && !nextProps.visible) { + this.animateOut() } - - render () { - if (this.state.visible) { - return ( - h(`div.global-alert${this.state.className}`, {}, - h('a.msg', {}, this.state.msg) - ) - ) - } - return null + } + + animateIn (props) { + this.setState({ + msg: props.msg, + visible: true, + className: '.visible', + }) + } + + animateOut () { + this.setState({ + msg: null, + className: '.hidden', + }) + + setTimeout(_ => { + this.setState({visible: false}) + }, 500) + + } + + render () { + if (this.state.visible) { + return ( + h(`div.global-alert${this.state.className}`, {}, + h('a.msg', {}, this.state.msg) + ) + ) } + return null + } } Alert.propTypes = { - visible: PropTypes.bool.isRequired, - msg: PropTypes.string, + visible: PropTypes.bool.isRequired, + msg: PropTypes.string, } module.exports = Alert diff --git a/ui/app/components/ui/button-group/tests/button-group-component.test.js b/ui/app/components/ui/button-group/tests/button-group-component.test.js index 0bece90d6..f2e512445 100644 --- a/ui/app/components/ui/button-group/tests/button-group-component.test.js +++ b/ui/app/components/ui/button-group/tests/button-group-component.test.js @@ -59,40 +59,40 @@ describe('ButtonGroup Component', function () { describe('renderButtons', () => { it('should render a button for each child', () => { - const childButtons = wrapper.find('.button-group__button') - assert.equal(childButtons.length, 3) + const childButtons = wrapper.find('.button-group__button') + assert.equal(childButtons.length, 3) }) it('should render the correct button with an active state', () => { - const childButtons = wrapper.find('.button-group__button') - const activeChildButton = wrapper.find('.button-group__button--active') - assert.deepEqual(childButtons.get(1), activeChildButton.get(0)) + const childButtons = wrapper.find('.button-group__button') + const activeChildButton = wrapper.find('.button-group__button--active') + assert.deepEqual(childButtons.get(1), activeChildButton.get(0)) }) it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => { - assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0) - assert.equal(childButtonSpies.onClick.callCount, 0) - const childButtons = wrapper.find('.button-group__button') - childButtons.at(0).props().onClick() - childButtons.at(1).props().onClick() - childButtons.at(2).props().onClick() - assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3) - assert.equal(childButtonSpies.onClick.callCount, 3) + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0) + assert.equal(childButtonSpies.onClick.callCount, 0) + const childButtons = wrapper.find('.button-group__button') + childButtons.at(0).props().onClick() + childButtons.at(1).props().onClick() + childButtons.at(2).props().onClick() + assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3) + assert.equal(childButtonSpies.onClick.callCount, 3) }) it('should render all child buttons as disabled if props.disabled is true', () => { - const childButtons = wrapper.find('.button-group__button') - childButtons.forEach(button => { - assert.equal(button.props().disabled, undefined) - }) - wrapper.setProps({ disabled: true }) - const disabledChildButtons = wrapper.find('[disabled=true]') - assert.equal(disabledChildButtons.length, 3) + const childButtons = wrapper.find('.button-group__button') + childButtons.forEach(button => { + assert.equal(button.props().disabled, undefined) + }) + wrapper.setProps({ disabled: true }) + const disabledChildButtons = wrapper.find('[disabled=true]') + assert.equal(disabledChildButtons.length, 3) }) it('should render the children of the button', () => { - const mockClass = wrapper.find('.mockClass') - assert.equal(mockClass.length, 1) + const mockClass = wrapper.find('.mockClass') + assert.equal(mockClass.length, 1) }) }) @@ -103,9 +103,9 @@ describe('ButtonGroup Component', function () { }) it('should call renderButtons when rendering', () => { - assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1) - wrapper.instance().render() - assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2) + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1) + wrapper.instance().render() + assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2) }) }) }) diff --git a/ui/app/components/ui/button/buttons.scss b/ui/app/components/ui/button/buttons.scss index f1366cffe..f6388fa47 100644 --- a/ui/app/components/ui/button/buttons.scss +++ b/ui/app/components/ui/button/buttons.scss @@ -22,7 +22,6 @@ $hover-orange: #FFD3B5; box-sizing: border-box; border-radius: 6px; width: 100%; - outline: none; transition: border-color .3s ease, background-color .3s ease; &--disabled, diff --git a/ui/app/components/ui/currency-input/currency-input.component.js b/ui/app/components/ui/currency-input/currency-input.component.js index 1876c9591..f7db2b829 100644 --- a/ui/app/components/ui/currency-input/currency-input.component.js +++ b/ui/app/components/ui/currency-input/currency-input.component.js @@ -141,22 +141,22 @@ export default class CurrencyInput extends PureComponent { const { decimalValue } = this.state return ( - <UnitInput - {...restProps} - suffix={this.shouldUseFiat() ? fiatSuffix : nativeSuffix} - onChange={this.handleChange} - onBlur={this.handleBlur} - value={decimalValue} - maxModeOn={maxModeOn} - actionComponent={( - <div - className="currency-input__swap-component" - onClick={this.swap} - /> - )} - > - { this.renderConversionComponent() } - </UnitInput> + <UnitInput + {...restProps} + suffix={this.shouldUseFiat() ? fiatSuffix : nativeSuffix} + onChange={this.handleChange} + onBlur={this.handleBlur} + value={decimalValue} + maxModeOn={maxModeOn} + actionComponent={( + <div + className="currency-input__swap-component" + onClick={this.swap} + /> + )} + > + { this.renderConversionComponent() } + </UnitInput> ) } } diff --git a/ui/app/components/ui/dialog/dialog.scss b/ui/app/components/ui/dialog/dialog.scss new file mode 100644 index 000000000..68b5ce329 --- /dev/null +++ b/ui/app/components/ui/dialog/dialog.scss @@ -0,0 +1,26 @@ +.dialog { + font-size: .75rem; + line-height: 1rem; + padding: 1rem; + border: 1px solid $black; + box-sizing: border-box; + border-radius: 8px; + + &--message { + border-color: $Blue-200; + color: $Blue-600; + background-color: $Blue-000; + } + + &--error { + border-color: $Red-300; + color: $Red-600; + background-color: $Red-000; + } + + &--warning { + border-color: $Orange-300; + color: $Orange-600; + background-color: $Orange-000; + } +} diff --git a/ui/app/components/ui/dialog/index.js b/ui/app/components/ui/dialog/index.js new file mode 100644 index 000000000..d7e522b22 --- /dev/null +++ b/ui/app/components/ui/dialog/index.js @@ -0,0 +1,26 @@ +import React from 'react' +import PropTypes from 'prop-types' +import c from 'classnames' + +export default function Dialog (props) { + const { children, type, className, onClick } = props + return ( + <div + className={c('dialog', className, { + 'dialog--message': type === 'message', + 'dialog--error': type === 'error', + 'dialog--warning': type === 'warning', + })} + onClick={onClick} + > + { children } + </div> + ) +} + +Dialog.propTypes = { + className: PropTypes.string, + children: PropTypes.node, + type: PropTypes.oneOf(['message', 'error', 'warning']), + onClick: PropTypes.func, +} diff --git a/ui/app/components/ui/identicon/identicon.component.js b/ui/app/components/ui/identicon/identicon.component.js index 88521247c..5582c7d12 100644 --- a/ui/app/components/ui/identicon/identicon.component.js +++ b/ui/app/components/ui/identicon/identicon.component.js @@ -16,6 +16,7 @@ const getStyles = diameter => ( export default class Identicon extends PureComponent { static propTypes = { + addBorder: PropTypes.bool, address: PropTypes.string, className: PropTypes.string, diameter: PropTypes.number, @@ -70,7 +71,7 @@ export default class Identicon extends PureComponent { } render () { - const { className, address, image, diameter, useBlockie } = this.props + const { className, address, image, diameter, useBlockie, addBorder } = this.props if (image) { return this.renderImage() @@ -83,9 +84,11 @@ export default class Identicon extends PureComponent { return this.renderJazzicon() } - return useBlockie - ? this.renderBlockie() - : this.renderJazzicon() + return ( + <div className={classnames({ 'identicon__address-wrapper': addBorder })}> + { useBlockie ? this.renderBlockie() : this.renderJazzicon() } + </div> + ) } return ( diff --git a/ui/app/components/ui/identicon/index.scss b/ui/app/components/ui/identicon/index.scss index 657afc48f..4c8213f01 100644 --- a/ui/app/components/ui/identicon/index.scss +++ b/ui/app/components/ui/identicon/index.scss @@ -4,4 +4,17 @@ align-items: center; justify-content: center; overflow: hidden; + + &__address-wrapper { + height: 40px; + width: 40px; + border-radius: 18px; + display: flex; + justify-content: center; + align-items: center; + border-style: solid; + border-radius: 50%; + border-width: 2px; + border-color: $curious-blue; + } } diff --git a/ui/app/components/ui/metafox-logo/index.js b/ui/app/components/ui/metafox-logo/index.js new file mode 100644 index 000000000..0aeaed743 --- /dev/null +++ b/ui/app/components/ui/metafox-logo/index.js @@ -0,0 +1 @@ +export { default } from './metafox-logo.component' diff --git a/ui/app/components/ui/metafox-logo/metafox-logo.component.js b/ui/app/components/ui/metafox-logo/metafox-logo.component.js new file mode 100644 index 000000000..041e354ef --- /dev/null +++ b/ui/app/components/ui/metafox-logo/metafox-logo.component.js @@ -0,0 +1,31 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' + +export default class MetaFoxLogo extends PureComponent { + static propTypes = { + onClick: PropTypes.func, + unsetIconHeight: PropTypes.bool, + } + + render () { + const iconProps = this.props.unsetIconHeight ? {} : { height: 42, width: 42 } + + return ( + <div + onClick={this.props.onClick} + className="app-header__logo-container" + > + <img + height={30} + src="/images/logo/metamask-logo-horizontal.svg" + className="app-header__metafox-logo app-header__metafox-logo--horizontal" + /> + <img + {...iconProps} + src="/images/logo/metamask-fox.svg" + className="app-header__metafox-logo app-header__metafox-logo--icon" + /> + </div> + ) + } +} diff --git a/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js b/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js new file mode 100644 index 000000000..c794a004f --- /dev/null +++ b/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js @@ -0,0 +1,25 @@ +import React from 'react' +import assert from 'assert' +import { mount } from 'enzyme' +import MetaFoxLogo from '../' + +describe('MetaFoxLogo', () => { + + it('sets icon height and width to 42 by default', () => { + const wrapper = mount( + <MetaFoxLogo /> + ) + + assert.equal(wrapper.find('img.app-header__metafox-logo--icon').prop('width'), 42) + assert.equal(wrapper.find('img.app-header__metafox-logo--icon').prop('height'), 42) + }) + + it('does not set icon height and width when unsetIconHeight is true', () => { + const wrapper = mount( + <MetaFoxLogo unsetIconHeight={true} /> + ) + + assert.equal(wrapper.find('img.app-header__metafox-logo--icon').prop('width'), null) + assert.equal(wrapper.find('img.app-header__metafox-logo--icon').prop('height'), null) + }) +}) diff --git a/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js index 08f9c7544..f1e15f10f 100644 --- a/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js +++ b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import classnames from 'classnames' +import c from 'classnames' export default class PageContainerHeader extends Component { static propTypes = { @@ -13,6 +13,7 @@ export default class PageContainerHeader extends Component { backButtonString: PropTypes.string, tabs: PropTypes.node, headerCloseText: PropTypes.string, + className: PropTypes.string, } renderTabs () { @@ -42,15 +43,14 @@ export default class PageContainerHeader extends Component { } render () { - const { title, subtitle, onClose, tabs, headerCloseText } = this.props + const { title, subtitle, onClose, tabs, headerCloseText, className } = this.props return ( - <div className={ - classnames( - 'page-container__header', - { 'page-container__header--no-padding-bottom': Boolean(tabs) } - ) - }> + <div + className={c('page-container__header', className, { + 'page-container__header--no-padding-bottom': Boolean(tabs), + })} + > { this.renderHeaderRow() } diff --git a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js index 57b595d48..a98a94101 100644 --- a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -64,10 +64,10 @@ export default class SenderToRecipient extends PureComponent { containerClassName="sender-to-recipient__tooltip-container" onHidden={() => this.setState({ senderAddressCopied: false })} > - <div className="sender-to-recipient__name"> - { addressOnly ? `${t('from')}: ${checksummedSenderAddress}` : senderName } - </div> - </Tooltip> + <div className="sender-to-recipient__name"> + { addressOnly ? `${t('from')}: ${checksummedSenderAddress}` : senderName } + </div> + </Tooltip> ) } diff --git a/ui/app/components/ui/text-field/text-field.component.js b/ui/app/components/ui/text-field/text-field.component.js index 1153a595b..ac7712c65 100644 --- a/ui/app/components/ui/text-field/text-field.component.js +++ b/ui/app/components/ui/text-field/text-field.component.js @@ -61,6 +61,9 @@ const styles = { ...inputLabelBase, fontSize: '.75rem', }, + inputMultiline: { + lineHeight: 'initial !important', + }, } const TextField = props => { diff --git a/ui/app/components/ui/toggle-button/index.js b/ui/app/components/ui/toggle-button/index.js new file mode 100644 index 000000000..7948d3ca1 --- /dev/null +++ b/ui/app/components/ui/toggle-button/index.js @@ -0,0 +1,2 @@ +import ToggleButton from './toggle-button.component' +module.exports = ToggleButton diff --git a/ui/app/components/ui/toggle-button/index.scss b/ui/app/components/ui/toggle-button/index.scss new file mode 100644 index 000000000..868d416c8 --- /dev/null +++ b/ui/app/components/ui/toggle-button/index.scss @@ -0,0 +1,14 @@ +.toggle-button { + display: flex; + + &__status-label { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 16px; + line-height: 23px; + display: flex; + align-items: center; + text-transform: uppercase; + } +}
\ No newline at end of file diff --git a/ui/app/components/ui/toggle-button/toggle-button.component.js b/ui/app/components/ui/toggle-button/toggle-button.component.js new file mode 100644 index 000000000..3f13203a5 --- /dev/null +++ b/ui/app/components/ui/toggle-button/toggle-button.component.js @@ -0,0 +1,75 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ReactToggleButton from 'react-toggle-button' + +const trackStyle = { + width: '40px', + height: '24px', + padding: '0px', + borderRadius: '26px', + border: '2px solid rgb(3, 125, 214)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +} + +const offTrackStyle = { + ...trackStyle, + border: '2px solid #8E8E8E', +} + +const thumbStyle = { + width: '18px', + height: '18px', + display: 'flex', + boxShadow: 'none', + alignSelf: 'center', + borderRadius: '50%', + position: 'relative', +} + +const colors = { + activeThumb: { + base: '#037DD6', + }, + inactiveThumb: { + base: '#037DD6', + }, + active: { + base: '#ffffff', + hover: '#ffffff', + }, + inactive: { + base: '#DADADA', + hover: '#DADADA', + }, +} + +const ToggleButton = props => { + const { value, onToggle, offLabel, onLabel } = props + + return ( + <div className="toggle-button"> + <ReactToggleButton + value={value} + onToggle={onToggle} + activeLabel="" + inactiveLabel="" + trackStyle={value ? trackStyle : offTrackStyle} + thumbStyle={thumbStyle} + thumbAnimateRange={[3, 18]} + colors={colors} + /> + <div className="toggle-button__status-label">{ value ? onLabel : offLabel }</div> + </div> + ) +} + +ToggleButton.propTypes = { + value: PropTypes.bool, + onToggle: PropTypes.func, + offLabel: PropTypes.string, + onLabel: PropTypes.string, +} + +export default ToggleButton diff --git a/ui/app/components/ui/unit-input/index.scss b/ui/app/components/ui/unit-input/index.scss index 58a10c9a1..338b3829f 100644 --- a/ui/app/components/ui/unit-input/index.scss +++ b/ui/app/components/ui/unit-input/index.scss @@ -38,7 +38,6 @@ font-size: 1rem; font-family: Roboto; border: none; - outline: 0 !important; max-width: 22ch; height: 16px; line-height: 18px; |