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

Add Connected Accounts modal (#8313)

This commit is contained in:
Whymarrh Whitby 2020-05-15 16:23:52 -02:30 committed by GitHub
parent 1c3d915aa3
commit d488c16df5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 966 additions and 5 deletions

View File

@ -10,6 +10,40 @@
"message": "$1 is not connected to any sites.",
"description": "$1 is the account name"
},
"connectedAccountsDescriptionSingular": {
"message": "You have 1 account connected to this site."
},
"connectedAccountsDescriptionPlural": {
"message": "You have $1 accounts connected to this site.",
"description": "$1 is the number of accounts"
},
"connectedAccountsEmptyDescription": {
"message": "MetaMask is not connected this site. To connect to a decentralized app (dapp), find the connect button on their site."
},
"primary": {
"message": "Primary"
},
"lastActive": {
"message": "Last active"
},
"switchToThisAccount": {
"message": "Switch to this account"
},
"disconnectThisAccount": {
"message": "Disconnect this account"
},
"viewConnectedSites": {
"message": "View connected sites"
},
"permissions": {
"message": "Permissions"
},
"showPermissions": {
"message": "Show permissions"
},
"authorizedPermissions": {
"message": "You have authorized the following permissions"
},
"connectManually": {
"message": "Manually connect to current site"
},

View File

@ -0,0 +1,54 @@
import classnames from 'classnames'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import Identicon from '../../../ui/identicon'
export default class ConnectedAccountsListItem extends PureComponent {
static contextTypes = {
t: PropTypes.func.isRequired,
}
static propTypes = {
address: PropTypes.string.isRequired,
className: PropTypes.string,
name: PropTypes.node.isRequired,
status: PropTypes.node.isRequired,
options: PropTypes.node,
}
static defaultProps = {
className: null,
options: null,
}
render () {
const {
address,
className,
name,
status,
options,
} = this.props
return (
<div className={classnames('connected-accounts-list__row', className)}>
<div className="connected-accounts-list__row-content">
<Identicon
className="connected-accounts-list__identicon"
address={address}
diameter={32}
/>
<div>
<p>
<strong className="connected-accounts-list__account-name">{name}</strong>
</p>
<p className="connected-accounts-list__account-status">
{status}
</p>
</div>
</div>
{options}
</div>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './connected-accounts-list-item.component'

View File

@ -0,0 +1,26 @@
import classnames from 'classnames'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
export default class ConnectedAccountsListOptionsItem extends PureComponent {
static propTypes = {
iconClassNames: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func,
}
static defaultProps = {
onClick: undefined,
}
render () {
const { children, iconClassNames, onClick } = this.props
return (
<button className="connected-accounts-options__row" onClick={onClick}>
<i className={classnames('connected-accounts-options__row-icon', iconClassNames)} />
<span>{children}</span>
</button>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './connected-accounts-list-options-item.component'

View File

@ -0,0 +1,34 @@
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import { Tooltip } from 'react-tippy'
export default class ConnectedAccountsListOptions extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
}
render () {
return (
<Tooltip
arrow={false}
animation="none"
animateFill={false}
transitionFlip={false}
hideDuration={0}
duration={0}
trigger="click"
interactive
theme="none"
position="bottom-end"
unmountHTMLWhenHide
html={(
<div className="connected-accounts-options">
{this.props.children}
</div>
)}
>
<i className="fas fa-ellipsis-v" />
</Tooltip>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './connected-accounts-list-options.component'

View File

@ -0,0 +1,72 @@
import classnames from 'classnames'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
export default class ConnectedAccountsListPermissions extends PureComponent {
static contextTypes = {
t: PropTypes.func.isRequired,
}
static defaultProps = {
permissions: [],
}
static propTypes = {
permissions: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
})),
}
state = {
expanded: false,
}
toggleExpanded = () => {
this.setState((prevState) => ({
expanded: !prevState.expanded,
}))
}
render () {
const { permissions } = this.props
const { t } = this.context
const { expanded } = this.state
if (permissions.length === 0) {
return null
}
return (
<div className="connected-accounts-permissions">
<p className="connected-accounts-permissions__header">
<strong>{t('permissions')}</strong>
<button
className={classnames('fas', {
'fa-angle-down': !expanded,
'fa-angle-up': expanded,
})}
title={t('showPermissions')}
onClick={this.toggleExpanded}
/>
</p>
{
expanded
? (
<>
<p>{t('authorizedPermissions')}:</p>
<ul className="connected-accounts-permissions__list">
{permissions.map(({ key, description }) => (
<li key={key} className="connected-accounts-permissions__list-item">
<i className="fas fa-check-square" />&nbsp;{description}
</li>
))}
</ul>
</>
)
: null
}
</div>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './connected-accounts-list-permissions.component'

View File

@ -0,0 +1,119 @@
import { DateTime } from 'luxon'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import ConnectedAccountsListPermissions from './connected-accounts-list-permissions'
import ConnectedAccountsListItem from './connected-accounts-list-item'
import ConnectedAccountsListOptions from './connected-accounts-list-options'
import ConnectedAccountsListOptionsItem from './connected-accounts-list-options-item'
export default class ConnectedAccountsList extends PureComponent {
static contextTypes = {
t: PropTypes.func.isRequired,
}
static defaultProps = {
accountToConnect: null,
permissions: undefined,
}
static propTypes = {
accountToConnect: PropTypes.shape({
address: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}),
connectedAccounts: PropTypes.arrayOf(PropTypes.shape({
address: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
lastActive: PropTypes.number.isRequired,
})).isRequired,
permissions: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
})),
selectedAddress: PropTypes.string.isRequired,
addPermittedAccount: PropTypes.func.isRequired,
removePermittedAccount: PropTypes.func.isRequired,
setSelectedAddress: PropTypes.func.isRequired,
}
connectAccount = (address) => () => {
this.props.addPermittedAccount(address)
}
disconnectAccount = (address) => () => {
this.props.removePermittedAccount(address)
}
switchAccount = (address) => () => {
this.props.setSelectedAddress(address)
}
renderUnconnectedAccount () {
const { accountToConnect } = this.props
const { t } = this.context
if (!accountToConnect) {
return null
}
const { address, name } = accountToConnect
return (
<ConnectedAccountsListItem
className="connected-accounts-list__row--highlight"
address={address}
name={`${name} (…${address.substr(-4, 4)})`}
status={(
<>
{t('statusNotConnected')}
&nbsp;&middot;&nbsp;
<a className="connected-accounts-list__account-status-link" onClick={this.connectAccount(address)}>
{t('connect')}
</a>
</>
)}
/>
)
}
render () {
const { connectedAccounts, permissions, selectedAddress } = this.props
const { t } = this.context
return (
<>
<main className="connected-accounts-list">
{this.renderUnconnectedAccount()}
{connectedAccounts.map(({ address, name, lastActive }, index) => (
<ConnectedAccountsListItem
key={address}
address={address}
name={`${name} (…${address.substr(-4, 4)})`}
status={index === 0 ? t('primary') : `${t('lastActive')}: ${DateTime.fromMillis(lastActive).toISODate()}`}
options={(
<ConnectedAccountsListOptions>
{
address === selectedAddress ? null : (
<ConnectedAccountsListOptionsItem
iconClassNames="fas fa-random"
onClick={this.switchAccount(address)}
>
{t('switchToThisAccount')}
</ConnectedAccountsListOptionsItem>
)
}
<ConnectedAccountsListOptionsItem
iconClassNames="fas fa-ban"
onClick={this.disconnectAccount(address)}
>
{t('disconnectThisAccount')}
</ConnectedAccountsListOptionsItem>
</ConnectedAccountsListOptions>
)}
/>
))}
</main>
<ConnectedAccountsListPermissions permissions={permissions} />
</>
)
}
}

View File

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

View File

@ -0,0 +1,152 @@
.connected-accounts-list {
display: flex;
flex-direction: column;
align-items: center;
&__identicon {
margin-right: 8px;
}
&__account-name {
font-weight: bold;
font-size: 14px;
line-height: 20px;
}
&__account-status {
color: $Grey-500;
}
&__account-status-link {
&, &:hover {
color: $curious-blue;
cursor: pointer;
}
}
&__account-status {
font-size: 12px;
line-height: 17px;
padding-top: 4px;
}
&__row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 16px 24px;
border-top: 1px solid $geyser;
&:last-of-type {
border-bottom: 1px solid $geyser;
}
&--highlight {
background-color: $warning-light-yellow;
border: 1px solid $warning-yellow;
box-sizing: border-box;
padding: 16px;
margin-bottom: 16px;
width: calc(100% - 16px)
}
}
&__row-content {
display: flex;
flex-direction: row;
align-items: center;
}
}
.connected-accounts-options {
background: $white;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.214);
border-radius: 8px;
min-width: 200px;
color: $Black-100;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 16px;
right: 24px;
font-family: Roboto, 'sans-serif';
font-size: 14px;
font-weight: normal;
line-height: 20px;
&__row {
background: none;
font-family: inherit;
font-size: inherit;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: 14px 0;
cursor: pointer;
&-icon {
margin-right: 8px;
}
}
}
.connected-accounts-permissions {
display: flex;
flex-direction: column;
padding: 24px;
font-size: 12px;
line-height: 17px;
color: #6A737D;
strong {
font-weight: bold;
}
p + p {
padding-top: 8px;
}
&__header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
font-size: 14px;
line-height: 20px;
color: #24292E;
button {
font-size: 16px;
line-height: 24px;
background: none;
padding: 0;
margin-left: 8px;
}
}
&__list {
padding-top: 8px;
}
&__list-item {
i {
padding-right: 8px;
font-size: 18px;
}
}
}
.tippy-tooltip.none-theme {
background: none;
padding: 0;
}

View File

@ -90,6 +90,8 @@
@import 'connected-sites-list/index';
@import 'connected-accounts-list/index';
@import '../ui/icon-with-fallback/index';
@import '../ui/circle-icon/index';

View File

@ -1 +1 @@
export { default } from './menu-bar.component'
export { default } from './menu-bar.container'

View File

@ -6,6 +6,7 @@ import ConnectedStatusIndicator from '../connected-status-indicator'
import AccountDetailsDropdown from '../dropdowns/account-details-dropdown'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
import { CONNECTED_ACCOUNTS_ROUTE } from '../../../helpers/constants/routes'
export default class MenuBar extends PureComponent {
static contextTypes = {
@ -13,9 +14,14 @@ export default class MenuBar extends PureComponent {
metricsEvent: PropTypes.func,
}
static propTypes = {
history: PropTypes.object.isRequired,
}
state = { accountDetailsMenuOpen: false }
render () {
const { history } = this.props
const { t } = this.context
const { accountDetailsMenuOpen } = this.state
@ -23,7 +29,7 @@ export default class MenuBar extends PureComponent {
<div className="menu-bar">
{
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
? <ConnectedStatusIndicator />
? <ConnectedStatusIndicator onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)} />
: null
}

View File

@ -0,0 +1,4 @@
import { withRouter } from 'react-router-dom'
import MenuBar from './menu-bar.component'
export default withRouter(MenuBar)

View File

@ -29,6 +29,7 @@ const SEND_ROUTE = '/send'
const CONNECT_ROUTE = '/connect'
const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'
const CONNECTED_ROUTE = '/connected'
const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'
const INITIALIZE_ROUTE = '/initialize'
const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome'
@ -106,4 +107,5 @@ export {
CONNECT_ROUTE,
CONNECT_CONFIRM_PERMISSIONS_ROUTE,
CONNECTED_ROUTE,
CONNECTED_ACCOUNTS_ROUTE,
}

View File

@ -0,0 +1,75 @@
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import { DEFAULT_ROUTE, CONNECTED_ROUTE } from '../../helpers/constants/routes'
import Popover from '../../components/ui/popover'
import ConnectedAccountsList from '../../components/app/connected-accounts-list'
export default class ConnectedAccounts extends PureComponent {
static contextTypes = {
t: PropTypes.func.isRequired,
}
static defaultProps = {
accountToConnect: null,
permissions: undefined,
}
static propTypes = {
accountToConnect: PropTypes.object,
activeTabOrigin: PropTypes.string.isRequired,
addPermittedAccount: PropTypes.func.isRequired,
connectedAccounts: PropTypes.array.isRequired,
permissions: PropTypes.array,
selectedAddress: PropTypes.string.isRequired,
removePermittedAccount: PropTypes.func.isRequired,
setSelectedAddress: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
}
viewConnectedSites = () => {
this.props.history.push(CONNECTED_ROUTE)
}
render () {
const {
accountToConnect,
activeTabOrigin,
addPermittedAccount,
connectedAccounts,
history,
permissions,
selectedAddress,
removePermittedAccount,
setSelectedAddress,
} = this.props
const { t } = this.context
const connectedAccountsDescription = connectedAccounts.length > 1
? t('connectedAccountsDescriptionPlural', [connectedAccounts.length])
: t('connectedAccountsDescriptionSingular')
return (
<Popover
title={activeTabOrigin}
subtitle={connectedAccounts.length ? connectedAccountsDescription : t('connectedAccountsEmptyDescription')}
onClose={() => history.push(DEFAULT_ROUTE)}
footerClassName="connected-accounts__footer"
footer={
connectedAccounts.length
? null
: <a onClick={this.viewConnectedSites}>{t('viewConnectedSites')}</a>
}
>
<ConnectedAccountsList
accountToConnect={accountToConnect}
addPermittedAccount={addPermittedAccount}
connectedAccounts={connectedAccounts}
permissions={permissions}
selectedAddress={selectedAddress}
removePermittedAccount={removePermittedAccount}
setSelectedAddress={setSelectedAddress}
/>
</Popover>
)
}
}

View File

@ -0,0 +1,47 @@
import { connect } from 'react-redux'
import ConnectedAccounts from './connected-accounts.component'
import {
getAccountToConnectToActiveTab,
getOrderedConnectedAccountsForActiveTab,
getPermissionsForActiveTab,
getSelectedAddress,
} from '../../selectors'
import { addPermittedAccount, removePermittedAccount, setSelectedAddress } from '../../store/actions'
const mapStateToProps = (state) => {
const { activeTab } = state
const accountToConnect = getAccountToConnectToActiveTab(state)
const connectedAccounts = getOrderedConnectedAccountsForActiveTab(state)
const permissions = getPermissionsForActiveTab(state)
const selectedAddress = getSelectedAddress(state)
return {
accountToConnect,
activeTabOrigin: activeTab.origin,
connectedAccounts,
permissions,
selectedAddress,
}
}
const mapDispatchToProps = (dispatch) => {
return {
addPermittedAccount: (origin, address) => dispatch(addPermittedAccount(origin, address)),
removePermittedAccount: (origin, address) => dispatch(removePermittedAccount(origin, address)),
setSelectedAddress: (address) => dispatch(setSelectedAddress(address)),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { activeTabOrigin: origin } = stateProps
return {
...ownProps,
...stateProps,
...dispatchProps,
addPermittedAccount: (address) => dispatchProps.addPermittedAccount(origin, address),
removePermittedAccount: (address) => dispatchProps.removePermittedAccount(origin, address),
}
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(ConnectedAccounts)

View File

@ -0,0 +1 @@
export { default } from './connected-accounts.container'

View File

@ -0,0 +1,9 @@
.connected-accounts {
&__footer {
a, a:hover {
color: #037DD6;
cursor: pointer;
font-size: 14px;
}
}
}

View File

@ -13,6 +13,7 @@ import MenuBar from '../../components/app/menu-bar'
import Popover from '../../components/ui/popover'
import Button from '../../components/ui/button'
import ConnectedSites from '../connected-sites'
import ConnectedAccounts from '../connected-accounts'
import { Tabs, Tab } from '../../components/ui/tabs'
import {
@ -22,6 +23,7 @@ import {
INITIALIZE_BACKUP_SEED_PHRASE_ROUTE,
CONNECT_ROUTE,
CONNECTED_ROUTE,
CONNECTED_ACCOUNTS_ROUTE,
} from '../../helpers/constants/routes'
export default class Home extends PureComponent {
@ -218,7 +220,8 @@ export default class Home extends PureComponent {
return (
<div className="main-container">
<Route path={CONNECTED_ROUTE} component={ConnectedSites} />
<Route path={CONNECTED_ROUTE} component={ConnectedSites} exact />
<Route path={CONNECTED_ACCOUNTS_ROUTE} component={ConnectedAccounts} exact />
<div className="home__container">
{ isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() : null }
<Media

View File

@ -12,6 +12,8 @@
@import 'connected-sites/index';
@import 'connected-accounts/index';
@import 'settings/index';
@import 'first-time-flow/index';

View File

@ -1,5 +1,5 @@
import { forOwn } from 'lodash'
import { getMetaMaskIdentities, getOriginOfCurrentTab } from './selectors'
import { getMetaMaskIdentities, getOriginOfCurrentTab, getSelectedAddress } from '.'
import {
CAVEAT_NAMES,
} from '../../../app/scripts/controllers/permissions/enums'
@ -194,3 +194,62 @@ function getAccountsCaveatFromPermission (accountsPermission = {}) {
function domainSelector (state, origin) {
return origin && state.metamask.domains?.[origin]
}
export function getAccountToConnectToActiveTab (state) {
const selectedAddress = getSelectedAddress(state)
const connectedAccounts = getPermittedAccountsForCurrentTab(state)
const { metamask: { identities } } = state
const numberOfAccounts = Object.keys(identities).length
if (connectedAccounts.length && connectedAccounts.length !== numberOfAccounts) {
if (connectedAccounts.findIndex((address) => address === selectedAddress) === -1) {
return identities[selectedAddress]
}
}
return undefined
}
export function getOrderedConnectedAccountsForActiveTab (state) {
const { activeTab, metamask } = state
const { identities, permissionsHistory } = metamask
const permissionsHistoryByAccount = permissionsHistory[activeTab.origin]?.['eth_accounts']?.accounts
return getPermittedAccountsForCurrentTab(state)
.map((address) => ({
address,
name: identities[address].name,
lastActive: permissionsHistoryByAccount[address],
}))
.sort(({ address: addressA }, { address: addressB }) => {
const lastSelectedA = identities[addressA].lastSelected
const lastSelectedB = identities[addressB].lastSelected
if (lastSelectedA === lastSelectedB) {
return 0
} else if (lastSelectedA === undefined) {
return 1
} else if (lastSelectedB === undefined) {
return -1
}
return lastSelectedB - lastSelectedA
})
}
export function getPermissionsForActiveTab (state) {
const { activeTab, metamask } = state
const {
domains = {},
permissionsDescriptions,
} = metamask
return domains[activeTab.origin]?.permissions?.map(({ parentCapability }) => {
const description = permissionsDescriptions[parentCapability]
return {
key: parentCapability,
description,
}
})
}

View File

@ -1,5 +1,9 @@
import assert from 'assert'
import { getConnectedDomainsForSelectedAddress } from '../permissions'
import {
getConnectedDomainsForSelectedAddress,
getOrderedConnectedAccountsForActiveTab,
getPermissionsForActiveTab,
} from '../permissions'
describe('selectors', function () {
@ -153,4 +157,255 @@ describe('selectors', function () {
})
})
describe('getConnectedAccountsForActiveTab', function () {
const mockState = {
activeTab: {
'title': 'Eth Sign Tests',
'origin': 'remix.ethereum.org',
'protocol': 'https:',
'url': 'https://remix.ethereum.org/',
},
metamask: {
identities: {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': {
'address': '0x7250739de134d33ec7ab1ee592711e15098c9d2d',
'name': 'Really Long Name That Should Be Truncated',
},
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
'address': '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
'name': 'Account 1',
},
'0xb3958fb96c8201486ae20be1d5c9f58083df343a': {
'address': '0xb3958fb96c8201486ae20be1d5c9f58083df343a',
'name': 'Account 2',
},
},
domains: {
'remix.ethereum.org': {
'permissions': [
{
'@context': [
'https://github.com/MetaMask/rpc-cap',
],
'caveats': [
{
'name': 'exposedAccounts',
'type': 'filterResponse',
'value': [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
],
},
],
'date': 1586359844177,
'id': '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
'invoker': 'remix.ethereum.org',
'parentCapability': 'eth_accounts',
},
],
},
'peepeth.com': {
'permissions': [
{
'@context': [
'https://github.com/MetaMask/rpc-cap',
],
'caveats': [
{
'name': 'exposedAccounts',
'type': 'filterResponse',
'value': [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
],
},
],
'date': 1585676177970,
'id': '840d72a0-925f-449f-830a-1aa1dd5ce151',
'invoker': 'peepeth.com',
'parentCapability': 'eth_accounts',
},
],
},
'uniswap.exchange': {
'permissions': [
{
'@context': [
'https://github.com/MetaMask/rpc-cap',
],
'caveats': [
{
'name': 'exposedAccounts',
'type': 'filterResponse',
'value': [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
],
},
],
'date': 1585616816623,
'id': 'ce625215-f2e9-48e7-93ca-21ba193244ff',
'invoker': 'uniswap.exchange',
'parentCapability': 'eth_accounts',
},
],
},
},
domainMetadata: {
'remix.ethereum.org': {
'icon': 'https://remix.ethereum.org/icon.png',
'name': 'Remix - Ethereum IDE',
},
},
permissionsHistory: {
'remix.ethereum.org': {
'eth_accounts': {
'accounts': {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': 1586359844192,
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': 1586359844192,
},
'lastApproved': 1586359844192,
},
},
},
permissionsDescriptions: {
'eth_accounts': "View the addresses of the user's chosen accounts.",
},
},
}
it('should return a list of connected accounts', function () {
assert.deepEqual(getOrderedConnectedAccountsForActiveTab(mockState), [{
address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
name: 'Account 1',
lastActive: 1586359844192,
}, {
address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d',
name: 'Really Long Name That Should Be Truncated',
lastActive: 1586359844192,
}])
})
})
describe('getPermissionsForActiveTab', function () {
const mockState = {
activeTab: {
'title': 'Eth Sign Tests',
'origin': 'remix.ethereum.org',
'protocol': 'https:',
'url': 'https://remix.ethereum.org/',
},
metamask: {
identities: {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': {
'address': '0x7250739de134d33ec7ab1ee592711e15098c9d2d',
'name': 'Really Long Name That Should Be Truncated',
},
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
'address': '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
'name': 'Account 1',
},
'0xb3958fb96c8201486ae20be1d5c9f58083df343a': {
'address': '0xb3958fb96c8201486ae20be1d5c9f58083df343a',
'name': 'Account 2',
},
},
domains: {
'remix.ethereum.org': {
'permissions': [
{
'@context': [
'https://github.com/MetaMask/rpc-cap',
],
'caveats': [
{
'name': 'exposedAccounts',
'type': 'filterResponse',
'value': [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
],
},
],
'date': 1586359844177,
'id': '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
'invoker': 'remix.ethereum.org',
'parentCapability': 'eth_accounts',
},
],
},
'peepeth.com': {
'permissions': [
{
'@context': [
'https://github.com/MetaMask/rpc-cap',
],
'caveats': [
{
'name': 'exposedAccounts',
'type': 'filterResponse',
'value': [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
],
},
],
'date': 1585676177970,
'id': '840d72a0-925f-449f-830a-1aa1dd5ce151',
'invoker': 'peepeth.com',
'parentCapability': 'eth_accounts',
},
],
},
'uniswap.exchange': {
'permissions': [
{
'@context': [
'https://github.com/MetaMask/rpc-cap',
],
'caveats': [
{
'name': 'exposedAccounts',
'type': 'filterResponse',
'value': [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
],
},
],
'date': 1585616816623,
'id': 'ce625215-f2e9-48e7-93ca-21ba193244ff',
'invoker': 'uniswap.exchange',
'parentCapability': 'eth_accounts',
},
],
},
},
domainMetadata: {
'remix.ethereum.org': {
'icon': 'https://remix.ethereum.org/icon.png',
'name': 'Remix - Ethereum IDE',
},
},
permissionsHistory: {
'remix.ethereum.org': {
'eth_accounts': {
'accounts': {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': 1586359844192,
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': 1586359844192,
},
'lastApproved': 1586359844192,
},
},
},
permissionsDescriptions: {
'eth_accounts': "View the addresses of the user's chosen accounts.",
},
},
}
it('should return a list of permissions strings', function () {
assert.deepEqual(getPermissionsForActiveTab(mockState), [{
key: 'eth_accounts',
description: "View the addresses of the user's chosen accounts.",
}])
})
})
})