1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Remove selected address history (#8104)

* remove selected address history, account switching; fix perm selectors, bugs

Co-Authored-By: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
Erik Marks 2020-02-25 14:39:38 -08:00 committed by GitHub
parent 8d0a757ab5
commit 83da3db37b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 84 additions and 453 deletions

View File

@ -61,8 +61,6 @@ class PreferencesController {
// ENS decentralized website resolution
ipfsGateway: 'ipfs.dweb.link',
lastSelectedAddressByOrigin: {},
}, opts.initState)
this.diagnostics = opts.diagnostics
@ -371,56 +369,6 @@ class PreferencesController {
return this.store.getState().selectedAddress
}
/**
* Update the last selected address for the given origin.
*
* @param {string} origin - The origin for which the address was selected.
* @param {string} address - The new selected address.
*/
setLastSelectedAddress (origin, address) {
const { lastSelectedAddressByOrigin } = this.store.getState()
// only update state if it's necessary
if (lastSelectedAddressByOrigin[origin] !== address) {
lastSelectedAddressByOrigin[origin] = address
this.store.updateState({ lastSelectedAddressByOrigin })
}
}
/**
* Remove the selected address history for the given origin.
*
* @param {Array<string>} origins - The origin to remove the last selected address for.
*/
removeLastSelectedAddressesFor (origins) {
if (
!Array.isArray(origins) ||
(origins.length > 0 && typeof origins[0] !== 'string')
) {
throw new Error('Expected array of strings')
}
if (origins.length === 0) {
return
}
const { lastSelectedAddressByOrigin } = this.store.getState()
origins.forEach((origin) => {
delete lastSelectedAddressByOrigin[origin]
})
this.store.updateState({ lastSelectedAddressByOrigin })
}
/**
* Clears the selected address history.
*/
clearLastSelectedAddressHistory () {
this.store.updateState({ lastSelectedAddressByOrigin: {} })
}
/**
* Contains data about tokens users add to their account.
* @typedef {Object} AddedToken

View File

@ -499,8 +499,6 @@ export default class MetamaskController extends EventEmitter {
setPreference: nodeify(preferencesController.setPreference, preferencesController),
completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController),
addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController),
clearLastSelectedAddressHistory: nodeify(preferencesController.clearLastSelectedAddressHistory, preferencesController),
removeLastSelectedAddressesFor: nodeify(preferencesController.removeLastSelectedAddressesFor, preferencesController),
// BlacklistController
whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),
@ -1054,7 +1052,6 @@ export default class MetamaskController extends EventEmitter {
*/
async handleNewAccountSelected (origin, address) {
this.permissionsController.handleNewAccountSelected(origin, address)
this.preferencesController.setLastSelectedAddress(origin, address)
}
// ---------------------------------------------------------------------------

View File

@ -15,7 +15,6 @@ import {
getMetaMaskKeyrings,
getOriginOfCurrentTab,
getSelectedAddress,
// getLastSelectedAddress,
// getPermittedAccounts,
} from '../../../selectors/selectors'
import AccountMenu from './account-menu.component'
@ -31,14 +30,6 @@ function mapStateToProps (state) {
const origin = getOriginOfCurrentTab(state)
const selectedAddress = getSelectedAddress(state)
/**
* TODO:LoginPerSite:ui
* - propagate the relevant props below after computing them
*/
// const lastSelectedAddress = getLastSelectedAddress(state, origin)
// const permittedAccounts = getPermittedAccounts(state, origin)
// const selectedAccountIsPermitted = permittedAccounts.includes(selectedAddress)
return {
isAccountMenuOpen,
addressConnectedDomainMap: getAddressConnectedDomainMap(state),

View File

@ -9,19 +9,19 @@ import {
import {
getRenderablePermissionsDomains,
getPermissionsDomains,
getAddressConnectedToCurrentTab,
getSelectedAddress,
getPermittedAccountsForCurrentTab,
} from '../../../selectors/selectors'
import { getOriginFromUrl } from '../../../helpers/utils/util'
const mapStateToProps = (state) => {
const addressConnectedToCurrentTab = getAddressConnectedToCurrentTab(state)
const { openMetaMaskTabs } = state.appState
const { title, url, id } = state.activeTab
const permittedAccounts = getPermittedAccountsForCurrentTab(state)
let tabToConnect
if (!addressConnectedToCurrentTab && url && !openMetaMaskTabs[id]) {
if (url && permittedAccounts.length === 0 && !openMetaMaskTabs[id]) {
tabToConnect = {
title,
origin: getOriginFromUrl(url),

View File

@ -10,7 +10,6 @@ import {
getNetworkIdentifier,
preferencesSelector,
hasPermissionRequests,
getAddressConnectedToCurrentTab,
} from '../../selectors/selectors'
import classnames from 'classnames'
@ -109,20 +108,6 @@ class Routes extends Component {
})
}
componentDidMount () {
const { addressConnectedToCurrentTab, showAccountDetail, selectedAddress } = this.props
if (addressConnectedToCurrentTab && addressConnectedToCurrentTab !== selectedAddress) {
showAccountDetail(addressConnectedToCurrentTab)
}
}
componentDidUpdate (prevProps) {
const { addressConnectedToCurrentTab, showAccountDetail } = this.props
if (addressConnectedToCurrentTab && addressConnectedToCurrentTab !== prevProps.addressConnectedToCurrentTab) {
showAccountDetail(addressConnectedToCurrentTab)
}
}
renderRoutes () {
const { autoLockTimeLimit, setLastActiveTime } = this.props
@ -363,7 +348,6 @@ Routes.propTypes = {
textDirection: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
selectedAddress: PropTypes.string,
frequentRpcListDetail: PropTypes.array,
sidebar: PropTypes.object,
alertOpen: PropTypes.bool,
@ -379,12 +363,6 @@ Routes.propTypes = {
providerId: PropTypes.string,
hasPermissionsRequests: PropTypes.bool,
autoLockTimeLimit: PropTypes.number,
addressConnectedToCurrentTab: PropTypes.string,
showAccountDetail: PropTypes.func,
}
Routes.defaultProps = {
selectedAddress: undefined,
}
function mapStateToProps (state) {
@ -418,7 +396,6 @@ function mapStateToProps (state) {
providerId: getNetworkIdentifier(state),
autoLockTimeLimit,
hasPermissionsRequests: hasPermissionRequests(state),
addressConnectedToCurrentTab: getAddressConnectedToCurrentTab(state),
}
}

View File

@ -1 +0,0 @@
export { default } from './send-dropdown-list.component'

View File

@ -1,56 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import AccountListItem from '../../account-list-item'
export default class SendDropdownList extends Component {
static propTypes = {
accounts: PropTypes.array,
closeDropdown: PropTypes.func,
onSelect: PropTypes.func,
activeAddress: PropTypes.string,
}
static contextTypes = {
t: PropTypes.func,
}
getListItemIcon (accountAddress, activeAddress) {
return accountAddress === activeAddress
? <i className="fa fa-check fa-lg" style={ { color: '#02c9b1' } } />
: null
}
render () {
const {
accounts,
closeDropdown,
onSelect,
activeAddress,
} = this.props
return (
<div>
<div
className="send-v2__from-dropdown__close-area"
onClick={() => closeDropdown()}
/>
<div className="send-v2__from-dropdown__list">
{accounts.map((account, index) => (
<AccountListItem
account={account}
className="account-list-item__dropdown"
handleClick={() => {
onSelect(account)
closeDropdown()
}}
icon={this.getListItemIcon(account.address, activeAddress)}
key={`send-dropdown-account-#${index}`}
/>
))}
</div>
</div>
)
}
}

View File

@ -1,112 +0,0 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import SendDropdownList from '../send-dropdown-list.component.js'
import AccountListItem from '../../../account-list-item/account-list-item.container'
describe('SendDropdownList Component', function () {
let wrapper
const propsMethodSpies = {
closeDropdown: sinon.spy(),
onSelect: sinon.spy(),
}
before(function () {
sinon.spy(SendDropdownList.prototype, 'getListItemIcon')
})
beforeEach(function () {
wrapper = shallow((
<SendDropdownList
accounts={[
{ address: 'mockAccount0' },
{ address: 'mockAccount1' },
{ address: 'mockAccount2' },
]}
closeDropdown={propsMethodSpies.closeDropdown}
onSelect={propsMethodSpies.onSelect}
activeAddress="mockAddress2"
/>
), { context: { t: (str) => str + '_t' } })
})
afterEach(function () {
propsMethodSpies.closeDropdown.resetHistory()
propsMethodSpies.onSelect.resetHistory()
SendDropdownList.prototype.getListItemIcon.resetHistory()
})
after(function () {
sinon.restore()
})
describe('getListItemIcon', function () {
it('should return check icon if the passed addresses are the same', function () {
assert.deepEqual(
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'),
<i className="fa fa-check fa-lg" style={ { color: '#02c9b1' } } />
)
})
it('should return null if the passed addresses are different', function () {
assert.equal(
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'),
null
)
})
})
describe('render', function () {
it('should render a single div with two children', function () {
assert(wrapper.is('div'))
assert.equal(wrapper.children().length, 2)
})
it('should render the children with the correct classes', function () {
assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area'))
assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list'))
})
it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', function () {
assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
wrapper.childAt(0).props().onClick()
assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
})
it('should render an AccountListItem for each item in accounts', function () {
assert.equal(wrapper.childAt(1).children().length, 3)
assert(wrapper.childAt(1).children().every(AccountListItem))
})
it('should pass the correct props to the AccountListItem', function () {
wrapper.childAt(1).children().forEach((accountListItem, index) => {
const {
account,
className,
handleClick,
} = accountListItem.props()
assert.deepEqual(account, { address: 'mockAccount' + index })
assert.equal(className, 'account-list-item__dropdown')
assert.equal(propsMethodSpies.onSelect.callCount, 0)
handleClick()
assert.equal(propsMethodSpies.onSelect.callCount, 1)
assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index })
propsMethodSpies.onSelect.resetHistory()
propsMethodSpies.closeDropdown.resetHistory()
assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
handleClick()
assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
propsMethodSpies.onSelect.resetHistory()
propsMethodSpies.closeDropdown.resetHistory()
})
})
it('should call this.getListItemIcon for each AccountListItem', function () {
assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3)
const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls()
assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index))
})
})
})

View File

@ -1 +0,0 @@
export { default } from './send-from-row.container'

View File

@ -1,27 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper'
import AccountListItem from '../../account-list-item'
export default class SendFromRow extends Component {
static propTypes = {
from: PropTypes.object,
}
static contextTypes = {
t: PropTypes.func,
}
render () {
const { t } = this.context
const { from } = this.props
return (
<SendRowWrapper label={`${t('from')}:`}>
<div className="send-v2__from-dropdown">
<AccountListItem account={from} />
</div>
</SendRowWrapper>
)
}
}

View File

@ -1,11 +0,0 @@
import { connect } from 'react-redux'
import { getSendFromObject } from '../../send.selectors.js'
import SendFromRow from './send-from-row.component'
function mapStateToProps (state) {
return {
from: getSendFromObject(state),
}
}
export default connect(mapStateToProps)(SendFromRow)

View File

@ -1,31 +0,0 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import SendFromRow from '../send-from-row.component.js'
import AccountListItem from '../../../account-list-item'
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
describe('SendFromRow Component', function () {
describe('render', function () {
const wrapper = shallow(
<SendFromRow
from={ { address: 'mockAddress' } }
/>,
{ context: { t: (str) => str + '_t' } }
)
it('should render a SendRowWrapper component', function () {
assert.equal(wrapper.find(SendRowWrapper).length, 1)
})
it('should pass the correct props to SendRowWrapper', function () {
const { label } = wrapper.find(SendRowWrapper).props()
assert.equal(label, 'from_t:')
})
it('should render the FromDropdown with the correct props', function () {
const { account } = wrapper.find(AccountListItem).props()
assert.deepEqual(account, { address: 'mockAddress' })
})
})
})

View File

@ -1,26 +0,0 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../send-from-row.container.js', {
'react-redux': {
connect: (ms) => {
mapStateToProps = ms
return () => ({})
},
},
'../../send.selectors.js': {
getSendFromObject: (s) => `mockFrom:${s}`,
},
})
describe('send-from-row container', function () {
describe('mapStateToProps()', function () {
it('should map the correct properties to props', function () {
assert.deepEqual(mapStateToProps('mockState'), {
from: 'mockFrom:mockState',
})
})
})
})

View File

@ -1,18 +1,49 @@
import { createSelector } from 'reselect'
import {
CAVEAT_NAMES,
} from '../../../app/scripts/controllers/permissions/enums'
const permissionsSelector = (state, origin) => {
return origin && state.metamask.domains && state.metamask.domains[origin]
// selectors
/**
* Selects the permitted accounts from the eth_accounts permission given state
* and an origin.
* @param {Object} state - The current state.
* @param {string} origin - The origin/domain to get the permitted accounts for.
* @returns {Array<string>} An empty array or an array of accounts.
*/
export function getPermittedAccounts (state, origin) {
return getAccountsFromPermission(
getAccountsPermissionFromDomain(
domainSelector(state, origin)
)
)
}
// all permissions for the origin probably too expensive for deep equality check
const accountsPermissionSelector = createSelector(
permissionsSelector,
(domain = {}) => {
/**
* Returns a map of permitted accounts by origin for all origins.
* @param {Object} state - The current state.
* @returns {Object} Permitted accounts by origin.
*/
export const getPermittedAccountsMap = createSelector(
allDomainsSelector,
(domains = {}) => {
return Object.keys(domains).reduce((acc, domainKey) => {
const accounts = getAccountsFromPermission(
getAccountsPermissionFromDomain(domains[domainKey])
)
if (accounts.length > 0) {
acc[domainKey] = accounts
}
return acc
}, {})
}
)
// selector helpers
function getAccountsPermissionFromDomain (domain = {}) {
return (
Array.isArray(domain.permissions)
? domain.permissions.find(
@ -21,28 +52,29 @@ const accountsPermissionSelector = createSelector(
: {}
)
}
)
/**
* Selects the permitted accounts from an eth_accounts permission.
* Expects input from accountsPermissionsSelector.
* @returns - An empty array or an array of accounts.
*/
export const getPermittedAccounts = createSelector(
accountsPermissionSelector, // deep equal check performed on this output
(accountsPermission = {}) => {
const accountsCaveat = (
Array.isArray(accountsPermission.caveats) &&
accountsPermission.caveats.find(
(c) => c.name === CAVEAT_NAMES.exposedAccounts
)
)
function getAccountsFromPermission (accountsPermission) {
const accountsCaveat = getAccountsCaveatFromPermission(accountsPermission)
return (
accountsCaveat && Array.isArray(accountsCaveat.value)
? accountsCaveat.value
: []
)
}
function getAccountsCaveatFromPermission (accountsPermission = {}) {
return (
Array.isArray(accountsPermission.caveats) &&
accountsPermission.caveats.find(
(c) => c.name === CAVEAT_NAMES.exposedAccounts
)
)
}
function allDomainsSelector (state) {
return state.metamask.domains
}
function domainSelector (state, origin) {
return origin && state.metamask.domains && state.metamask.domains[origin]
}

View File

@ -1,5 +1,4 @@
import { NETWORK_TYPES } from '../helpers/constants/common'
import { mapObjectValues } from '../../../app/scripts/lib/util'
import { stripHexPrefix, addHexPrefix } from 'ethereumjs-util'
import { createSelector } from 'reselect'
@ -12,7 +11,7 @@ import {
getOriginFromUrl,
} from '../helpers/utils/util'
import { getPermittedAccounts } from './permissions'
import { getPermittedAccountsMap } from './permissions'
export { getPermittedAccounts } from './permissions'
@ -91,21 +90,6 @@ export function getSelectedAddress (state) {
return selectedAddress
}
function lastSelectedAddressSelector (state, origin) {
return state.metamask.lastSelectedAddressByOrigin[origin] || null
}
// not using reselect here since the returns are contingent;
// we have no reasons to recompute the permitted accounts if there
// exists a lastSelectedAddress
export function getLastSelectedAddress (state, origin) {
return (
lastSelectedAddressSelector(state, origin) ||
getPermittedAccounts(state, origin)[0] || // always returns array
getSelectedAddress(state)
)
}
export function getSelectedIdentity (state) {
const selectedAddress = getSelectedAddress(state)
const identities = state.metamask.identities
@ -434,60 +418,29 @@ export function getPermissionsDomains (state) {
export function getAddressConnectedDomainMap (state) {
const {
domains,
domainMetadata,
} = state.metamask
const accountsMap = getPermittedAccountsMap(state)
const addressConnectedIconMap = {}
if (domains) {
Object.keys(domains).forEach((domainKey) => {
const { permissions } = domains[domainKey]
Object.keys(accountsMap).forEach((domainKey) => {
const { icon, name } = domainMetadata[domainKey] || {}
permissions.forEach((perm) => {
const caveats = perm.caveats || []
const exposedAccountCaveat = caveats.find((caveat) => caveat.name === 'exposedAccounts')
if (exposedAccountCaveat && exposedAccountCaveat.value && exposedAccountCaveat.value.length) {
exposedAccountCaveat.value.forEach((address) => {
accountsMap[domainKey].forEach((address) => {
const nameToRender = name || domainKey
addressConnectedIconMap[address] = addressConnectedIconMap[address]
? { ...addressConnectedIconMap[address], [domainKey]: { icon, name: nameToRender } }
: { [domainKey]: { icon, name: nameToRender } }
})
}
})
})
}
return addressConnectedIconMap
}
export function getDomainToConnectedAddressMap (state) {
const { domains = {} } = state.metamask
const domainToConnectedAddressMap = mapObjectValues(domains, (_, { permissions }) => {
const ethAccountsPermissions = permissions.filter((permission) => permission.parentCapability === 'eth_accounts')
const ethAccountsPermissionsExposedAccountAddresses = ethAccountsPermissions.map((permission) => {
const caveats = permission.caveats
const exposedAccountsCaveats = caveats.filter((caveat) => caveat.name === 'exposedAccounts')
const exposedAccountsAddresses = exposedAccountsCaveats.map((caveat) => caveat.value[0])
return exposedAccountsAddresses
})
const allAddressesConnectedToDomain = ethAccountsPermissionsExposedAccountAddresses.reduce((acc, arrayOfAddresses) => {
return [ ...acc, ...arrayOfAddresses ]
}, [])
return allAddressesConnectedToDomain
})
return domainToConnectedAddressMap
}
export function getAddressConnectedToCurrentTab (state) {
const domainToConnectedAddressMap = getDomainToConnectedAddressMap(state)
export function getPermittedAccountsForCurrentTab (state) {
const permittedAccountsMap = getPermittedAccountsMap(state)
const originOfCurrentTab = getOriginOfCurrentTab(state)
const addressesConnectedToCurrentTab = domainToConnectedAddressMap[originOfCurrentTab]
const addressConnectedToCurrentTab = addressesConnectedToCurrentTab && addressesConnectedToCurrentTab[0]
return addressConnectedToCurrentTab
return permittedAccountsMap[originOfCurrentTab] || []
}
export function getRenderablePermissionsDomains (state) {

View File

@ -2322,7 +2322,6 @@ export function legacyExposeAccounts (origin, accounts) {
export function removePermissionsFor (domains) {
return () => {
background.removePermissionsFor(domains)
background.removeLastSelectedAddressesFor(Object.keys(domains))
}
}
@ -2332,7 +2331,6 @@ export function removePermissionsFor (domains) {
export function clearPermissions () {
return () => {
background.clearPermissions()
background.clearLastSelectedAddressHistory()
}
}