1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

New settings custom rpc form (#6490)

* Add networks tab to settings, with header.

* Adds network list to settings network tab.

* Adds form to settings networks tab and connects it to network list.

* Network tab: form adding and editing working

* Settings network form properly handles input errors

* Add translations for settings network form

* Clean up styles of settings network tab.

* Add popup-view styles and behaviour to settings network tab.

* Fix save button on settings network form

* Adds 'Add Network' button and addMode to settings networks tab

* Lint fix for settings networks tab addition

* Fix navigation in settings networks tab.

* Editing an rpcurl in networks tab does not create new network, just changes rpc of old

* Fix layout of settings tabs other than network

* Networks dropdown 'Custom Rpc' item links to networks tab in settings.

* Update settings sidebar networks subheader.

* Make networks tab buttons width consistent with input widths in extension view.

* Fix settings screen subheader height in popup view

* Fix height of add networks button in popup view

* Add optional label to chainId and symbol form labels in networks setting tab

* Style fixes for networks tab headers

* Add ability to customize block explorer used by custom rpc

* Stylistic improvements+fixes to custom rpc form.

* Hide cancel button.

* Highlight and show network form of provider by default.

* Standardize network subheader name to 'Networks'

* Update e2e tests for new settings network form

* Update unit tests for new rpcPrefs prop

* Extract blockexplorer url construction into method.

* Fix broken styles on non-network tabs in popup mode

* Fix block explorer url links for cases when provider in state has not been updated.

* Fix vertical spacing of network form

* Don't allow click of save button on network form if nothing has changed

* Ensure add network button is shown in popup view

* Lint fix for networks tab

* Fix block explorer url preference setting.

* Fix e2e tests for custom blockexplorer in account details modal changes.

* Update integration test states to include frequentRpcList property

* Fix some capitalizations in en/messages.json

* Remove some console.logs added during custom rpc form work

* Fix external account link text and url for modal and dropdown.

* Documentation, url validation, proptype required additions and lint fixes on network tab and form.
This commit is contained in:
Dan J Miller 2019-05-09 14:57:14 -02:30 committed by GitHub
parent 094e4cf555
commit 13be683701
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1028 additions and 68 deletions

View File

@ -83,6 +83,9 @@
"address": { "address": {
"message": "Address" "message": "Address"
}, },
"addNetwork": {
"message": "Add Network"
},
"advanced": { "advanced": {
"message": "Advanced" "message": "Advanced"
}, },
@ -191,6 +194,13 @@
"message": "must be greater than or equal to $1 and less than or equal to $2.", "message": "must be greater than or equal to $1 and less than or equal to $2.",
"description": "helper for inputting hex as decimal input" "description": "helper for inputting hex as decimal input"
}, },
"blockExplorerUrl": {
"message": "Block Explorer"
},
"blockExplorerView": {
"message": "View account at $1",
"description": "$1 replaced by URL for custom block explorer"
},
"blockiesIdenticon": { "blockiesIdenticon": {
"message": "Use Blockies Identicon" "message": "Use Blockies Identicon"
}, },
@ -230,6 +240,9 @@
"ok": { "ok": {
"message": "Ok" "message": "Ok"
}, },
"optionalBlockExplorerUrl": {
"message": "Block Explorer URL (optional)"
},
"cancel": { "cancel": {
"message": "Cancel" "message": "Cancel"
}, },
@ -245,6 +258,9 @@
"cancelN": { "cancelN": {
"message": "Cancel all $1 transactions" "message": "Cancel all $1 transactions"
}, },
"chainId": {
"message": "Chain ID"
},
"classicInterface": { "classicInterface": {
"message": "Use classic interface" "message": "Use classic interface"
}, },
@ -502,6 +518,9 @@
"edit": { "edit": {
"message": "Edit" "message": "Edit"
}, },
"editNetwork": {
"message": "Edit Network"
},
"editAccountName": { "editAccountName": {
"message": "Edit Account Name" "message": "Edit Account Name"
}, },
@ -934,9 +953,15 @@
"negativeETH": { "negativeETH": {
"message": "Can not send negative amounts of ETH." "message": "Can not send negative amounts of ETH."
}, },
"networkName": {
"message": "Network Name"
},
"networks": { "networks": {
"message": "Networks" "message": "Networks"
}, },
"networkSettingsDescription": {
"message": "Add and edit custom RPC networks"
},
"nevermind": { "nevermind": {
"message": "Nevermind" "message": "Nevermind"
}, },
@ -977,7 +1002,7 @@
"protectYourKeysMessage2": { "protectYourKeysMessage2": {
"message": "Keep your phrase safe. If you see something fishy, or youre uncertain about a website, email support@metamask.io" "message": "Keep your phrase safe. If you see something fishy, or youre uncertain about a website, email support@metamask.io"
}, },
"rpcURL": { "rpcUrl": {
"message": "New RPC URL" "message": "New RPC URL"
}, },
"showAdvancedOptions": { "showAdvancedOptions": {
@ -1492,6 +1517,9 @@
"supportCenter": { "supportCenter": {
"message": "Visit our Support Center" "message": "Visit our Support Center"
}, },
"symbol": {
"message": "Symbol"
},
"symbolBetweenZeroTwelve": { "symbolBetweenZeroTwelve": {
"message": "Symbol must be between 0 and 12 characters." "message": "Symbol must be between 0 and 12 characters."
}, },
@ -1714,9 +1742,15 @@
"viewAccount": { "viewAccount": {
"message": "View Account" "message": "View Account"
}, },
"viewOnCustomBlockExplorer": {
"message": "View at $1"
},
"viewOnEtherscan": { "viewOnEtherscan": {
"message": "View on Etherscan" "message": "View on Etherscan"
}, },
"viewNetworkInfo": {
"message": "View Network Info"
},
"visitWebSite": { "visitWebSite": {
"message": "Visit our web site" "message": "Visit our web site"
}, },

View File

@ -129,13 +129,14 @@ module.exports = class NetworkController extends EventEmitter {
}) })
} }
setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '') { setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs) {
const providerConfig = { const providerConfig = {
type: 'rpc', type: 'rpc',
rpcTarget, rpcTarget,
chainId, chainId,
ticker, ticker,
nickname, nickname,
rpcPrefs,
} }
this.providerConfig = providerConfig this.providerConfig = providerConfig
} }

View File

@ -488,8 +488,8 @@ class PreferencesController {
rpcList[index] = updatedRpc rpcList[index] = updatedRpc
this.store.updateState({ frequentRpcListDetail: rpcList }) this.store.updateState({ frequentRpcListDetail: rpcList })
} else { } else {
const { rpcUrl, chainId, ticker, nickname } = newRpcDetails const { rpcUrl, chainId, ticker, nickname, rpcPrefs = {} } = newRpcDetails
return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname) return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs)
} }
return Promise.resolve(rpcList) return Promise.resolve(rpcList)
} }
@ -503,22 +503,22 @@ class PreferencesController {
* @returns {Promise<array>} Promise resolving to updated frequentRpcList. * @returns {Promise<array>} Promise resolving to updated frequentRpcList.
* *
*/ */
addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '') { addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) {
const rpcList = this.getFrequentRpcListDetail() const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) const index = rpcList.findIndex((element) => { return element.rpcUrl === url })
if (index !== -1) { if (index !== -1) {
rpcList.splice(index, 1) rpcList.splice(index, 1)
}
if (url !== 'http://localhost:8545') {
let checkedChainId
if (!!chainId && !Number.isNaN(parseInt(chainId))) {
checkedChainId = chainId
} }
rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname }) if (url !== 'http://localhost:8545') {
let checkedChainId
if (!!chainId && !Number.isNaN(parseInt(chainId))) {
checkedChainId = chainId
}
rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname, rpcPrefs })
}
this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
} }
this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
}
/** /**
* Removes custom RPC url from state. * Removes custom RPC url from state.

View File

@ -1633,9 +1633,9 @@ module.exports = class MetamaskController extends EventEmitter {
* @returns {Promise<String>} - The RPC Target URL confirmed. * @returns {Promise<String>} - The RPC Target URL confirmed.
*/ */
async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname) { async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname, rpcPrefs) {
await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname }) await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname, rpcPrefs })
this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname) this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname, rpcPrefs)
return rpcUrl return rpcUrl
} }
@ -1648,15 +1648,15 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {string} nickname - Optional nickname of the selected network. * @param {string} nickname - Optional nickname of the selected network.
* @returns {Promise<String>} - The RPC Target URL confirmed. * @returns {Promise<String>} - The RPC Target URL confirmed.
*/ */
async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '') { async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) {
const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail() const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail()
const rpcSettings = frequentRpcListDetail.find((rpc) => rpcTarget === rpc.rpcUrl) const rpcSettings = frequentRpcListDetail.find((rpc) => rpcTarget === rpc.rpcUrl)
if (rpcSettings) { if (rpcSettings) {
this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname) this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname, rpcPrefs)
} else { } else {
this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname) this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname, rpcPrefs)
await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname) await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname, rpcPrefs)
} }
return rpcTarget return rpcTarget
} }

View File

@ -192,7 +192,8 @@
"type": "testnet" "type": "testnet"
}, },
"shapeShiftTxList": [], "shapeShiftTxList": [],
"lostAccounts": [] "lostAccounts": [],
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -134,7 +134,8 @@
"useNativeCurrencyAsPrimaryCurrency": true, "useNativeCurrencyAsPrimaryCurrency": true,
"showFiatInTestnets": true "showFiatInTestnets": true
}, },
"completedUiMigration": true "completedUiMigration": true,
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -157,7 +157,8 @@
"preferences": { "preferences": {
"useNativeCurrencyAsPrimaryCurrency": true "useNativeCurrencyAsPrimaryCurrency": true
}, },
"completedUiMigration": true "completedUiMigration": true,
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -116,7 +116,8 @@
"useNativeCurrencyAsPrimaryCurrency": true, "useNativeCurrencyAsPrimaryCurrency": true,
"showFiatInTestnets": true "showFiatInTestnets": true
}, },
"completedUiMigration": true "completedUiMigration": true,
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -138,7 +138,8 @@
"useNativeCurrencyAsPrimaryCurrency": true, "useNativeCurrencyAsPrimaryCurrency": true,
"showFiatInTestnets": true "showFiatInTestnets": true
}, },
"completedUiMigration": true "completedUiMigration": true,
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -117,7 +117,8 @@
"useNativeCurrencyAsPrimaryCurrency": true, "useNativeCurrencyAsPrimaryCurrency": true,
"showFiatInTestnets": true "showFiatInTestnets": true
}, },
"completedUiMigration": true "completedUiMigration": true,
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -87,7 +87,8 @@
"type": "testnet" "type": "testnet"
}, },
"shapeShiftTxList": [], "shapeShiftTxList": [],
"lostAccounts": [] "lostAccounts": [],
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -1059,7 +1059,8 @@
"preferences": { "preferences": {
"useNativeCurrencyAsPrimaryCurrency": true "useNativeCurrencyAsPrimaryCurrency": true
}, },
"completedUiMigration": true "completedUiMigration": true,
"frequentRpcListDetail": []
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,

View File

@ -1341,11 +1341,14 @@ describe('MetaMask', function () {
await customRpcButton.click() await customRpcButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]')) await findElement(driver, By.css('.settings-page__sub-header-text'))
const customRpcInputs = await findElements(driver, By.css('input[type="text"]'))
const customRpcInput = customRpcInputs[1]
await customRpcInput.clear() await customRpcInput.clear()
await customRpcInput.sendKeys(customRpcUrl) await customRpcInput.sendKeys(customRpcUrl)
const customRpcSave = await findElement(driver, By.css('.settings-tab__rpc-save-button')) const customRpcSave = await findElement(driver, By.css('.page-container__footer-button'))
await customRpcSave.click() await customRpcSave.click()
await delay(largeDelayMs * 2) await delay(largeDelayMs * 2)
}) })

View File

@ -527,14 +527,14 @@ describe('preferences controller', function () {
it('should add custom RPC url to state', function () { it('should add custom RPC url to state', function () {
preferencesController.addToFrequentRpcList('rpc_url', 1) preferencesController.addToFrequentRpcList('rpc_url', 1)
preferencesController.addToFrequentRpcList('http://localhost:8545', 1) preferencesController.addToFrequentRpcList('http://localhost:8545', 1)
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }]) assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }])
preferencesController.addToFrequentRpcList('rpc_url', 1) preferencesController.addToFrequentRpcList('rpc_url', 1)
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }]) assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }])
}) })
it('should remove custom RPC url from state', function () { it('should remove custom RPC url from state', function () {
preferencesController.addToFrequentRpcList('rpc_url', 1) preferencesController.addToFrequentRpcList('rpc_url', 1)
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }]) assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }])
preferencesController.removeFromFrequentRpcList('other_rpc_url') preferencesController.removeFromFrequentRpcList('other_rpc_url')
preferencesController.removeFromFrequentRpcList('http://localhost:8545') preferencesController.removeFromFrequentRpcList('http://localhost:8545')
preferencesController.removeFromFrequentRpcList('rpc_url') preferencesController.removeFromFrequentRpcList('rpc_url')

View File

@ -4,7 +4,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../../store/actions') const actions = require('../../../store/actions')
const { getSelectedIdentity } = require('../../../selectors/selectors') const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors')
const genAccountLink = require('../../../../lib/account-link.js') const genAccountLink = require('../../../../lib/account-link.js')
const { Menu, Item, CloseArea } = require('./components/menu') const { Menu, Item, CloseArea } = require('./components/menu')
@ -20,6 +20,7 @@ function mapStateToProps (state) {
selectedIdentity: getSelectedIdentity(state), selectedIdentity: getSelectedIdentity(state),
network: state.metamask.network, network: state.metamask.network,
keyrings: state.metamask.keyrings, keyrings: state.metamask.keyrings,
rpcPrefs: getRpcPrefsForCurrentProvider(state),
} }
} }
@ -28,8 +29,8 @@ function mapDispatchToProps (dispatch) {
showAccountDetailModal: () => { showAccountDetailModal: () => {
dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
}, },
viewOnEtherscan: (address, network) => { viewOnEtherscan: (address, network, rpcPrefs) => {
global.platform.openWindow({ url: genAccountLink(address, network) }) global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) })
}, },
showRemoveAccountConfirmationModal: (identity) => { showRemoveAccountConfirmationModal: (identity) => {
return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
@ -56,7 +57,9 @@ AccountDetailsDropdown.prototype.render = function () {
keyrings, keyrings,
showAccountDetailModal, showAccountDetailModal,
viewOnEtherscan, viewOnEtherscan,
showRemoveAccountConfirmationModal } = this.props showRemoveAccountConfirmationModal,
rpcPrefs,
} = this.props
const address = selectedIdentity.address const address = selectedIdentity.address
@ -112,10 +115,12 @@ AccountDetailsDropdown.prototype.render = function () {
name: 'Clicked View on Etherscan', name: 'Clicked View on Etherscan',
}, },
}) })
viewOnEtherscan(address, network) viewOnEtherscan(address, network, rpcPrefs)
this.props.onClose() this.props.onClose()
}, },
text: this.context.t('viewOnEtherscan'), text: (rpcPrefs.blockExplorerUrl
? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]])
: this.context.t('viewOnEtherscan')),
icon: h(`img`, { src: 'images/open-etherscan.svg', style: { height: '15px' } }), icon: h(`img`, { src: 'images/open-etherscan.svg', style: { height: '15px' } }),
}), }),
isRemovable ? h(Item, { isRemovable ? h(Item, {

View File

@ -10,7 +10,7 @@ const Dropdown = require('./components/dropdown').Dropdown
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
const NetworkDropdownIcon = require('./components/network-dropdown-icon') const NetworkDropdownIcon = require('./components/network-dropdown-icon')
const R = require('ramda') const R = require('ramda')
const { ADVANCED_ROUTE } = require('../../../helpers/constants/routes') const { NETWORKS_ROUTE } = require('../../../helpers/constants/routes')
// classes from nodes of the toggle element. // classes from nodes of the toggle element.
const notToggleElementClassnames = [ const notToggleElementClassnames = [
@ -49,6 +49,7 @@ function mapDispatchToProps (dispatch) {
}, },
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
setNetworksTabAddMode: isInAddMode => dispatch(actions.setNetworksTabAddMode(isInAddMode)),
} }
} }
@ -72,7 +73,7 @@ module.exports = compose(
// TODO: specify default props and proptypes // TODO: specify default props and proptypes
NetworkDropdown.prototype.render = function () { NetworkDropdown.prototype.render = function () {
const props = this.props const props = this.props
const { provider: { type: providerType, rpcTarget: activeNetwork } } = props const { provider: { type: providerType, rpcTarget: activeNetwork }, setNetworksTabAddMode } = props
const rpcListDetail = props.frequentRpcListDetail const rpcListDetail = props.frequentRpcListDetail
const isOpen = this.props.networkDropdownOpen const isOpen = this.props.networkDropdownOpen
const dropdownMenuItemStyle = { const dropdownMenuItemStyle = {
@ -255,7 +256,10 @@ NetworkDropdown.prototype.render = function () {
DropdownMenuItem, DropdownMenuItem,
{ {
closeMenu: () => this.props.hideNetworkDropdown(), closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => this.props.history.push(ADVANCED_ROUTE), onClick: () => {
setNetworksTabAddMode(true)
this.props.history.push(NETWORKS_ROUTE)
},
style: dropdownMenuItemStyle, style: dropdownMenuItemStyle,
}, },
[ [

View File

@ -5,7 +5,7 @@ const inherits = require('util').inherits
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../../store/actions') const actions = require('../../../store/actions')
const AccountModalContainer = require('./account-modal-container') const AccountModalContainer = require('./account-modal-container')
const { getSelectedIdentity } = require('../../../selectors/selectors') const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors')
const genAccountLink = require('../../../../lib/account-link.js') const genAccountLink = require('../../../../lib/account-link.js')
const QrView = require('../../ui/qr-code') const QrView = require('../../ui/qr-code')
const EditableLabel = require('../../ui/editable-label') const EditableLabel = require('../../ui/editable-label')
@ -17,6 +17,7 @@ function mapStateToProps (state) {
network: state.metamask.network, network: state.metamask.network,
selectedIdentity: getSelectedIdentity(state), selectedIdentity: getSelectedIdentity(state),
keyrings: state.metamask.keyrings, keyrings: state.metamask.keyrings,
rpcPrefs: getRpcPrefsForCurrentProvider(state),
} }
} }
@ -54,6 +55,7 @@ AccountDetailsModal.prototype.render = function () {
showExportPrivateKeyModal, showExportPrivateKeyModal,
setAccountLabel, setAccountLabel,
keyrings, keyrings,
rpcPrefs,
} = this.props } = this.props
const { name, address } = selectedIdentity const { name, address } = selectedIdentity
@ -86,8 +88,12 @@ AccountDetailsModal.prototype.render = function () {
h(Button, { h(Button, {
type: 'secondary', type: 'secondary',
className: 'account-modal__button', className: 'account-modal__button',
onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }), onClick: () => {
}, this.context.t('etherscanView')), global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) })
},
}, (rpcPrefs.blockExplorerUrl
? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]])
: this.context.t('viewOnEtherscan'))),
// Holding on redesign for Export Private Key functionality // Holding on redesign for Export Private Key functionality

View File

@ -1,13 +1,15 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import copyToClipboard from 'copy-to-clipboard' import copyToClipboard from 'copy-to-clipboard'
import {
getBlockExplorerUrlForTx,
} from '../../../helpers/utils/transactions.util'
import SenderToRecipient from '../../ui/sender-to-recipient' import SenderToRecipient from '../../ui/sender-to-recipient'
import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants' import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants'
import TransactionActivityLog from '../transaction-activity-log' import TransactionActivityLog from '../transaction-activity-log'
import TransactionBreakdown from '../transaction-breakdown' import TransactionBreakdown from '../transaction-breakdown'
import Button from '../../ui/button' import Button from '../../ui/button'
import Tooltip from '../../ui/tooltip' import Tooltip from '../../ui/tooltip'
import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network'
export default class TransactionListItemDetails extends PureComponent { export default class TransactionListItemDetails extends PureComponent {
static contextTypes = { static contextTypes = {
@ -22,6 +24,7 @@ export default class TransactionListItemDetails extends PureComponent {
showRetry: PropTypes.bool, showRetry: PropTypes.bool,
cancelDisabled: PropTypes.bool, cancelDisabled: PropTypes.bool,
transactionGroup: PropTypes.object, transactionGroup: PropTypes.object,
rpcPrefs: PropTypes.object,
} }
state = { state = {
@ -30,12 +33,9 @@ export default class TransactionListItemDetails extends PureComponent {
} }
handleEtherscanClick = () => { handleEtherscanClick = () => {
const { transactionGroup: { primaryTransaction } } = this.props const { transactionGroup: { primaryTransaction }, rpcPrefs } = this.props
const { hash, metamaskNetworkId } = primaryTransaction const { hash, metamaskNetworkId } = primaryTransaction
const prefix = prefixForNetwork(metamaskNetworkId)
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
this.context.metricsEvent({ this.context.metricsEvent({
eventOpts: { eventOpts: {
category: 'Navigation', category: 'Navigation',
@ -44,7 +44,7 @@ export default class TransactionListItemDetails extends PureComponent {
}, },
}) })
global.platform.openWindow({ url: etherscanUrl }) global.platform.openWindow({ url: getBlockExplorerUrlForTx(metamaskNetworkId, hash, rpcPrefs) })
} }
handleCancel = event => { handleCancel = event => {
@ -125,6 +125,7 @@ export default class TransactionListItemDetails extends PureComponent {
showRetry, showRetry,
onCancel, onCancel,
onRetry, onRetry,
rpcPrefs: { blockExplorerUrl } = {},
} = this.props } = this.props
const { primaryTransaction: transaction } = transactionGroup const { primaryTransaction: transaction } = transactionGroup
const { txParams: { to, from } = {} } = transaction const { txParams: { to, from } = {} } = transaction
@ -158,7 +159,7 @@ export default class TransactionListItemDetails extends PureComponent {
/> />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title={t('viewOnEtherscan')}> <Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('viewOnEtherscan')}>
<Button <Button
type="raised" type="raised"
onClick={this.handleEtherscanClick} onClick={this.handleEtherscanClick}

View File

@ -33,6 +33,7 @@ export default class TransactionListItem extends PureComponent {
value: PropTypes.string, value: PropTypes.string,
fetchBasicGasAndTimeEstimates: PropTypes.func, fetchBasicGasAndTimeEstimates: PropTypes.func,
fetchGasEstimates: PropTypes.func, fetchGasEstimates: PropTypes.func,
rpcPrefs: PropTypes.object,
} }
static defaultProps = { static defaultProps = {
@ -161,6 +162,7 @@ export default class TransactionListItem extends PureComponent {
showRetry, showRetry,
tokenData, tokenData,
transactionGroup, transactionGroup,
rpcPrefs,
} = this.props } = this.props
const { txParams = {} } = transaction const { txParams = {} } = transaction
const { showTransactionDetails } = this.state const { showTransactionDetails } = this.state
@ -216,6 +218,7 @@ export default class TransactionListItem extends PureComponent {
onCancel={this.handleCancel} onCancel={this.handleCancel}
showCancel={showCancel} showCancel={showCancel}
cancelDisabled={!hasEnoughCancelGas} cancelDisabled={!hasEnoughCancelGas}
rpcPrefs={rpcPrefs}
/> />
</div> </div>
) )

View File

@ -18,12 +18,14 @@ import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSe
import { isBalanceSufficient } from '../../../pages/send/send.utils' import { isBalanceSufficient } from '../../../pages/send/send.utils'
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const { metamask: { knownMethodData, accounts } } = state const { metamask: { knownMethodData, accounts, provider, frequentRpcListDetail } } = state
const { showFiatInTestnets } = preferencesSelector(state) const { showFiatInTestnets } = preferencesSelector(state)
const isMainnet = getIsMainnet(state) const isMainnet = getIsMainnet(state)
const { transactionGroup: { primaryTransaction } = {} } = ownProps const { transactionGroup: { primaryTransaction } = {} } = ownProps
const { txParams: { gas: gasLimit, gasPrice } = {} } = primaryTransaction const { txParams: { gas: gasLimit, gasPrice } = {} } = primaryTransaction
const selectedAccountBalance = accounts[getSelectedAddress(state)].balance const selectedAccountBalance = accounts[getSelectedAddress(state)].balance
const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget)
const { rpcPrefs } = selectRpcInfo || {}
const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({ const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({
amount: '0x0', amount: '0x0',
@ -40,6 +42,7 @@ const mapStateToProps = (state, ownProps) => {
showFiat: (isMainnet || !!showFiatInTestnets), showFiat: (isMainnet || !!showFiatInTestnets),
selectedAccountBalance, selectedAccountBalance,
hasEnoughCancelGas, hasEnoughCancelGas,
rpcPrefs,
} }
} }

View File

@ -41,11 +41,11 @@ const styles = {
inputFocused: {}, inputFocused: {},
inputRoot: { inputRoot: {
'label + &': { 'label + &': {
marginTop: '8px', marginTop: '9px',
}, },
border: '1px solid #d2d8dd', border: '2px solid #BBC0C5',
height: '48px', height: '48px',
borderRadius: '4px', borderRadius: '6px',
padding: '0 16px', padding: '0 16px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

View File

@ -77,6 +77,8 @@ function reduceApp (state, action) {
ledger: `m/44'/60'/0'/0/0`, ledger: `m/44'/60'/0'/0/0`,
}, },
lastSelectedProvider: null, lastSelectedProvider: null,
networksTabSelectedRpcUrl: '',
networksTabIsInAddMode: false,
}, state.appState) }, state.appState)
switch (action.type) { switch (action.type) {
@ -751,6 +753,16 @@ function reduceApp (state, action) {
lastSelectedProvider: action.value, lastSelectedProvider: action.value,
}) })
case actions.SET_SELECTED_SETTINGS_RPC_URL:
return extend(appState, {
networksTabSelectedRpcUrl: action.value,
})
case actions.SET_NETWORKS_TAB_ADD_MODE:
return extend(appState, {
networksTabIsInAddMode: action.value,
})
default: default:
return appState return appState
} }

View File

@ -8,6 +8,7 @@ const ADVANCED_ROUTE = '/settings/advanced'
const SECURITY_ROUTE = '/settings/security' const SECURITY_ROUTE = '/settings/security'
const COMPANY_ROUTE = '/settings/company' const COMPANY_ROUTE = '/settings/company'
const ABOUT_US_ROUTE = '/settings/about-us' const ABOUT_US_ROUTE = '/settings/about-us'
const NETWORKS_ROUTE = '/settings/networks'
const REVEAL_SEED_ROUTE = '/seed' const REVEAL_SEED_ROUTE = '/seed'
const MOBILE_SYNC_ROUTE = '/mobile-sync' const MOBILE_SYNC_ROUTE = '/mobile-sync'
const CONFIRM_SEED_ROUTE = '/confirm-seed' const CONFIRM_SEED_ROUTE = '/confirm-seed'
@ -86,4 +87,5 @@ module.exports = {
COMPANY_ROUTE, COMPANY_ROUTE,
GENERAL_ROUTE, GENERAL_ROUTE,
ABOUT_US_ROUTE, ABOUT_US_ROUTE,
NETWORKS_ROUTE,
} }

View File

@ -6,6 +6,8 @@ import {
TRANSACTION_TYPE_CANCEL, TRANSACTION_TYPE_CANCEL,
TRANSACTION_STATUS_CONFIRMED, TRANSACTION_STATUS_CONFIRMED,
} from '../../../../app/scripts/controllers/transactions/enums' } from '../../../../app/scripts/controllers/transactions/enums'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
import { import {
TOKEN_METHOD_TRANSFER, TOKEN_METHOD_TRANSFER,
@ -188,3 +190,17 @@ export function getStatusKey (transaction) {
return transaction.status return transaction.status
} }
/**
* Returns an external block explorer URL at which a transaction can be viewed.
* @param {number} networkId
* @param {string} hash
* @param {Object} rpcPrefs
*/
export function getBlockExplorerUrlForTx (networkId, hash, rpcPrefs = {}) {
if (rpcPrefs.blockExplorerUrl) {
return `${rpcPrefs.blockExplorerUrl}/tx/${hash}`
}
const prefix = prefixForNetwork(networkId)
return `https://${prefix}etherscan.io/tx/${hash}`
}

View File

@ -1,5 +1,7 @@
@import 'info-tab/index'; @import 'info-tab/index';
@import 'networks-tab/index';
@import 'settings-tab/index'; @import 'settings-tab/index';
.settings-page { .settings-page {
@ -13,7 +15,6 @@
flex-flow: row nowrap; flex-flow: row nowrap;
padding: 12px 24px; padding: 12px 24px;
align-items: center; align-items: center;
border-bottom: 1px solid $alto;
flex: 0 0 auto; flex: 0 0 auto;
&__title { &__title {
@ -33,6 +34,34 @@
} }
} }
&__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 { &__back-button {
display: none; display: none;
@ -60,8 +89,9 @@
&__content { &__content {
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
height: auto; height: 100%;
overflow: auto; overflow: auto;
border-top: 1px solid #D8D8D8;
&__tabs { &__tabs {
display: flex; display: flex;
@ -93,7 +123,7 @@
&__body { &__body {
padding: 12px 24px; padding: 12px 24px;
@media screen and (min-width: 576px) { @media screen and (min-width: 576px) {
padding: 12px; padding: 12px;
} }

View File

@ -0,0 +1 @@
export { default } from './networks-tab.container'

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export { default } from './network-form.component'

View File

@ -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>
)
}
}

View File

@ -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()
}
}

View File

@ -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,
}

View File

@ -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)

View File

@ -6,6 +6,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import TabBar from '../../components/app/tab-bar' import TabBar from '../../components/app/tab-bar'
import c from 'classnames' import c from 'classnames'
import SettingsTab from './settings-tab' import SettingsTab from './settings-tab'
import NetworksTab from './networks-tab'
import AdvancedTab from './advanced-tab' import AdvancedTab from './advanced-tab'
import InfoTab from './info-tab' import InfoTab from './info-tab'
import SecurityTab from './security-tab' import SecurityTab from './security-tab'
@ -16,6 +17,7 @@ import {
GENERAL_ROUTE, GENERAL_ROUTE,
ABOUT_US_ROUTE, ABOUT_US_ROUTE,
SETTINGS_ROUTE, SETTINGS_ROUTE,
NETWORKS_ROUTE,
} from '../../helpers/constants/routes' } from '../../helpers/constants/routes'
const ROUTES_TO_I18N_KEYS = { const ROUTES_TO_I18N_KEYS = {
@ -55,7 +57,7 @@ class SettingsPage extends PureComponent {
> >
<div className="settings-page__header"> <div className="settings-page__header">
{ {
!this.isCurrentPath(SETTINGS_ROUTE) && ( !this.isCurrentPath(SETTINGS_ROUTE) && !this.isCurrentPath(NETWORKS_ROUTE) && (
<div <div
className="settings-page__back-button" className="settings-page__back-button"
onClick={() => history.push(SETTINGS_ROUTE)} onClick={() => history.push(SETTINGS_ROUTE)}
@ -104,6 +106,7 @@ class SettingsPage extends PureComponent {
{ content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE }, { content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE },
{ content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE }, { content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE },
{ content: t('securityAndPrivacy'), description: t('securitySettingsDescription'), key: SECURITY_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 }, { content: t('about'), description: t('aboutSettingsDescription'), key: ABOUT_US_ROUTE },
]} ]}
isActive={key => { isActive={key => {
@ -135,6 +138,11 @@ class SettingsPage extends PureComponent {
path={ADVANCED_ROUTE} path={ADVANCED_ROUTE}
component={AdvancedTab} component={AdvancedTab}
/> />
<Route
exact
path={NETWORKS_ROUTE}
component={NetworksTab}
/>
<Route <Route
exact exact
path={SECURITY_ROUTE} path={SECURITY_ROUTE}

View File

@ -49,6 +49,7 @@ const selectors = {
getNumberOfTokens, getNumberOfTokens,
isEthereumNetwork, isEthereumNetwork,
getMetaMetricState, getMetaMetricState,
getRpcPrefsForCurrentProvider,
} }
module.exports = selectors module.exports = selectors
@ -327,3 +328,10 @@ function getMetaMetricState (state) {
participateInMetaMetrics: state.metamask.participateInMetaMetrics, participateInMetaMetrics: state.metamask.participateInMetaMetrics,
} }
} }
function getRpcPrefsForCurrentProvider (state) {
const { frequentRpcListDetail, provider } = state.metamask
const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget)
const { rpcPrefs = {} } = selectRpcInfo || {}
return rpcPrefs
}

View File

@ -239,6 +239,7 @@ var actions = {
updateAndSetCustomRpc: updateAndSetCustomRpc, updateAndSetCustomRpc: updateAndSetCustomRpc,
setRpcTarget: setRpcTarget, setRpcTarget: setRpcTarget,
delRpcTarget: delRpcTarget, delRpcTarget: delRpcTarget,
editRpc: editRpc,
setProviderType: setProviderType, setProviderType: setProviderType,
SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH', SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
setHardwareWalletDefaultHdPath, setHardwareWalletDefaultHdPath,
@ -350,6 +351,11 @@ var actions = {
setFirstTimeFlowType, setFirstTimeFlowType,
SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE', SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE',
SET_SELECTED_SETTINGS_RPC_URL: 'SET_SELECTED_SETTINGS_RPC_URL',
setSelectedSettingsRpcUrl,
SET_NETWORKS_TAB_ADD_MODE: 'SET_NETWORKS_TAB_ADD_MODE',
setNetworksTabAddMode,
} }
module.exports = actions module.exports = actions
@ -1958,10 +1964,10 @@ function setPreviousProvider (type) {
} }
} }
function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) { function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) {
return (dispatch) => { return (dispatch) => {
log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`) log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`)
background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err) => { background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => {
if (err) { if (err) {
log.error(err) log.error(err)
return dispatch(actions.displayWarning('Had a problem changing networks!')) return dispatch(actions.displayWarning('Had a problem changing networks!'))
@ -1974,6 +1980,29 @@ function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) {
} }
} }
function editRpc (oldRpc, newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) {
return (dispatch) => {
log.debug(`background.delRpcTarget: ${oldRpc}`)
background.delCustomRpc(oldRpc, (err) => {
if (err) {
log.error(err)
return dispatch(self.displayWarning('Had a problem removing network!'))
}
dispatch(actions.setSelectedToken())
background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => {
if (err) {
log.error(err)
return dispatch(actions.displayWarning('Had a problem changing networks!'))
}
dispatch({
type: actions.SET_RPC_TARGET,
value: newRpc,
})
})
})
}
}
function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) { function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) {
return (dispatch) => { return (dispatch) => {
log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`) log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`)
@ -2000,6 +2029,7 @@ function delRpcTarget (oldRpc) {
} }
} }
// Calls the addressBookController to add a new address. // Calls the addressBookController to add a new address.
function addToAddressBook (recipient, nickname = '') { function addToAddressBook (recipient, nickname = '') {
log.debug(`background.addToAddressBook`) log.debug(`background.addToAddressBook`)
@ -2716,3 +2746,17 @@ function setFirstTimeFlowType (type) {
}) })
} }
} }
function setSelectedSettingsRpcUrl (newRpcUrl) {
return {
type: actions.SET_SELECTED_SETTINGS_RPC_URL,
value: newRpcUrl,
}
}
function setNetworksTabAddMode (isInAddMode) {
return {
type: actions.SET_NETWORKS_TAB_ADD_MODE,
value: isInAddMode,
}
}

View File

@ -1,4 +1,8 @@
module.exports = function (address, network) { module.exports = function (address, network, rpcPrefs) {
if (rpcPrefs.blockExplorerUrl) {
return `${rpcPrefs.blockExplorerUrl}/address/${address}`
}
const net = parseInt(network) const net = parseInt(network)
let link let link
switch (net) { switch (net) {