diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5e844bc97..19314d0be 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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" }, diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js new file mode 100644 index 000000000..edb8bca5b --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js @@ -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 ( +
+
+ +
+

+ {name} +

+

+ {status} +

+
+
+ {options} +
+ ) + } +} diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/index.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/index.js new file mode 100644 index 000000000..3fb33c23e --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-item/index.js @@ -0,0 +1 @@ +export { default } from './connected-accounts-list-item.component' diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-options-item/connected-accounts-list-options-item.component.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options-item/connected-accounts-list-options-item.component.js new file mode 100644 index 000000000..e1fe4e167 --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options-item/connected-accounts-list-options-item.component.js @@ -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 ( + + ) + } +} diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-options-item/index.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options-item/index.js new file mode 100644 index 000000000..fd9f7f347 --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options-item/index.js @@ -0,0 +1 @@ +export { default } from './connected-accounts-list-options-item.component' diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-options/connected-accounts-list-options.component.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options/connected-accounts-list-options.component.js new file mode 100644 index 000000000..e1eb29def --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options/connected-accounts-list-options.component.js @@ -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 ( + + {this.props.children} + + )} + > + + + ) + } +} diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-options/index.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options/index.js new file mode 100644 index 000000000..6e4db3446 --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-options/index.js @@ -0,0 +1 @@ +export { default } from './connected-accounts-list-options.component' diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/connected-accounts-list-permissions.component.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/connected-accounts-list-permissions.component.js new file mode 100644 index 000000000..92215a1d7 --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/connected-accounts-list-permissions.component.js @@ -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 ( +
+

+ {t('permissions')} +

+ ) + } +} diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js new file mode 100644 index 000000000..7733018f8 --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js @@ -0,0 +1 @@ +export { default } from './connected-accounts-list-permissions.component' diff --git a/ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js b/ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js new file mode 100644 index 000000000..50bb32698 --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js @@ -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 ( + + {t('statusNotConnected')} +  ·  + + {t('connect')} + + + )} + /> + ) + } + + render () { + const { connectedAccounts, permissions, selectedAddress } = this.props + const { t } = this.context + + return ( + <> +
+ {this.renderUnconnectedAccount()} + {connectedAccounts.map(({ address, name, lastActive }, index) => ( + + { + address === selectedAddress ? null : ( + + {t('switchToThisAccount')} + + ) + } + + {t('disconnectThisAccount')} + + + )} + /> + ))} +
+ + + ) + } +} diff --git a/ui/app/components/app/connected-accounts-list/index.js b/ui/app/components/app/connected-accounts-list/index.js new file mode 100644 index 000000000..ee436f58a --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/index.js @@ -0,0 +1 @@ +export { default } from './connected-accounts-list.component' diff --git a/ui/app/components/app/connected-accounts-list/index.scss b/ui/app/components/app/connected-accounts-list/index.scss new file mode 100644 index 000000000..3c9f78313 --- /dev/null +++ b/ui/app/components/app/connected-accounts-list/index.scss @@ -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; +} diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index 347108b2e..f4b028291 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -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'; diff --git a/ui/app/components/app/menu-bar/index.js b/ui/app/components/app/menu-bar/index.js index 533e01c85..c5760847f 100644 --- a/ui/app/components/app/menu-bar/index.js +++ b/ui/app/components/app/menu-bar/index.js @@ -1 +1 @@ -export { default } from './menu-bar.component' +export { default } from './menu-bar.container' diff --git a/ui/app/components/app/menu-bar/menu-bar.component.js b/ui/app/components/app/menu-bar/menu-bar.component.js index fdbdfd5e4..a5a7a9e32 100644 --- a/ui/app/components/app/menu-bar/menu-bar.component.js +++ b/ui/app/components/app/menu-bar/menu-bar.component.js @@ -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 {
{ getEnvironmentType() === ENVIRONMENT_TYPE_POPUP - ? + ? history.push(CONNECTED_ACCOUNTS_ROUTE)} /> : null } diff --git a/ui/app/components/app/menu-bar/menu-bar.container.js b/ui/app/components/app/menu-bar/menu-bar.container.js new file mode 100644 index 000000000..80648bd35 --- /dev/null +++ b/ui/app/components/app/menu-bar/menu-bar.container.js @@ -0,0 +1,4 @@ +import { withRouter } from 'react-router-dom' +import MenuBar from './menu-bar.component' + +export default withRouter(MenuBar) diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index afbd7f4ca..f5a2934b0 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -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, } diff --git a/ui/app/pages/connected-accounts/connected-accounts.component.js b/ui/app/pages/connected-accounts/connected-accounts.component.js new file mode 100644 index 000000000..1e5ae6e65 --- /dev/null +++ b/ui/app/pages/connected-accounts/connected-accounts.component.js @@ -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 ( + history.push(DEFAULT_ROUTE)} + footerClassName="connected-accounts__footer" + footer={ + connectedAccounts.length + ? null + : {t('viewConnectedSites')} + } + > + + + ) + } +} diff --git a/ui/app/pages/connected-accounts/connected-accounts.container.js b/ui/app/pages/connected-accounts/connected-accounts.container.js new file mode 100644 index 000000000..ae4d3cedc --- /dev/null +++ b/ui/app/pages/connected-accounts/connected-accounts.container.js @@ -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) diff --git a/ui/app/pages/connected-accounts/index.js b/ui/app/pages/connected-accounts/index.js new file mode 100644 index 000000000..7497bc560 --- /dev/null +++ b/ui/app/pages/connected-accounts/index.js @@ -0,0 +1 @@ +export { default } from './connected-accounts.container' diff --git a/ui/app/pages/connected-accounts/index.scss b/ui/app/pages/connected-accounts/index.scss new file mode 100644 index 000000000..1ac7f3a58 --- /dev/null +++ b/ui/app/pages/connected-accounts/index.scss @@ -0,0 +1,9 @@ +.connected-accounts { + &__footer { + a, a:hover { + color: #037DD6; + cursor: pointer; + font-size: 14px; + } + } +} diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 56fe85b08..c3aaea152 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -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 (
- + +
{ isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() : null } 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, + } + }) +} diff --git a/ui/app/selectors/tests/permissions.test.js b/ui/app/selectors/tests/permissions.test.js index 3217ba10e..ad214c572 100644 --- a/ui/app/selectors/tests/permissions.test.js +++ b/ui/app/selectors/tests/permissions.test.js @@ -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.", + }]) + }) + }) + })