diff options
Merge branch 'develop' into clearNotices
Diffstat (limited to 'ui/app/pages/settings')
-rw-r--r-- | ui/app/pages/settings/advanced-tab/advanced-tab.component.js | 378 | ||||
-rw-r--r-- | ui/app/pages/settings/advanced-tab/advanced-tab.container.js | 48 | ||||
-rw-r--r-- | ui/app/pages/settings/advanced-tab/index.js | 1 | ||||
-rw-r--r-- | ui/app/pages/settings/index.js | 1 | ||||
-rw-r--r-- | ui/app/pages/settings/index.scss | 143 | ||||
-rw-r--r-- | ui/app/pages/settings/info-tab/index.js | 1 | ||||
-rw-r--r-- | ui/app/pages/settings/info-tab/index.scss | 56 | ||||
-rw-r--r-- | ui/app/pages/settings/info-tab/info-tab.component.js | 140 | ||||
-rw-r--r-- | ui/app/pages/settings/security-tab/index.js | 1 | ||||
-rw-r--r-- | ui/app/pages/settings/security-tab/security-tab.component.js | 195 | ||||
-rw-r--r-- | ui/app/pages/settings/security-tab/security-tab.container.js | 42 | ||||
-rw-r--r-- | ui/app/pages/settings/settings-tab/index.js | 1 | ||||
-rw-r--r-- | ui/app/pages/settings/settings-tab/index.scss | 69 | ||||
-rw-r--r-- | ui/app/pages/settings/settings-tab/settings-tab.component.js | 200 | ||||
-rw-r--r-- | ui/app/pages/settings/settings-tab/settings-tab.container.js | 53 | ||||
-rw-r--r-- | ui/app/pages/settings/settings.component.js | 137 |
16 files changed, 1466 insertions, 0 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 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 ( + <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')) + } + } + } + + renderMobileSync () { + const { t } = this.context + const { history } = this.props +// + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('syncWithMobile') }</span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <Button + type="primary" + large + onClick={event => { + event.preventDefault() + history.push(MOBILE_SYNC_ROUTE) + }} + > + { t('syncWithMobile') } + </Button> + </div> + </div> + </div> + ) + } + + renderStateLogs () { + const { t } = this.context + const { displayWarning } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('stateLogs') }</span> + <span className="settings-page__content-description"> + { t('stateLogsDescription') } + </span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <Button + type="primary" + large + onClick={() => { + window.logStateString((err, result) => { + if (err) { + displayWarning(t('stateLogError')) + } else { + exportAsFile('MetaMask State Logs.json', result) + } + }) + }} + > + { t('downloadStateLogs') } + </Button> + </div> + </div> + </div> + ) + } + + renderResetAccount () { + const { t } = this.context + const { showResetAccountConfirmationModal } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('resetAccount') }</span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <Button + type="secondary" + large + className="settings-tab__button--orange" + onClick={event => { + event.preventDefault() + this.context.metricsEvent({ + eventOpts: { + category: 'Settings', + action: 'Reset Account', + name: 'Reset Account', + }, + }) + showResetAccountConfirmationModal() + }} + > + { t('resetAccount') } + </Button> + </div> + </div> + </div> + ) + } + + renderHexDataOptIn () { + const { t } = this.context + const { sendHexData, setHexDataFeatureFlag } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('showHexData') }</span> + <div className="settings-page__content-description"> + { t('showHexDataDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <ToggleButton + value={sendHexData} + onToggle={value => setHexDataFeatureFlag(!value)} + activeLabel="" + inactiveLabel="" + /> + </div> + </div> + </div> + ) + } + + renderAdvancedGasInputInline () { + const { t } = this.context + const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('showAdvancedGasInline') }</span> + <div className="settings-page__content-description"> + { t('showAdvancedGasInlineDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <ToggleButton + value={advancedInlineGas} + onToggle={value => setAdvancedInlineGasFeatureFlag(!value)} + activeLabel="" + inactiveLabel="" + /> + </div> + </div> + </div> + ) + } + + renderShowConversionInTestnets () { + const { t } = this.context + const { + showFiatInTestnets, + setShowFiatConversionOnTestnetsPreference, + } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('showFiatConversionInTestnets') }</span> + <div className="settings-page__content-description"> + { t('showFiatConversionInTestnetsDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <ToggleButton + value={showFiatInTestnets} + onToggle={value => setShowFiatConversionOnTestnetsPreference(!value)} + activeLabel="" + inactiveLabel="" + /> + </div> + </div> + </div> + ) + } + + renderContent () { + const { warning } = this.props + + return ( + <div className="settings-page__body"> + { warning && <div className="settings-tab__error">{ warning }</div> } + { this.renderStateLogs() } + { this.renderMobileSync() } + { this.renderNewRpcUrl() } + { this.renderResetAccount() } + { this.renderAdvancedGasInputInline() } + { this.renderHexDataOptIn() } + { this.renderShowConversionInTestnets() } + </div> + ) + } + + 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.js b/ui/app/pages/settings/index.js new file mode 100644 index 000000000..44a9ffa63 --- /dev/null +++ b/ui/app/pages/settings/index.js @@ -0,0 +1 @@ +export { default } from './settings.component' diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss new file mode 100644 index 000000000..52208dc85 --- /dev/null +++ b/ui/app/pages/settings/index.scss @@ -0,0 +1,143 @@ +@import 'info-tab/index'; + +@import 'settings-tab/index'; + +.settings-page { + position: relative; + background: $white; + display: flex; + flex-flow: column nowrap; + + &__header { + 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; + cursor: pointer; + } + + &__content { + 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: column; + padding: 10px 0 20px; + } + + &__content-item { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + padding: 0 5px; + min-height: 71px; + + @media screen and (max-width: 575px) { + height: initial; + padding: 5px 0; + } + + &--without-height { + height: initial; + } + } + + &__content-label { + text-transform: capitalize; + } + + &__content-description { + font-size: 14px; + color: $dusty-gray; + padding-top: 5px; + } + + &__content-item-col { + max-width: 300px; + display: flex; + flex-direction: column; + + @media screen and (max-width: 575px) { + max-width: 100%; + 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/index.js b/ui/app/pages/settings/info-tab/index.js new file mode 100644 index 000000000..7556a258d --- /dev/null +++ b/ui/app/pages/settings/info-tab/index.js @@ -0,0 +1 @@ +export { default } from './info-tab.component' diff --git a/ui/app/pages/settings/info-tab/index.scss b/ui/app/pages/settings/info-tab/index.scss new file mode 100644 index 000000000..43ad6f652 --- /dev/null +++ b/ui/app/pages/settings/info-tab/index.scss @@ -0,0 +1,56 @@ +.info-tab { + &__logo-wrapper { + height: 80px; + margin-bottom: 20px; + } + + &__logo { + max-height: 100%; + max-width: 100%; + } + + &__item { + padding: 10px 0; + } + + &__link-header { + padding-bottom: 15px; + + @media screen and (max-width: 575px) { + padding-bottom: 5px; + } + } + + &__link-item { + padding: 15px 0; + + @media screen and (max-width: 575px) { + padding: 5px 0; + } + } + + &__link-text { + color: $curious-blue; + } + + &__version-number { + padding-top: 5px; + font-size: 13px; + color: $dusty-gray; + } + + &__separator { + margin: 15px 0; + width: 80px; + border-color: $alto; + border: none; + height: 1px; + background-color: $alto; + color: $alto; + } + + &__about { + color: $dusty-gray; + margin-bottom: 15px; + } +} diff --git a/ui/app/pages/settings/info-tab/info-tab.component.js b/ui/app/pages/settings/info-tab/info-tab.component.js new file mode 100644 index 000000000..552dd156e --- /dev/null +++ b/ui/app/pages/settings/info-tab/info-tab.component.js @@ -0,0 +1,140 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' + +export default class InfoTab extends PureComponent { + state = { + version: global.platform.getVersion(), + } + + static propTypes = { + tab: PropTypes.string, + metamask: PropTypes.object, + setCurrentCurrency: PropTypes.func, + setRpcTarget: PropTypes.func, + displayWarning: PropTypes.func, + revealSeedConfirmation: PropTypes.func, + warning: PropTypes.string, + location: PropTypes.object, + history: PropTypes.object, + } + + static contextTypes = { + t: PropTypes.func, + } + + renderInfoLinks () { + const { t } = this.context + + return ( + <div className="settings-page__content-item settings-page__content-item--without-height"> + <div className="info-tab__link-header"> + { t('links') } + </div> + <div className="info-tab__link-item"> + <a + href="https://metamask.io/privacy.html" + target="_blank" + rel="noopener noreferrer" + > + <span className="info-tab__link-text"> + { t('privacyMsg') } + </span> + </a> + </div> + <div className="info-tab__link-item"> + <a + href="https://metamask.io/terms.html" + target="_blank" + rel="noopener noreferrer" + > + <span className="info-tab__link-text"> + { t('terms') } + </span> + </a> + </div> + <div className="info-tab__link-item"> + <a + href="https://metamask.io/attributions.html" + target="_blank" + rel="noopener noreferrer" + > + <span className="info-tab__link-text"> + { t('attributions') } + </span> + </a> + </div> + <hr className="info-tab__separator" /> + <div className="info-tab__link-item"> + <a + href="https://support.metamask.io" + target="_blank" + rel="noopener noreferrer" + > + <span className="info-tab__link-text"> + { t('supportCenter') } + </span> + </a> + </div> + <div className="info-tab__link-item"> + <a + href="https://metamask.io/" + target="_blank" + rel="noopener noreferrer" + > + <span className="info-tab__link-text"> + { t('visitWebSite') } + </span> + </a> + </div> + <div className="info-tab__link-item"> + <a + href="mailto:help@metamask.io?subject=Feedback" + target="_blank" + rel="noopener noreferrer" + > + <span className="info-tab__link-text"> + { t('emailUs') } + </span> + </a> + </div> + </div> + ) + } + + renderContent () { + const { t } = this.context + + return ( + <div className="settings-page__body"> + <div className="settings-page__content-row"> + <div className="settings-page__content-item settings-page__content-item--without-height"> + <div className="info-tab__logo-wrapper"> + <img + src="images/info-logo.png" + className="info-tab__logo" + /> + </div> + <div className="info-tab__item"> + <div className="info-tab__version-header"> + { t('metamaskVersion') } + </div> + <div className="info-tab__version-number"> + { this.state.version } + </div> + </div> + <div className="info-tab__item"> + <div className="info-tab__about"> + { t('builtInCalifornia') } + </div> + </div> + </div> + { this.renderInfoLinks() } + </div> + </div> + ) + } + + 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 ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('stateLogs') }</span> + <span className="settings-page__content-description"> + { t('stateLogsDescription') } + </span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <Button + type="primary" + large + onClick={() => { + window.logStateString((err, result) => { + if (err) { + displayWarning(t('stateLogError')) + } else { + exportAsFile('MetaMask State Logs.json', result) + } + }) + }} + > + { t('downloadStateLogs') } + </Button> + </div> + </div> + </div> + ) + } + + renderClearApproval () { + const { t } = this.context + const { showClearApprovalModal } = this.props + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('approvalData') }</span> + <span className="settings-page__content-description"> + { t('approvalDataDescription') } + </span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <Button + type="secondary" + large + className="settings-tab__button--orange" + onClick={event => { + event.preventDefault() + showClearApprovalModal() + }} + > + { t('clearApprovalData') } + </Button> + </div> + </div> + </div> + ) + } + + renderSeedWords () { + const { t } = this.context + const { history } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('revealSeedWords') }</span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <Button + type="secondary" + large + onClick={event => { + event.preventDefault() + this.context.metricsEvent({ + eventOpts: { + category: 'Settings', + action: 'Reveal Seed Phrase', + name: 'Reveal Seed Phrase', + }, + }) + history.push(REVEAL_SEED_ROUTE) + }} + > + { t('revealSeedWords') } + </Button> + </div> + </div> + </div> + ) + } + + renderPrivacyOptIn () { + const { t } = this.context + const { privacyMode, setPrivacyMode } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('privacyMode') }</span> + <div className="settings-page__content-description"> + { t('privacyModeDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <ToggleButton + value={privacyMode} + onToggle={value => setPrivacyMode(!value)} + activeLabel="" + inactiveLabel="" + /> + </div> + </div> + </div> + ) + } + + renderMetaMetricsOptIn () { + const { t } = this.context + const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('participateInMetaMetrics') }</span> + <div className="settings-page__content-description"> + <span>{ t('participateInMetaMetricsDescription') }</span> + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <ToggleButton + value={participateInMetaMetrics} + onToggle={value => setParticipateInMetaMetrics(!value)} + activeLabel="" + inactiveLabel="" + /> + </div> + </div> + </div> + ) + } + + renderContent () { + const { warning } = this.props + + return ( + <div className="settings-page__body"> + { warning && <div className="settings-tab__error">{ warning }</div> } + { this.renderPrivacyOptIn() } + { this.renderClearApproval() } + { this.renderSeedWords() } + { this.renderMetaMetricsOptIn() } + </div> + ) + } + + 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/index.js b/ui/app/pages/settings/settings-tab/index.js new file mode 100644 index 000000000..9fdaafd3f --- /dev/null +++ b/ui/app/pages/settings/settings-tab/index.js @@ -0,0 +1 @@ +export { default } from './settings-tab.container' diff --git a/ui/app/pages/settings/settings-tab/index.scss b/ui/app/pages/settings/settings-tab/index.scss new file mode 100644 index 000000000..ef32b0e4c --- /dev/null +++ b/ui/app/pages/settings/settings-tab/index.scss @@ -0,0 +1,69 @@ +.settings-tab { + &__error { + padding-bottom: 20px; + text-align: center; + color: $crimson; + } + + &__advanced-link { + color: $curious-blue; + padding-left: 5px; + } + + &__rpc-save-button { + align-self: flex-end; + padding: 5px; + text-transform: uppercase; + color: $dusty-gray; + cursor: pointer; + width: 25%; + min-width: 80px; + height: 33px; + } + + &__button--red { + border-color: lighten($monzo, 10%); + color: $monzo; + + &:active { + background: lighten($monzo, 55%); + border-color: $monzo; + } + + &:hover { + border-color: $monzo; + } + } + + &__button--orange { + border-color: lighten($ecstasy, 20%); + color: $ecstasy; + + &:active { + background: lighten($ecstasy, 40%); + border-color: $ecstasy; + } + + &:hover { + border-color: $ecstasy; + } + } + + &__radio-buttons { + display: flex; + align-items: center; + } + + &__radio-button { + display: flex; + align-items: center; + + &:not(:last-child) { + margin-right: 16px; + } + } + + &__radio-label { + padding-left: 4px; + } +} diff --git a/ui/app/pages/settings/settings-tab/settings-tab.component.js b/ui/app/pages/settings/settings-tab/settings-tab.component.js new file mode 100644 index 000000000..57e80be0d --- /dev/null +++ b/ui/app/pages/settings/settings-tab/settings-tab.component.js @@ -0,0 +1,200 @@ +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 locales from '../../../../../app/_locales/index.json' + +const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { + return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase()) +}) + +const infuraCurrencyOptions = sortedCurrencies.map(({ quote: { code, name } }) => { + return { + displayValue: `${code.toUpperCase()} - ${name}`, + key: code, + value: code, + } +}) + +const localeOptions = locales.map(locale => { + return { + displayValue: `${locale.name}`, + key: locale.code, + value: locale.code, + } +}) + +export default class SettingsTab extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static propTypes = { + setUseBlockie: PropTypes.func, + setCurrentCurrency: PropTypes.func, + displayWarning: PropTypes.func, + warning: PropTypes.string, + history: PropTypes.object, + updateCurrentLocale: PropTypes.func, + currentLocale: PropTypes.string, + useBlockie: PropTypes.bool, + currentCurrency: PropTypes.string, + conversionDate: PropTypes.number, + nativeCurrency: PropTypes.string, + useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, + setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, + } + + renderCurrentConversion () { + const { t } = this.context + const { currentCurrency, conversionDate, setCurrentCurrency } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('currencyConversion') }</span> + <span className="settings-page__content-description"> + { t('updatedWithDate', [Date(conversionDate)]) } + </span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <SimpleDropdown + placeholder={t('selectCurrency')} + options={infuraCurrencyOptions} + selectedOption={currentCurrency} + onSelect={newCurrency => setCurrentCurrency(newCurrency)} + /> + </div> + </div> + </div> + ) + } + + renderCurrentLocale () { + const { t } = this.context + const { updateCurrentLocale, currentLocale } = this.props + const currentLocaleMeta = locales.find(locale => locale.code === currentLocale) + const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : '' + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span className="settings-page__content-label"> + { t('currentLanguage') } + </span> + <span className="settings-page__content-description"> + { currentLocaleName } + </span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <SimpleDropdown + placeholder={t('selectLocale')} + options={localeOptions} + selectedOption={currentLocale} + onSelect={async newLocale => updateCurrentLocale(newLocale)} + /> + </div> + </div> + </div> + ) + } + + + renderBlockieOptIn () { + const { useBlockie, setUseBlockie } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ this.context.t('blockiesIdenticon') }</span> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <ToggleButton + value={useBlockie} + onToggle={value => setUseBlockie(!value)} + activeLabel="" + inactiveLabel="" + /> + </div> + </div> + </div> + ) + } + + renderUsePrimaryCurrencyOptions () { + const { t } = this.context + const { + nativeCurrency, + setUseNativeCurrencyAsPrimaryCurrencyPreference, + useNativeCurrencyAsPrimaryCurrency, + } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('primaryCurrencySetting') }</span> + <div className="settings-page__content-description"> + { t('primaryCurrencySettingDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <div className="settings-tab__radio-buttons"> + <div className="settings-tab__radio-button"> + <input + type="radio" + id="native-primary-currency" + onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(true)} + checked={Boolean(useNativeCurrencyAsPrimaryCurrency)} + /> + <label + htmlFor="native-primary-currency" + className="settings-tab__radio-label" + > + { nativeCurrency } + </label> + </div> + <div className="settings-tab__radio-button"> + <input + type="radio" + id="fiat-primary-currency" + onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(false)} + checked={!useNativeCurrencyAsPrimaryCurrency} + /> + <label + htmlFor="fiat-primary-currency" + className="settings-tab__radio-label" + > + { t('fiat') } + </label> + </div> + </div> + </div> + </div> + </div> + ) + } + + renderContent () { + const { warning } = this.props + + return ( + <div className="settings-page__body"> + { warning && <div className="settings-tab__error">{ warning }</div> } + { this.renderCurrentConversion() } + { this.renderUsePrimaryCurrencyOptions() } + { this.renderCurrentLocale() } + { this.renderBlockieOptIn() } + </div> + ) + } + + 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 new file mode 100644 index 000000000..d3d8457f0 --- /dev/null +++ b/ui/app/pages/settings/settings-tab/settings-tab.container.js @@ -0,0 +1,53 @@ +import SettingsTab from './settings-tab.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { + setCurrentCurrency, + displayWarning, + setUseBlockie, + updateCurrentLocale, + setUseNativeCurrencyAsPrimaryCurrencyPreference, + setParticipateInMetaMetrics, +} from '../../../store/actions' +import { preferencesSelector } from '../../../selectors/selectors' + +const mapStateToProps = state => { + const { appState: { warning }, metamask } = state + const { + currentCurrency, + conversionDate, + nativeCurrency, + useBlockie, + currentLocale, + } = metamask + const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) + + return { + warning, + currentLocale, + currentCurrency, + conversionDate, + nativeCurrency, + useBlockie, + useNativeCurrencyAsPrimaryCurrency, + } +} + +const mapDispatchToProps = dispatch => { + return { + setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)), + displayWarning: warning => dispatch(displayWarning(warning)), + setUseBlockie: value => dispatch(setUseBlockie(value)), + updateCurrentLocale: key => dispatch(updateCurrentLocale(key)), + setUseNativeCurrencyAsPrimaryCurrencyPreference: value => { + return dispatch(setUseNativeCurrencyAsPrimaryCurrencyPreference(value)) + }, + setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(SettingsTab) diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js new file mode 100644 index 000000000..3d415c6b8 --- /dev/null +++ b/ui/app/pages/settings/settings.component.js @@ -0,0 +1,137 @@ +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 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 = { + location: PropTypes.object, + history: PropTypes.object, + t: PropTypes.func, + } + + static contextTypes = { + 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 ( + <div + className={c('main-container settings-page', { + 'settings-page--selected': !this.isCurrentPath(SETTINGS_ROUTE), + })} + > + <div className="settings-page__header"> + { + !this.isCurrentPath(SETTINGS_ROUTE) && ( + <div + className="settings-page__back-button" + onClick={() => history.push(SETTINGS_ROUTE)} + /> + ) + } + <div className="settings-page__header__title"> + {t(pathnameI18nKey && isPopupView ? pathnameI18nKey : 'settings')} + </div> + <div + className="settings-page__close-button" + onClick={() => history.push(DEFAULT_ROUTE)} + /> + </div> + <div className="settings-page__content"> + <div className="settings-page__content__tabs"> + { this.renderTabs() } + </div> + <div className="settings-page__content__modules"> + { this.renderContent() } + </div> + </div> + </div> + ) + } + + renderTabs () { + const { history, location } = this.props + const { t } = this.context + + return ( + <TabBar + tabs={[ + { content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE }, + { content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE }, + { content: t('securityAndPrivacy'), description: t('securitySettingsDescription'), key: SECURITY_ROUTE }, + { content: t('aboutUs'), key: ABOUT_US_ROUTE }, + ]} + isActive={key => { + 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 ( + <Switch> + <Route + exact + path={GENERAL_ROUTE} + component={SettingsTab} + /> + <Route + exact + path={ABOUT_US_ROUTE} + component={InfoTab} + /> + <Route + exact + path={ADVANCED_ROUTE} + component={AdvancedTab} + /> + <Route + exact + path={SECURITY_ROUTE} + component={SecurityTab} + /> + <Route + component={SettingsTab} + /> + </Switch> + ) + } +} |