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:
parent
1c3d915aa3
commit
d488c16df5
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './connected-accounts-list-item.component'
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './connected-accounts-list-options-item.component'
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './connected-accounts-list-options.component'
|
@ -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" /> {description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './connected-accounts-list-permissions.component'
|
@ -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')}
|
||||
·
|
||||
<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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/app/connected-accounts-list/index.js
Normal file
1
ui/app/components/app/connected-accounts-list/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './connected-accounts-list.component'
|
152
ui/app/components/app/connected-accounts-list/index.scss
Normal file
152
ui/app/components/app/connected-accounts-list/index.scss
Normal 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;
|
||||
}
|
@ -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';
|
||||
|
@ -1 +1 @@
|
||||
export { default } from './menu-bar.component'
|
||||
export { default } from './menu-bar.container'
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
4
ui/app/components/app/menu-bar/menu-bar.container.js
Normal file
4
ui/app/components/app/menu-bar/menu-bar.container.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import MenuBar from './menu-bar.component'
|
||||
|
||||
export default withRouter(MenuBar)
|
@ -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,
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
1
ui/app/pages/connected-accounts/index.js
Normal file
1
ui/app/pages/connected-accounts/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './connected-accounts.container'
|
9
ui/app/pages/connected-accounts/index.scss
Normal file
9
ui/app/pages/connected-accounts/index.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.connected-accounts {
|
||||
&__footer {
|
||||
a, a:hover {
|
||||
color: #037DD6;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -12,6 +12,8 @@
|
||||
|
||||
@import 'connected-sites/index';
|
||||
|
||||
@import 'connected-accounts/index';
|
||||
|
||||
@import 'settings/index';
|
||||
|
||||
@import 'first-time-flow/index';
|
||||
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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.",
|
||||
}])
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user