aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/pages
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/pages')
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js11
-rw-r--r--ui/app/pages/create-account/connect-hardware/account-list.js4
-rw-r--r--ui/app/pages/create-account/connect-hardware/connect-screen.js4
-rw-r--r--ui/app/pages/create-account/connect-hardware/index.js4
-rw-r--r--ui/app/pages/create-account/import-account/seed.js2
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js2
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js191
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js41
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js126
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss93
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js76
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js169
-rw-r--r--ui/app/pages/home/home.component.js19
-rw-r--r--ui/app/pages/provider-approval/provider-approval.component.js10
-rw-r--r--ui/app/pages/provider-approval/provider-approval.container.js6
-rw-r--r--ui/app/pages/routes/index.js24
-rw-r--r--ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js2
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js13
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js7
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js2
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js2
-rw-r--r--ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js5
-rw-r--r--ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js10
-rw-r--r--ui/app/pages/send/tests/send-container.test.js2
-rw-r--r--ui/app/pages/send/tests/send-utils.test.js4
-rw-r--r--ui/app/pages/send/to-autocomplete/to-autocomplete.js2
-rw-r--r--ui/app/pages/settings/advanced-tab/advanced-tab.component.js47
-rw-r--r--ui/app/pages/settings/advanced-tab/advanced-tab.container.js11
-rw-r--r--ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js44
-rw-r--r--ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js46
-rw-r--r--ui/app/pages/settings/index.scss60
-rw-r--r--ui/app/pages/settings/networks-tab/index.js1
-rw-r--r--ui/app/pages/settings/networks-tab/index.scss200
-rw-r--r--ui/app/pages/settings/networks-tab/network-form/index.js1
-rw-r--r--ui/app/pages/settings/networks-tab/network-form/network-form.component.js225
-rw-r--r--ui/app/pages/settings/networks-tab/networks-tab.component.js214
-rw-r--r--ui/app/pages/settings/networks-tab/networks-tab.constants.js50
-rw-r--r--ui/app/pages/settings/networks-tab/networks-tab.container.js77
-rw-r--r--ui/app/pages/settings/settings.component.js28
39 files changed, 1644 insertions, 191 deletions
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
index 1cbe5951d..3c4e6dcac 100644
--- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -18,6 +18,7 @@ import AdvancedGasInputs from '../../components/app/gas-customization/advanced-g
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
t: PropTypes.func,
+ tOrKey: PropTypes.func.isRequired,
metricsEvent: PropTypes.func,
}
@@ -99,15 +100,18 @@ export default class ConfirmTransactionBase extends Component {
submitError: null,
}
- componentDidUpdate () {
+ componentDidUpdate (prevProps) {
const {
transactionStatus,
showTransactionConfirmedModal,
history,
clearConfirmTransaction,
} = this.props
+ const { transactionStatus: prevTxStatus } = prevProps
+ const statusUpdated = transactionStatus !== prevTxStatus
+ const txDroppedOrConfirmed = transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS
- if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) {
+ if (statusUpdated && txDroppedOrConfirmed) {
showTransactionConfirmedModal({
onSubmit: () => {
clearConfirmTransaction()
@@ -543,7 +547,8 @@ export default class ConfirmTransactionBase extends Component {
toName={toName}
toAddress={toAddress}
showEdit={onEdit && !isTxReprice}
- action={this.context.t(actionKey) || getMethodName(name) || this.context.t('contractInteraction')}
+ // In the event that the key is falsy (and inherently invalid), use a fallback string
+ action={this.context.tOrKey(actionKey) || getMethodName(name) || this.context.t('contractInteraction')}
title={title}
titleComponent={this.renderTitleComponent()}
subtitle={subtitle}
diff --git a/ui/app/pages/create-account/connect-hardware/account-list.js b/ui/app/pages/create-account/connect-hardware/account-list.js
index a521c7eaf..247c27a5d 100644
--- a/ui/app/pages/create-account/connect-hardware/account-list.js
+++ b/ui/app/pages/create-account/connect-hardware/account-list.js
@@ -6,10 +6,6 @@ const Select = require('react-select').default
import Button from '../../../components/ui/button'
class AccountList extends Component {
- constructor (props, context) {
- super(props)
- }
-
getHdPaths () {
return [
{
diff --git a/ui/app/pages/create-account/connect-hardware/connect-screen.js b/ui/app/pages/create-account/connect-hardware/connect-screen.js
index f5a83e6cf..a3b8ad246 100644
--- a/ui/app/pages/create-account/connect-hardware/connect-screen.js
+++ b/ui/app/pages/create-account/connect-hardware/connect-screen.js
@@ -4,7 +4,7 @@ const h = require('react-hyperscript')
import Button from '../../../components/ui/button'
class ConnectScreen extends Component {
- constructor (props, context) {
+ constructor (props) {
super(props)
this.state = {
selectedDevice: null,
@@ -103,7 +103,7 @@ class ConnectScreen extends Component {
}
- scrollToTutorial = (e) => {
+ scrollToTutorial = () => {
if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
}
diff --git a/ui/app/pages/create-account/connect-hardware/index.js b/ui/app/pages/create-account/connect-hardware/index.js
index 1398fa680..5a91a2725 100644
--- a/ui/app/pages/create-account/connect-hardware/index.js
+++ b/ui/app/pages/create-account/connect-hardware/index.js
@@ -12,7 +12,7 @@ const { getPlatform } = require('../../../../../app/scripts/lib/util')
const { PLATFORM_FIREFOX } = require('../../../../../app/scripts/lib/enums')
class ConnectHardwareForm extends Component {
- constructor (props, context) {
+ constructor (props) {
super(props)
this.state = {
error: null,
@@ -101,7 +101,7 @@ class ConnectHardwareForm extends Component {
const newState = { unlocked: true, device, error: null }
// Default to the first account
if (this.state.selectedAccount === null) {
- accounts.forEach((a, i) => {
+ accounts.forEach((a) => {
if (a.address.toLowerCase() === this.props.address) {
newState.selectedAccount = a.index.toString()
}
diff --git a/ui/app/pages/create-account/import-account/seed.js b/ui/app/pages/create-account/import-account/seed.js
index d98909baa..73332f926 100644
--- a/ui/app/pages/create-account/import-account/seed.js
+++ b/ui/app/pages/create-account/import-account/seed.js
@@ -11,7 +11,7 @@ SeedImportSubview.contextTypes = {
module.exports = connect(mapStateToProps)(SeedImportSubview)
-function mapStateToProps (state) {
+function mapStateToProps () {
return {}
}
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
index ffaff9acf..6b9d06cf9 100644
--- a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
@@ -119,7 +119,7 @@ export default class MetaMetricsOptIn extends Component {
hideCancel={false}
onSubmit={() => {
setParticipateInMetaMetrics(true)
- .then(([participateStatus, metaMetricsId]) => {
+ .then(([_, metaMetricsId]) => {
const promise = participateInMetaMetrics !== true
? metricsEvent({
eventOpts: {
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
index f3bfc3171..4cfc38fdf 100644
--- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
@@ -8,7 +8,9 @@ import {
INITIALIZE_SEED_PHRASE_ROUTE,
} from '../../../../helpers/constants/routes'
import { exportAsFile } from '../../../../helpers/utils/util'
-import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
+import DraggableSeed from './draggable-seed.component'
+
+const EMPTY_SEEDS = Array(12).fill(null)
export default class ConfirmSeedPhrase extends PureComponent {
static contextTypes = {
@@ -27,10 +29,32 @@ export default class ConfirmSeedPhrase extends PureComponent {
}
state = {
- selectedSeedWords: [],
+ selectedSeedIndices: [],
shuffledSeedWords: [],
- // Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number}
- selectedSeedWordsHash: {},
+ pendingSeedIndices: [],
+ draggingSeedIndex: -1,
+ hoveringIndex: -1,
+ isDragging: false,
+ }
+
+ shouldComponentUpdate (nextProps, nextState) {
+ const { seedPhrase } = this.props
+ const {
+ selectedSeedIndices,
+ shuffledSeedWords,
+ pendingSeedIndices,
+ draggingSeedIndex,
+ hoveringIndex,
+ isDragging,
+ } = this.state
+
+ return seedPhrase !== nextProps.seedPhrase ||
+ draggingSeedIndex !== nextState.draggingSeedIndex ||
+ isDragging !== nextState.isDragging ||
+ hoveringIndex !== nextState.hoveringIndex ||
+ selectedSeedIndices.join(' ') !== nextState.selectedSeedIndices.join(' ') ||
+ shuffledSeedWords.join(' ') !== nextState.shuffledSeedWords.join(' ') ||
+ pendingSeedIndices.join(' ') !== nextState.pendingSeedIndices.join(' ')
}
componentDidMount () {
@@ -39,6 +63,26 @@ export default class ConfirmSeedPhrase extends PureComponent {
this.setState({ shuffledSeedWords })
}
+ setDraggingSeedIndex = draggingSeedIndex => this.setState({ draggingSeedIndex })
+
+ setHoveringIndex = hoveringIndex => this.setState({ hoveringIndex })
+
+ onDrop = targetIndex => {
+ const {
+ selectedSeedIndices,
+ draggingSeedIndex,
+ } = this.state
+
+ const indices = insert(selectedSeedIndices, draggingSeedIndex, targetIndex, true)
+
+ this.setState({
+ selectedSeedIndices: indices,
+ pendingSeedIndices: indices,
+ draggingSeedIndex: -1,
+ hoveringIndex: -1,
+ })
+ }
+
handleExport = () => {
exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
}
@@ -64,24 +108,35 @@ export default class ConfirmSeedPhrase extends PureComponent {
}
}
- handleSelectSeedWord = (word, shuffledIndex) => {
- this.setState(selectSeedWord(word, shuffledIndex))
+ handleSelectSeedWord = (shuffledIndex) => {
+ this.setState({
+ selectedSeedIndices: [...this.state.selectedSeedIndices, shuffledIndex],
+ pendingSeedIndices: [...this.state.pendingSeedIndices, shuffledIndex],
+ })
}
handleDeselectSeedWord = shuffledIndex => {
- this.setState(deselectSeedWord(shuffledIndex))
+ this.setState({
+ selectedSeedIndices: this.state.selectedSeedIndices.filter(i => shuffledIndex !== i),
+ pendingSeedIndices: this.state.pendingSeedIndices.filter(i => shuffledIndex !== i),
+ })
}
isValid () {
const { seedPhrase } = this.props
- const { selectedSeedWords } = this.state
+ const { selectedSeedIndices, shuffledSeedWords } = this.state
+ const selectedSeedWords = selectedSeedIndices.map(i => shuffledSeedWords[i])
return seedPhrase === selectedSeedWords.join(' ')
}
render () {
const { t } = this.context
const { history } = this.props
- const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state
+ const {
+ selectedSeedIndices,
+ shuffledSeedWords,
+ draggingSeedIndex,
+ } = this.state
return (
<div className="confirm-seed-phrase">
@@ -102,41 +157,39 @@ export default class ConfirmSeedPhrase extends PureComponent {
<div className="first-time-flow__text-block">
{ t('selectEachPhrase') }
</div>
- <div className="confirm-seed-phrase__selected-seed-words">
- {
- selectedSeedWords.map((word, index) => (
- <div
- key={index}
- className="confirm-seed-phrase__seed-word"
- >
- { word }
- </div>
- ))
- }
+ <div
+ className={classnames('confirm-seed-phrase__selected-seed-words', {
+ 'confirm-seed-phrase__selected-seed-words--dragging': draggingSeedIndex > -1,
+ })}
+ >
+ { this.renderPendingSeeds() }
+ { this.renderSelectedSeeds() }
</div>
<div className="confirm-seed-phrase__shuffled-seed-words">
{
shuffledSeedWords.map((word, index) => {
- const isSelected = index in selectedSeedWordsHash
+ const isSelected = selectedSeedIndices.includes(index)
return (
- <div
+ <DraggableSeed
key={index}
- className={classnames(
- 'confirm-seed-phrase__seed-word',
- 'confirm-seed-phrase__seed-word--shuffled',
- { 'confirm-seed-phrase__seed-word--selected': isSelected }
- )}
+ seedIndex={index}
+ index={index}
+ draggingSeedIndex={this.state.draggingSeedIndex}
+ setDraggingSeedIndex={this.setDraggingSeedIndex}
+ setHoveringIndex={this.setHoveringIndex}
+ onDrop={this.onDrop}
+ className="confirm-seed-phrase__seed-word--shuffled"
+ selected={isSelected}
onClick={() => {
if (!isSelected) {
- this.handleSelectSeedWord(word, index)
+ this.handleSelectSeedWord(index)
} else {
this.handleDeselectSeedWord(index)
}
}}
- >
- { word }
- </div>
+ word={word}
+ />
)
})
}
@@ -152,4 +205,80 @@ export default class ConfirmSeedPhrase extends PureComponent {
</div>
)
}
+
+ renderSelectedSeeds () {
+ const { shuffledSeedWords, selectedSeedIndices, draggingSeedIndex } = this.state
+ return EMPTY_SEEDS.map((_, index) => {
+ const seedIndex = selectedSeedIndices[index]
+ const word = shuffledSeedWords[seedIndex]
+
+ return (
+ <DraggableSeed
+ key={`selected-${seedIndex}-${index}`}
+ className="confirm-seed-phrase__selected-seed-words__selected-seed"
+ index={index}
+ seedIndex={seedIndex}
+ word={word}
+ draggingSeedIndex={draggingSeedIndex}
+ setDraggingSeedIndex={this.setDraggingSeedIndex}
+ setHoveringIndex={this.setHoveringIndex}
+ onDrop={this.onDrop}
+ draggable
+ />
+ )
+ })
+ }
+
+ renderPendingSeeds () {
+ const {
+ pendingSeedIndices,
+ shuffledSeedWords,
+ draggingSeedIndex,
+ hoveringIndex,
+ } = this.state
+
+ const indices = insert(pendingSeedIndices, draggingSeedIndex, hoveringIndex)
+
+ return EMPTY_SEEDS.map((_, index) => {
+ const seedIndex = indices[index]
+ const word = shuffledSeedWords[seedIndex]
+
+ return (
+ <DraggableSeed
+ key={`pending-${seedIndex}-${index}`}
+ index={index}
+ className={classnames('confirm-seed-phrase__selected-seed-words__pending-seed', {
+ 'confirm-seed-phrase__seed-word--hidden': draggingSeedIndex === seedIndex && index !== hoveringIndex,
+ })}
+ seedIndex={seedIndex}
+ word={word}
+ draggingSeedIndex={draggingSeedIndex}
+ setDraggingSeedIndex={this.setDraggingSeedIndex}
+ setHoveringIndex={this.setHoveringIndex}
+ onDrop={this.onDrop}
+ droppable={!!word}
+ />
+ )
+ })
+ }
+}
+
+function insert (list, value, target, removeOld) {
+ let nextList = [...list]
+
+ if (typeof list[target] === 'number') {
+ nextList = [...list.slice(0, target), value, ...list.slice(target)]
+ }
+
+ if (removeOld) {
+ nextList = nextList.filter((seed, i) => {
+ return seed !== value || i === target
+ })
+ }
+
+ if (nextList.length > 12) {
+ nextList.pop()
+ }
+
+ return nextList
}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
deleted file mode 100644
index f2476fc5c..000000000
--- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
+++ /dev/null
@@ -1,41 +0,0 @@
-export function selectSeedWord (word, shuffledIndex) {
- return function update (state) {
- const { selectedSeedWords, selectedSeedWordsHash } = state
- const nextSelectedIndex = selectedSeedWords.length
-
- return {
- selectedSeedWords: [ ...selectedSeedWords, word ],
- selectedSeedWordsHash: { ...selectedSeedWordsHash, [shuffledIndex]: nextSelectedIndex },
- }
- }
-}
-
-export function deselectSeedWord (shuffledIndex) {
- return function update (state) {
- const {
- selectedSeedWords: prevSelectedSeedWords,
- selectedSeedWordsHash: prevSelectedSeedWordsHash,
- } = state
-
- const selectedSeedWords = [...prevSelectedSeedWords]
- const indexToRemove = prevSelectedSeedWordsHash[shuffledIndex]
- selectedSeedWords.splice(indexToRemove, 1)
- const selectedSeedWordsHash = Object.keys(prevSelectedSeedWordsHash).reduce((acc, index) => {
- const output = { ...acc }
- const selectedSeedWordIndex = prevSelectedSeedWordsHash[index]
-
- if (selectedSeedWordIndex < indexToRemove) {
- output[index] = selectedSeedWordIndex
- } else if (selectedSeedWordIndex > indexToRemove) {
- output[index] = selectedSeedWordIndex - 1
- }
-
- return output
- }, {})
-
- return {
- selectedSeedWords,
- selectedSeedWordsHash,
- }
- }
-}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js
new file mode 100644
index 000000000..cdb881921
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js
@@ -0,0 +1,126 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { DragSource, DropTarget } from 'react-dnd'
+
+class DraggableSeed extends Component {
+
+ static propTypes = {
+ // React DnD Props
+ connectDragSource: PropTypes.func.isRequired,
+ connectDropTarget: PropTypes.func.isRequired,
+ isDragging: PropTypes.bool,
+ isOver: PropTypes.bool,
+ canDrop: PropTypes.bool,
+ // Own Props
+ onClick: PropTypes.func.isRequired,
+ setHoveringIndex: PropTypes.func.isRequired,
+ index: PropTypes.number,
+ draggingSeedIndex: PropTypes.number,
+ word: PropTypes.string,
+ className: PropTypes.string,
+ selected: PropTypes.bool,
+ droppable: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ className: '',
+ onClick () {},
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { isOver, setHoveringIndex } = this.props
+ if (isOver && !nextProps.isOver) {
+ setHoveringIndex(-1)
+ }
+ }
+
+ render () {
+ const {
+ connectDragSource,
+ connectDropTarget,
+ isDragging,
+ index,
+ word,
+ selected,
+ className,
+ onClick,
+ isOver,
+ canDrop,
+ } = this.props
+
+ return connectDropTarget(connectDragSource(
+ <div
+ key={index}
+ className={classnames('btn-secondary confirm-seed-phrase__seed-word', className, {
+ 'confirm-seed-phrase__seed-word--selected btn-primary': selected,
+ 'confirm-seed-phrase__seed-word--dragging': isDragging,
+ 'confirm-seed-phrase__seed-word--empty': !word,
+ 'confirm-seed-phrase__seed-word--active-drop': !isOver && canDrop,
+ 'confirm-seed-phrase__seed-word--drop-hover': isOver && canDrop,
+ })}
+ onClick={onClick}
+ >
+ { word }
+ </div>
+ ))
+ }
+}
+
+const SEEDWORD = 'SEEDWORD'
+
+const seedSource = {
+ beginDrag (props) {
+ setTimeout(() => props.setDraggingSeedIndex(props.seedIndex), 0)
+ return {
+ seedIndex: props.seedIndex,
+ word: props.word,
+ }
+ },
+ canDrag (props) {
+ return props.draggable
+ },
+ endDrag (props, monitor) {
+ const dropTarget = monitor.getDropResult()
+
+ if (!dropTarget) {
+ setTimeout(() => props.setDraggingSeedIndex(-1), 0)
+ return
+ }
+
+ props.onDrop(dropTarget.targetIndex)
+ },
+}
+
+const seedTarget = {
+ drop (props) {
+ return {
+ targetIndex: props.index,
+ }
+ },
+ canDrop (props) {
+ return props.droppable
+ },
+ hover (props) {
+ props.setHoveringIndex(props.index)
+ },
+}
+
+const collectDrag = (connect, monitor) => {
+ return {
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging(),
+ }
+}
+
+const collectDrop = (connect, monitor) => {
+ return {
+ connectDropTarget: connect.dropTarget(),
+ isOver: monitor.isOver(),
+ canDrop: monitor.canDrop(),
+ }
+}
+
+export default DropTarget(SEEDWORD, seedTarget, collectDrop)(DragSource(SEEDWORD, seedSource, collectDrag)(DraggableSeed))
+
+
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
index 93137618c..f025a503f 100644
--- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
@@ -3,37 +3,58 @@
margin-bottom: 12px;
}
- &__selected-seed-words {
- min-height: 190px;
- max-width: 496px;
- border: 1px solid #CDCDCD;
- border-radius: 6px;
- background-color: $white;
- margin: 24px 0 36px;
- padding: 12px;
- }
-
&__shuffled-seed-words {
- max-width: 496px;
+ max-width: 575px;
}
&__seed-word {
- display: inline-block;
- color: #5B5D67;
- background-color: #E7E7E7;
+ display: inline-flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ justify-content: center;
padding: 8px 18px;
- min-width: 64px;
+ width: 128px;
+ height: 41px;
margin: 4px;
text-align: center;
+ border-radius: 4px;
+ cursor: move;
+
+ &--shuffled {
+ cursor: pointer;
+ margin: 6px;
+ }
&--selected {
- background-color: #85D1CC;
color: $white;
}
- &--shuffled {
- cursor: pointer;
- margin: 6px;
+ &--dragging {
+ margin: 0;
+ }
+
+ &--empty {
+ background-color: transparent;
+ border-color: transparent;
+ cursor: default;
+
+ &:hover,
+ &:active {
+ background-color: transparent;
+ border-color: transparent;
+ cursor: default;
+ box-shadow: none !important;
+ }
+ }
+
+ &--hidden {
+ display: none !important;
+ }
+
+ &--drop-hover {
+ background-color: transparent;
+ border-color: transparent;
+ color: transparent;
}
@media screen and (max-width: 575px) {
@@ -42,7 +63,37 @@
}
}
- button {
- margin-top: 0xp;
+ &__selected-seed-words {
+ display: flex;
+ flex-flow: row wrap;
+ min-height: 161px;
+ max-width: 575px;
+ border: 1px solid #CDCDCD;
+ border-radius: 6px;
+ background-color: $white;
+ margin: 24px 0 36px;
+ padding: 12px;
+
+ &__pending-seed {
+ display: none;
+ }
+
+ &__selected-seed {
+ display: inline-flex;
+
+ &:hover {
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
+ }
+ }
+
+ &--dragging {
+ .confirm-seed-phrase__selected-seed-words__pending-seed {
+ display: inline-flex;
+ }
+
+ .confirm-seed-phrase__selected-seed-words__selected-seed {
+ display: none;
+ }
+ }
}
}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
index 9a9f84049..0b19af18c 100644
--- a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
+++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
@@ -8,6 +8,8 @@ import {
INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE,
DEFAULT_ROUTE,
} from '../../../helpers/constants/routes'
+import HTML5Backend from 'react-dnd-html5-backend'
+import {DragDropContextProvider} from 'react-dnd'
export default class SeedPhrase extends PureComponent {
static propTypes = {
@@ -28,43 +30,45 @@ export default class SeedPhrase extends PureComponent {
const { seedPhrase } = this.props
return (
- <div className="first-time-flow__wrapper">
- <div className="app-header__logo-container">
- <img
- className="app-header__metafox-logo app-header__metafox-logo--horizontal"
- src="/images/logo/metamask-logo-horizontal.svg"
- height={30}
- />
- <img
- className="app-header__metafox-logo app-header__metafox-logo--icon"
- src="/images/logo/metamask-fox.svg"
- height={42}
- width={42}
- />
+ <DragDropContextProvider backend={HTML5Backend}>
+ <div className="first-time-flow__wrapper">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <Switch>
+ <Route
+ exact
+ path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE}
+ render={props => (
+ <ConfirmSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_SEED_PHRASE_ROUTE}
+ render={props => (
+ <RevealSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ </Switch>
</div>
- <Switch>
- <Route
- exact
- path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE}
- render={props => (
- <ConfirmSeedPhrase
- { ...props }
- seedPhrase={seedPhrase}
- />
- )}
- />
- <Route
- exact
- path={INITIALIZE_SEED_PHRASE_ROUTE}
- render={props => (
- <RevealSeedPhrase
- { ...props }
- seedPhrase={seedPhrase}
- />
- )}
- />
- </Switch>
- </div>
+ </DragDropContextProvider>
)
}
}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js b/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js
new file mode 100644
index 000000000..8339a6f6f
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js
@@ -0,0 +1,169 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ConfirmSeedPhrase from '../confirm-seed-phrase/confirm-seed-phrase.component'
+
+function shallowRender (props = {}, context = {}) {
+ return shallow(
+ <ConfirmSeedPhrase {...props} />,
+ {
+ context: {
+ t: str => str + '_t',
+ ...context,
+ },
+ }
+ )
+}
+
+describe('ConfirmSeedPhrase Component', () => {
+ it('should render correctly', () => {
+ const root = shallowRender({
+ seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬',
+ })
+
+ assert.equal(
+ root.find('.confirm-seed-phrase__seed-word--shuffled').length,
+ 12,
+ 'should render 12 seed phrases'
+ )
+ })
+
+ it('should add/remove selected on click', () => {
+ const metricsEventSpy = sinon.spy()
+ const pushSpy = sinon.spy()
+ const root = shallowRender(
+ {
+ seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬',
+ history: { push: pushSpy },
+ },
+ {
+ metricsEvent: metricsEventSpy,
+ }
+ )
+
+ const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled')
+
+ // Click on 3 seeds to add to selected
+ seeds.at(0).simulate('click')
+ seeds.at(1).simulate('click')
+ seeds.at(2).simulate('click')
+
+ assert.deepEqual(
+ root.state().selectedSeedIndices,
+ [0, 1, 2],
+ 'should add seed phrase to selected on click',
+ )
+
+ // Click on a selected seed to remove
+ root.state()
+ root.update()
+ root.state()
+ root.find('.confirm-seed-phrase__seed-word--shuffled').at(1).simulate('click')
+ assert.deepEqual(
+ root.state().selectedSeedIndices,
+ [0, 2],
+ 'should remove seed phrase from selected when click again',
+ )
+ })
+
+ it('should render correctly on hover', () => {
+ const metricsEventSpy = sinon.spy()
+ const pushSpy = sinon.spy()
+ const root = shallowRender(
+ {
+ seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬',
+ history: { push: pushSpy },
+ },
+ {
+ metricsEvent: metricsEventSpy,
+ }
+ )
+
+ const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled')
+
+ // Click on 3 seeds to add to selected
+ seeds.at(0).simulate('click')
+ seeds.at(1).simulate('click')
+ seeds.at(2).simulate('click')
+
+ // Dragging Seed # 2 to 0 placeth
+ root.instance().setDraggingSeedIndex(2)
+ root.instance().setHoveringIndex(0)
+
+ root.update()
+
+ const pendingSeeds = root.find('.confirm-seed-phrase__selected-seed-words__pending-seed')
+
+ assert.equal(pendingSeeds.at(0).props().seedIndex, 2)
+ assert.equal(pendingSeeds.at(1).props().seedIndex, 0)
+ assert.equal(pendingSeeds.at(2).props().seedIndex, 1)
+ })
+
+ it('should insert seed in place on drop', () => {
+ const metricsEventSpy = sinon.spy()
+ const pushSpy = sinon.spy()
+ const root = shallowRender(
+ {
+ seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬',
+ history: { push: pushSpy },
+ },
+ {
+ metricsEvent: metricsEventSpy,
+ }
+ )
+
+ const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled')
+
+ // Click on 3 seeds to add to selected
+ seeds.at(0).simulate('click')
+ seeds.at(1).simulate('click')
+ seeds.at(2).simulate('click')
+
+ // Drop Seed # 2 to 0 placeth
+ root.instance().setDraggingSeedIndex(2)
+ root.instance().setHoveringIndex(0)
+ root.instance().onDrop(0)
+
+ root.update()
+
+ assert.deepEqual(root.state().selectedSeedIndices, [2, 0, 1])
+ assert.deepEqual(root.state().pendingSeedIndices, [2, 0, 1])
+ })
+
+ it('should submit correctly', () => {
+ const originalSeed = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬']
+ const metricsEventSpy = sinon.spy()
+ const pushSpy = sinon.spy()
+ const root = shallowRender(
+ {
+ seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬',
+ history: { push: pushSpy },
+ },
+ {
+ metricsEvent: metricsEventSpy,
+ }
+ )
+
+ const shuffled = root.state().shuffledSeedWords
+ const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled')
+
+
+ originalSeed.forEach(seed => {
+ const seedIndex = shuffled.findIndex(s => s === seed)
+ seeds.at(seedIndex).simulate('click')
+ })
+
+ root.update()
+
+ root.find('.first-time-flow__button').simulate('click')
+ assert.deepEqual(metricsEventSpy.args[0][0], {
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Verify Complete',
+ },
+ })
+ assert.equal(pushSpy.args[0][0], '/initialize/end-of-flow')
+ })
+})
diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js
index 29d93a9fa..4d96c3131 100644
--- a/ui/app/pages/home/home.component.js
+++ b/ui/app/pages/home/home.component.js
@@ -23,21 +23,27 @@ export default class Home extends PureComponent {
providerRequests: PropTypes.array,
}
+ componentWillMount () {
+ const {
+ history,
+ unconfirmedTransactionsCount = 0,
+ } = this.props
+
+ if (unconfirmedTransactionsCount > 0) {
+ history.push(CONFIRM_TRANSACTION_ROUTE)
+ }
+ }
+
componentDidMount () {
const {
history,
suggestedTokens = {},
- unconfirmedTransactionsCount = 0,
} = this.props
// suggested new tokens
if (Object.keys(suggestedTokens).length > 0) {
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
}
-
- if (unconfirmedTransactionsCount > 0) {
- history.push(CONFIRM_TRANSACTION_ROUTE)
- }
}
render () {
@@ -45,6 +51,7 @@ export default class Home extends PureComponent {
forgottenPassword,
seedWords,
providerRequests,
+ history,
} = this.props
// seed words
@@ -69,7 +76,7 @@ export default class Home extends PureComponent {
query="(min-width: 576px)"
render={() => <WalletView />}
/>
- <TransactionView />
+ { !history.location.pathname.match(/^\/confirm-transaction/) ? <TransactionView /> : null }
</div>
</div>
)
diff --git a/ui/app/pages/provider-approval/provider-approval.component.js b/ui/app/pages/provider-approval/provider-approval.component.js
index 1f1d68da7..70d3d0007 100644
--- a/ui/app/pages/provider-approval/provider-approval.component.js
+++ b/ui/app/pages/provider-approval/provider-approval.component.js
@@ -4,9 +4,9 @@ import ProviderPageContainer from '../../components/app/provider-page-container'
export default class ProviderApproval extends Component {
static propTypes = {
- approveProviderRequest: PropTypes.func.isRequired,
+ approveProviderRequestByOrigin: PropTypes.func.isRequired,
+ rejectProviderRequestByOrigin: PropTypes.func.isRequired,
providerRequest: PropTypes.object.isRequired,
- rejectProviderRequest: PropTypes.func.isRequired,
};
static contextTypes = {
@@ -14,13 +14,13 @@ export default class ProviderApproval extends Component {
};
render () {
- const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props
+ const { approveProviderRequestByOrigin, providerRequest, rejectProviderRequestByOrigin } = this.props
return (
<ProviderPageContainer
- approveProviderRequest={approveProviderRequest}
+ approveProviderRequestByOrigin={approveProviderRequestByOrigin}
+ rejectProviderRequestByOrigin={rejectProviderRequestByOrigin}
origin={providerRequest.origin}
tabID={providerRequest.tabID}
- rejectProviderRequest={rejectProviderRequest}
siteImage={providerRequest.siteImage}
siteTitle={providerRequest.siteTitle}
/>
diff --git a/ui/app/pages/provider-approval/provider-approval.container.js b/ui/app/pages/provider-approval/provider-approval.container.js
index d53c0ae4d..1e167ddb7 100644
--- a/ui/app/pages/provider-approval/provider-approval.container.js
+++ b/ui/app/pages/provider-approval/provider-approval.container.js
@@ -1,11 +1,11 @@
import { connect } from 'react-redux'
import ProviderApproval from './provider-approval.component'
-import { approveProviderRequest, rejectProviderRequest } from '../../store/actions'
+import { approveProviderRequestByOrigin, rejectProviderRequestByOrigin } from '../../store/actions'
function mapDispatchToProps (dispatch) {
return {
- approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)),
- rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)),
+ approveProviderRequestByOrigin: origin => dispatch(approveProviderRequestByOrigin(origin)),
+ rejectProviderRequestByOrigin: origin => dispatch(rejectProviderRequestByOrigin(origin)),
}
}
diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js
index e38a6d6ce..9eeac2da2 100644
--- a/ui/app/pages/routes/index.js
+++ b/ui/app/pages/routes/index.js
@@ -5,7 +5,8 @@ import { Route, Switch, withRouter, matchPath } from 'react-router-dom'
import { compose } from 'recompose'
import actions from '../../store/actions'
import log from 'loglevel'
-import { getMetaMaskAccounts, getNetworkIdentifier } from '../../selectors/selectors'
+import IdleTimer from 'react-idle-timer'
+import {getMetaMaskAccounts, getNetworkIdentifier, preferencesSelector} from '../../selectors/selectors'
// init
import FirstTimeFlow from '../first-time-flow'
@@ -98,7 +99,9 @@ class Routes extends Component {
}
renderRoutes () {
- return (
+ const { autoLogoutTimeLimit, setLastActiveTime } = this.props
+
+ const routes = (
<Switch>
<Route path={LOCK_ROUTE} component={Lock} exact />
<Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} />
@@ -116,6 +119,16 @@ class Routes extends Component {
<Authenticated path={DEFAULT_ROUTE} component={Home} exact />
</Switch>
)
+
+ if (autoLogoutTimeLimit > 0) {
+ return (
+ <IdleTimer onAction={setLastActiveTime} throttle={1000}>
+ {routes}
+ </IdleTimer>
+ )
+ }
+
+ return routes
}
onInitializationUnlockPage () {
@@ -322,6 +335,7 @@ Routes.propTypes = {
networkDropdownOpen: PropTypes.bool,
showNetworkDropdown: PropTypes.func,
hideNetworkDropdown: PropTypes.func,
+ setLastActiveTime: PropTypes.func,
history: PropTypes.object,
location: PropTypes.object,
dispatch: PropTypes.func,
@@ -344,6 +358,7 @@ Routes.propTypes = {
t: PropTypes.func,
providerId: PropTypes.string,
providerRequests: PropTypes.array,
+ autoLogoutTimeLimit: PropTypes.number,
}
function mapStateToProps (state) {
@@ -358,6 +373,7 @@ function mapStateToProps (state) {
} = appState
const accounts = getMetaMaskAccounts(state)
+ const { autoLogoutTimeLimit = 0 } = preferencesSelector(state)
const {
identities,
@@ -409,6 +425,7 @@ function mapStateToProps (state) {
Qr: state.appState.Qr,
welcomeScreenSeen: state.metamask.welcomeScreenSeen,
providerId: getNetworkIdentifier(state),
+ autoLogoutTimeLimit,
// state needed to get account dropdown temporarily rendering from app bar
identities,
@@ -418,7 +435,7 @@ function mapStateToProps (state) {
}
}
-function mapDispatchToProps (dispatch, ownProps) {
+function mapDispatchToProps (dispatch) {
return {
dispatch,
hideSidebar: () => dispatch(actions.hideSidebar()),
@@ -427,6 +444,7 @@ function mapDispatchToProps (dispatch, ownProps) {
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
+ setLastActiveTime: () => dispatch(actions.setLastActiveTime()),
}
}
diff --git a/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js
index 33f932daf..1580fd497 100644
--- a/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js
+++ b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js
@@ -5,7 +5,7 @@ let mapStateToProps
proxyquire('../account-list-item.container.js', {
'react-redux': {
- connect: (ms, md) => {
+ connect: (ms) => {
mapStateToProps = ms
return () => ({})
},
diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
index f17137c1e..e256d1442 100644
--- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
+++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
@@ -15,6 +15,7 @@ export default class AmountMaxButton extends Component {
static contextTypes = {
t: PropTypes.func,
+ metricsEvent: PropTypes.func,
}
setMaxAmount () {
@@ -35,11 +36,15 @@ export default class AmountMaxButton extends Component {
}
onMaxClick = (event) => {
- const { setMaxModeTo, selectedToken } = this.props
+ const { setMaxModeTo } = this.props
+ const { metricsEvent } = this.context
- fetch('https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&e_c=send&e_a=amountMax&e_n=' + (selectedToken ? 'token' : 'eth'), {
- 'headers': {},
- 'method': 'GET',
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Clicked "Amount Max"',
+ },
})
event.preventDefault()
diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
index b04d3897f..a6cb29d4c 100644
--- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
+++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
@@ -26,7 +26,12 @@ describe('AmountMaxButton Component', function () {
setAmountToMax={propsMethodSpies.setAmountToMax}
setMaxModeTo={propsMethodSpies.setMaxModeTo}
tokenBalance={'mockTokenBalance'}
- />, { context: { t: str => str + '_t' } })
+ />, {
+ context: {
+ t: str => str + '_t',
+ metricsEvent: () => {},
+ },
+ })
instance = wrapper.instance()
})
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
index eecff165d..2013e3200 100644
--- a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
@@ -5,7 +5,7 @@ let mapStateToProps
proxyquire('../send-row-error-message.container.js', {
'react-redux': {
- connect: (ms, md) => {
+ connect: (ms) => {
mapStateToProps = ms
return () => ({})
},
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
index 225bf056c..6c0739f0e 100644
--- a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
@@ -5,7 +5,7 @@ let mapStateToProps
proxyquire('../send-row-warning-message.container.js', {
'react-redux': {
- connect: (ms, md) => {
+ connect: (ms) => {
mapStateToProps = ms
return () => ({})
},
diff --git a/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js
index d0a43f086..b3b0d2da3 100644
--- a/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js
+++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js
@@ -10,16 +10,15 @@ import { checkExistingAddresses } from '../../../add-token/util'
const ethUtil = require('ethereumjs-util')
const contractMap = require('eth-contract-metadata')
-function getToErrorObject (to, toError = null, hasHexData = false, tokens = [], selectedToken = null, network) {
+function getToErrorObject (to, toError = null, hasHexData = false, _, __, network) {
if (!to) {
if (!hasHexData) {
toError = REQUIRED_ERROR
}
} else if (!isValidAddress(to, network) && !toError) {
toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR
- } else if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) {
- toError = KNOWN_RECIPIENT_ADDRESS_ERROR
}
+
return { to: toError }
}
diff --git a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js
index f29f5efec..f8a6dd96f 100644
--- a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js
+++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js
@@ -55,9 +55,9 @@ describe('send-to-row utils', () => {
})
})
- it('should return a known address recipient if to is truthy but part of state tokens', () => {
+ it('should return null if to is truthy but part of state tokens', () => {
assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ to: null,
})
})
@@ -67,14 +67,14 @@ describe('send-to-row utils', () => {
})
})
- it('should return a known address recipient if to is truthy but part of contract metadata', () => {
+ it('should return null if to is truthy but part of contract metadata', () => {
assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ to: null,
})
})
it('should null if to is truthy part of contract metadata but selectedToken falsy', () => {
assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
- to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ to: null,
})
})
})
diff --git a/ui/app/pages/send/tests/send-container.test.js b/ui/app/pages/send/tests/send-container.test.js
index b3e202030..131c42f59 100644
--- a/ui/app/pages/send/tests/send-container.test.js
+++ b/ui/app/pages/send/tests/send-container.test.js
@@ -24,7 +24,7 @@ proxyquire('../send.container.js', {
},
},
'react-router-dom': { withRouter: () => {} },
- 'recompose': { compose: (arg1, arg2) => () => arg2() },
+ 'recompose': { compose: (_, arg2) => () => arg2() },
'./send.selectors': {
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`,
diff --git a/ui/app/pages/send/tests/send-utils.test.js b/ui/app/pages/send/tests/send-utils.test.js
index b19535b9e..bf9cba14a 100644
--- a/ui/app/pages/send/tests/send-utils.test.js
+++ b/ui/app/pages/send/tests/send-utils.test.js
@@ -17,12 +17,12 @@ const {
} = require('../send.constants')
const stubs = {
- addCurrencies: sinon.stub().callsFake((a, b, obj) => {
+ addCurrencies: sinon.stub().callsFake((a, b) => {
if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2))
if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2))
return a + b
}),
- conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
+ conversionUtil: sinon.stub().callsFake((val) => parseInt(val, 16)),
conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value),
multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
diff --git a/ui/app/pages/send/to-autocomplete/to-autocomplete.js b/ui/app/pages/send/to-autocomplete/to-autocomplete.js
index b246413fb..328a5b62b 100644
--- a/ui/app/pages/send/to-autocomplete/to-autocomplete.js
+++ b/ui/app/pages/send/to-autocomplete/to-autocomplete.js
@@ -84,7 +84,7 @@ ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) {
cb && cb(event.target.value)
}
-ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) {
+ToAutoComplete.prototype.componentDidUpdate = function (nextProps) {
if (this.props.to !== nextProps.to) {
this.handleInputEvent()
}
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 14b9daae6..3d27fe349 100644
--- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js
+++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js
@@ -24,6 +24,8 @@ export default class AdvancedTab extends PureComponent {
setAdvancedInlineGasFeatureFlag: PropTypes.func,
advancedInlineGas: PropTypes.bool,
showFiatInTestnets: PropTypes.bool,
+ autoLogoutTimeLimit: PropTypes.number,
+ setAutoLogoutTimeLimit: PropTypes.func.isRequired,
setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
}
@@ -49,7 +51,7 @@ export default class AdvancedTab extends PureComponent {
<TextField
type="text"
id="new-rpc"
- placeholder={t('rpcURL')}
+ placeholder={t('rpcUrl')}
value={newRpc}
onChange={e => this.setState({ newRpc: e.target.value })}
onKeyPress={e => {
@@ -355,6 +357,48 @@ export default class AdvancedTab extends PureComponent {
)
}
+ renderAutoLogoutTimeLimit () {
+ const { t } = this.context
+ const {
+ autoLogoutTimeLimit,
+ setAutoLogoutTimeLimit,
+ } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('autoLogoutTimeLimit') }</span>
+ <div className="settings-page__content-description">
+ { t('autoLogoutTimeLimitDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <TextField
+ type="number"
+ id="autoTimeout"
+ placeholder="5"
+ value={this.state.autoLogoutTimeLimit}
+ defaultValue={autoLogoutTimeLimit}
+ onChange={e => this.setState({ autoLogoutTimeLimit: Math.max(Number(e.target.value), 0) })}
+ fullWidth
+ margin="dense"
+ min={0}
+ />
+ <button
+ className="button btn-primary settings-tab__rpc-save-button"
+ onClick={() => {
+ setAutoLogoutTimeLimit(this.state.autoLogoutTimeLimit)
+ }}
+ >
+ { t('save') }
+ </button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
renderContent () {
const { warning } = this.props
@@ -368,6 +412,7 @@ export default class AdvancedTab extends PureComponent {
{ this.renderAdvancedGasInputInline() }
{ this.renderHexDataOptIn() }
{ this.renderShowConversionInTestnets() }
+ { this.renderAutoLogoutTimeLimit() }
</div>
)
}
diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js
index 69d7e07e6..bcac55f5e 100644
--- a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js
+++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js
@@ -8,10 +8,11 @@ import {
setFeatureFlag,
showModal,
setShowFiatConversionOnTestnetsPreference,
+ setAutoLogoutTimeLimit,
} from '../../../store/actions'
import {preferencesSelector} from '../../../selectors/selectors'
-const mapStateToProps = state => {
+export const mapStateToProps = state => {
const { appState: { warning }, metamask } = state
const {
featureFlags: {
@@ -19,17 +20,18 @@ const mapStateToProps = state => {
advancedInlineGas,
} = {},
} = metamask
- const { showFiatInTestnets } = preferencesSelector(state)
+ const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state)
return {
warning,
sendHexData,
advancedInlineGas,
showFiatInTestnets,
+ autoLogoutTimeLimit,
}
}
-const mapDispatchToProps = dispatch => {
+export const mapDispatchToProps = dispatch => {
return {
setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)),
@@ -39,6 +41,9 @@ const mapDispatchToProps = dispatch => {
setShowFiatConversionOnTestnetsPreference: value => {
return dispatch(setShowFiatConversionOnTestnetsPreference(value))
},
+ setAutoLogoutTimeLimit: value => {
+ return dispatch(setAutoLogoutTimeLimit(value))
+ },
}
}
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
new file mode 100644
index 000000000..f81329533
--- /dev/null
+++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import assert from 'assert'
+import sinon from 'sinon'
+import { shallow } from 'enzyme'
+import AdvancedTab from '../advanced-tab.component'
+import TextField from '../../../../components/ui/text-field'
+
+describe('AdvancedTab Component', () => {
+ it('should render correctly', () => {
+ const root = shallow(
+ <AdvancedTab />,
+ {
+ context: {
+ t: s => `_${s}`,
+ },
+ }
+ )
+
+ assert.equal(root.find('.settings-page__content-row').length, 8)
+ })
+
+ it('should update autoLogoutTimeLimit', () => {
+ const setAutoLogoutTimeLimitSpy = sinon.spy()
+ const root = shallow(
+ <AdvancedTab
+ setAutoLogoutTimeLimit={setAutoLogoutTimeLimitSpy}
+ />,
+ {
+ context: {
+ t: s => `_${s}`,
+ },
+ }
+ )
+
+ const autoTimeout = root.find('.settings-page__content-row').last()
+ const textField = autoTimeout.find(TextField)
+
+ textField.props().onChange({ target: { value: 1440 } })
+ assert.equal(root.state().autoLogoutTimeLimit, 1440)
+
+ autoTimeout.find('button').simulate('click')
+ assert.equal(setAutoLogoutTimeLimitSpy.args[0][0], 1440)
+ })
+})
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
new file mode 100644
index 000000000..62122073d
--- /dev/null
+++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js
@@ -0,0 +1,46 @@
+import assert from 'assert'
+import { mapStateToProps, mapDispatchToProps } from '../advanced-tab.container'
+
+const defaultState = {
+ appState: {
+ warning: null,
+ },
+ metamask: {
+ featureFlags: {
+ sendHexData: false,
+ advancedInlineGas: false,
+ },
+ preferences: {
+ autoLogoutTimeLimit: 0,
+ showFiatInTestnets: false,
+ useNativeCurrencyAsPrimaryCurrency: true,
+ },
+ },
+}
+
+describe('AdvancedTab Container', () => {
+ it('should map state to props correctly', () => {
+ const props = mapStateToProps(defaultState)
+ const expected = {
+ warning: null,
+ sendHexData: false,
+ advancedInlineGas: false,
+ showFiatInTestnets: false,
+ autoLogoutTimeLimit: 0,
+ }
+
+ assert.deepEqual(props, expected)
+ })
+
+ 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')
+ })
+})
diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss
index 52208dc85..66959ba93 100644
--- a/ui/app/pages/settings/index.scss
+++ b/ui/app/pages/settings/index.scss
@@ -1,5 +1,7 @@
@import 'info-tab/index';
+@import 'networks-tab/index';
+
@import 'settings-tab/index';
.settings-page {
@@ -13,7 +15,6 @@
flex-flow: row nowrap;
padding: 12px 24px;
align-items: center;
- border-bottom: 1px solid $alto;
flex: 0 0 auto;
&__title {
@@ -22,6 +23,45 @@
}
}
+ &__subheader {
+ padding: 16px 4px;
+ font-size: 20px;
+ border-bottom: 1px solid $alto;
+ margin-right: 24px;
+
+ @media screen and (max-width: 575px) {
+ display: none;
+ }
+ }
+
+ &__sub-header {
+ height: 72px;
+ border-bottom: 1px solid #D8D8D8;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ @media screen and (max-width: 575px) {
+ height: 69px;
+ position: relative;
+ text-align: center;
+ }
+ }
+
+ &__sub-header-text {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 24px;
+ line-height: 24px;
+ color: black;
+
+ @media screen and (max-width: 575px) {
+ font-size: 16px;
+ width: 100%;
+ }
+ }
+
&__back-button {
display: none;
@@ -49,8 +89,9 @@
&__content {
display: flex;
flex-flow: row nowrap;
- height: auto;
+ height: 100%;
overflow: auto;
+ border-top: 1px solid #D8D8D8;
&__tabs {
display: flex;
@@ -58,9 +99,15 @@
flex: 1 1 auto;
@media screen and (min-width: 576px) {
- flex: 0 0 32%;
+ flex: 0 0 40%;
max-width: 210px;
- border-right: 1px solid $alto;
+ padding-top: 8px;
+ }
+
+ .tab-bar__tab {
+ @media screen and (min-width: 576px) {
+ padding: 16px 24px 0;
+ }
}
}
@@ -76,6 +123,10 @@
&__body {
padding: 12px 24px;
+
+ @media screen and (min-width: 576px) {
+ padding: 12px;
+ }
}
&__content-row {
@@ -89,7 +140,6 @@
min-width: 0;
display: flex;
flex-direction: column;
- padding: 0 5px;
min-height: 71px;
@media screen and (max-width: 575px) {
diff --git a/ui/app/pages/settings/networks-tab/index.js b/ui/app/pages/settings/networks-tab/index.js
new file mode 100644
index 000000000..362004498
--- /dev/null
+++ b/ui/app/pages/settings/networks-tab/index.js
@@ -0,0 +1 @@
+export { default } from './networks-tab.container'
diff --git a/ui/app/pages/settings/networks-tab/index.scss b/ui/app/pages/settings/networks-tab/index.scss
new file mode 100644
index 000000000..b0020437d
--- /dev/null
+++ b/ui/app/pages/settings/networks-tab/index.scss
@@ -0,0 +1,200 @@
+.networks-tab {
+ &__content {
+ margin-top: 24px;
+ display: flex;
+ height: 100%;
+ max-width: 739px;
+ justify-content: space-between;
+
+ @media screen and (max-width: 575px) {
+ margin-top: 0px;
+ }
+ }
+
+ &__body {
+ padding: 12px 24px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ @media screen and (max-width: 575px) {
+ padding: 0;
+ }
+ }
+
+ &__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;
+ position: absolute;
+ margin-left: 10px;
+ }
+ }
+
+ &__network-form {
+ flex: 0.5 0 auto;
+ max-width: 343px;
+ max-height: 465px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ .page-container__footer {
+ border-top: none;
+
+ @media screen and (max-width: 575px) {
+ width: 93%;
+ }
+
+ header {
+ padding: 10px 0px;
+ }
+ }
+
+ @media screen and (max-width: 575px) {
+ display: flex;
+ flex: auto;
+ max-width: 100%;
+ max-height: 100%;
+ align-items: center;
+ width: 100%;
+ margin-top: 10px;
+ }
+ }
+
+ &__network-form-row {
+ @media screen and (max-width: 575px) {
+ display: flex;
+ flex-direction: column;
+ width: 93%;
+ }
+ }
+
+ &__network-form-label {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 14px;
+ line-height: 20px;
+ color: #000000;
+ }
+
+ &__networks-list {
+ flex: 0.5 0 auto;
+ max-width: 343px;
+
+ @media screen and (max-width: 575px) {
+ max-width: 100vw;
+ width: 100vw;
+ overflow-y: scroll;
+ }
+ }
+
+ &__add-network-button-wrapper {
+ display: none;
+
+ @media screen and (max-width: 575px) {
+ display: flex;
+ padding-top: 19px;
+ padding-bottom: 23px;
+ justify-content: center;
+ align-items: center;
+ border-top: 1px solid #D8D8D8;
+
+ .button {
+ width: 178px;
+ }
+ }
+ }
+
+ &__add-network-header-button-wrapper {
+ padding-top: 15px;
+ padding-bottom: 21px;
+ justify-content: center;
+
+ .button {
+ width: 178px;
+ }
+
+ @media screen and (max-width: 575px) {
+ display: none;
+ }
+ }
+
+ &__networks-list--selection {
+ @media screen and (max-width: 575px) {
+ display: none;
+ }
+ }
+
+ &__networks-list-item {
+ display: flex;
+ padding: 13px 0px 13px 17px;
+ position: relative;
+
+ .menu-icon-circle {
+ &:hover {
+ cursor: pointer;
+ }
+ }
+
+ @media screen and (max-width: 575px) {
+ padding: 20px 23px 21px 17px;
+ border-bottom: 1px solid #D8D8D8;
+ }
+ }
+
+ &__networks-list-item:last-of-type {
+ @media screen and (max-width: 575px) {
+ border-bottom: none;
+ }
+ }
+
+ &__networks-list-name {
+ margin-left: 11px;
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 16px;
+ line-height: 23px;
+ color: #6A737D;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+
+ &__networks-list-arrow {
+ display: none;
+
+ @media screen and (max-width: 575px) {
+ display: block;
+ background-image: url('/images/caret-right.svg');
+ width: 24px;
+ height: 24px;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ right: 10px;
+ cursor: pointer;
+ position: absolute;
+ width: 24px;
+ height: 24px;
+ }
+ }
+
+ &__networks-list-name--selected {
+ font-weight: bold;
+ color: #000000;
+ }
+} \ No newline at end of file
diff --git a/ui/app/pages/settings/networks-tab/network-form/index.js b/ui/app/pages/settings/networks-tab/network-form/index.js
new file mode 100644
index 000000000..89d9de42b
--- /dev/null
+++ b/ui/app/pages/settings/networks-tab/network-form/index.js
@@ -0,0 +1 @@
+export { default } from './network-form.component'
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
new file mode 100644
index 000000000..5e455b65e
--- /dev/null
+++ b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js
@@ -0,0 +1,225 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import validUrl from 'valid-url'
+import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer'
+import TextField from '../../../../components/ui/text-field'
+
+export default class NetworksTab extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func.isRequired,
+ metricsEvent: PropTypes.func.isRequired,
+ }
+
+ static propTypes = {
+ editRpc: PropTypes.func.isRequired,
+ rpcUrl: PropTypes.string,
+ chainId: PropTypes.string,
+ ticker: PropTypes.string,
+ viewOnly: PropTypes.bool,
+ networkName: PropTypes.string,
+ onClear: PropTypes.func.isRequired,
+ setRpcTarget: PropTypes.func.isRequired,
+ networksTabIsInAddMode: PropTypes.bool,
+ blockExplorerUrl: PropTypes.string,
+ rpcPrefs: PropTypes.object,
+ }
+
+ state = {
+ rpcUrl: this.props.rpcUrl,
+ chainId: this.props.chainId,
+ ticker: this.props.ticker,
+ networkName: this.props.networkName,
+ blockExplorerUrl: this.props.blockExplorerUrl,
+ errors: {},
+ }
+
+ componentDidUpdate (prevProps) {
+ const { rpcUrl: prevRpcUrl, networksTabIsInAddMode: prevAddMode } = prevProps
+ const {
+ rpcUrl,
+ chainId,
+ ticker,
+ networkName,
+ networksTabIsInAddMode,
+ blockExplorerUrl,
+ } = this.props
+
+ if (!prevAddMode && networksTabIsInAddMode) {
+ this.setState({
+ rpcUrl: '',
+ chainId: '',
+ ticker: '',
+ networkName: '',
+ blockExplorerUrl: '',
+ errors: {},
+ })
+ } else if (prevRpcUrl !== rpcUrl) {
+ this.setState({ rpcUrl, chainId, ticker, networkName, blockExplorerUrl, errors: {} })
+ }
+ }
+
+ componentWillUnmount () {
+ this.props.onClear()
+ this.setState({
+ rpcUrl: '',
+ chainId: '',
+ ticker: '',
+ networkName: '',
+ blockExplorerUrl: '',
+ errors: {},
+ })
+ }
+
+ stateIsUnchanged () {
+ const {
+ rpcUrl,
+ chainId,
+ ticker,
+ networkName,
+ blockExplorerUrl,
+ } = this.props
+
+ const {
+ rpcUrl: stateRpcUrl,
+ chainId: stateChainId,
+ ticker: stateTicker,
+ networkName: stateNetworkName,
+ blockExplorerUrl: stateBlockExplorerUrl,
+ } = this.state
+
+ return (
+ stateRpcUrl === rpcUrl &&
+ stateChainId === chainId &&
+ stateTicker === ticker &&
+ stateNetworkName === networkName &&
+ stateBlockExplorerUrl === blockExplorerUrl
+ )
+ }
+
+ renderFormTextField (fieldKey, textFieldId, onChange, value, optionalTextFieldKey) {
+ const { errors } = this.state
+ const { viewOnly } = this.props
+
+ return (
+ <div className="networks-tab__network-form-row">
+ <div className="networks-tab__network-form-label">{this.context.t(optionalTextFieldKey || fieldKey)}</div>
+ <TextField
+ type="text"
+ id={textFieldId}
+ onChange={onChange}
+ fullWidth
+ margin="dense"
+ value={value}
+ disabled={viewOnly}
+ error={errors[fieldKey]}
+ />
+ </div>
+ )
+ }
+
+ setStateWithValue = (stateKey, validator) => {
+ return (e) => {
+ validator && validator(e.target.value, stateKey)
+ this.setState({ [stateKey]: e.target.value })
+ }
+ }
+
+ setErrorTo = (errorKey, errorVal) => {
+ this.setState({
+ errors: {
+ ...this.state.errors,
+ [errorKey]: errorVal,
+ },
+ })
+ }
+
+ validateChainId = (chainId) => {
+ this.setErrorTo('chainId', !!chainId && Number.isNaN(parseInt(chainId))
+ ? `${this.context.t('invalidInput')} chainId`
+ : ''
+ )
+ }
+
+ validateUrl = (url, stateKey) => {
+ if (validUrl.isWebUri(url)) {
+ this.setErrorTo(stateKey, '')
+ } else {
+ const appendedRpc = `http://${url}`
+ const validWhenAppended = validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/)
+
+ this.setErrorTo(stateKey, this.context.t(validWhenAppended ? 'uriErrorMsg' : 'invalidRPC'))
+ }
+ }
+
+ render () {
+ const { setRpcTarget, viewOnly, rpcUrl: propsRpcUrl, editRpc, rpcPrefs = {} } = this.props
+ const {
+ networkName,
+ rpcUrl,
+ chainId,
+ ticker,
+ blockExplorerUrl,
+ errors,
+ } = this.state
+
+
+ return (
+ <div className="networks-tab__network-form">
+ {this.renderFormTextField(
+ 'networkName',
+ 'network-name',
+ this.setStateWithValue('networkName'),
+ networkName,
+ )}
+ {this.renderFormTextField(
+ 'rpcUrl',
+ 'rpc-url',
+ this.setStateWithValue('rpcUrl', this.validateUrl),
+ rpcUrl,
+ )}
+ {this.renderFormTextField(
+ 'chainId',
+ 'chainId',
+ this.setStateWithValue('chainId', this.validateChainId),
+ chainId,
+ 'optionalChainId',
+ )}
+ {this.renderFormTextField(
+ 'symbol',
+ 'network-ticker',
+ this.setStateWithValue('ticker'),
+ ticker,
+ 'optionalSymbol',
+ )}
+ {this.renderFormTextField(
+ 'blockExplorerUrl',
+ 'block-explorer-url',
+ this.setStateWithValue('blockExplorerUrl', this.validateUrl),
+ blockExplorerUrl,
+ 'optionalBlockExplorerUrl',
+ )}
+ <PageContainerFooter
+ cancelText={this.context.t('cancel')}
+ hideCancel={true}
+ onSubmit={() => {
+ if (propsRpcUrl && rpcUrl !== propsRpcUrl) {
+ editRpc(propsRpcUrl, rpcUrl, chainId, ticker, networkName, {
+ blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
+ ...rpcPrefs,
+ })
+ } else {
+ setRpcTarget(rpcUrl, chainId, ticker, networkName, {
+ blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
+ ...rpcPrefs,
+ })
+ }
+ }}
+ submitText={this.context.t('save')}
+ submitButtonType={'confirm'}
+ disabled={viewOnly || this.stateIsUnchanged() || Object.values(errors).some(x => x) || !rpcUrl}
+ />
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/pages/settings/networks-tab/networks-tab.component.js b/ui/app/pages/settings/networks-tab/networks-tab.component.js
new file mode 100644
index 000000000..2f921a892
--- /dev/null
+++ b/ui/app/pages/settings/networks-tab/networks-tab.component.js
@@ -0,0 +1,214 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { SETTINGS_ROUTE } from '../../../helpers/constants/routes'
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
+import classnames from 'classnames'
+import Button from '../../../components/ui/button'
+import NetworkForm from './network-form'
+import NetworkDropdownIcon from '../../../components/app/dropdowns/components/network-dropdown-icon'
+
+export default class NetworksTab extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func.isRequired,
+ metricsEvent: PropTypes.func.isRequired,
+ }
+
+ static propTypes = {
+ editRpc: PropTypes.func.isRequired,
+ history: PropTypes.object.isRequired,
+ location: PropTypes.object.isRequired,
+ networkIsSelected: PropTypes.bool,
+ networksTabIsInAddMode: PropTypes.bool,
+ networksToRender: PropTypes.array.isRequired,
+ selectedNetwork: PropTypes.object,
+ setNetworksTabAddMode: PropTypes.func.isRequired,
+ setRpcTarget: PropTypes.func.isRequired,
+ setSelectedSettingsRpcUrl: PropTypes.func.isRequired,
+ providerUrl: PropTypes.string,
+ providerType: PropTypes.string,
+ networkDefaultedToProvider: PropTypes.bool,
+ }
+
+ componentWillMount () {
+ this.props.setSelectedSettingsRpcUrl(null)
+ }
+
+ isCurrentPath (pathname) {
+ return this.props.location.pathname === pathname
+ }
+
+ renderSubHeader () {
+ const {
+ networkIsSelected,
+ setSelectedSettingsRpcUrl,
+ setNetworksTabAddMode,
+ networksTabIsInAddMode,
+ networkDefaultedToProvider,
+ } = this.props
+
+ 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)
+ }
+ />
+ <span className="settings-page__sub-header-text">{ this.context.t('networks') }</span>
+ <div className="networks-tab__add-network-header-button-wrapper">
+ <Button
+ type="primary"
+ onClick={event => {
+ event.preventDefault()
+ setSelectedSettingsRpcUrl(null)
+ setNetworksTabAddMode(true)
+ }}
+ >
+ { this.context.t('addNetwork') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+
+ renderNetworkListItem (network, selectRpcUrl) {
+ const {
+ setSelectedSettingsRpcUrl,
+ setNetworksTabAddMode,
+ networkIsSelected,
+ providerUrl,
+ providerType,
+ networksTabIsInAddMode,
+ } = this.props
+ const {
+ border,
+ iconColor,
+ label,
+ labelKey,
+ rpcUrl,
+ providerType: currentProviderType,
+ } = network
+
+ const listItemNetworkIsSelected = selectRpcUrl && selectRpcUrl === rpcUrl
+ const listItemUrlIsProviderUrl = rpcUrl === providerUrl
+ const listItemTypeIsProviderNonRpcType = providerType !== 'rpc' && currentProviderType === providerType
+ const listItemNetworkIsCurrentProvider = !networkIsSelected && !networksTabIsInAddMode && (listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType)
+ const displayNetworkListItemAsSelected = listItemNetworkIsSelected || listItemNetworkIsCurrentProvider
+
+ return (
+ <div
+ key={'settings-network-list-item:' + rpcUrl}
+ className="networks-tab__networks-list-item"
+ onClick={ () => {
+ setNetworksTabAddMode(false)
+ setSelectedSettingsRpcUrl(rpcUrl)
+ }}
+ >
+ <NetworkDropdownIcon
+ backgroundColor={iconColor || 'white'}
+ innerBorder={border}
+ />
+ <div className={ classnames('networks-tab__networks-list-name', {
+ 'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
+ }) }>
+ { label || this.context.t(labelKey) }
+ </div>
+ <div className="networks-tab__networks-list-arrow" />
+ </div>
+ )
+ }
+
+ renderNetworksList () {
+ const { networksToRender, selectedNetwork, networkIsSelected, networksTabIsInAddMode, networkDefaultedToProvider } = this.props
+
+ return (
+ <div className={classnames('networks-tab__networks-list', {
+ 'networks-tab__networks-list--selection': (networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode,
+ })}>
+ { networksToRender.map(network => this.renderNetworkListItem(network, selectedNetwork.rpcUrl)) }
+ </div>
+ )
+ }
+
+ renderNetworksTabContent () {
+ const {
+ setRpcTarget,
+ setSelectedSettingsRpcUrl,
+ setNetworksTabAddMode,
+ selectedNetwork: {
+ labelKey,
+ label,
+ rpcUrl,
+ chainId,
+ ticker,
+ viewOnly,
+ rpcPrefs,
+ blockExplorerUrl,
+ },
+ networksTabIsInAddMode,
+ editRpc,
+ networkDefaultedToProvider,
+ } = this.props
+ const envIsPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
+
+ return (
+ <div className="networks-tab__content">
+ {this.renderNetworksList()}
+ {networksTabIsInAddMode || !envIsPopup || (envIsPopup && !networkDefaultedToProvider)
+ ? <NetworkForm
+ setRpcTarget={setRpcTarget}
+ editRpc={editRpc}
+ networkName={label || labelKey && this.context.t(labelKey) || ''}
+ rpcUrl={rpcUrl}
+ chainId={chainId}
+ ticker={ticker}
+ onClear={() => {
+ setNetworksTabAddMode(false)
+ setSelectedSettingsRpcUrl(null)
+ }}
+ viewOnly={viewOnly}
+ networksTabIsInAddMode={networksTabIsInAddMode}
+ rpcPrefs={rpcPrefs}
+ blockExplorerUrl={blockExplorerUrl}
+ />
+ : null
+ }
+ </div>
+ )
+ }
+
+ renderContent () {
+ const { setNetworksTabAddMode, setSelectedSettingsRpcUrl, networkIsSelected, networksTabIsInAddMode } = this.props
+
+ return (
+ <div className="networks-tab__body">
+ {this.renderSubHeader()}
+ {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>
+ </div>
+ : null
+ }
+ </div>
+ )
+ }
+
+ render () {
+ return this.renderContent()
+ }
+}
diff --git a/ui/app/pages/settings/networks-tab/networks-tab.constants.js b/ui/app/pages/settings/networks-tab/networks-tab.constants.js
new file mode 100644
index 000000000..d3d1a01cc
--- /dev/null
+++ b/ui/app/pages/settings/networks-tab/networks-tab.constants.js
@@ -0,0 +1,50 @@
+const defaultNetworksData = [
+ {
+ labelKey: 'mainnet',
+ iconColor: '#29B6AF',
+ providerType: 'mainnet',
+ rpcUrl: 'https://api.infura.io/v1/jsonrpc/mainnet',
+ chainId: '1',
+ ticker: 'ETH',
+ blockExplorerUrl: 'https://etherscan.io',
+ },
+ {
+ labelKey: 'ropsten',
+ iconColor: '#FF4A8D',
+ providerType: 'ropsten',
+ rpcUrl: 'https://api.infura.io/v1/jsonrpc/ropsten',
+ chainId: '3',
+ ticker: 'ETH',
+ blockExplorerUrl: 'https://ropsten.etherscan.io',
+ },
+ {
+ labelKey: 'kovan',
+ iconColor: '#9064FF',
+ providerType: 'kovan',
+ rpcUrl: 'https://api.infura.io/v1/jsonrpc/kovan',
+ chainId: '4',
+ ticker: 'ETH',
+ blockExplorerUrl: 'https://etherscan.io',
+ },
+ {
+ labelKey: 'rinkeby',
+ iconColor: '#F6C343',
+ providerType: 'rinkeby',
+ rpcUrl: 'https://api.infura.io/v1/jsonrpc/rinkeby',
+ chainId: '42',
+ ticker: 'ETH',
+ blockExplorerUrl: 'https://rinkeby.etherscan.io',
+ },
+ {
+ labelKey: 'localhost',
+ iconColor: 'white',
+ border: '1px solid #6A737D',
+ providerType: 'localhost',
+ rpcUrl: 'http://localhost:8545/',
+ blockExplorerUrl: 'https://etherscan.io',
+ },
+]
+
+export {
+ defaultNetworksData,
+}
diff --git a/ui/app/pages/settings/networks-tab/networks-tab.container.js b/ui/app/pages/settings/networks-tab/networks-tab.container.js
new file mode 100644
index 000000000..a5d71f714
--- /dev/null
+++ b/ui/app/pages/settings/networks-tab/networks-tab.container.js
@@ -0,0 +1,77 @@
+import NetworksTab from './networks-tab.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import {
+ setSelectedSettingsRpcUrl,
+ updateAndSetCustomRpc,
+ displayWarning,
+ setNetworksTabAddMode,
+ editRpc,
+} from '../../../store/actions'
+import { defaultNetworksData } from './networks-tab.constants'
+const defaultNetworks = defaultNetworksData.map(network => ({ ...network, viewOnly: true }))
+
+const mapStateToProps = state => {
+ const {
+ frequentRpcListDetail,
+ provider,
+ } = state.metamask
+ const {
+ networksTabSelectedRpcUrl,
+ networksTabIsInAddMode,
+ } = state.appState
+
+ const frequentRpcNetworkListDetails = frequentRpcListDetail.map(rpc => {
+ return {
+ label: rpc.nickname,
+ iconColor: '#6A737D',
+ providerType: 'rpc',
+ rpcUrl: rpc.rpcUrl,
+ chainId: rpc.chainId,
+ ticker: rpc.ticker,
+ blockExplorerUrl: rpc.rpcPrefs && rpc.rpcPrefs.blockExplorerUrl || '',
+ }
+ })
+
+ const networksToRender = [ ...defaultNetworks, ...frequentRpcNetworkListDetails ]
+ let selectedNetwork = networksToRender.find(network => network.rpcUrl === networksTabSelectedRpcUrl) || {}
+ const networkIsSelected = Boolean(selectedNetwork.rpcUrl)
+
+ let networkDefaultedToProvider = false
+ if (!networkIsSelected && !networksTabIsInAddMode) {
+ selectedNetwork = networksToRender.find(network => {
+ return network.rpcUrl === provider.rpcTarget || network.providerType !== 'rpc' && network.providerType === provider.type
+ }) || {}
+ networkDefaultedToProvider = true
+ }
+
+ return {
+ selectedNetwork,
+ networksToRender,
+ networkIsSelected,
+ networksTabIsInAddMode,
+ providerType: provider.type,
+ providerUrl: provider.rpcTarget,
+ networkDefaultedToProvider,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setSelectedSettingsRpcUrl: newRpcUrl => dispatch(setSelectedSettingsRpcUrl(newRpcUrl)),
+ setRpcTarget: (newRpc, chainId, ticker, nickname, rpcPrefs) => {
+ dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname, rpcPrefs))
+ },
+ displayWarning: warning => dispatch(displayWarning(warning)),
+ setNetworksTabAddMode: isInAddMode => dispatch(setNetworksTabAddMode(isInAddMode)),
+ editRpc: (oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs) => {
+ dispatch(editRpc(oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs))
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(NetworksTab)
diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js
index 061e65060..a2f137264 100644
--- a/ui/app/pages/settings/settings.component.js
+++ b/ui/app/pages/settings/settings.component.js
@@ -1,11 +1,12 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
-import { Switch, Route, matchPath } from 'react-router-dom'
+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'
+import NetworksTab from './networks-tab'
import AdvancedTab from './advanced-tab'
import InfoTab from './info-tab'
import SecurityTab from './security-tab'
@@ -16,6 +17,7 @@ import {
GENERAL_ROUTE,
ABOUT_US_ROUTE,
SETTINGS_ROUTE,
+ NETWORKS_ROUTE,
} from '../../helpers/constants/routes'
const ROUTES_TO_I18N_KEYS = {
@@ -25,7 +27,7 @@ const ROUTES_TO_I18N_KEYS = {
[ABOUT_US_ROUTE]: 'about',
}
-export default class SettingsPage extends PureComponent {
+class SettingsPage extends PureComponent {
static propTypes = {
location: PropTypes.object,
history: PropTypes.object,
@@ -55,7 +57,7 @@ export default class SettingsPage extends PureComponent {
>
<div className="settings-page__header">
{
- !this.isCurrentPath(SETTINGS_ROUTE) && (
+ !this.isCurrentPath(SETTINGS_ROUTE) && !this.isCurrentPath(NETWORKS_ROUTE) && (
<div
className="settings-page__back-button"
onClick={() => history.push(SETTINGS_ROUTE)}
@@ -75,6 +77,7 @@ export default class SettingsPage extends PureComponent {
{ this.renderTabs() }
</div>
<div className="settings-page__content__modules">
+ { this.renderSubHeader() }
{ this.renderContent() }
</div>
</div>
@@ -82,6 +85,17 @@ export default class SettingsPage extends PureComponent {
)
}
+ renderSubHeader () {
+ const { t } = this.context
+ const { location: { pathname } } = this.props
+
+ return (
+ <div className="settings-page__subheader">
+ {t(ROUTES_TO_I18N_KEYS[pathname] || 'general')}
+ </div>
+ )
+ }
+
renderTabs () {
const { history, location } = this.props
const { t } = this.context
@@ -92,6 +106,7 @@ export default class SettingsPage extends PureComponent {
{ 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('networks'), description: t('networkSettingsDescription'), key: NETWORKS_ROUTE },
{ content: t('about'), description: t('aboutSettingsDescription'), key: ABOUT_US_ROUTE },
]}
isActive={key => {
@@ -125,6 +140,11 @@ export default class SettingsPage extends PureComponent {
/>
<Route
exact
+ path={NETWORKS_ROUTE}
+ component={NetworksTab}
+ />
+ <Route
+ exact
path={SECURITY_ROUTE}
component={SecurityTab}
/>
@@ -135,3 +155,5 @@ export default class SettingsPage extends PureComponent {
)
}
}
+
+export default withRouter(SettingsPage)