diff options
author | Dan J Miller <danjm.com@gmail.com> | 2019-08-01 03:56:44 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-01 03:56:44 +0800 |
commit | e9c7df28ed88f6dc3a5074cf873f3920429d1803 (patch) | |
tree | 6075769d6a43cda8ce7de0a141b8f55065f05727 /ui/app/pages/settings/contact-list-tab | |
parent | 1fd3dc9ecf16f00d721078e114138e529a7b8e16 (diff) | |
download | tangerine-wallet-browser-e9c7df28ed88f6dc3a5074cf873f3920429d1803.tar.gz tangerine-wallet-browser-e9c7df28ed88f6dc3a5074cf873f3920429d1803.tar.zst tangerine-wallet-browser-e9c7df28ed88f6dc3a5074cf873f3920429d1803.zip |
Address book send plus contact list (#6914)
* Style Send Header
* Move Send to-row to send view and restyle
* Add "Recents" group to select recipient view
* Rename SendToRow to AddRecipient
* Basic UI and Layout
* New ENSInput component
* wip - fuzzy search for input
* small refactor
* Add Dialog
* contact list initial
* initial error on invalid address
* clean up edit
* Click to open modal
* Create AddToAddressBookModal component
* Modal styling and layout
* modal i18n
* Add to Addressbook
* ens wip
* ens wip
* ENS Resolution
* Reset input
* Send to explicit address
* Happy Path Complete
* Add back error checking
* Reset send-to when emptying input
* Add back warning object
* Fix linter
* Fix unit test #1 - fix import paths
* Remove dead tests
* One more to go
* Fix all unit tests
* add unit test for reducers and actions
* test rendering AddRecipient
* Add tests for dialog boxes in AddRecipient
* Add test for validating
* Fix linter
* Fix e2e tests
* Token send e2e fix
* Style View Contact
* Style edit-contact
* Fix e2e
* Fix from-import-beta-ui e2e spec
* Make section header say "add recipient” by default
* Auto-focus add recipient input
* Update placeholder text
* Update input title font size
* Auto advance to next step if user paste a valid address
* Ellipsify address when recipient is selected
* Fix app header background color on desktop
* Give each form row a margin of 16px
* Use .container/.component naming pattern for ens-input
* Auto-focus on input when add to addressbook modal is opened; Save on Enter
* Fix and add unit test
* Fix selectors name in e2e tests
* Correct e2e test token amount for address-book-send changes
* Adds e2e test for editing a transaction
* Delete test/integration/lib/send-new-ui.js
* Add tests for amount max button and high value error on send screen to test/e2e/metamask-ui.spec.js
* lint and revert to address as object keys
* add chainId based on current network to address book entry
* fix test
* only display contacts for the current network
* Improve ENS message when not found on current network
* Add error to indicate when network does not support ENS
* bump gaba
* address book, resolve comments
* Move contact-list to its own component
* De-duplicate getaddressbook selector and refactor name selection logic in contact-list-tab/
* Use contact-list component in contact-list-tab.component (i.e. in settings)
* Improve/fix settings headers for popup and browser views
* Lint fixes related to address book updates
* Add 'My accounts' page to settings address book
* Update add new contact button in settings to match floating circular design
* Improve styles of view contact page
* Improve styles and labels of the add-contact.component
* Further lint fixes related to address book updates
* Update unit tests as per address book updates
* Ensure that contact list groups are sorted alphabetically
* Refactor settings component to use a container for connection to redux; allow display of addressbook name in settings header
* Decouple ens-input.component from send context
* Add ens resolution to add contact screen in settings
* Switching networks when an ens address is shown on send form removes the ens address.
* Resolve send screen search for ensAddress to matching address book entry if it exists
* Show resolved ens icon and address if exists (settings: add-contact.component)
* Make the displayed and copied address in view-contact.component the checksummed address
* Default alias state prop in AddToAddressBookModal to empty string
* Use keyCode to detect enter key in AddToAddressBookModal
* Ensure add-contact component properly updates after QR code detection
* Fix display of all recents after clicking 'Load More' in contact list
* Fix send screen contact searching after network switching
* Code cleanup related to address book changes
* Update unit tests for address book changes
* Update ENS name not found on network message
* Add ens registration error message
* Cancel on edit mode takes user back to view screen
* Adds support for memo to settings contact list view and edit screens
* Modify designs of edit and view contact in popup environment
* Update settings content list UX to show split columns in fullscreen and proper internal navigation
* Correct background address book API usages in UI
Diffstat (limited to 'ui/app/pages/settings/contact-list-tab')
16 files changed, 946 insertions, 0 deletions
diff --git a/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js b/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js new file mode 100644 index 000000000..871b2128b --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js @@ -0,0 +1,131 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Identicon from '../../../../components/ui/identicon' +import TextField from '../../../../components/ui/text-field' +import { CONTACT_LIST_ROUTE } from '../../../../helpers/constants/routes' +import { isValidAddress, isValidENSAddress } from '../../../../helpers/utils/util' +import EnsInput from '../../../../pages/send/send-content/add-recipient/ens-input' +import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' +import debounce from 'lodash.debounce' + +export default class AddContact extends PureComponent { + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + addToAddressBook: PropTypes.func, + history: PropTypes.object, + scanQrCode: PropTypes.func, + qrCodeData: PropTypes.object, + qrCodeDetected: PropTypes.func, + } + + state = { + nickname: '', + ethAddress: '', + ensAddress: '', + error: '', + ensError: '', + } + + constructor (props) { + super(props) + this.dValidate = debounce(this.validate, 1000) + } + + componentWillReceiveProps (nextProps) { + if (nextProps.qrCodeData) { + if (nextProps.qrCodeData.type === 'address') { + const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase() + const currentAddress = this.state.ensAddress || this.state.ethAddress + if (currentAddress.toLowerCase() !== scannedAddress) { + this.setState({ ethAddress: scannedAddress, ensAddress: '' }) + // Clean up QR code data after handling + this.props.qrCodeDetected(null) + } + } + } + } + + validate = address => { + const valid = isValidAddress(address) + const validEnsAddress = isValidENSAddress(address) + if (valid || validEnsAddress || address === '') { + this.setState({ error: '', ethAddress: address }) + } else { + this.setState({ error: 'Invalid Address' }) + } + } + + renderInput () { + return ( + <EnsInput + className="send__to-row" + scanQrCode={_ => { this.props.scanQrCode() }} + onChange={this.dValidate} + onPaste={text => this.setState({ ethAddress: text })} + onReset={() => this.setState({ ethAddress: '', ensAddress: '' })} + updateEnsResolution={address => { + this.setState({ ensAddress: address, error: '', ensError: '' }) + }} + updateEnsResolutionError={message => this.setState({ ensError: message })} + /> + ) + } + + render () { + const { t } = this.context + const { history, addToAddressBook } = this.props + + const errorToRender = this.state.ensError || this.state.error + + return ( + <div className="settings-page__content-row address-book__add-contact"> + {this.state.ensAddress && <div className="address-book__view-contact__group"> + <Identicon address={this.state.ensAddress} diameter={60} /> + <div className="address-book__view-contact__group__value"> + { this.state.ensAddress } + </div> + </div>} + <div className="address-book__add-contact__content"> + <div className="address-book__view-contact__group"> + <div className="address-book__view-contact__group__label"> + { t('userName') } + </div> + <TextField + type="text" + id="nickname" + value={this.state.newName} + onChange={e => this.setState({ newName: e.target.value })} + fullWidth + margin="dense" + /> + </div> + + <div className="address-book__view-contact__group"> + <div className="address-book__view-contact__group__label"> + { t('ethereumPublicAddress') } + </div> + { this.renderInput() } + { errorToRender && <div className="address-book__add-contact__error">{errorToRender}</div>} + </div> + </div> + <PageContainerFooter + cancelText={this.context.t('cancel')} + disabled={Boolean(this.state.error)} + onSubmit={() => { + addToAddressBook(this.state.ensAddress || this.state.ethAddress, this.state.newName) + history.push(CONTACT_LIST_ROUTE) + }} + onCancel={() => { + history.push(CONTACT_LIST_ROUTE) + }} + submitText={this.context.t('save')} + submitButtonType={'confirm'} + /> + </div> + ) + } +} diff --git a/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.container.js b/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.container.js new file mode 100644 index 000000000..0a0fc450c --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/add-contact/add-contact.container.js @@ -0,0 +1,30 @@ +import AddContact from './add-contact.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { addToAddressBook, showQrScanner, qrCodeDetected } from '../../../../store/actions' +import { + CONTACT_ADD_ROUTE, +} from '../../../../helpers/constants/routes' +import { + getQrCodeData, +} from '../../../../pages/send/send.selectors' + +const mapStateToProps = state => { + return { + qrCodeData: getQrCodeData(state), + } +} + +const mapDispatchToProps = dispatch => { + return { + addToAddressBook: (recipient, nickname) => dispatch(addToAddressBook(recipient, nickname)), + scanQrCode: () => dispatch(showQrScanner(CONTACT_ADD_ROUTE)), + qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(AddContact) diff --git a/ui/app/pages/settings/contact-list-tab/add-contact/index.js b/ui/app/pages/settings/contact-list-tab/add-contact/index.js new file mode 100644 index 000000000..ce73025a3 --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/add-contact/index.js @@ -0,0 +1 @@ +export { default } from './add-contact.container' diff --git a/ui/app/pages/settings/contact-list-tab/contact-list-tab.component.js b/ui/app/pages/settings/contact-list-tab/contact-list-tab.component.js new file mode 100644 index 000000000..f7a01d672 --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/contact-list-tab.component.js @@ -0,0 +1,132 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ContactList from '../../../components/app/contact-list' +import EditContact from './edit-contact' +import AddContact from './add-contact' +import ViewContact from './view-contact' +import MyAccounts from './my-accounts' +import { + CONTACT_ADD_ROUTE, + CONTACT_VIEW_ROUTE, + CONTACT_MY_ACCOUNTS_ROUTE, +} from '../../../helpers/constants/routes' + +export default class ContactListTab extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + addressBook: PropTypes.array, + history: PropTypes.object, + selectedAddress: PropTypes.string, + viewingContact: PropTypes.bool, + editingContact: PropTypes.bool, + addingContact: PropTypes.bool, + showContactContent: PropTypes.bool, + hideAddressBook: PropTypes.bool, + showingMyAccounts: PropTypes.bool, + } + + renderAddresses () { + const { addressBook, history, selectedAddress } = this.props + const contacts = addressBook.filter(({ name }) => !!name) + const nonContacts = addressBook.filter(({ name }) => !name) + + return ( + <div> + <ContactList + searchForContacts={() => contacts} + searchForRecents={() => nonContacts} + selectRecipient={(address) => { + history.push(`${CONTACT_VIEW_ROUTE}/${address}`) + }} + selectedAddress={selectedAddress} + /> + </div> + ) + } + + renderAddButton () { + const { history } = this.props + return <div + className="address-book-add-button__button" + onClick={() => { + history.push(CONTACT_ADD_ROUTE) + }}> + <img + className="account-menu__item-icon" + src="images/plus-btn-white.svg" + /> + </div> + } + + renderMyAccountsButton () { + const { history } = this.props + const { t } = this.context + return ( + <div + className="address-book__my-accounts-button" + onClick={() => { + history.push(CONTACT_MY_ACCOUNTS_ROUTE) + }} + > + <div className="address-book__my-accounts-button__header">{t('myWalletAccounts')}</div> + <div className="address-book__my-accounts-button__content"> + <div className="address-book__my-accounts-button__text"> + { t('myWalletAccountsDescription') } + </div> + <div className="address-book__my-accounts-button__caret" /> + </div> + </div> + ) + } + + renderContactContent () { + const { viewingContact, editingContact, addingContact, showContactContent } = this.props + + if (!showContactContent) { + return null + } + + let ContactContentComponent = null + if (viewingContact) { + ContactContentComponent = ViewContact + } else if (editingContact) { + ContactContentComponent = EditContact + } else if (addingContact) { + ContactContentComponent = AddContact + } + + return (ContactContentComponent && <div className="address-book-contact-content"> + <ContactContentComponent /> + </div>) + } + + renderAddressBookContent () { + const { hideAddressBook, showingMyAccounts } = this.props + + if (!hideAddressBook && !showingMyAccounts) { + return (<div className="address-book"> + { this.renderMyAccountsButton() } + { this.renderAddresses() } + </div>) + } else if (!hideAddressBook && showingMyAccounts) { + return (<MyAccounts />) + } + } + + render () { + const { addingContact } = this.props + + return ( + <div className="address-book-wrapper"> + { this.renderAddressBookContent() } + { this.renderContactContent() } + {!addingContact && <div className="address-book-add-button"> + { this.renderAddButton() } + </div>} + </div> + ) + } +} diff --git a/ui/app/pages/settings/contact-list-tab/contact-list-tab.container.js b/ui/app/pages/settings/contact-list-tab/contact-list-tab.container.js new file mode 100644 index 000000000..2c7139b5d --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/contact-list-tab.container.js @@ -0,0 +1,54 @@ +import ContactListTab from './contact-list-tab.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { getAddressBook } from '../../../selectors/selectors' +import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' + +import { + CONTACT_ADD_ROUTE, + CONTACT_EDIT_ROUTE, + CONTACT_VIEW_ROUTE, + CONTACT_MY_ACCOUNTS_ROUTE, + CONTACT_MY_ACCOUNTS_VIEW_ROUTE, + CONTACT_MY_ACCOUNTS_EDIT_ROUTE, +} from '../../../helpers/constants/routes' + + +const mapStateToProps = (state, ownProps) => { + const { location } = ownProps + const { pathname } = location + + const pathNameTail = pathname.match(/[^/]+$/)[0] + const pathNameTailIsAddress = pathNameTail.includes('0x') + + const viewingContact = Boolean(pathname.match(CONTACT_VIEW_ROUTE) || pathname.match(CONTACT_MY_ACCOUNTS_VIEW_ROUTE)) + const editingContact = Boolean(pathname.match(CONTACT_EDIT_ROUTE) || pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE)) + const addingContact = Boolean(pathname.match(CONTACT_ADD_ROUTE)) + const showingMyAccounts = Boolean( + pathname.match(CONTACT_MY_ACCOUNTS_ROUTE) || + pathname.match(CONTACT_MY_ACCOUNTS_VIEW_ROUTE) || + pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE) + ) + const envIsPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP + + const hideAddressBook = envIsPopup && (viewingContact || editingContact || addingContact) + + return { + viewingContact, + editingContact, + addingContact, + showingMyAccounts, + addressBook: getAddressBook(state), + selectedAddress: pathNameTailIsAddress ? pathNameTail : '', + hideAddressBook, + envIsPopup, + showContactContent: !envIsPopup || hideAddressBook, + } +} + +export default compose( + withRouter, + connect(mapStateToProps) +)(ContactListTab) diff --git a/ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js b/ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js new file mode 100644 index 000000000..e9c2fed6f --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js @@ -0,0 +1,135 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Identicon from '../../../../components/ui/identicon' +import Button from '../../../../components/ui/button/button.component' +import TextField from '../../../../components/ui/text-field' +import { isValidAddress } from '../../../../helpers/utils/util' +import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' + +export default class EditContact extends PureComponent { + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + addToAddressBook: PropTypes.func, + removeFromAddressBook: PropTypes.func, + history: PropTypes.object, + name: PropTypes.string, + address: PropTypes.string, + memo: PropTypes.string, + viewRoute: PropTypes.string, + listRoute: PropTypes.string, + setAccountLabel: PropTypes.func, + } + + state = { + newName: '', + newAddress: '', + newMemo: '', + error: '', + } + + render () { + const { t } = this.context + const { history, name, addToAddressBook, removeFromAddressBook, address, memo, viewRoute, listRoute, setAccountLabel } = this.props + + return ( + <div className="settings-page__content-row address-book__edit-contact"> + <div className="settings-page__header address-book__header--edit"> + <Identicon address={address} diameter={60}/> + <Button + type="link" + className="settings-page__address-book-button" + onClick={() => { + removeFromAddressBook(address) + history.push(listRoute) + }} + > + {t('deleteAccount')} + </Button> + </div> + <div className="address-book__edit-contact__content"> + <div className="address-book__view-contact__group"> + <div className="address-book__view-contact__group__label"> + { t('userName') } + </div> + <TextField + type="text" + id="nickname" + placeholder={this.context.t('addAlias')} + value={this.state.newName || name} + onChange={e => this.setState({ newName: e.target.value })} + fullWidth + margin="dense" + /> + </div> + + <div className="address-book__view-contact__group"> + <div className="address-book__view-contact__group__label"> + { t('ethereumPublicAddress') } + </div> + <TextField + type="text" + id="address" + placeholder={address} + value={this.state.newAddress || address} + error={this.state.error} + onChange={e => this.setState({ newAddress: e.target.value })} + fullWidth + margin="dense" + /> + </div> + + <div className="address-book__view-contact__group"> + <div className="address-book__view-contact__group__label--capitalized"> + { t('memo') } + </div> + <TextField + type="text" + id="memo" + placeholder={memo} + value={this.state.newMemo || memo} + onChange={e => this.setState({ newMemo: e.target.value })} + fullWidth + margin="dense" + multiline={true} + rows={3} + classes={{ + inputMultiline: 'address-book__view-contact__text-area', + inputRoot: 'address-book__view-contact__text-area-wrapper', + }} + /> + </div> + </div> + <PageContainerFooter + cancelText={this.context.t('cancel')} + onSubmit={() => { + if (this.state.newAddress !== '' && this.state.newAddress !== address) { + // if the user makes a valid change to the address field, remove the original address + if (isValidAddress(this.state.newAddress)) { + removeFromAddressBook(address) + addToAddressBook(this.state.newAddress, this.state.newName || name, this.state.newMemo || memo) + setAccountLabel(this.state.newAddress, this.state.newName || name) + history.push(listRoute) + } else { + this.setState({ error: 'invalid address' }) + } + } else { + // update name + addToAddressBook(address, this.state.newName || name, this.state.newMemo || memo) + setAccountLabel(address, this.state.newName || name) + history.push(listRoute) + } + }} + onCancel={() => { + history.push(`${viewRoute}/${address}`) + }} + submitText={this.context.t('save')} + submitButtonType={'confirm'} + /> + </div> + ) + } +} diff --git a/ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js b/ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js new file mode 100644 index 000000000..8841ff791 --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js @@ -0,0 +1,47 @@ +import EditContact from './edit-contact.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { getAddressBookEntry } from '../../../../selectors/selectors' +import { + CONTACT_VIEW_ROUTE, + CONTACT_MY_ACCOUNTS_ROUTE, + CONTACT_MY_ACCOUNTS_VIEW_ROUTE, + CONTACT_MY_ACCOUNTS_EDIT_ROUTE, + CONTACT_LIST_ROUTE, +} from '../../../../helpers/constants/routes' +import { addToAddressBook, removeFromAddressBook, setAccountLabel } from '../../../../store/actions' + +const mapStateToProps = (state, ownProps) => { + const { location } = ownProps + const { pathname } = location + const pathNameTail = pathname.match(/[^/]+$/)[0] + const pathNameTailIsAddress = pathNameTail.includes('0x') + const address = pathNameTailIsAddress ? pathNameTail.toLowerCase() : ownProps.match.params.id + + const { memo, name } = getAddressBookEntry(state, address) || state.metamask.identities[address] + + const showingMyAccounts = Boolean(pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE)) + + return { + address, + name, + memo, + viewRoute: showingMyAccounts ? CONTACT_MY_ACCOUNTS_VIEW_ROUTE : CONTACT_VIEW_ROUTE, + listRoute: showingMyAccounts ? CONTACT_MY_ACCOUNTS_ROUTE : CONTACT_LIST_ROUTE, + showingMyAccounts, + } +} + +const mapDispatchToProps = dispatch => { + return { + addToAddressBook: (recipient, nickname, memo) => dispatch(addToAddressBook(recipient, nickname, memo)), + removeFromAddressBook: (addressToRemove) => dispatch(removeFromAddressBook(addressToRemove)), + setAccountLabel: (address, label) => dispatch(setAccountLabel(address, label)), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(EditContact) diff --git a/ui/app/pages/settings/contact-list-tab/edit-contact/index.js b/ui/app/pages/settings/contact-list-tab/edit-contact/index.js new file mode 100644 index 000000000..fe5ee206a --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/edit-contact/index.js @@ -0,0 +1 @@ +export { default } from './edit-contact.container' diff --git a/ui/app/pages/settings/contact-list-tab/index.js b/ui/app/pages/settings/contact-list-tab/index.js new file mode 100644 index 000000000..c09e9787b --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/index.js @@ -0,0 +1 @@ +export { default } from './contact-list-tab.container' diff --git a/ui/app/pages/settings/contact-list-tab/index.scss b/ui/app/pages/settings/contact-list-tab/index.scss new file mode 100644 index 000000000..c7e99095f --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/index.scss @@ -0,0 +1,234 @@ +.address-book-wrapper { + display: flex; + justify-content: space-between; + height: 100%; +} + +.address-book { + flex: 0.4 1 40%; + max-width: 40%; + + @media screen and (max-width: 576px) { + flex: 1; + max-width: 100%; + } + + &__entry { + display: flex; + flex-flow: row nowrap; + padding: 16px 14px; + flex: 0 0 auto; + border-bottom: 1px solid #dedede; + + &:hover { + border: 1px solid #037DD6; + cursor: pointer; + } + } + + &__name { + padding: 3px; + } + + &__header, &__header--edit { + &__name { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 24px; + line-height: 34px; + margin-left: 24px; + } + } + + &__header--edit { + display: flex; + justify-content: space-between; + + .button { + justify-content: flex-end; + color: #D73A49; + font-size: 14px; + } + } + + &__input { + @extend %input-2; + margin-top: .25rem; + + &--address { + font-size: 0.875rem; + } + } + + &__view-contact { + &__text-area-wrapper { + height: 96px !important; + } + + &__text-area { + line-height: initial !important; + } + + &__group { + display: flex; + flex-flow: column nowrap; + padding: 1.5rem 1.5rem 0 1.5rem; + + &__label, &__label--capitalized { + font-size: .75rem; + color: $Grey-500; + margin-bottom: .25rem; + } + + &__label--capitalized { + text-transform: capitalize; + } + + &__value, &__static-address { + display: flex; + flex-flow: row nowrap; + font-size: 1.125rem; + color: $Grey-800; + word-break: break-word; + + &--address { + font-size: 0.875rem; + } + + &--copy-icon { + padding-left: 4px; + } + } + + &__static-address { + font-size: 0.875rem; + &--copy-icon { + cursor: pointer; + + &:hover { + color: black; + } + } + } + + .unit-input__input { + max-width: 100%; + width: 100%; + } + } + } + + &__edit-contact { + display: flex; + flex-flow: column nowrap; + padding-bottom: 0 !important; + height: 100%; + + &__content { + flex: 1 1 auto; + + > div { + padding-top: 0; + } + + } + + .page-container__footer { + border-top: none; + } + } + + &__add-contact { + display: flex; + flex-flow: column nowrap; + padding-bottom: 0 !important; + height: 100%; + + &__content { + flex: 1 1 auto; + height: 100%; + } + + &__error { + font-size: 12px; + line-height: 12px; + left: 8px; + color: $red; + } + } + + &__my-accounts-button { + display: flex; + flex-flow: column; + cursor: pointer; + padding: 15px; + + &:hover { + background-color: rgba(222, 222, 222, 0.2); + } + + &__header { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 18px; + line-height: 25px; + color: #000000; + } + + &__content { + display: flex; + justify-content: space-between; + } + + &__text { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #6A737D; + } + + &__caret { + display: block; + background-image: url(/images/caret-right.svg); + width: 30px; + opacity: .5; + background-repeat: no-repeat; + } + } +} + +.address-book-add-button { + &__button { + position: absolute; + top: 10px; + right: 16px; + height: 56px; + width: 56px; + border-radius: 18px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + border-width: 2px; + background: #037DD6; + margin-right: 5px; + cursor: pointer; + box-shadow: 0px 2px 16px rgba(0, 0, 0, 0.25); + } +} + +.address-book--hidden { + display: none; +} + +.address-book-contact-content { + flex: 0.4 1 40%; + + @media screen and (max-width: 576px) { + flex: 1 + } +} diff --git a/ui/app/pages/settings/contact-list-tab/my-accounts/index.js b/ui/app/pages/settings/contact-list-tab/my-accounts/index.js new file mode 100644 index 000000000..13a7a9cbf --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/my-accounts/index.js @@ -0,0 +1 @@ +export { default } from './my-accounts.container' diff --git a/ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.component.js b/ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.component.js new file mode 100644 index 000000000..f43b59e07 --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.component.js @@ -0,0 +1,39 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import ContactList from '../../../../components/app/contact-list' +import { CONTACT_MY_ACCOUNTS_VIEW_ROUTE } from '../../../../helpers/constants/routes' + +export default class ViewContact extends PureComponent { + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + myAccounts: PropTypes.array, + history: PropTypes.object, + } + + renderMyAccounts () { + const { myAccounts, history } = this.props + + return ( + <div> + <ContactList + searchForMyAccounts={() => myAccounts} + selectRecipient={(address) => { + history.push(`${CONTACT_MY_ACCOUNTS_VIEW_ROUTE}/${address}`) + }} + /> + </div> + ) + } + + render () { + return ( + <div className="address-book"> + { this.renderMyAccounts() } + </div> + ) + } +} diff --git a/ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.container.js b/ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.container.js new file mode 100644 index 000000000..6380c9d4c --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.container.js @@ -0,0 +1,18 @@ +import ViewContact from './my-accounts.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { accountsWithSendEtherInfoSelector } from '../../../../selectors/selectors' + +const mapStateToProps = (state,) => { + const myAccounts = accountsWithSendEtherInfoSelector(state) + + return { + myAccounts, + } +} + +export default compose( + withRouter, + connect(mapStateToProps) +)(ViewContact) diff --git a/ui/app/pages/settings/contact-list-tab/view-contact/index.js b/ui/app/pages/settings/contact-list-tab/view-contact/index.js new file mode 100644 index 000000000..78bf19d18 --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/view-contact/index.js @@ -0,0 +1 @@ +export { default } from './view-contact.container' diff --git a/ui/app/pages/settings/contact-list-tab/view-contact/view-contact.component.js b/ui/app/pages/settings/contact-list-tab/view-contact/view-contact.component.js new file mode 100644 index 000000000..4f37b853b --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/view-contact/view-contact.component.js @@ -0,0 +1,78 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Identicon from '../../../../components/ui/identicon' + +import Button from '../../../../components/ui/button/button.component' +import copyToClipboard from 'copy-to-clipboard' + +function quadSplit (address) { + return '0x ' + address.slice(2).match(/.{1,4}/g).join(' ') +} + +export default class ViewContact extends PureComponent { + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + removeFromAddressBook: PropTypes.func, + name: PropTypes.string, + address: PropTypes.string, + history: PropTypes.object, + checkSummedAddress: PropTypes.string, + memo: PropTypes.string, + editRoute: PropTypes.string, + } + + render () { + const { t } = this.context + const { history, name, address, checkSummedAddress, memo, editRoute } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <div className="settings-page__header address-book__header"> + <Identicon address={address} diameter={60} /> + <div className="address-book__header__name">{ name }</div> + </div> + <div className="address-book__view-contact__group"> + <Button + type="secondary" + onClick={() => { + history.push(`${editRoute}/${address}`) + }} + > + {t('edit')} + </Button> + </div> + <div className="address-book__view-contact__group"> + <div className="address-book__view-contact__group__label"> + { t('ethereumPublicAddress') } + </div> + <div className="address-book__view-contact__group__value"> + <div + className="address-book__view-contact__group__static-address" + > + { quadSplit(checkSummedAddress) } + </div> + <img + className="address-book__view-contact__group__static-address--copy-icon" + onClick={() => copyToClipboard(checkSummedAddress)} + src="/images/copy-to-clipboard.svg" + /> + </div> + </div> + <div className="address-book__view-contact__group"> + <div className="address-book__view-contact__group__label--capitalized"> + { t('memo') } + </div> + <div className="address-book__view-contact__group__static-address"> + { memo } + </div> + </div> + </div> + </div> + ) + } +} diff --git a/ui/app/pages/settings/contact-list-tab/view-contact/view-contact.container.js b/ui/app/pages/settings/contact-list-tab/view-contact/view-contact.container.js new file mode 100644 index 000000000..b1196d936 --- /dev/null +++ b/ui/app/pages/settings/contact-list-tab/view-contact/view-contact.container.js @@ -0,0 +1,43 @@ +import ViewContact from './view-contact.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { getAddressBookEntry } from '../../../../selectors/selectors' +import { removeFromAddressBook } from '../../../../store/actions' +import { checksumAddress } from '../../../../helpers/utils/util' +import { + CONTACT_EDIT_ROUTE, + CONTACT_MY_ACCOUNTS_EDIT_ROUTE, + CONTACT_MY_ACCOUNTS_VIEW_ROUTE, +} from '../../../../helpers/constants/routes' + +const mapStateToProps = (state, ownProps) => { + const { location } = ownProps + const { pathname } = location + const pathNameTail = pathname.match(/[^/]+$/)[0] + const pathNameTailIsAddress = pathNameTail.includes('0x') + const address = pathNameTailIsAddress ? pathNameTail.toLowerCase() : ownProps.match.params.id + + const { memo, name } = getAddressBookEntry(state, address) || state.metamask.identities[address] + + const showingMyAccounts = Boolean(pathname.match(CONTACT_MY_ACCOUNTS_VIEW_ROUTE)) + + return { + name, + address, + checkSummedAddress: checksumAddress(address), + memo, + editRoute: showingMyAccounts ? CONTACT_MY_ACCOUNTS_EDIT_ROUTE : CONTACT_EDIT_ROUTE, + } +} + +const mapDispatchToProps = dispatch => { + return { + removeFromAddressBook: (addressToRemove) => dispatch(removeFromAddressBook(addressToRemove)), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ViewContact) |