From 961ad267df93cbb3fc61d0a999bd78f132c877b1 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Mon, 25 Mar 2019 13:43:23 -0230 Subject: [PATCH] New settings page rebased (#6333) * New setting tab * Add InfoTab * Add Advanced tab * Add Security Tab * Finish mobile view * Make new setting page responsive * Fix linter * Fix y scrolling * Update link in network dropdown * Fix e2e tests * Remove duplicate translation key * Resolve merge conflict * Only change settings header in popup view. * Place mobile-sync button in advanced-tab of settings --- app/_locales/en/messages.json | 30 +- app/images/caret-left-black.svg | 18 + package-lock.json | 14 +- test/e2e/beta/metamask-beta-ui.spec.js | 16 +- .../app/dropdowns/network-dropdown.js | 4 +- ui/app/components/app/tab-bar.js | 30 +- ui/app/css/itcss/components/tab-bar.scss | 68 ++- ui/app/helpers/constants/routes.js | 10 + .../advanced-tab/advanced-tab.component.js | 378 ++++++++++++++ .../advanced-tab/advanced-tab.container.js | 48 ++ ui/app/pages/settings/advanced-tab/index.js | 1 + ui/app/pages/settings/index.scss | 85 ++- .../settings/info-tab/info-tab.component.js | 8 +- ui/app/pages/settings/security-tab/index.js | 1 + .../security-tab/security-tab.component.js | 195 +++++++ .../security-tab/security-tab.container.js | 42 ++ .../settings-tab/settings-tab.component.js | 486 +----------------- .../settings-tab/settings-tab.container.js | 30 +- ui/app/pages/settings/settings.component.js | 127 ++++- 19 files changed, 1010 insertions(+), 581 deletions(-) create mode 100644 app/images/caret-left-black.svg create mode 100644 ui/app/pages/settings/advanced-tab/advanced-tab.component.js create mode 100644 ui/app/pages/settings/advanced-tab/advanced-tab.container.js create mode 100644 ui/app/pages/settings/advanced-tab/index.js create mode 100644 ui/app/pages/settings/security-tab/index.js create mode 100644 ui/app/pages/settings/security-tab/security-tab.component.js create mode 100644 ui/app/pages/settings/security-tab/security-tab.container.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index cb785023e..ca6761183 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -44,6 +44,9 @@ "providerRequestInfo": { "message": "This site is requesting access to view your current account address. Always make sure you trust the sites you interact with." }, + "aboutUs": { + "message": "About Us" + }, "accept": { "message": "Accept" }, @@ -74,6 +77,12 @@ "address": { "message": "Address" }, + "advanced": { + "message": "Advanced" + }, + "advancedSettingsDescription": { + "message": "Access developer features, download State Logs, Reset Account, setup testnets and custom RPC." + }, "advancedOptions": { "message": "Advanced Options" }, @@ -92,9 +101,6 @@ "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, - "advanced": { - "message": "Advanced" - }, "agreeTermsOfService": { "message": "I agree to the Terms of Service" }, @@ -236,6 +242,9 @@ "chromeRequiredForHardwareWallets": { "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." }, + "company": { + "message": "Company" + }, "confirm": { "message": "Confirm" }, @@ -619,6 +628,12 @@ "gasPriceRequired": { "message": "Gas Price Required" }, + "general": { + "message": "General" + }, + "generalSettingsDescription": { + "message": "Currency conversion, primary currency, language, blockies identicon" + }, "generatingTransaction": { "message": "Generating transaction" }, @@ -790,6 +805,9 @@ "ledgerAccountRestriction": { "message": "You need to make use your last account before you can add a new one." }, + "legal": { + "message": "Legal" + }, "lessThanMax": { "message": "must be less than or equal to $1.", "description": "helper for inputting hex as decimal input" @@ -1240,6 +1258,12 @@ "secretPhrase": { "message": "Enter your secret twelve word phrase here to restore your vault." }, + "securityAndPrivacy": { + "message": "Security & Privacy" + }, + "securitySettingsDescription": { + "message": "Privacy settings and wallet seed phrase" + }, "secondsShorthand": { "message": "Sec" }, diff --git a/app/images/caret-left-black.svg b/app/images/caret-left-black.svg new file mode 100644 index 000000000..872135ece --- /dev/null +++ b/app/images/caret-left-black.svg @@ -0,0 +1,18 @@ + + + + 8439120D-5704-4273-B416-FEE134322584 + Created with sketchtool. + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 1d2b3b724..d753a1e9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9849,7 +9849,7 @@ "dependencies": { "babelify": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "requires": { "babel-core": "^6.0.14", @@ -9901,7 +9901,7 @@ } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.11.8", @@ -10189,7 +10189,7 @@ } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.11.8", @@ -10484,7 +10484,7 @@ } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.11.8", @@ -24076,7 +24076,7 @@ "dependencies": { "babelify": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "requires": { "babel-core": "^6.0.14", @@ -24115,7 +24115,7 @@ }, "babelify": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "requires": { "babel-core": "^6.0.14", @@ -26541,7 +26541,7 @@ "dependencies": { "babelify": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "resolved": "http://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", "requires": { "babel-core": "^6.0.14", diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index 06f67ad95..e25b7edf1 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -233,7 +233,11 @@ describe('MetaMask', function () { await customRpcButton.click() await delay(regularDelayMs) - const privacyToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(10) .settings-page__content-item-col > div')) + const securityTab = await findElement(driver, By.xpath(`//div[contains(text(), 'Security & Privacy')]`)) + await securityTab.click() + await delay(regularDelayMs) + + const privacyToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(1) .settings-page__content-item-col > div')) await privacyToggle.click() await delay(largeDelayMs * 2) }) @@ -472,15 +476,19 @@ describe('MetaMask', function () { const settingsButton = await findElement(driver, By.xpath(`//div[contains(text(), 'Settings')]`)) settingsButton.click() - await findElement(driver, By.css('.tab-bar')) + // await findElement(driver, By.css('.tab-bar')) + + const advancedTab = await findElement(driver, By.xpath(`//div[contains(text(), 'Advanced')]`)) + await advancedTab.click() + await delay(regularDelayMs) - const showConversionToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(3) .settings-page__content-item-col > div')) + const showConversionToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(7) .settings-page__content-item-col > div')) await showConversionToggle.click() const advancedGasTitle = await findElement(driver, By.xpath(`//span[contains(text(), 'Advanced gas controls')]`)) await driver.executeScript('arguments[0].scrollIntoView(true)', advancedGasTitle) - const advancedGasToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(12) .settings-page__content-item-col > div')) + const advancedGasToggle = await findElement(driver, By.css('.settings-page__content-row:nth-of-type(5) .settings-page__content-item-col > div')) await advancedGasToggle.click() windowHandles = await driver.getAllWindowHandles() extension = windowHandles[0] diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index fcc7a8681..3d9037a06 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -10,7 +10,7 @@ const Dropdown = require('./components/dropdown').Dropdown const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkDropdownIcon = require('./components/network-dropdown-icon') const R = require('ramda') -const { SETTINGS_ROUTE } = require('../../../helpers/constants/routes') +const { ADVANCED_ROUTE } = require('../../../helpers/constants/routes') // classes from nodes of the toggle element. const notToggleElementClassnames = [ @@ -233,7 +233,7 @@ NetworkDropdown.prototype.render = function () { DropdownMenuItem, { closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => this.props.history.push(SETTINGS_ROUTE), + onClick: () => this.props.history.push(ADVANCED_ROUTE), style: dropdownMenuItemStyle, }, [ diff --git a/ui/app/components/app/tab-bar.js b/ui/app/components/app/tab-bar.js index 0016a09c1..43923989a 100644 --- a/ui/app/components/app/tab-bar.js +++ b/ui/app/components/app/tab-bar.js @@ -1,5 +1,4 @@ -const { Component } = require('react') -const h = require('react-hyperscript') +import React, { Component } from 'react' const PropTypes = require('prop-types') const classnames = require('classnames') @@ -8,18 +7,23 @@ class TabBar extends Component { const { tabs = [], onSelect, isActive } = this.props return ( - h('.tab-bar', {}, [ - tabs.map(({ key, content }) => { - return h('div', { - className: classnames('tab-bar__tab pointer', { +
+ {tabs.map(({ key, content, description }) => ( +
onSelect(key), - key, - }, content) - }), - h('div.tab-bar__tab.tab-bar__grow-tab'), - ]) + })} + onClick={() => onSelect(key)} + > +
+
{content}
+
{description}
+
+
+
+ ))} +
) } } diff --git a/ui/app/css/itcss/components/tab-bar.scss b/ui/app/css/itcss/components/tab-bar.scss index 4f3077974..bb9f8f261 100644 --- a/ui/app/css/itcss/components/tab-bar.scss +++ b/ui/app/css/itcss/components/tab-bar.scss @@ -1,21 +1,73 @@ .tab-bar { display: flex; - flex-direction: row; + flex-direction: column; justify-content: flex-start; - align-items: flex-end; } .tab-bar__tab { + display: flex; + flex-flow: row nowrap; + align-items: flex-start; min-width: 0; flex: 0 0 auto; - padding: 15px 25px; - border-bottom: 1px solid $alto; box-sizing: border-box; - font-size: 18px; -} + font-size: 16px; + padding: 16px 24px; + opacity: .5; + transition: opacity 200ms ease-in-out; + + @media screen and (min-width: 576px) { + &:hover { + opacity: .4; + } + + &:active { + opacity: .6; + } + } + + @media screen and (max-width: 575px) { + font-size: 18px; + padding: 24px; + border-bottom: 1px solid $alto; + opacity: 1; + } + + &__content { + flex: 1 1 auto; + width: 0; + + &__description { + display: none; + + @media screen and (max-width: 575px) { + display: block; + font-size: 14px; + font-weight: 300; + margin-top: 8px; + min-height: 14px; + } + } + } + + &__caret { + display: none; + + @media screen and (max-width: 575px) { + display: block; + background-image: url('/images/caret-right.svg'); + width: 36px; + height: 36px; + opacity: .5; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + } + } -.tab-bar__tab--active { - border-color: $black; + &--active { + opacity: 1 !important; + } } .tab-bar__grow-tab { diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index 932dfa7df..c15027ff4 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -2,7 +2,12 @@ const DEFAULT_ROUTE = '/' const UNLOCK_ROUTE = '/unlock' const LOCK_ROUTE = '/lock' const SETTINGS_ROUTE = '/settings' +const GENERAL_ROUTE = '/settings/general' const INFO_ROUTE = '/settings/info' +const ADVANCED_ROUTE = '/settings/advanced' +const SECURITY_ROUTE = '/settings/security' +const COMPANY_ROUTE = '/settings/company' +const ABOUT_US_ROUTE = '/settings/about-us' const REVEAL_SEED_ROUTE = '/seed' const MOBILE_SYNC_ROUTE = '/mobile-sync' const CONFIRM_SEED_ROUTE = '/confirm-seed' @@ -80,4 +85,9 @@ module.exports = { CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, INITIALIZE_METAMETRICS_OPT_IN_ROUTE, + ADVANCED_ROUTE, + SECURITY_ROUTE, + COMPANY_ROUTE, + GENERAL_ROUTE, + ABOUT_US_ROUTE, } diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js new file mode 100644 index 000000000..d1cad1746 --- /dev/null +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js @@ -0,0 +1,378 @@ +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 TextField from '../../../components/ui/text-field' +import Button from '../../../components/ui/button' +import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes' + +export default class AdvancedTab extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static propTypes = { + setHexDataFeatureFlag: PropTypes.func, + setRpcTarget: PropTypes.func, + displayWarning: PropTypes.func, + showResetAccountConfirmationModal: PropTypes.func, + warning: PropTypes.string, + history: PropTypes.object, + sendHexData: PropTypes.bool, + setAdvancedInlineGasFeatureFlag: PropTypes.func, + advancedInlineGas: PropTypes.bool, + showFiatInTestnets: PropTypes.bool, + setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, + } + + state = { + newRpc: '', + chainId: '', + showOptions: false, + ticker: '', + nickname: '', + } + + renderNewRpcUrl () { + const { t } = this.context + const { newRpc, chainId, ticker, nickname } = this.state + + return ( +
+
+ { t('newNetwork') } +
+
+
+ this.setState({ newRpc: e.target.value })} + onKeyPress={e => { + if (e.key === 'Enter') { + this.validateRpc(newRpc, chainId, ticker, nickname) + } + }} + fullWidth + margin="dense" + /> + 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" + /> + 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" + /> + 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" + /> +
+ { + e.preventDefault() + this.setState({ showOptions: !this.state.showOptions }) + }} + > + { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') } + + +
+
+
+
+ ) + } + + 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')) + } + } + } + + renderMobileSync () { + const { t } = this.context + const { history } = this.props +// + return ( +
+
+ { t('syncWithMobile') } +
+
+
+ +
+
+
+ ) + } + + renderStateLogs () { + const { t } = this.context + const { displayWarning } = this.props + + return ( +
+
+ { t('stateLogs') } + + { t('stateLogsDescription') } + +
+
+
+ +
+
+
+ ) + } + + renderResetAccount () { + const { t } = this.context + const { showResetAccountConfirmationModal } = this.props + + return ( +
+
+ { t('resetAccount') } +
+
+
+ +
+
+
+ ) + } + + renderHexDataOptIn () { + const { t } = this.context + const { sendHexData, setHexDataFeatureFlag } = this.props + + return ( +
+
+ { t('showHexData') } +
+ { t('showHexDataDescription') } +
+
+
+
+ setHexDataFeatureFlag(!value)} + activeLabel="" + inactiveLabel="" + /> +
+
+
+ ) + } + + renderAdvancedGasInputInline () { + const { t } = this.context + const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props + + return ( +
+
+ { t('showAdvancedGasInline') } +
+ { t('showAdvancedGasInlineDescription') } +
+
+
+
+ setAdvancedInlineGasFeatureFlag(!value)} + activeLabel="" + inactiveLabel="" + /> +
+
+
+ ) + } + + renderShowConversionInTestnets () { + const { t } = this.context + const { + showFiatInTestnets, + setShowFiatConversionOnTestnetsPreference, + } = this.props + + return ( +
+
+ { t('showFiatConversionInTestnets') } +
+ { t('showFiatConversionInTestnetsDescription') } +
+
+
+
+ setShowFiatConversionOnTestnetsPreference(!value)} + activeLabel="" + inactiveLabel="" + /> +
+
+
+ ) + } + + renderContent () { + const { warning } = this.props + + return ( +
+ { warning &&
{ warning }
} + { this.renderStateLogs() } + { this.renderMobileSync() } + { this.renderNewRpcUrl() } + { this.renderResetAccount() } + { this.renderAdvancedGasInputInline() } + { this.renderHexDataOptIn() } + { this.renderShowConversionInTestnets() } +
+ ) + } + + render () { + return this.renderContent() + } +} diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js new file mode 100644 index 000000000..69d7e07e6 --- /dev/null +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js @@ -0,0 +1,48 @@ +import AdvancedTab from './advanced-tab.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { + updateAndSetCustomRpc, + displayWarning, + setFeatureFlag, + showModal, + setShowFiatConversionOnTestnetsPreference, +} from '../../../store/actions' +import {preferencesSelector} from '../../../selectors/selectors' + +const mapStateToProps = state => { + const { appState: { warning }, metamask } = state + const { + featureFlags: { + sendHexData, + advancedInlineGas, + } = {}, + } = metamask + const { showFiatInTestnets } = preferencesSelector(state) + + return { + warning, + sendHexData, + advancedInlineGas, + showFiatInTestnets, + } +} + +const mapDispatchToProps = dispatch => { + return { + setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), + setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)), + displayWarning: warning => dispatch(displayWarning(warning)), + showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), + setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)), + setShowFiatConversionOnTestnetsPreference: value => { + return dispatch(setShowFiatConversionOnTestnetsPreference(value)) + }, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(AdvancedTab) diff --git a/ui/app/pages/settings/advanced-tab/index.js b/ui/app/pages/settings/advanced-tab/index.js new file mode 100644 index 000000000..85955174e --- /dev/null +++ b/ui/app/pages/settings/advanced-tab/index.js @@ -0,0 +1 @@ +export { default } from './advanced-tab.container' diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss index 0e8482c63..52208dc85 100644 --- a/ui/app/pages/settings/index.scss +++ b/ui/app/pages/settings/index.scss @@ -9,34 +9,79 @@ flex-flow: column nowrap; &__header { - padding: 25px 25px 0; + display: flex; + flex-flow: row nowrap; + padding: 12px 24px; + align-items: center; + border-bottom: 1px solid $alto; + flex: 0 0 auto; + + &__title { + flex: 1 0 auto; + font-size: 24px; + } + } + + &__back-button { + display: none; + + @media screen and (max-width: 575px) { + display: block; + background-image: url('/images/caret-left-black.svg'); + width: 18px; + height: 18px; + opacity: .5; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 16px; + cursor: pointer; + } } &__close-button::after { content: '\00D7'; font-size: 40px; color: $dusty-gray; - position: absolute; - top: 25px; - right: 30px; cursor: pointer; } &__content { - padding: 25px; + display: flex; + flex-flow: row nowrap; height: auto; overflow: auto; + + &__tabs { + display: flex; + flex-direction: column; + flex: 1 1 auto; + + @media screen and (min-width: 576px) { + flex: 0 0 32%; + max-width: 210px; + border-right: 1px solid $alto; + } + } + + &__modules { + overflow-y: auto; + flex: 1 1 auto; + + @media screen and (max-width: 575px) { + display: none; + } + } + } + + &__body { + padding: 12px 24px; } &__content-row { display: flex; - flex-direction: row; + flex-direction: column; padding: 10px 0 20px; - - @media screen and (max-width: 575px) { - flex-direction: column; - padding: 10px 0; - } } &__content-item { @@ -77,4 +122,22 @@ width: 100%; } } + + &--selected { + .settings-page { + &__content { + &__tabs { + @media screen and (max-width: 575px) { + display: none; + } + } + + &__modules { + @media screen and (max-width: 575px) { + display: block; + } + } + } + } + } } diff --git a/ui/app/pages/settings/info-tab/info-tab.component.js b/ui/app/pages/settings/info-tab/info-tab.component.js index 72f7d835e..552dd156e 100644 --- a/ui/app/pages/settings/info-tab/info-tab.component.js +++ b/ui/app/pages/settings/info-tab/info-tab.component.js @@ -101,11 +101,11 @@ export default class InfoTab extends PureComponent { ) } - render () { + renderContent () { const { t } = this.context return ( -
+
@@ -133,4 +133,8 @@ export default class InfoTab extends PureComponent {
) } + + render () { + return this.renderContent() + } } diff --git a/ui/app/pages/settings/security-tab/index.js b/ui/app/pages/settings/security-tab/index.js new file mode 100644 index 000000000..7ffc291a2 --- /dev/null +++ b/ui/app/pages/settings/security-tab/index.js @@ -0,0 +1 @@ +export { default } from './security-tab.container' diff --git a/ui/app/pages/settings/security-tab/security-tab.component.js b/ui/app/pages/settings/security-tab/security-tab.component.js new file mode 100644 index 000000000..233561115 --- /dev/null +++ b/ui/app/pages/settings/security-tab/security-tab.component.js @@ -0,0 +1,195 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { exportAsFile } from '../../../helpers/utils/util' +import ToggleButton from 'react-toggle-button' +import { REVEAL_SEED_ROUTE } from '../../../helpers/constants/routes' +import Button from '../../../components/ui/button' + +export default class SecurityTab extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static propTypes = { + setPrivacyMode: PropTypes.func, + privacyMode: PropTypes.bool, + displayWarning: PropTypes.func, + revealSeedConfirmation: PropTypes.func, + showClearApprovalModal: PropTypes.func, + warning: PropTypes.string, + history: PropTypes.object, + mobileSync: PropTypes.bool, + participateInMetaMetrics: PropTypes.bool, + setParticipateInMetaMetrics: PropTypes.func, + } + + renderStateLogs () { + const { t } = this.context + const { displayWarning } = this.props + + return ( +
+
+ { t('stateLogs') } + + { t('stateLogsDescription') } + +
+
+
+ +
+
+
+ ) + } + + renderClearApproval () { + const { t } = this.context + const { showClearApprovalModal } = this.props + return ( +
+
+ { t('approvalData') } + + { t('approvalDataDescription') } + +
+
+
+ +
+
+
+ ) + } + + renderSeedWords () { + const { t } = this.context + const { history } = this.props + + return ( +
+
+ { t('revealSeedWords') } +
+
+
+ +
+
+
+ ) + } + + renderPrivacyOptIn () { + const { t } = this.context + const { privacyMode, setPrivacyMode } = this.props + + return ( +
+
+ { t('privacyMode') } +
+ { t('privacyModeDescription') } +
+
+
+
+ setPrivacyMode(!value)} + activeLabel="" + inactiveLabel="" + /> +
+
+
+ ) + } + + renderMetaMetricsOptIn () { + const { t } = this.context + const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props + + return ( +
+
+ { t('participateInMetaMetrics') } +
+ { t('participateInMetaMetricsDescription') } +
+
+
+
+ setParticipateInMetaMetrics(!value)} + activeLabel="" + inactiveLabel="" + /> +
+
+
+ ) + } + + renderContent () { + const { warning } = this.props + + return ( +
+ { warning &&
{ warning }
} + { this.renderPrivacyOptIn() } + { this.renderClearApproval() } + { this.renderSeedWords() } + { this.renderMetaMetricsOptIn() } +
+ ) + } + + render () { + return this.renderContent() + } +} diff --git a/ui/app/pages/settings/security-tab/security-tab.container.js b/ui/app/pages/settings/security-tab/security-tab.container.js new file mode 100644 index 000000000..6036f4eda --- /dev/null +++ b/ui/app/pages/settings/security-tab/security-tab.container.js @@ -0,0 +1,42 @@ +import SecurityTab from './security-tab.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { + displayWarning, + revealSeedConfirmation, + setFeatureFlag, + showModal, + setParticipateInMetaMetrics, +} from '../../../store/actions' + +const mapStateToProps = state => { + const { appState: { warning }, metamask } = state + const { + featureFlags: { + privacyMode, + } = {}, + participateInMetaMetrics, + } = metamask + + return { + warning, + privacyMode, + participateInMetaMetrics, + } +} + +const mapDispatchToProps = dispatch => { + return { + displayWarning: warning => dispatch(displayWarning(warning)), + revealSeedConfirmation: () => dispatch(revealSeedConfirmation()), + setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)), + showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })), + setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(SecurityTab) 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 f69c21e82..57e80be0d 100644 --- a/ui/app/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/app/pages/settings/settings-tab/settings-tab.component.js @@ -1,14 +1,9 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import infuraCurrencies from '../../../helpers/constants/infura-conversion.json' -import validUrl from 'valid-url' -import { exportAsFile } from '../../../helpers/utils/util' import SimpleDropdown from '../../../components/app/dropdowns/simple-dropdown' import ToggleButton from 'react-toggle-button' -import { REVEAL_SEED_ROUTE, MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes' import locales from '../../../../../app/_locales/index.json' -import TextField from '../../../components/ui/text-field' -import Button from '../../../components/ui/button' const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase()) @@ -37,44 +32,19 @@ export default class SettingsTab extends PureComponent { } static propTypes = { - metamask: PropTypes.object, setUseBlockie: PropTypes.func, - setHexDataFeatureFlag: PropTypes.func, - setPrivacyMode: PropTypes.func, - privacyMode: PropTypes.bool, setCurrentCurrency: PropTypes.func, - setRpcTarget: PropTypes.func, - delRpcTarget: PropTypes.func, displayWarning: PropTypes.func, - revealSeedConfirmation: PropTypes.func, - setFeatureFlagToBeta: PropTypes.func, - showClearApprovalModal: PropTypes.func, - showResetAccountConfirmationModal: PropTypes.func, warning: PropTypes.string, history: PropTypes.object, updateCurrentLocale: PropTypes.func, currentLocale: PropTypes.string, useBlockie: PropTypes.bool, - sendHexData: PropTypes.bool, currentCurrency: PropTypes.string, conversionDate: PropTypes.number, nativeCurrency: PropTypes.string, useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, - setAdvancedInlineGasFeatureFlag: PropTypes.func, - advancedInlineGas: PropTypes.bool, - showFiatInTestnets: PropTypes.bool, - setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, - participateInMetaMetrics: PropTypes.bool, - setParticipateInMetaMetrics: PropTypes.func, - } - - state = { - newRpc: '', - chainId: '', - showOptions: false, - ticker: '', - nickname: '', } renderCurrentConversion () { @@ -133,310 +103,6 @@ export default class SettingsTab extends PureComponent { ) } - renderNewRpcUrl () { - const { t } = this.context - const { newRpc, chainId, ticker, nickname } = this.state - - return ( -
-
- { t('newNetwork') } -
-
-
- this.setState({ newRpc: e.target.value })} - onKeyPress={e => { - if (e.key === 'Enter') { - this.validateRpc(newRpc, chainId, ticker, nickname) - } - }} - fullWidth - margin="dense" - /> - 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" - /> - 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" - /> - 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" - /> -
- { - e.preventDefault() - this.setState({ showOptions: !this.state.showOptions }) - }} - > - { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') } - - -
-
-
-
- ) - } - - 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')) - } - } - } - - renderStateLogs () { - const { t } = this.context - const { displayWarning } = this.props - - return ( -
-
- { t('stateLogs') } - - { t('stateLogsDescription') } - -
-
-
- -
-
-
- ) - } - - renderClearApproval () { - const { t } = this.context - const { showClearApprovalModal } = this.props - return ( -
-
- { t('approvalData') } - - { t('approvalDataDescription') } - -
-
-
- -
-
-
- ) - } - - renderSeedWords () { - const { t } = this.context - const { history } = this.props - - return ( -
-
- { t('revealSeedWords') } -
-
-
- -
-
-
- ) - } - - - renderMobileSync () { - const { t } = this.context - const { history } = this.props - - return ( -
-
- { t('syncWithMobile') } -
-
-
- -
-
-
- ) - } - - - renderResetAccount () { - const { t } = this.context - const { showResetAccountConfirmationModal } = this.props - - return ( -
-
- { t('resetAccount') } -
-
-
- -
-
-
- ) - } renderBlockieOptIn () { const { useBlockie, setUseBlockie } = this.props @@ -460,58 +126,6 @@ export default class SettingsTab extends PureComponent { ) } - renderHexDataOptIn () { - const { t } = this.context - const { sendHexData, setHexDataFeatureFlag } = this.props - - return ( -
-
- { t('showHexData') } -
- { t('showHexDataDescription') } -
-
-
-
- setHexDataFeatureFlag(!value)} - activeLabel="" - inactiveLabel="" - /> -
-
-
- ) - } - - renderAdvancedGasInputInline () { - const { t } = this.context - const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props - - return ( -
-
- { t('showAdvancedGasInline') } -
- { t('showAdvancedGasInlineDescription') } -
-
-
-
- setAdvancedInlineGasFeatureFlag(!value)} - activeLabel="" - inactiveLabel="" - /> -
-
-
- ) - } - renderUsePrimaryCurrencyOptions () { const { t } = this.context const { @@ -566,109 +180,21 @@ export default class SettingsTab extends PureComponent { ) } - renderShowConversionInTestnets () { - const { t } = this.context - const { - showFiatInTestnets, - setShowFiatConversionOnTestnetsPreference, - } = this.props - - return ( -
-
- { t('showFiatConversionInTestnets') } -
- { t('showFiatConversionInTestnetsDescription') } -
-
-
-
- setShowFiatConversionOnTestnetsPreference(!value)} - activeLabel="" - inactiveLabel="" - /> -
-
-
- ) - } - - renderPrivacyOptIn () { - const { t } = this.context - const { privacyMode, setPrivacyMode } = this.props - - return ( -
-
- { t('privacyMode') } -
- { t('privacyModeDescription') } -
-
-
-
- setPrivacyMode(!value)} - activeLabel="" - inactiveLabel="" - /> -
-
-
- ) - } - - renderMetaMetricsOptIn () { - const { t } = this.context - const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props - - return ( -
-
- { t('participateInMetaMetrics') } -
- { t('participateInMetaMetricsDescription') } -
-
-
-
- setParticipateInMetaMetrics(!value)} - activeLabel="" - inactiveLabel="" - /> -
-
-
- ) - } - - render () { + renderContent () { const { warning } = this.props return ( -
+
{ warning &&
{ warning }
} { this.renderCurrentConversion() } { this.renderUsePrimaryCurrencyOptions() } - { this.renderShowConversionInTestnets() } { this.renderCurrentLocale() } - { this.renderNewRpcUrl() } - { this.renderStateLogs() } - { this.renderSeedWords() } - { this.renderResetAccount() } - { this.renderClearApproval() } - { this.renderPrivacyOptIn() } - { this.renderHexDataOptIn() } - { this.renderAdvancedGasInputInline() } { this.renderBlockieOptIn() } - { this.renderMobileSync() } - { this.renderMetaMetricsOptIn() }
) } + + render () { + return this.renderContent() + } } diff --git a/ui/app/pages/settings/settings-tab/settings-tab.container.js b/ui/app/pages/settings/settings-tab/settings-tab.container.js index 3ae4985d7..d3d8457f0 100644 --- a/ui/app/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/app/pages/settings/settings-tab/settings-tab.container.js @@ -4,15 +4,10 @@ import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { setCurrentCurrency, - updateAndSetCustomRpc, displayWarning, - revealSeedConfirmation, setUseBlockie, updateCurrentLocale, - setFeatureFlag, - showModal, setUseNativeCurrencyAsPrimaryCurrencyPreference, - setShowFiatConversionOnTestnetsPreference, setParticipateInMetaMetrics, } from '../../../store/actions' import { preferencesSelector } from '../../../selectors/selectors' @@ -24,16 +19,9 @@ const mapStateToProps = state => { conversionDate, nativeCurrency, useBlockie, - featureFlags: { - sendHexData, - privacyMode, - advancedInlineGas, - } = {}, - provider = {}, currentLocale, - participateInMetaMetrics, } = metamask - const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets } = preferencesSelector(state) + const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) return { warning, @@ -42,35 +30,19 @@ const mapStateToProps = state => { conversionDate, nativeCurrency, useBlockie, - sendHexData, - advancedInlineGas, - privacyMode, - provider, useNativeCurrencyAsPrimaryCurrency, - showFiatInTestnets, - participateInMetaMetrics, } } const mapDispatchToProps = dispatch => { return { setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)), - setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)), displayWarning: warning => dispatch(displayWarning(warning)), - revealSeedConfirmation: () => dispatch(revealSeedConfirmation()), setUseBlockie: value => dispatch(setUseBlockie(value)), updateCurrentLocale: key => dispatch(updateCurrentLocale(key)), - setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), - setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)), - setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)), - showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), setUseNativeCurrencyAsPrimaryCurrencyPreference: value => { return dispatch(setUseNativeCurrencyAsPrimaryCurrencyPreference(value)) }, - setShowFiatConversionOnTestnetsPreference: value => { - return dispatch(setShowFiatConversionOnTestnetsPreference(value)) - }, - showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })), setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)), } } diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js index d67d3fcfe..3d415c6b8 100644 --- a/ui/app/pages/settings/settings.component.js +++ b/ui/app/pages/settings/settings.component.js @@ -1,10 +1,29 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import { Switch, Route, matchPath } 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' +import AdvancedTab from './advanced-tab' import InfoTab from './info-tab' -import { DEFAULT_ROUTE, SETTINGS_ROUTE, INFO_ROUTE } from '../../helpers/constants/routes' +import SecurityTab from './security-tab' +import { + DEFAULT_ROUTE, + ADVANCED_ROUTE, + SECURITY_ROUTE, + GENERAL_ROUTE, + ABOUT_US_ROUTE, + SETTINGS_ROUTE, +} from '../../helpers/constants/routes' + +const ROUTES_TO_I18N_KEYS = { + [GENERAL_ROUTE]: 'general', + [ADVANCED_ROUTE]: 'advanced', + [SECURITY_ROUTE]: 'securityAndPrivacy', + [ABOUT_US_ROUTE]: 'aboutUs', +} export default class SettingsPage extends PureComponent { static propTypes = { @@ -17,38 +36,102 @@ export default 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 + return ( -
+
+ { + !this.isCurrentPath(SETTINGS_ROUTE) && ( +
history.push(SETTINGS_ROUTE)} + /> + ) + } +
+ {t(pathnameI18nKey && isPopupView ? pathnameI18nKey : 'settings')} +
history.push(DEFAULT_ROUTE)} /> - matchPath(location.pathname, { path: key, exact: true })} - onSelect={key => history.push(key)} - />
- - - - +
+
+ { this.renderTabs() } +
+
+ { this.renderContent() } +
+
) } + + renderTabs () { + const { history, location } = this.props + const { t } = this.context + + return ( + { + if (key === GENERAL_ROUTE && this.isCurrentPath(SETTINGS_ROUTE)) { + return true + } + return matchPath(location.pathname, { path: key, exact: true }) + }} + onSelect={key => history.push(key)} + /> + ) + } + + renderContent () { + return ( + + + + + + + + ) + } }