1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-23 03:36:18 +02:00

Enable disconnecting a single or all accounts (#8496)

* enable disconnecting single or all accounts

* break up ConnectedSites render methods

* style and selector fixup
This commit is contained in:
Erik Marks 2020-05-01 20:38:12 -07:00 committed by GitHub
commit 11aa4029e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 204 additions and 96 deletions

View File

@ -16,10 +16,13 @@
"disconnect": { "disconnect": {
"message": "Disconnect" "message": "Disconnect"
}, },
"disconnectSite": { "disconnectAllAccounts": {
"message": "Disconnect $1?" "message": "Disconnect all accounts"
}, },
"disconnectAccountConfirmationDescription": { "disconnectPrompt": {
"message": "Disconnect $1"
},
"disconnectAllAccountsConfirmationDescription": {
"message": "Are you sure you want to disconnect? You may lose site functionality." "message": "Are you sure you want to disconnect? You may lose site functionality."
}, },
"dismiss": { "dismiss": {

View File

@ -138,7 +138,7 @@ describe('MetaMask', function () {
await driver.findElement(By.xpath(`//h2[contains(text(), 'Connected Sites')]`)) await driver.findElement(By.xpath(`//h2[contains(text(), 'Connected Sites')]`))
const domains = await driver.findClickableElements(By.css('.connected-sites__domain-name')) const domains = await driver.findClickableElements(By.css('.connected-sites-list__domain-name'))
assert.equal(domains.length, 1) assert.equal(domains.length, 1)
}) })

View File

@ -15,7 +15,6 @@ import {
getMetaMaskKeyrings, getMetaMaskKeyrings,
getOriginOfCurrentTab, getOriginOfCurrentTab,
getSelectedAddress, getSelectedAddress,
// getPermittedAccounts,
} from '../../../selectors/selectors' } from '../../../selectors/selectors'
import AccountMenu from './account-menu.component' import AccountMenu from './account-menu.component'

View File

@ -13,20 +13,20 @@ export default class ConnectedSitesList extends Component {
icon: PropTypes.string, icon: PropTypes.string,
key: PropTypes.string, key: PropTypes.string,
})).isRequired, })).isRequired,
onDisconnectSite: PropTypes.func.isRequired, onDisconnect: PropTypes.func.isRequired,
} }
render () { render () {
const { connectedDomains, onDisconnectSite } = this.props const { connectedDomains, onDisconnect } = this.props
const { t } = this.context const { t } = this.context
return ( return (
<main className="connected-sites__content-rows"> <main className="connected-sites-list__content-rows">
{ connectedDomains.map((domain) => ( { connectedDomains.map((domain) => (
<div key={domain.key} className="connected-sites__content-row"> <div key={domain.key} className="connected-sites-list__content-row">
<div className="connected-sites__domain-info"> <div className="connected-sites-list__domain-info">
<IconWithFallBack icon={domain.icon} name={domain.name} /> <IconWithFallBack icon={domain.icon} name={domain.name} />
<span className="connected-sites__domain-name" title={domain.extensionId || domain.key}> <span className="connected-sites-list__domain-name" title={domain.extensionId || domain.key}>
{ {
domain.extensionId domain.extensionId
? t('externalExtension') ? t('externalExtension')
@ -35,9 +35,9 @@ export default class ConnectedSitesList extends Component {
</span> </span>
</div> </div>
<i <i
className="fas fa-trash-alt connected-sites__trash" className="fas fa-trash-alt connected-sites-list__trash"
title={t('disconnect')} title={t('disconnect')}
onClick={() => onDisconnectSite(domain.key, domain.name)} onClick={() => onDisconnect(domain.key, domain.name)}
/> />
</div> </div>
)) } )) }

View File

@ -1,4 +1,4 @@
.connected-sites { .connected-sites-list {
&__content-rows { &__content-rows {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -2,10 +2,9 @@ import PropTypes from 'prop-types'
import React, { Component } from 'react' import React, { Component } from 'react'
import ConnectedSitesList from '../../components/app/connected-sites-list' import ConnectedSitesList from '../../components/app/connected-sites-list'
import Popover from '../../components/ui/popover/popover.component' import Popover from '../../components/ui/popover/popover.component'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
import Button from '../../components/ui/button' import Button from '../../components/ui/button'
export default class ConnectSites extends Component { export default class ConnectedSites extends Component {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
@ -15,25 +14,29 @@ export default class ConnectSites extends Component {
} }
static propTypes = { static propTypes = {
connectedDomains: PropTypes.arrayOf(PropTypes.object).isRequired,
accountLabel: PropTypes.string.isRequired, accountLabel: PropTypes.string.isRequired,
closePopover: PropTypes.func.isRequired,
connectedDomains: PropTypes.arrayOf(PropTypes.object).isRequired,
disconnectAllAccounts: PropTypes.func.isRequired,
disconnectAccount: PropTypes.func.isRequired, disconnectAccount: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
tabToConnect: PropTypes.object,
legacyExposeAccount: PropTypes.func.isRequired,
getOpenMetamaskTabsIds: PropTypes.func.isRequired, getOpenMetamaskTabsIds: PropTypes.func.isRequired,
legacyExposeAccount: PropTypes.func.isRequired,
permittedAccountsByOrigin: PropTypes.objectOf(
PropTypes.arrayOf(PropTypes.string),
).isRequired,
tabToConnect: PropTypes.object,
} }
state = { state = {
sitePendingDisconnect: null, sitePendingDisconnect: null,
} }
UNSAFE_componentWillMount () { componentDidMount () {
const { getOpenMetamaskTabsIds } = this.props const { getOpenMetamaskTabsIds } = this.props
getOpenMetamaskTabsIds() getOpenMetamaskTabsIds()
} }
setSitePendingDisconnect = (domainKey, domainName) => { setPendingDisconnect = (domainKey, domainName) => {
this.setState({ this.setState({
sitePendingDisconnect: { sitePendingDisconnect: {
domainKey, domainKey,
@ -42,73 +45,130 @@ export default class ConnectSites extends Component {
}) })
} }
clearSitePendingDisconnect = () => { clearPendingDisconnect = () => {
this.setState({ this.setState({
sitePendingDisconnect: null, sitePendingDisconnect: null,
}) })
} }
disconnect = () => { disconnectAccount = () => {
const { disconnectAccount } = this.props const { disconnectAccount } = this.props
const { sitePendingDisconnect } = this.state const { sitePendingDisconnect } = this.state
disconnectAccount(sitePendingDisconnect.domainKey) disconnectAccount(sitePendingDisconnect.domainKey)
this.clearSitePendingDisconnect() this.clearPendingDisconnect()
} }
renderConnectedSites () { disconnectAllAccounts = () => {
const { disconnectAllAccounts } = this.props
const { sitePendingDisconnect } = this.state
disconnectAllAccounts(sitePendingDisconnect.domainKey)
this.clearPendingDisconnect()
}
renderConnectedSitesList () {
return ( return (
<ConnectedSitesList <ConnectedSitesList
connectedDomains={this.props.connectedDomains} connectedDomains={this.props.connectedDomains}
onDisconnectSite={this.setSitePendingDisconnect} onDisconnect={this.setPendingDisconnect}
/>
)
}
renderConnectedSitesPopover () {
const {
accountLabel,
closePopover,
connectedDomains,
legacyExposeAccount,
tabToConnect,
} = this.props
const { t } = this.context
return (
<Popover
className="connected-sites"
title={t('connectedSites')}
subtitle={connectedDomains.length
? t('connectedSitesDescription', [accountLabel])
: t('connectedSitesEmptyDescription', [accountLabel])
}
onClose={closePopover}
footer={
tabToConnect
? (
<a
className="connected-sites__text-button"
onClick={legacyExposeAccount}
>
{t('connectManually')}
</a>
)
: null
}
footerClassName="connected-sites__add-site-manually"
>
{this.renderConnectedSitesList()}
</Popover>
)
}
renderDisconnectPopover () {
const { closePopover, permittedAccountsByOrigin } = this.props
const { t } = this.context
const { sitePendingDisconnect: { domainKey, domainName } } = this.state
const numPermittedAccounts = permittedAccountsByOrigin[domainKey].length
return (
<Popover
className="connected-sites"
title={t('disconnectPrompt', [domainName])}
subtitle={t('disconnectAllAccountsConfirmationDescription')}
onClose={closePopover}
footer={(
<>
<div className="connected-sites__footer-row">
<Button type="secondary" onClick={this.clearPendingDisconnect}>
{ t('cancel') }
</Button>
<Button
type="primary"
onClick={this.disconnectAccount}
>
{ t('disconnect') }
</Button>
</div>
{
numPermittedAccounts > 1
? (
<div className="connected-sites__footer-row">
<a
className="connected-sites__text-button"
onClick={this.disconnectAllAccounts}
>
{t('disconnectAllAccounts')}
</a>
</div>
)
: null
}
</>
)}
footerClassName="connected-sites__confirmation"
/> />
) )
} }
render () { render () {
const { accountLabel, history, legacyExposeAccount, tabToConnect, connectedDomains } = this.props
const { t } = this.context
const { sitePendingDisconnect } = this.state const { sitePendingDisconnect } = this.state
return ( return (
sitePendingDisconnect sitePendingDisconnect
? ( ? this.renderDisconnectPopover()
<Popover : this.renderConnectedSitesPopover()
title={t('disconnectSite', [sitePendingDisconnect.domainName])}
subtitle={t('disconnectAccountConfirmationDescription')}
onClose={() => history.push(DEFAULT_ROUTE)}
footer={(
<>
<Button type="secondary" onClick={this.clearSitePendingDisconnect}>
{ t('cancel') }
</Button>
<Button type="primary" onClick={this.disconnect}>
{ t('disconnect') }
</Button>
</>
)}
footerClassName="connected-sites__confirmation"
/>
)
: (
<Popover
title={t('connectedSites')}
subtitle={connectedDomains.length
? t('connectedSitesDescription', [accountLabel])
: t('connectedSitesEmptyDescription', [accountLabel])
}
onClose={() => history.push(DEFAULT_ROUTE)}
footer={
tabToConnect
? (
<a onClick={legacyExposeAccount}>{ t('connectManually') }</a>
)
: null
}
footerClassName="connected-sites__add-site-manually"
>
{this.renderConnectedSites()}
</Popover>
)
) )
} }
} }

View File

@ -1,23 +1,38 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import ConnectedSites from './connected-sites.component' import ConnectedSites from './connected-sites.component'
import { getOpenMetamaskTabsIds, legacyExposeAccounts, removePermissionsFor } from '../../store/actions' import {
getOpenMetamaskTabsIds,
legacyExposeAccounts,
removePermissionsFor,
removePermittedAccount,
} from '../../store/actions'
import { import {
getConnectedDomainsForSelectedAddress, getConnectedDomainsForSelectedAddress,
getCurrentAccountWithSendEtherInfo, getCurrentAccountWithSendEtherInfo,
getPermissionsDomains, getOriginOfCurrentTab,
getPermittedAccountsForCurrentTab,
getSelectedAddress, getSelectedAddress,
} from '../../selectors/selectors' } from '../../selectors/selectors'
import {
getPermissionsDomains,
getPermittedAccountsByOrigin,
} from '../../selectors/permissions'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
import { getOriginFromUrl } from '../../helpers/utils/util' import { getOriginFromUrl } from '../../helpers/utils/util'
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { openMetaMaskTabs } = state.appState const { openMetaMaskTabs } = state.appState
const { title, url, id } = state.activeTab const { title, url, id } = state.activeTab
const permittedAccounts = getPermittedAccountsForCurrentTab(state)
const connectedDomains = getConnectedDomainsForSelectedAddress(state) const connectedDomains = getConnectedDomainsForSelectedAddress(state)
const originOfCurrentTab = getOriginOfCurrentTab(state)
const permittedAccountsByOrigin = getPermittedAccountsByOrigin(state)
const selectedAddress = getSelectedAddress(state)
const currentTabHasNoAccounts = !permittedAccountsByOrigin[
originOfCurrentTab
]?.length
let tabToConnect let tabToConnect
if (url && permittedAccounts.length === 0 && !openMetaMaskTabs[id]) { if (url && currentTabHasNoAccounts && !openMetaMaskTabs[id]) {
tabToConnect = { tabToConnect = {
title, title,
origin: getOriginFromUrl(url), origin: getOriginFromUrl(url),
@ -28,7 +43,8 @@ const mapStateToProps = (state) => {
accountLabel: getCurrentAccountWithSendEtherInfo(state).name, accountLabel: getCurrentAccountWithSendEtherInfo(state).name,
connectedDomains, connectedDomains,
domains: getPermissionsDomains(state), domains: getPermissionsDomains(state),
selectedAddress: getSelectedAddress(state), permittedAccountsByOrigin,
selectedAddress,
tabToConnect, tabToConnect,
} }
} }
@ -36,7 +52,10 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
getOpenMetamaskTabsIds: () => dispatch(getOpenMetamaskTabsIds()), getOpenMetamaskTabsIds: () => dispatch(getOpenMetamaskTabsIds()),
disconnectAccount: (domainKey, domain) => { disconnectAccount: (domainKey, address) => {
dispatch(removePermittedAccount(domainKey, address))
},
disconnectAllAccounts: (domainKey, domain) => {
const permissionMethodNames = domain.permissions.map(({ parentCapability }) => parentCapability) const permissionMethodNames = domain.permissions.map(({ parentCapability }) => parentCapability)
dispatch(removePermissionsFor({ dispatch(removePermissionsFor({
[domainKey]: permissionMethodNames, [domainKey]: permissionMethodNames,
@ -47,17 +66,38 @@ const mapDispatchToProps = (dispatch) => {
} }
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { domains, selectedAddress, tabToConnect } = stateProps const {
connectedDomains,
domains,
selectedAddress,
tabToConnect,
} = stateProps
const { const {
disconnectAccount, disconnectAccount,
disconnectAllAccounts,
legacyExposeAccounts: dispatchLegacyExposeAccounts, legacyExposeAccounts: dispatchLegacyExposeAccounts,
} = dispatchProps } = dispatchProps
const { history } = ownProps
const closePopover = () => history.push(DEFAULT_ROUTE)
return { return {
...ownProps, ...ownProps,
...stateProps, ...stateProps,
...dispatchProps, ...dispatchProps,
disconnectAccount: (domainKey) => disconnectAccount(domainKey, domains[domainKey]), closePopover,
disconnectAccount: (domainKey) => {
disconnectAccount(domainKey, selectedAddress)
if (connectedDomains.length === 1) {
closePopover()
}
},
disconnectAllAccounts: (domainKey) => {
disconnectAllAccounts(domainKey, domains[domainKey])
if (connectedDomains.length === 1) {
closePopover()
}
},
legacyExposeAccount: () => dispatchLegacyExposeAccounts(tabToConnect.origin, selectedAddress), legacyExposeAccount: () => dispatchLegacyExposeAccounts(tabToConnect.origin, selectedAddress),
} }
} }

View File

@ -1,22 +1,32 @@
.connected-sites { .connected-sites {
h2 {
text-overflow: ellipsis;
margin-right: 10px;
}
&__confirmation { &__confirmation {
flex-direction: column;
button:first-child { button:first-child {
margin-right: 24px; margin-right: 24px;
} }
} }
&__add-site-manually { &__footer-row {
margin-top: -1px; display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
&__footer-row + &__footer-row {
margin-top: 15px;
}
a, a:hover {
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
color: #037DD6;
& :only-child { cursor: pointer;
margin: 0;
}
a, a:hover {
cursor: pointer;
color: #037DD6;
}
} }
} }

View File

@ -6,9 +6,11 @@ import {
getNativeCurrency, getNativeCurrency,
getAccountsWithLabels, getAccountsWithLabels,
getLastConnectedInfo, getLastConnectedInfo,
getPermissionsDomains,
getTargetDomainMetadata, getTargetDomainMetadata,
} from '../../selectors/selectors' } from '../../selectors/selectors'
import {
getPermissionsDomains,
} from '../../selectors/permissions'
import { formatDate } from '../../helpers/utils/util' import { formatDate } from '../../helpers/utils/util'
import { approvePermissionsRequest, rejectPermissionsRequest, showModal, getCurrentWindowTab, getRequestAccountTabIds } from '../../store/actions' import { approvePermissionsRequest, rejectPermissionsRequest, showModal, getCurrentWindowTab, getRequestAccountTabIds } from '../../store/actions'
import { import {

View File

@ -25,7 +25,7 @@ export function getPermittedAccounts (state, origin) {
* @returns {Object} Permitted accounts by origin. * @returns {Object} Permitted accounts by origin.
*/ */
export function getPermittedAccountsByOrigin (state) { export function getPermittedAccountsByOrigin (state) {
const domains = allDomainsSelector(state) const domains = getPermissionsDomains(state)
return Object.keys(domains).reduce((acc, domainKey) => { return Object.keys(domains).reduce((acc, domainKey) => {
const accounts = getAccountsFromPermission( const accounts = getAccountsFromPermission(
getAccountsPermissionFromDomain(domains[domainKey]) getAccountsPermissionFromDomain(domains[domainKey])
@ -67,7 +67,7 @@ function getAccountsCaveatFromPermission (accountsPermission = {}) {
) )
} }
function allDomainsSelector (state) { export function getPermissionsDomains (state) {
return state.metamask.domains || {} return state.metamask.domains || {}
} }

View File

@ -14,8 +14,6 @@ import {
import { getPermittedAccountsByOrigin } from './permissions' import { getPermittedAccountsByOrigin } from './permissions'
export { getPermittedAccounts } from './permissions'
export function getNetworkIdentifier (state) { export function getNetworkIdentifier (state) {
const { metamask: { provider: { type, nickname, rpcTarget } } } = state const { metamask: { provider: { type, nickname, rpcTarget } } } = state
@ -431,10 +429,6 @@ export function hasPermissionRequests (state) {
return Boolean(getFirstPermissionRequest(state)) return Boolean(getFirstPermissionRequest(state))
} }
export function getPermissionsDomains (state) {
return state.metamask.domains
}
export function getAddressConnectedDomainMap (state) { export function getAddressConnectedDomainMap (state) {
const { const {
domainMetadata, domainMetadata,