diff options
Diffstat (limited to 'ui/app/pages/send/send-content/add-recipient')
12 files changed, 1080 insertions, 0 deletions
diff --git a/ui/app/pages/send/send-content/add-recipient/add-recipient.component.js b/ui/app/pages/send/send-content/add-recipient/add-recipient.component.js new file mode 100644 index 000000000..e5edbc08d --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/add-recipient.component.js @@ -0,0 +1,243 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Fuse from 'fuse.js' +import Identicon from '../../../../components/ui/identicon' +import {isValidAddress} from '../../../../helpers/utils/util' +import Dialog from '../../../../components/ui/dialog' +import ContactList from '../../../../components/app/contact-list' +import RecipientGroup from '../../../../components/app/contact-list/recipient-group/recipient-group.component' +import {ellipsify} from '../../send.utils' + +export default class AddRecipient extends Component { + + static propTypes = { + className: PropTypes.string, + query: PropTypes.string, + ownedAccounts: PropTypes.array, + addressBook: PropTypes.array, + updateGas: PropTypes.func, + updateSendTo: PropTypes.func, + ensResolution: PropTypes.string, + toError: PropTypes.string, + toWarning: PropTypes.string, + ensResolutionError: PropTypes.string, + selectedToken: PropTypes.object, + hasHexData: PropTypes.bool, + tokens: PropTypes.array, + addressBookEntryName: PropTypes.string, + contacts: PropTypes.array, + nonContacts: PropTypes.array, + } + + constructor (props) { + super(props) + this.recentFuse = new Fuse(props.nonContacts, { + shouldSort: true, + threshold: 0.45, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + { name: 'address', weight: 0.5 }, + ], + }) + + this.contactFuse = new Fuse(props.contacts, { + shouldSort: true, + threshold: 0.45, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + { name: 'name', weight: 0.5 }, + { name: 'address', weight: 0.5 }, + ], + }) + } + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + state = { + isShowingTransfer: false, + isShowingAllRecent: false, + } + + selectRecipient = (to, nickname = '') => { + const { updateSendTo, updateGas } = this.props + + updateSendTo(to, nickname) + updateGas({ to }) + } + + searchForContacts = () => { + const { query, contacts } = this.props + + let _contacts = contacts + + if (query) { + this.contactFuse.setCollection(contacts) + _contacts = this.contactFuse.search(query) + } + + return _contacts + } + + searchForRecents = () => { + const { query, nonContacts } = this.props + + let _nonContacts = nonContacts + + if (query) { + this.recentFuse.setCollection(nonContacts) + _nonContacts = this.recentFuse.search(query) + } + + return _nonContacts + } + + render () { + const { ensResolution, query, addressBookEntryName } = this.props + const { isShowingTransfer } = this.state + + let content + + if (isValidAddress(query)) { + content = this.renderExplicitAddress(query) + } else if (ensResolution) { + content = this.renderExplicitAddress(ensResolution, addressBookEntryName || query) + } else if (isShowingTransfer) { + content = this.renderTransfer() + } + + return ( + <div className="send__select-recipient-wrapper"> + { this.renderDialogs() } + { content || this.renderMain() } + </div> + ) + } + + renderExplicitAddress (address, name) { + return ( + <div + key={address} + className="send__select-recipient-wrapper__group-item" + onClick={() => this.selectRecipient(address, name)} + > + <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> + ) + } + + renderTransfer () { + const { ownedAccounts } = this.props + const { t } = this.context + + return ( + <div className="send__select-recipient-wrapper__list"> + <div + className="send__select-recipient-wrapper__list__link" + onClick={() => this.setState({ isShowingTransfer: false })} + > + <div className="send__select-recipient-wrapper__list__back-caret"/> + { t('backToAll') } + </div> + <RecipientGroup + label={t('myAccounts')} + items={ownedAccounts} + onSelect={this.selectRecipient} + /> + </div> + ) + } + + renderMain () { + const { t } = this.context + const { query, ownedAccounts = [], addressBook } = this.props + + return ( + <div className="send__select-recipient-wrapper__list"> + <ContactList + addressBook={addressBook} + searchForContacts={this.searchForContacts.bind(this)} + searchForRecents={this.searchForRecents.bind(this)} + selectRecipient={this.selectRecipient.bind(this)} + > + { + (ownedAccounts && ownedAccounts.length > 1) && !query && ( + <div + className="send__select-recipient-wrapper__list__link" + onClick={() => this.setState({ isShowingTransfer: true })} + > + { t('transferBetweenAccounts') } + </div> + ) + } + </ContactList> + </div> + ) + } + + renderDialogs () { + const { toError, toWarning, ensResolutionError, ensResolution } = this.props + const { t } = this.context + const contacts = this.searchForContacts() + const recents = this.searchForRecents() + + if (contacts.length || recents.length) { + return null + } + + if (ensResolutionError) { + return ( + <Dialog + type="error" + className="send__error-dialog" + > + {ensResolutionError} + </Dialog> + ) + } + + if (toError && toError !== 'required' && !ensResolution) { + return ( + <Dialog + type="error" + className="send__error-dialog" + > + {t(toError)} + </Dialog> + ) + } + + + if (toWarning) { + return ( + <Dialog + type="warning" + className="send__error-dialog" + > + {t(toWarning)} + </Dialog> + ) + } + } + +} diff --git a/ui/app/pages/send/send-content/add-recipient/add-recipient.container.js b/ui/app/pages/send/send-content/add-recipient/add-recipient.container.js new file mode 100644 index 000000000..eb980aa82 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/add-recipient.container.js @@ -0,0 +1,44 @@ +import { connect } from 'react-redux' +import { + accountsWithSendEtherInfoSelector, + getSendEnsResolution, + getSendEnsResolutionError, +} from '../../send.selectors.js' +import { + getAddressBook, + getAddressBookEntry, +} from '../../../../selectors/selectors' +import { + updateSendTo, +} from '../../../../store/actions' +import AddRecipient from './add-recipient.component' + +export default connect(mapStateToProps, mapDispatchToProps)(AddRecipient) + +function mapStateToProps (state) { + const ensResolution = getSendEnsResolution(state) + + let addressBookEntryName = '' + if (ensResolution) { + const addressBookEntry = getAddressBookEntry(state, ensResolution) || {} + addressBookEntryName = addressBookEntry.name + } + + const addressBook = getAddressBook(state) + + return { + ownedAccounts: accountsWithSendEtherInfoSelector(state), + addressBook, + ensResolution, + addressBookEntryName, + ensResolutionError: getSendEnsResolutionError(state), + contacts: addressBook.filter(({ name }) => !!name), + nonContacts: addressBook.filter(({ name }) => !name), + } +} + +function mapDispatchToProps (dispatch) { + return { + updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), + } +} diff --git a/ui/app/pages/send/send-content/add-recipient/add-recipient.js b/ui/app/pages/send/send-content/add-recipient/add-recipient.js new file mode 100644 index 000000000..b3b0d2da3 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/add-recipient.js @@ -0,0 +1,35 @@ +const { + REQUIRED_ERROR, + INVALID_RECIPIENT_ADDRESS_ERROR, + KNOWN_RECIPIENT_ADDRESS_ERROR, + INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR, +} = require('../../send.constants') +const { isValidAddress, isEthNetwork } = require('../../../../helpers/utils/util') +import { checkExistingAddresses } from '../../../add-token/util' + +const ethUtil = require('ethereumjs-util') +const contractMap = require('eth-contract-metadata') + +function getToErrorObject (to, toError = null, hasHexData = false, _, __, network) { + if (!to) { + if (!hasHexData) { + toError = REQUIRED_ERROR + } + } else if (!isValidAddress(to, network) && !toError) { + toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR + } + + return { to: toError } +} + +function getToWarningObject (to, toWarning = null, tokens = [], selectedToken = null) { + if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) { + toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR + } + return { to: toWarning } +} + +module.exports = { + getToErrorObject, + getToWarningObject, +} diff --git a/ui/app/pages/send/send-content/add-recipient/add-recipient.selectors.js b/ui/app/pages/send/send-content/add-recipient/add-recipient.selectors.js new file mode 100644 index 000000000..a39db7813 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/add-recipient.selectors.js @@ -0,0 +1,24 @@ +const selectors = { + getToDropdownOpen, + getTokens, + sendToIsInError, + sendToIsInWarning, +} + +module.exports = selectors + +function getToDropdownOpen (state) { + return state.send.toDropdownOpen +} + +function sendToIsInError (state) { + return Boolean(state.send.errors.to) +} + +function sendToIsInWarning (state) { + return Boolean(state.send.warnings.to) +} + +function getTokens (state) { + return state.metamask.tokens +} diff --git a/ui/app/pages/send/send-content/add-recipient/ens-input.component.js b/ui/app/pages/send/send-content/add-recipient/ens-input.component.js new file mode 100644 index 000000000..498d72605 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/ens-input.component.js @@ -0,0 +1,272 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import c from 'classnames' +import { isValidENSAddress, isValidAddress, isValidAddressHead } from '../../../../helpers/utils/util' +import {ellipsify} from '../../send.utils' + +import debounce from 'debounce' +import copyToClipboard from 'copy-to-clipboard/index' +import ENS from 'ethjs-ens' +import networkMap from 'ethjs-ens/lib/network-map.json' +import log from 'loglevel' + + +// Local Constants +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const ZERO_X_ERROR_ADDRESS = '0x' + +export default class EnsInput extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + className: PropTypes.string, + network: PropTypes.string, + selectedAddress: PropTypes.string, + selectedName: PropTypes.string, + onChange: PropTypes.func, + updateSendTo: PropTypes.func, + updateEnsResolution: PropTypes.func, + scanQrCode: PropTypes.func, + updateEnsResolutionError: PropTypes.func, + addressBook: PropTypes.array, + onPaste: PropTypes.func, + onReset: PropTypes.func, + onValidAddressTyped: PropTypes.func, + } + + state = { + recipient: null, + input: '', + toError: null, + toWarning: null, + } + + componentDidMount () { + 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, 200) + } + } + + // 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. + componentDidUpdate (prevProps) { + const { + input, + } = this.state + const { + network, + } = this.props + + if (prevProps.network !== network) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.onChange({ target: { value: input } }) + } + } + + resetInput = () => { + const { updateEnsResolution, updateEnsResolutionError, onReset } = this.props + this.onChange({ target: { value: '' } }) + onReset() + updateEnsResolution('') + updateEnsResolutionError('') + } + + lookupEnsName = (recipient) => { + recipient = recipient.trim() + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName')) + if (address === ZERO_X_ERROR_ADDRESS) throw new Error(this.context.t('ensRegistrationError')) + this.props.updateEnsResolution(address) + }) + .catch((reason) => { + if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') { + this.props.updateEnsResolutionError(this.context.t('ensNotFoundOnCurrentNetwork')) + } else { + log.error(reason) + this.props.updateEnsResolutionError(reason.message) + } + }) + } + + onPaste = event => { + event.clipboardData.items[0].getAsString(text => { + if (isValidAddress(text)) { + this.props.onPaste(text) + } + }) + } + + onChange = e => { + const { network, onChange, updateEnsResolution, updateEnsResolutionError, onValidAddressTyped } = this.props + const input = e.target.value + const networkHasEnsSupport = getNetworkEnsSupport(network) + + this.setState({ input }, () => onChange(input)) + + // Empty ENS state if input is empty + // maybe scan ENS + + if (!networkHasEnsSupport && !isValidAddress(input) && !isValidAddressHead(input)) { + updateEnsResolution('') + updateEnsResolutionError(!networkHasEnsSupport ? 'Network does not support ENS' : '') + return + } + + if (isValidENSAddress(input)) { + this.lookupEnsName(input) + } else if (onValidAddressTyped && isValidAddress(input)) { + onValidAddressTyped(input) + } else { + updateEnsResolution('') + updateEnsResolutionError('') + } + } + + render () { + const { t } = this.context + const { className, selectedAddress } = this.props + const { input } = this.state + + if (selectedAddress) { + return this.renderSelected() + } + + return ( + <div className={c('ens-input', className)}> + <div + className={c('ens-input__wrapper', { + 'ens-input__wrapper__status-icon--error': false, + 'ens-input__wrapper__status-icon--valid': false, + })} + > + <div className="ens-input__wrapper__status-icon" /> + <input + className="ens-input__wrapper__input" + type="text" + placeholder={t('recipientAddressPlaceholder')} + onChange={this.onChange} + onPaste={this.onPaste} + value={selectedAddress || input} + autoFocus + /> + <div + className={c('ens-input__wrapper__action-icon', { + 'ens-input__wrapper__action-icon--erase': input, + 'ens-input__wrapper__action-icon--qrcode': !input, + })} + onClick={() => { + if (input) { + this.resetInput() + } else { + this.props.scanQrCode() + } + }} + /> + </div> + </div> + ) + } + + renderSelected () { + const { t } = this.context + const { className, selectedAddress, selectedName, addressBook } = this.props + const contact = addressBook.filter(item => item.address === selectedAddress)[0] || {} + const name = contact.name || selectedName + + + return ( + <div className={c('ens-input', className)}> + <div + className="ens-input__wrapper ens-input__wrapper--valid" + > + <div className="ens-input__wrapper__status-icon ens-input__wrapper__status-icon--valid" /> + <div + className="ens-input__wrapper__input ens-input__wrapper__input--selected" + placeholder={t('recipientAddress')} + onChange={this.onChange} + > + <div className="ens-input__selected-input__title"> + {name || ellipsify(selectedAddress)} + </div> + { name && <div className="ens-input__selected-input__subtitle">{selectedAddress}</div> } + </div> + <div + className="ens-input__wrapper__action-icon ens-input__wrapper__action-icon--erase" + onClick={this.resetInput} + /> + </div> + </div> + ) + } + + ensIcon (recipient) { + const { hoverText } = this.state + + return ( + <span + className="#ensIcon" + title={hoverText} + style={{ + position: 'absolute', + top: '16px', + left: '-25px', + }} + > + { this.ensIconContents(recipient) } + </span> + ) + } + + ensIconContents () { + const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } + + if (toError) return + + if (loadingEns) { + return ( + <img + src="images/loading.svg" + style={{ + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }} + /> + ) + } + + if (ensFailure) { + return <i className="fa fa-warning fa-lg warning'" /> + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return ( + <i + className="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/pages/send/send-content/add-recipient/ens-input.container.js b/ui/app/pages/send/send-content/add-recipient/ens-input.container.js new file mode 100644 index 000000000..d74f44832 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/ens-input.container.js @@ -0,0 +1,20 @@ +import EnsInput from './ens-input.component' +import { + getCurrentNetwork, + getSendTo, + getSendToNickname, +} from '../../send.selectors' +import { + getAddressBook, +} from '../../../../selectors/selectors' +const connect = require('react-redux').connect + + +export default connect( + state => ({ + network: getCurrentNetwork(state), + selectedAddress: getSendTo(state), + selectedName: getSendToNickname(state), + addressBook: getAddressBook(state), + }) +)(EnsInput) diff --git a/ui/app/pages/send/send-content/add-recipient/ens-input.js b/ui/app/pages/send/send-content/add-recipient/ens-input.js new file mode 100644 index 000000000..6833ccd03 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/ens-input.js @@ -0,0 +1 @@ +export { default } from './ens-input.container' diff --git a/ui/app/pages/send/send-content/add-recipient/index.js b/ui/app/pages/send/send-content/add-recipient/index.js new file mode 100644 index 000000000..d661bd74b --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/index.js @@ -0,0 +1 @@ +export { default } from './add-recipient.container' diff --git a/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-component.test.js b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-component.test.js new file mode 100644 index 000000000..7570e7fcb --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-component.test.js @@ -0,0 +1,202 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import AddRecipient from '../add-recipient.component' +import Dialog from '../../../../../components/ui/dialog' + +const propsMethodSpies = { + closeToDropdown: sinon.spy(), + openToDropdown: sinon.spy(), + updateGas: sinon.spy(), + updateSendTo: sinon.spy(), + updateSendToError: sinon.spy(), + updateSendToWarning: sinon.spy(), +} + +describe('AddRecipient Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(<AddRecipient + closeToDropdown={propsMethodSpies.closeToDropdown} + inError={false} + inWarning={false} + network={'mockNetwork'} + openToDropdown={propsMethodSpies.openToDropdown} + to={'mockTo'} + toAccounts={['mockAccount']} + toDropdownOpen={false} + updateGas={propsMethodSpies.updateGas} + updateSendTo={propsMethodSpies.updateSendTo} + updateSendToError={propsMethodSpies.updateSendToError} + updateSendToWarning={propsMethodSpies.updateSendToWarning} + addressBook={[{ address: '0x80F061544cC398520615B5d3e7A3BedD70cd4510', name: 'Fav 5' }]} + nonContacts={[{ address: '0x70F061544cC398520615B5d3e7A3BedD70cd4510', name: 'Fav 7' }]} + contacts={[{ address: '0x60F061544cC398520615B5d3e7A3BedD70cd4510', name: 'Fav 6' }]} + />, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.closeToDropdown.resetHistory() + propsMethodSpies.openToDropdown.resetHistory() + propsMethodSpies.updateSendTo.resetHistory() + propsMethodSpies.updateSendToError.resetHistory() + propsMethodSpies.updateSendToWarning.resetHistory() + propsMethodSpies.updateGas.resetHistory() + }) + + describe('selectRecipient', () => { + + it('should call updateSendTo', () => { + assert.equal(propsMethodSpies.updateSendTo.callCount, 0) + instance.selectRecipient('mockTo2', 'mockNickname') + assert.equal(propsMethodSpies.updateSendTo.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendTo.getCall(0).args, + ['mockTo2', 'mockNickname'] + ) + }) + + it('should call updateGas if there is no to error', () => { + assert.equal(propsMethodSpies.updateGas.callCount, 0) + instance.selectRecipient(false) + assert.equal(propsMethodSpies.updateGas.callCount, 1) + }) + }) + + describe('render', () => { + it('should render a component', () => { + assert.equal(wrapper.find('.send__select-recipient-wrapper').length, 1) + }) + + it('should render no content if there are no recents, transfers, and contacts', () => { + wrapper.setProps({ + ownedAccounts: [], + addressBook: [], + }) + + assert.equal(wrapper.find('.send__select-recipient-wrapper__list__link').length, 0) + assert.equal(wrapper.find('.send__select-recipient-wrapper__group').length, 0) + }) + + it('should render transfer', () => { + wrapper.setProps({ + ownedAccounts: [{ address: '0x123', name: '123' }, { address: '0x124', name: '124' }], + addressBook: [{ address: '0x456', name: 'test-name' }], + }) + wrapper.setState({ isShowingTransfer: true }) + + const xferLink = wrapper.find('.send__select-recipient-wrapper__list__link') + assert.equal(xferLink.length, 1) + + + const groups = wrapper.find('RecipientGroup') + assert.equal(groups.shallow().find('.send__select-recipient-wrapper__group').length, 1) + }) + + it('should render ContactList', () => { + wrapper.setProps({ + ownedAccounts: [{ address: '0x123', name: '123' }, { address: '0x124', name: '124' }], + addressBook: [{ address: '0x125' }], + }) + + const contactList = wrapper.find('ContactList') + + assert.equal(contactList.length, 1) + }) + + it('should render contacts', () => { + wrapper.setProps({ + addressBook: [ + { address: '0x125', name: 'alice' }, + { address: '0x126', name: 'alex' }, + { address: '0x127', name: 'catherine' }, + ], + }) + wrapper.setState({ isShowingTransfer: false }) + + const xferLink = wrapper.find('.send__select-recipient-wrapper__list__link') + assert.equal(xferLink.length, 0) + + const groups = wrapper.find('ContactList') + assert.equal(groups.length, 1) + + assert.equal(groups.find('.send__select-recipient-wrapper__group-item').length, 0) + }) + + it('should render error when query has no results', () => { + wrapper.setProps({ + addressBook: [], + toError: 'bad', + contacts: [], + nonContacts: [], + }) + + const dialog = wrapper.find(Dialog) + + assert.equal(dialog.props().type, 'error') + assert.equal(dialog.props().children, 'bad_t') + assert.equal(dialog.length, 1) + }) + + it('should render error when query has ens does not resolve', () => { + wrapper.setProps({ + addressBook: [], + toError: 'bad', + ensResolutionError: 'very bad', + contacts: [], + nonContacts: [], + }) + + const dialog = wrapper.find(Dialog) + + assert.equal(dialog.props().type, 'error') + assert.equal(dialog.props().children, 'very bad') + assert.equal(dialog.length, 1) + }) + + it('should render warning', () => { + wrapper.setProps({ + addressBook: [], + query: 'yo', + toWarning: 'watchout', + }) + + const dialog = wrapper.find(Dialog) + + assert.equal(dialog.props().type, 'warning') + assert.equal(dialog.props().children, 'watchout_t') + assert.equal(dialog.length, 1) + }) + + it('should not render error when ens resolved', () => { + wrapper.setProps({ + addressBook: [], + toError: 'bad', + ensResolution: '0x128', + }) + + const dialog = wrapper.find(Dialog) + + assert.equal(dialog.length, 0) + }) + + it('should not render error when query has results', () => { + wrapper.setProps({ + addressBook: [ + { address: '0x125', name: 'alice' }, + { address: '0x126', name: 'alex' }, + { address: '0x127', name: 'catherine' }, + ], + toError: 'bad', + }) + + const dialog = wrapper.find(Dialog) + + assert.equal(dialog.length, 0) + }) + }) +}) diff --git a/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-container.test.js b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-container.test.js new file mode 100644 index 000000000..5ca0b2c23 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-container.test.js @@ -0,0 +1,72 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + updateSendTo: sinon.spy(), +} + +proxyquire('../add-recipient.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../send.selectors.js': { + getSendEnsResolution: (s) => `mockSendEnsResolution:${s}`, + getSendEnsResolutionError: (s) => `mockSendEnsResolutionError:${s}`, + accountsWithSendEtherInfoSelector: (s) => `mockAccountsWithSendEtherInfoSelector:${s}`, + }, + '../../../../selectors/selectors': { + getAddressBook: (s) => [{ name: `mockAddressBook:${s}` }], + getAddressBookEntry: (s) => `mockAddressBookEntry:${s}`, + }, + '../../../../store/actions': actionSpies, +}) + +describe('add-recipient container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + addressBook: [{ name: 'mockAddressBook:mockState' }], + contacts: [{ name: 'mockAddressBook:mockState' }], + ensResolution: 'mockSendEnsResolution:mockState', + ensResolutionError: 'mockSendEnsResolutionError:mockState', + ownedAccounts: 'mockAccountsWithSendEtherInfoSelector:mockState', + addressBookEntryName: undefined, + nonContacts: [], + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('updateSendTo()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname') + assert(dispatchSpy.calledOnce) + assert(actionSpies.updateSendTo.calledOnce) + assert.deepEqual( + actionSpies.updateSendTo.getCall(0).args, + ['mockTo', 'mockNickname'] + ) + }) + }) + }) + +}) diff --git a/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-selectors.test.js b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-selectors.test.js new file mode 100644 index 000000000..82f481187 --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-selectors.test.js @@ -0,0 +1,59 @@ +import assert from 'assert' +import { + getToDropdownOpen, + getTokens, + sendToIsInError, +} from '../add-recipient.selectors.js' + +describe('add-recipient selectors', () => { + + describe('getToDropdownOpen()', () => { + it('should return send.getToDropdownOpen', () => { + const state = { + send: { + toDropdownOpen: false, + }, + } + + assert.equal(getToDropdownOpen(state), false) + }) + }) + + describe('sendToIsInError()', () => { + it('should return true if send.errors.to is truthy', () => { + const state = { + send: { + errors: { + to: 'abc', + }, + }, + } + + assert.equal(sendToIsInError(state), true) + }) + + it('should return false if send.errors.to is falsy', () => { + const state = { + send: { + errors: { + to: null, + }, + }, + } + + assert.equal(sendToIsInError(state), false) + }) + }) + + describe('getTokens()', () => { + it('should return empty array if no tokens in state', () => { + const state = { + metamask: { + tokens: [], + }, + } + + assert.deepStrictEqual(getTokens(state), []) + }) + }) +}) diff --git a/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-utils.test.js b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-utils.test.js new file mode 100644 index 000000000..182504c5d --- /dev/null +++ b/ui/app/pages/send/send-content/add-recipient/tests/add-recipient-utils.test.js @@ -0,0 +1,107 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +import { + REQUIRED_ERROR, + INVALID_RECIPIENT_ADDRESS_ERROR, + KNOWN_RECIPIENT_ADDRESS_ERROR, +} from '../../../send.constants' + +const stubs = { + isValidAddress: sinon.stub().callsFake(to => Boolean(to.match(/^[0xabcdef123456798]+$/))), +} + +const toRowUtils = proxyquire('../add-recipient.js', { + '../../../../helpers/utils/util': { + isValidAddress: stubs.isValidAddress, + }, +}) +const { + getToErrorObject, + getToWarningObject, +} = toRowUtils + +describe('add-recipient utils', () => { + + describe('getToErrorObject()', () => { + it('should return a required error if to is falsy', () => { + assert.deepEqual(getToErrorObject(null), { + to: REQUIRED_ERROR, + }) + }) + + it('should return null if to is falsy and hexData is truthy', () => { + assert.deepEqual(getToErrorObject(null, undefined, true), { + to: null, + }) + }) + + it('should return an invalid recipient error if to is truthy but invalid', () => { + assert.deepEqual(getToErrorObject('mockInvalidTo'), { + to: INVALID_RECIPIENT_ADDRESS_ERROR, + }) + }) + + it('should return null if to is truthy and valid', () => { + assert.deepEqual(getToErrorObject('0xabc123'), { + to: null, + }) + }) + + it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => { + assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), { + to: 'someExplicitError', + }) + }) + + it('should return null if to is truthy but part of state tokens', () => { + assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: null, + }) + }) + + it('should null if to is truthy part of tokens but selectedToken falsy', () => { + assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}]), { + to: null, + }) + }) + + it('should return null if to is truthy but part of contract metadata', () => { + assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: null, + }) + }) + it('should null if to is truthy part of contract metadata but selectedToken falsy', () => { + assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: null, + }) + }) + }) + + describe('getToWarningObject()', () => { + it('should return a known address recipient if to is truthy but part of state tokens', () => { + assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + + it('should null if to is truthy part of tokens but selectedToken falsy', () => { + assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}]), { + to: null, + }) + }) + + it('should return a known address recipient if to is truthy but part of contract metadata', () => { + assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + it('should null if to is truthy part of contract metadata but selectedToken falsy', () => { + assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + }) + +}) |