diff options
author | Dan Finlay <542863+danfinlay@users.noreply.github.com> | 2019-08-07 05:53:50 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-07 05:53:50 +0800 |
commit | db08881d4527e8a037f401ef22b849e52152864f (patch) | |
tree | 6032d7a4ae67371889eece1d8490c26d5a119dd5 /ui/app/pages/settings | |
parent | 4139019d0f4dd83f56da400ca7e0e6d1976d1716 (diff) | |
parent | 86ad9564a064fd6158dab6a3c9e5b10614ef6e68 (diff) | |
download | tangerine-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')
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) |