aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/pages/settings
diff options
context:
space:
mode:
authorDan Finlay <542863+danfinlay@users.noreply.github.com>2019-08-07 05:53:50 +0800
committerGitHub <noreply@github.com>2019-08-07 05:53:50 +0800
commitdb08881d4527e8a037f401ef22b849e52152864f (patch)
tree6032d7a4ae67371889eece1d8490c26d5a119dd5 /ui/app/pages/settings
parent4139019d0f4dd83f56da400ca7e0e6d1976d1716 (diff)
parent86ad9564a064fd6158dab6a3c9e5b10614ef6e68 (diff)
downloadtangerine-wallet-browser-db08881d4527e8a037f401ef22b849e52152864f.tar.gz
tangerine-wallet-browser-db08881d4527e8a037f401ef22b849e52152864f.tar.zst
tangerine-wallet-browser-db08881d4527e8a037f401ef22b849e52152864f.zip
Merge pull request #6969 from MetaMask/developv7.0.0
Master Version Bump
Diffstat (limited to 'ui/app/pages/settings')
-rw-r--r--ui/app/pages/settings/advanced-tab/advanced-tab.component.js168
-rw-r--r--ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js2
-rw-r--r--ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js14
-rw-r--r--ui/app/pages/settings/contact-list-tab/add-contact/add-contact.component.js131
-rw-r--r--ui/app/pages/settings/contact-list-tab/add-contact/add-contact.container.js30
-rw-r--r--ui/app/pages/settings/contact-list-tab/add-contact/index.js1
-rw-r--r--ui/app/pages/settings/contact-list-tab/contact-list-tab.component.js132
-rw-r--r--ui/app/pages/settings/contact-list-tab/contact-list-tab.container.js54
-rw-r--r--ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.component.js135
-rw-r--r--ui/app/pages/settings/contact-list-tab/edit-contact/edit-contact.container.js47
-rw-r--r--ui/app/pages/settings/contact-list-tab/edit-contact/index.js1
-rw-r--r--ui/app/pages/settings/contact-list-tab/index.js1
-rw-r--r--ui/app/pages/settings/contact-list-tab/index.scss234
-rw-r--r--ui/app/pages/settings/contact-list-tab/my-accounts/index.js1
-rw-r--r--ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.component.js39
-rw-r--r--ui/app/pages/settings/contact-list-tab/my-accounts/my-accounts.container.js18
-rw-r--r--ui/app/pages/settings/contact-list-tab/view-contact/index.js1
-rw-r--r--ui/app/pages/settings/contact-list-tab/view-contact/view-contact.component.js78
-rw-r--r--ui/app/pages/settings/contact-list-tab/view-contact/view-contact.container.js43
-rw-r--r--ui/app/pages/settings/index.js2
-rw-r--r--ui/app/pages/settings/index.scss49
-rw-r--r--ui/app/pages/settings/networks-tab/network-form/network-form.component.js14
-rw-r--r--ui/app/pages/settings/networks-tab/networks-tab.component.js48
-rw-r--r--ui/app/pages/settings/networks-tab/networks-tab.container.js6
-rw-r--r--ui/app/pages/settings/security-tab/security-tab.component.js10
-rw-r--r--ui/app/pages/settings/settings-tab/settings-tab.component.js7
-rw-r--r--ui/app/pages/settings/settings.component.js142
-rw-r--r--ui/app/pages/settings/settings.container.js92
28 files changed, 1259 insertions, 241 deletions
diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js
index 3d27fe349..d92b14501 100644
--- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js
+++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js
@@ -1,8 +1,7 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
-import validUrl from 'valid-url'
import { exportAsFile } from '../../../helpers/utils/util'
-import ToggleButton from 'react-toggle-button'
+import ToggleButton from '../../../components/ui/toggle-button'
import TextField from '../../../components/ui/text-field'
import Button from '../../../components/ui/button'
import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes'
@@ -29,160 +28,12 @@ export default class AdvancedTab extends PureComponent {
setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
}
- state = {
- newRpc: '',
- chainId: '',
- showOptions: false,
- ticker: '',
- nickname: '',
- }
-
- renderNewRpcUrl () {
- const { t } = this.context
- const { newRpc, chainId, ticker, nickname } = this.state
-
- return (
- <div className="settings-page__content-row">
- <div className="settings-page__content-item">
- <span>{ t('newNetwork') }</span>
- </div>
- <div className="settings-page__content-item">
- <div className="settings-page__content-item-col">
- <TextField
- type="text"
- id="new-rpc"
- placeholder={t('rpcUrl')}
- value={newRpc}
- onChange={e => this.setState({ newRpc: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- fullWidth
- margin="dense"
- />
- <TextField
- type="text"
- id="chainid"
- placeholder={t('optionalChainId')}
- value={chainId}
- onChange={e => this.setState({ chainId: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- style={{
- display: this.state.showOptions ? null : 'none',
- }}
- fullWidth
- margin="dense"
- />
- <TextField
- type="text"
- id="ticker"
- placeholder={t('optionalSymbol')}
- value={ticker}
- onChange={e => this.setState({ ticker: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- style={{
- display: this.state.showOptions ? null : 'none',
- }}
- fullWidth
- margin="dense"
- />
- <TextField
- type="text"
- id="nickname"
- placeholder={t('optionalNickname')}
- value={nickname}
- onChange={e => this.setState({ nickname: e.target.value })}
- onKeyPress={e => {
- if (e.key === 'Enter') {
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }
- }}
- style={{
- display: this.state.showOptions ? null : 'none',
- }}
- fullWidth
- margin="dense"
- />
- <div className="flex-row flex-align-center space-between">
- <span className="settings-tab__advanced-link"
- onClick={e => {
- e.preventDefault()
- this.setState({ showOptions: !this.state.showOptions })
- }}
- >
- { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') }
- </span>
- <button
- className="button btn-primary settings-tab__rpc-save-button"
- onClick={e => {
- e.preventDefault()
- this.validateRpc(newRpc, chainId, ticker, nickname)
- }}
- >
- { t('save') }
- </button>
- </div>
- </div>
- </div>
- </div>
- )
- }
-
- validateRpc (newRpc, chainId, ticker = 'ETH', nickname) {
- const { setRpcTarget, displayWarning } = this.props
- if (validUrl.isWebUri(newRpc)) {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Settings',
- action: 'Custom RPC',
- name: 'Success',
- },
- customVariables: {
- networkId: newRpc,
- chainId,
- },
- })
- if (!!chainId && Number.isNaN(parseInt(chainId))) {
- return displayWarning(`${this.context.t('invalidInput')} chainId`)
- }
-
- setRpcTarget(newRpc, chainId, ticker, nickname)
- } else {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Settings',
- action: 'Custom RPC',
- name: 'Error',
- },
- customVariables: {
- networkId: newRpc,
- chainId,
- },
- })
- const appendedRpc = `http://${newRpc}`
-
- if (validUrl.isWebUri(appendedRpc)) {
- displayWarning(this.context.t('uriErrorMsg'))
- } else {
- displayWarning(this.context.t('invalidRPC'))
- }
- }
- }
+ state = { autoLogoutTimeLimit: this.props.autoLogoutTimeLimit }
renderMobileSync () {
const { t } = this.context
const { history } = this.props
-//
+ //
return (
<div className="settings-page__content-row">
<div className="settings-page__content-item">
@@ -293,8 +144,8 @@ export default class AdvancedTab extends PureComponent {
<ToggleButton
value={sendHexData}
onToggle={value => setHexDataFeatureFlag(!value)}
- activeLabel=""
- inactiveLabel=""
+ offLabel={t('off')}
+ onLabel={t('on')}
/>
</div>
</div>
@@ -319,8 +170,8 @@ export default class AdvancedTab extends PureComponent {
<ToggleButton
value={advancedInlineGas}
onToggle={value => setAdvancedInlineGasFeatureFlag(!value)}
- activeLabel=""
- inactiveLabel=""
+ offLabel={t('off')}
+ onLabel={t('on')}
/>
</div>
</div>
@@ -348,8 +199,8 @@ export default class AdvancedTab extends PureComponent {
<ToggleButton
value={showFiatInTestnets}
onToggle={value => setShowFiatConversionOnTestnetsPreference(!value)}
- activeLabel=""
- inactiveLabel=""
+ offLabel={t('off')}
+ onLabel={t('on')}
/>
</div>
</div>
@@ -407,7 +258,6 @@ export default class AdvancedTab extends PureComponent {
{ warning && <div className="settings-tab__error">{ warning }</div> }
{ this.renderStateLogs() }
{ this.renderMobileSync() }
- { this.renderNewRpcUrl() }
{ this.renderResetAccount() }
{ this.renderAdvancedGasInputInline() }
{ this.renderHexDataOptIn() }
diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js
index f81329533..31cdd747c 100644
--- a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js
+++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js
@@ -16,7 +16,7 @@ describe('AdvancedTab Component', () => {
}
)
- assert.equal(root.find('.settings-page__content-row').length, 8)
+ assert.equal(root.find('.settings-page__content-row').length, 7)
})
it('should update autoLogoutTimeLimit', () => {
diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js
index 62122073d..3f54350c5 100644
--- a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js
+++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js
@@ -35,12 +35,12 @@ describe('AdvancedTab Container', () => {
it('should map dispatch to props correctly', () => {
const props = mapDispatchToProps(() => 'mockDispatch')
- assert.ok(typeof props.setHexDataFeatureFlag === 'function')
- assert.ok(typeof props.setRpcTarget === 'function')
- assert.ok(typeof props.displayWarning === 'function')
- assert.ok(typeof props.showResetAccountConfirmationModal === 'function')
- assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function')
- assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function')
- assert.ok(typeof props.setAutoLogoutTimeLimit === 'function')
+ assert.ok(typeof props.setHexDataFeatureFlag === 'function')
+ assert.ok(typeof props.setRpcTarget === 'function')
+ assert.ok(typeof props.displayWarning === 'function')
+ assert.ok(typeof props.showResetAccountConfirmationModal === 'function')
+ assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function')
+ assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function')
+ assert.ok(typeof props.setAutoLogoutTimeLimit === 'function')
})
})
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..f8c079fc3
--- /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..4852bbc6a
--- /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..d4fe045eb
--- /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)
diff --git a/ui/app/pages/settings/index.js b/ui/app/pages/settings/index.js
index 44a9ffa63..d2dd7f795 100644
--- a/ui/app/pages/settings/index.js
+++ b/ui/app/pages/settings/index.js
@@ -1 +1 @@
-export { default } from './settings.component'
+export { default } from './settings.container'
diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss
index c516a84bb..73f36806d 100644
--- a/ui/app/pages/settings/index.scss
+++ b/ui/app/pages/settings/index.scss
@@ -4,6 +4,8 @@
@import 'settings-tab/index';
+@import 'contact-list-tab/index';
+
.settings-page {
position: relative;
background: $white;
@@ -23,7 +25,7 @@
}
}
- &__subheader {
+ &__subheader, &__subheader--link {
padding: 16px 4px;
font-size: 20px;
border-bottom: 1px solid $alto;
@@ -38,6 +40,16 @@
}
}
+ &__subheader--link {
+ cursor: pointer;
+ margin-right: 4px;
+ }
+
+ &__subheader--link:hover {
+ cursor: pointer;
+ color: #037DD6;
+ }
+
&__sub-header {
height: 72px;
border-bottom: 1px solid #D8D8D8;
@@ -116,6 +128,8 @@
&__modules {
overflow-y: auto;
flex: 1 1 auto;
+ display: flex;
+ flex-flow: column;
@media screen and (max-width: 575px) {
display: none;
@@ -142,7 +156,7 @@
min-width: 0;
display: flex;
flex-direction: column;
- min-height: 71px;
+ margin-bottom: 20px;
@media screen and (max-width: 575px) {
height: initial;
@@ -175,6 +189,37 @@
}
}
+ &__copyable-address {
+ display: flex;
+ }
+
+ &__copy-icon {
+ padding-left: 4px;
+ }
+
+ &__button-group {
+ display:flex;
+ margin-left: auto;
+ }
+
+ &__address-book-button {
+ //align-self: flex-end;
+ //padding: 5px;
+ //text-transform: uppercase;
+ //cursor: pointer;
+ //width: 25%;
+ //min-width: 80px;
+ //height: 33px;
+ font-size: 1rem;
+ line-height: 1.1875rem;
+ padding: 0;
+
+ }
+
+ &__address-book-button + &__address-book-button {
+ margin-left: 1.875rem;
+ }
+
&--selected {
.settings-page {
&__content {
diff --git a/ui/app/pages/settings/networks-tab/network-form/network-form.component.js b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js
index 388e2665f..0349aa14f 100644
--- a/ui/app/pages/settings/networks-tab/network-form/network-form.component.js
+++ b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js
@@ -12,7 +12,7 @@ export default class NetworkForm extends PureComponent {
static propTypes = {
editRpc: PropTypes.func.isRequired,
- delRpcTarget: PropTypes.func.isRequired,
+ showConfirmDeleteNetworkModal: PropTypes.func.isRequired,
rpcUrl: PropTypes.string,
chainId: PropTypes.string,
ticker: PropTypes.string,
@@ -131,10 +131,14 @@ export default class NetworkForm extends PureComponent {
}
onDelete = () => {
- const { delRpcTarget, rpcUrl, onClear } = this.props
- delRpcTarget(rpcUrl)
- this.resetForm()
- onClear()
+ const { showConfirmDeleteNetworkModal, rpcUrl, onClear } = this.props
+ showConfirmDeleteNetworkModal({
+ target: rpcUrl,
+ onConfirm: () => {
+ this.resetForm()
+ onClear()
+ },
+ })
}
stateIsUnchanged () {
diff --git a/ui/app/pages/settings/networks-tab/networks-tab.component.js b/ui/app/pages/settings/networks-tab/networks-tab.component.js
index f6c8443cf..40e1a902f 100644
--- a/ui/app/pages/settings/networks-tab/networks-tab.component.js
+++ b/ui/app/pages/settings/networks-tab/networks-tab.component.js
@@ -25,7 +25,7 @@ export default class NetworksTab extends PureComponent {
setNetworksTabAddMode: PropTypes.func.isRequired,
setRpcTarget: PropTypes.func.isRequired,
setSelectedSettingsRpcUrl: PropTypes.func.isRequired,
- delRpcTarget: PropTypes.func.isRequired,
+ showConfirmDeleteNetworkModal: PropTypes.func.isRequired,
providerUrl: PropTypes.string,
providerType: PropTypes.string,
networkDefaultedToProvider: PropTypes.bool,
@@ -50,16 +50,16 @@ export default class NetworksTab extends PureComponent {
return (
<div className="settings-page__sub-header">
- <div
- className="networks-tab__back-button"
- onClick={(networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode
- ? () => {
- setNetworksTabAddMode(false)
- setSelectedSettingsRpcUrl(null)
- }
- : () => this.props.history.push(SETTINGS_ROUTE)
+ <div
+ className="networks-tab__back-button"
+ onClick={(networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode
+ ? () => {
+ setNetworksTabAddMode(false)
+ setSelectedSettingsRpcUrl(null)
}
- />
+ : () => this.props.history.push(SETTINGS_ROUTE)
+ }
+ />
<span className="settings-page__sub-header-text">{ this.context.t('networks') }</span>
<div className="networks-tab__add-network-header-button-wrapper">
<Button
@@ -109,7 +109,7 @@ export default class NetworksTab extends PureComponent {
setNetworksTabAddMode(false)
setSelectedSettingsRpcUrl(rpcUrl)
}}
- >
+ >
<NetworkDropdownIcon
backgroundColor={iconColor || 'white'}
innerBorder={border}
@@ -126,7 +126,7 @@ export default class NetworksTab extends PureComponent {
renderNetworksList () {
const { networksToRender, selectedNetwork, networkIsSelected, networksTabIsInAddMode, networkDefaultedToProvider } = this.props
- console.log(networksToRender)
+
return (
<div
className={classnames('networks-tab__networks-list', {
@@ -160,7 +160,7 @@ export default class NetworksTab extends PureComponent {
const { t } = this.context
const {
setRpcTarget,
- delRpcTarget,
+ showConfirmDeleteNetworkModal,
setSelectedSettingsRpcUrl,
setNetworksTabAddMode,
selectedNetwork: {
@@ -199,7 +199,7 @@ export default class NetworksTab extends PureComponent {
setNetworksTabAddMode(false)
setSelectedSettingsRpcUrl(null)
}}
- delRpcTarget={delRpcTarget}
+ showConfirmDeleteNetworkModal={showConfirmDeleteNetworkModal}
viewOnly={viewOnly}
isCurrentRpcTarget={providerUrl === rpcUrl}
networksTabIsInAddMode={networksTabIsInAddMode}
@@ -223,16 +223,16 @@ export default class NetworksTab extends PureComponent {
{this.renderNetworksTabContent()}
{!networkIsSelected && !networksTabIsInAddMode
? <div className="networks-tab__add-network-button-wrapper">
- <Button
- type="primary"
- onClick={event => {
- event.preventDefault()
- setSelectedSettingsRpcUrl(null)
- setNetworksTabAddMode(true)
- }}
- >
- { this.context.t('addNetwork') }
- </Button>
+ <Button
+ type="primary"
+ onClick={event => {
+ event.preventDefault()
+ setSelectedSettingsRpcUrl(null)
+ setNetworksTabAddMode(true)
+ }}
+ >
+ { this.context.t('addNetwork') }
+ </Button>
</div>
: null
}
diff --git a/ui/app/pages/settings/networks-tab/networks-tab.container.js b/ui/app/pages/settings/networks-tab/networks-tab.container.js
index 9e1098922..8cc18a4bd 100644
--- a/ui/app/pages/settings/networks-tab/networks-tab.container.js
+++ b/ui/app/pages/settings/networks-tab/networks-tab.container.js
@@ -8,7 +8,7 @@ import {
displayWarning,
setNetworksTabAddMode,
editRpc,
- delRpcTarget,
+ showModal,
} from '../../../store/actions'
import { defaultNetworksData } from './networks-tab.constants'
const defaultNetworks = defaultNetworksData.map(network => ({ ...network, viewOnly: true }))
@@ -64,8 +64,8 @@ const mapDispatchToProps = dispatch => {
setRpcTarget: (newRpc, chainId, ticker, nickname, rpcPrefs) => {
dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname, rpcPrefs))
},
- delRpcTarget: (target) => {
- dispatch(delRpcTarget(target))
+ showConfirmDeleteNetworkModal: ({ target, onConfirm }) => {
+ return dispatch(showModal({ name: 'CONFIRM_DELETE_NETWORK', target, onConfirm }))
},
displayWarning: warning => dispatch(displayWarning(warning)),
setNetworksTabAddMode: isInAddMode => dispatch(setNetworksTabAddMode(isInAddMode)),
diff --git a/ui/app/pages/settings/security-tab/security-tab.component.js b/ui/app/pages/settings/security-tab/security-tab.component.js
index 01a28bac7..0d367abfb 100644
--- a/ui/app/pages/settings/security-tab/security-tab.component.js
+++ b/ui/app/pages/settings/security-tab/security-tab.component.js
@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { exportAsFile } from '../../../helpers/utils/util'
-import ToggleButton from 'react-toggle-button'
+import ToggleButton from '../../../components/ui/toggle-button'
import { REVEAL_SEED_ROUTE } from '../../../helpers/constants/routes'
import Button from '../../../components/ui/button'
@@ -140,8 +140,8 @@ export default class SecurityTab extends PureComponent {
<ToggleButton
value={privacyMode}
onToggle={value => setPrivacyMode(!value)}
- activeLabel=""
- inactiveLabel=""
+ offLabel={t('off')}
+ onLabel={t('on')}
/>
</div>
</div>
@@ -166,8 +166,8 @@ export default class SecurityTab extends PureComponent {
<ToggleButton
value={participateInMetaMetrics}
onToggle={value => setParticipateInMetaMetrics(!value)}
- activeLabel=""
- inactiveLabel=""
+ offLabel={t('off')}
+ onLabel={t('on')}
/>
</div>
</div>
diff --git a/ui/app/pages/settings/settings-tab/settings-tab.component.js b/ui/app/pages/settings/settings-tab/settings-tab.component.js
index 57e80be0d..f8daa98f9 100644
--- a/ui/app/pages/settings/settings-tab/settings-tab.component.js
+++ b/ui/app/pages/settings/settings-tab/settings-tab.component.js
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import infuraCurrencies from '../../../helpers/constants/infura-conversion.json'
import SimpleDropdown from '../../../components/app/dropdowns/simple-dropdown'
-import ToggleButton from 'react-toggle-button'
+import ToggleButton from '../../../components/ui/toggle-button'
import locales from '../../../../../app/_locales/index.json'
const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
@@ -105,6 +105,7 @@ export default class SettingsTab extends PureComponent {
renderBlockieOptIn () {
+ const { t } = this.context
const { useBlockie, setUseBlockie } = this.props
return (
@@ -117,8 +118,8 @@ export default class SettingsTab extends PureComponent {
<ToggleButton
value={useBlockie}
onToggle={value => setUseBlockie(!value)}
- activeLabel=""
- inactiveLabel=""
+ offLabel={t('off')}
+ onLabel={t('on')}
/>
</div>
</div>
diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js
index 7f2045244..79f383dc4 100644
--- a/ui/app/pages/settings/settings.component.js
+++ b/ui/app/pages/settings/settings.component.js
@@ -1,8 +1,6 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Switch, Route, matchPath, withRouter } from 'react-router-dom'
-import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums'
-import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import TabBar from '../../components/app/tab-bar'
import c from 'classnames'
import SettingsTab from './settings-tab'
@@ -10,6 +8,7 @@ import NetworksTab from './networks-tab'
import AdvancedTab from './advanced-tab'
import InfoTab from './info-tab'
import SecurityTab from './security-tab'
+import ContactListTab from './contact-list-tab'
import {
DEFAULT_ROUTE,
ADVANCED_ROUTE,
@@ -18,19 +17,28 @@ import {
ABOUT_US_ROUTE,
SETTINGS_ROUTE,
NETWORKS_ROUTE,
+ CONTACT_LIST_ROUTE,
+ 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 ROUTES_TO_I18N_KEYS = {
- [GENERAL_ROUTE]: 'general',
- [ADVANCED_ROUTE]: 'advanced',
- [SECURITY_ROUTE]: 'securityAndPrivacy',
- [ABOUT_US_ROUTE]: 'about',
-}
-
class SettingsPage extends PureComponent {
static propTypes = {
- location: PropTypes.object,
+ addressName: PropTypes.string,
+ backRoute: PropTypes.string,
+ currentPath: PropTypes.string,
history: PropTypes.object,
+ isAddressEntryPage: PropTypes.bool,
+ isPopupView: PropTypes.bool,
+ location: PropTypes.object,
+ pathnameI18nKey: PropTypes.string,
+ initialBreadCrumbRoute: PropTypes.string,
+ breadCrumbTextKey: PropTypes.string,
+ initialBreadCrumbKey: PropTypes.string,
t: PropTypes.func,
}
@@ -38,35 +46,25 @@ class SettingsPage extends PureComponent {
t: PropTypes.func,
}
- isCurrentPath (pathname) {
- return this.props.location.pathname === pathname
- }
-
render () {
- const { t } = this.context
- const { history, location } = this.props
-
- const pathnameI18nKey = ROUTES_TO_I18N_KEYS[location.pathname]
- const isPopupView = getEnvironmentType(location.href) === ENVIRONMENT_TYPE_POPUP
+ const { history, backRoute, currentPath } = this.props
return (
<div
className={c('main-container settings-page', {
- 'settings-page--selected': !this.isCurrentPath(SETTINGS_ROUTE),
+ 'settings-page--selected': currentPath !== SETTINGS_ROUTE,
})}
>
<div className="settings-page__header">
{
- !this.isCurrentPath(SETTINGS_ROUTE) && !this.isCurrentPath(NETWORKS_ROUTE) && (
+ currentPath !== SETTINGS_ROUTE && currentPath !== NETWORKS_ROUTE && (
<div
className="settings-page__back-button"
- onClick={() => history.push(SETTINGS_ROUTE)}
+ onClick={() => history.push(backRoute)}
/>
)
}
- <div className="settings-page__header__title">
- {t(pathnameI18nKey && isPopupView ? pathnameI18nKey : 'settings')}
- </div>
+ { this.renderTitle() }
<div
className="settings-page__close-button"
onClick={() => history.push(DEFAULT_ROUTE)}
@@ -85,19 +83,65 @@ class SettingsPage extends PureComponent {
)
}
+ renderTitle () {
+ const { t } = this.context
+ const { isPopupView, pathnameI18nKey, addressName } = this.props
+
+ let titleText
+
+ if (isPopupView && addressName) {
+ titleText = addressName
+ } else if (pathnameI18nKey && isPopupView) {
+ titleText = t(pathnameI18nKey)
+ } else {
+ titleText = t('settings')
+ }
+
+ return (
+ <div className="settings-page__header__title">
+ {titleText}
+ </div>
+ )
+ }
+
renderSubHeader () {
const { t } = this.context
- const { location: { pathname } } = this.props
+ const {
+ currentPath,
+ isPopupView,
+ isAddressEntryPage,
+ pathnameI18nKey,
+ addressName,
+ initialBreadCrumbRoute,
+ breadCrumbTextKey,
+ history,
+ initialBreadCrumbKey,
+ } = this.props
+
+ let subheaderText
- return pathname !== NETWORKS_ROUTE && (
+ if (isPopupView && isAddressEntryPage) {
+ subheaderText = t('settings')
+ } else if (initialBreadCrumbKey) {
+ subheaderText = t(initialBreadCrumbKey)
+ } else {
+ subheaderText = t(pathnameI18nKey || 'general')
+ }
+
+ return currentPath !== NETWORKS_ROUTE && (
<div className="settings-page__subheader">
- {t(ROUTES_TO_I18N_KEYS[pathname] || 'general')}
+ <div
+ className={c({ 'settings-page__subheader--link': initialBreadCrumbRoute })}
+ onClick={() => initialBreadCrumbRoute && history.push(initialBreadCrumbRoute)}
+ >{subheaderText}</div>
+ {breadCrumbTextKey && <div><span>{'> '}</span>{t(breadCrumbTextKey)}</div>}
+ {isAddressEntryPage && <div><span>{' > '}</span>{addressName}</div>}
</div>
)
}
renderTabs () {
- const { history, location } = this.props
+ const { history, currentPath } = this.props
const { t } = this.context
return (
@@ -105,15 +149,16 @@ class SettingsPage extends PureComponent {
tabs={[
{ content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE },
{ content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE },
+ { content: t('contactList'), description: t('contactListDescription'), key: CONTACT_LIST_ROUTE },
{ content: t('securityAndPrivacy'), description: t('securitySettingsDescription'), key: SECURITY_ROUTE },
{ content: t('networks'), description: t('networkSettingsDescription'), key: NETWORKS_ROUTE },
{ content: t('about'), description: t('aboutSettingsDescription'), key: ABOUT_US_ROUTE },
]}
isActive={key => {
- if (key === GENERAL_ROUTE && this.isCurrentPath(SETTINGS_ROUTE)) {
+ if (key === GENERAL_ROUTE && currentPath === SETTINGS_ROUTE) {
return true
}
- return matchPath(location.pathname, { path: key, exact: true })
+ return matchPath(currentPath, { path: key, exact: true })
}}
onSelect={key => history.push(key)}
/>
@@ -149,6 +194,41 @@ class SettingsPage extends PureComponent {
component={SecurityTab}
/>
<Route
+ exact
+ path={CONTACT_LIST_ROUTE}
+ component={ContactListTab}
+ />
+ <Route
+ exact
+ path={CONTACT_ADD_ROUTE}
+ component={ContactListTab}
+ />
+ <Route
+ exact
+ path={CONTACT_MY_ACCOUNTS_ROUTE}
+ component={ContactListTab}
+ />
+ <Route
+ exact
+ path={`${CONTACT_EDIT_ROUTE}/:id`}
+ component={ContactListTab}
+ />
+ <Route
+ exact
+ path={`${CONTACT_VIEW_ROUTE}/:id`}
+ component={ContactListTab}
+ />
+ <Route
+ exact
+ path={`${CONTACT_MY_ACCOUNTS_VIEW_ROUTE}/:id`}
+ component={ContactListTab}
+ />
+ <Route
+ exact
+ path={`${CONTACT_MY_ACCOUNTS_EDIT_ROUTE}/:id`}
+ component={ContactListTab}
+ />
+ <Route
component={SettingsTab}
/>
</Switch>
diff --git a/ui/app/pages/settings/settings.container.js b/ui/app/pages/settings/settings.container.js
new file mode 100644
index 000000000..79b191483
--- /dev/null
+++ b/ui/app/pages/settings/settings.container.js
@@ -0,0 +1,92 @@
+import Settings from './settings.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { getAddressBookEntryName } from '../../selectors/selectors'
+import { isValidAddress } from '../../helpers/utils/util'
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../app/scripts/lib/util'
+
+import {
+ ADVANCED_ROUTE,
+ SECURITY_ROUTE,
+ GENERAL_ROUTE,
+ ABOUT_US_ROUTE,
+ SETTINGS_ROUTE,
+ CONTACT_LIST_ROUTE,
+ CONTACT_ADD_ROUTE,
+ CONTACT_EDIT_ROUTE,
+ CONTACT_VIEW_ROUTE,
+ CONTACT_MY_ACCOUNTS_ROUTE,
+ CONTACT_MY_ACCOUNTS_EDIT_ROUTE,
+ CONTACT_MY_ACCOUNTS_VIEW_ROUTE,
+} from '../../helpers/constants/routes'
+
+const ROUTES_TO_I18N_KEYS = {
+ [GENERAL_ROUTE]: 'general',
+ [ADVANCED_ROUTE]: 'advanced',
+ [SECURITY_ROUTE]: 'securityAndPrivacy',
+ [ABOUT_US_ROUTE]: 'about',
+ [CONTACT_LIST_ROUTE]: 'contactList',
+ [CONTACT_ADD_ROUTE]: 'newContact',
+ [CONTACT_EDIT_ROUTE]: 'editContact',
+ [CONTACT_VIEW_ROUTE]: 'viewContact',
+ [CONTACT_MY_ACCOUNTS_ROUTE]: 'myAccounts',
+}
+
+const mapStateToProps = (state, ownProps) => {
+ const { location } = ownProps
+ const { pathname } = location
+ const pathNameTail = pathname.match(/[^/]+$/)[0]
+
+ const isAddressEntryPage = pathNameTail.includes('0x')
+ const isMyAccountsPage = pathname.match('my-accounts')
+ const isAddContactPage = Boolean(pathname.match(CONTACT_ADD_ROUTE))
+ const isEditContactPage = Boolean(pathname.match(CONTACT_EDIT_ROUTE))
+ const isEditMyAccountsContactPage = Boolean(pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE))
+
+ const isPopupView = getEnvironmentType(location.href) === ENVIRONMENT_TYPE_POPUP
+ const pathnameI18nKey = ROUTES_TO_I18N_KEYS[pathname]
+
+ let backRoute
+ if (isMyAccountsPage && isAddressEntryPage) {
+ backRoute = CONTACT_MY_ACCOUNTS_ROUTE
+ } else if (isEditContactPage) {
+ backRoute = `${CONTACT_VIEW_ROUTE}/${pathNameTail}`
+ } else if (isEditMyAccountsContactPage) {
+ backRoute = `${CONTACT_MY_ACCOUNTS_VIEW_ROUTE}/${pathNameTail}`
+ } else if (isAddressEntryPage || isMyAccountsPage || isAddContactPage) {
+ backRoute = CONTACT_LIST_ROUTE
+ } else {
+ backRoute = SETTINGS_ROUTE
+ }
+
+ let initialBreadCrumbRoute
+ let breadCrumbTextKey
+ let initialBreadCrumbKey
+ if (isMyAccountsPage) {
+ initialBreadCrumbRoute = CONTACT_LIST_ROUTE
+ breadCrumbTextKey = 'myWalletAccounts'
+ initialBreadCrumbKey = ROUTES_TO_I18N_KEYS[initialBreadCrumbRoute]
+ }
+
+ const addressName = getAddressBookEntryName(state, isValidAddress(pathNameTail) ? pathNameTail : '')
+
+ return {
+ isAddressEntryPage,
+ isMyAccountsPage,
+ backRoute,
+ currentPath: pathname,
+ isPopupView,
+ pathnameI18nKey,
+ addressName,
+ initialBreadCrumbRoute,
+ breadCrumbTextKey,
+ initialBreadCrumbKey,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps)
+)(Settings)