Consolidate connected account alerts (#8802)

* update connected accounts appearance

* consolidate account alerts

* UnconnectedAccountAlert: use ConnectedAccountsList

* move switch account action out of menu in all views

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
feature/default_network_editable
Erik Marks 5 years ago committed by GitHub
parent 3f8fa161ca
commit 4dfe4e7463
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 44
      app/_locales/en/messages.json
  2. 19
      app/scripts/controllers/alert.js
  3. 2
      app/scripts/metamask-controller.js
  4. 3
      development/states/confirm-sig-requests.json
  5. 3
      development/states/currency-localization.json
  6. 3
      development/states/tx-list-items.json
  7. 7
      ui/app/components/app/alerts/alerts.js
  8. 2
      ui/app/components/app/alerts/alerts.scss
  9. 1
      ui/app/components/app/alerts/switch-to-connected-alert/index.js
  10. 121
      ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.js
  11. 66
      ui/app/components/app/alerts/switch-to-connected-alert/switch-to-connected-alert.scss
  12. 81
      ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js
  13. 34
      ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss
  14. 11
      ui/app/components/app/connected-accounts-list/connected-accounts-list-item/connected-accounts-list-item.component.js
  15. 1
      ui/app/components/app/connected-accounts-list/connected-accounts-list-permissions/index.js
  16. 123
      ui/app/components/app/connected-accounts-list/connected-accounts-list.component.js
  17. 88
      ui/app/components/app/connected-accounts-list/index.scss
  18. 2
      ui/app/components/app/connected-accounts-permissions/connected-accounts-permissions.component.js
  19. 1
      ui/app/components/app/connected-accounts-permissions/index.js
  20. 64
      ui/app/components/app/connected-accounts-permissions/index.scss
  21. 2
      ui/app/components/app/index.scss
  22. 2
      ui/app/components/ui/popover/index.scss
  23. 1
      ui/app/ducks/alerts/index.js
  24. 111
      ui/app/ducks/alerts/switch-to-connected.js
  25. 40
      ui/app/ducks/alerts/unconnected-account.js
  26. 3
      ui/app/ducks/index.js
  27. 4
      ui/app/ducks/metamask/metamask.js
  28. 24
      ui/app/helpers/utils/util.js
  29. 15
      ui/app/pages/connected-accounts/connected-accounts.component.js
  30. 11
      ui/app/pages/connected-accounts/connected-accounts.container.js
  31. 4
      ui/app/pages/settings/alerts-tab/alerts-tab.js
  32. 10
      ui/app/store/actions.js
  33. 18
      ui/index.js

@ -28,11 +28,8 @@
"connectedAccountsEmptyDescription": {
"message": "MetaMask is not connected this site. To connect to a Web3 site, find the connect button on their site."
},
"primary": {
"message": "Primary"
},
"lastActive": {
"message": "Last active"
"currentAccountNotConnected": {
"message": "Your current account is not connected"
},
"switchToThisAccount": {
"message": "Switch to this account"
@ -139,6 +136,9 @@
"accountSelectionRequired": {
"message": "You need to select an account!"
},
"active": {
"message": "Active"
},
"activity": {
"message": "Activity"
},
@ -187,17 +187,11 @@
"alertsSettingsDescription": {
"message": "Enable or disable each alert"
},
"alertSettingsSwitchToConnected": {
"message": "Opening popup with an unconnected account selected"
},
"alertSettingsSwitchToConnectedDescription": {
"message": "This alert is shown when you open the popup with an unconnected account selected."
},
"alertSettingsUnconnectedAccount": {
"message": "Switching to an unconnected account"
"message": "Browsing a website with an unconnected account selected"
},
"alertSettingsUnconnectedAccountDescription": {
"message": "This alert is shown in the popup when you switch from a connected account to an unconnected account."
"message": "This alert is shown in the popup when you are browsing a connected Web3 site, but the currently selected account is not connected."
},
"allowOriginSpendToken": {
"message": "Allow $1 to spend your $2?",
@ -991,9 +985,6 @@
"noAlreadyHaveSeed": {
"message": "No, I already have a seed phrase"
},
"notConnected": {
"message": "Not connected"
},
"protectYourKeys": {
"message": "Protect Your Keys!"
},
@ -1478,16 +1469,6 @@
"supportCenter": {
"message": "Visit our Support Center"
},
"switchAccounts": {
"message": "Switch accounts"
},
"switchToConnectedAlertMultipleAccountsDescription": {
"message": "This account is not connected. Switch to a connected account?"
},
"switchToConnectedAlertSingleAccountDescription": {
"message": "This account is not connected. Switch to a connected account ($1)?",
"description": "$1 will be replaced by the name of the connected account"
},
"symbol": {
"message": "Symbol"
},
@ -1635,10 +1616,7 @@
"unapproved": {
"message": "Unapproved"
},
"unconnectedAccountAlertDescription": {
"message": "$1 is not connected to $2."
},
"unconnectedAccountAlertDisableTooltip": {
"alertDisableTooltip": {
"message": "This can be changed in \"Settings > Alerts\""
},
"units": {
@ -1776,11 +1754,5 @@
"encryptionPublicKeyNotice": {
"message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.",
"description": "$1 is website or dapp name"
},
"thisSite": {
"message": "this site"
},
"thisAccount": {
"message": "This account"
}
}

@ -12,7 +12,6 @@ import ObservableStore from 'obs-store'
*/
export const ALERT_TYPES = {
switchToConnected: 'switchToConnected',
unconnectedAccount: 'unconnectedAccount',
}
@ -25,7 +24,7 @@ const defaultState = {
},
{}
),
switchToConnectedAlertShown: {},
unconnectedAccountAlertShownOrigins: {},
}
/**
@ -44,7 +43,7 @@ export default class AlertController {
defaultState,
initState,
{
switchToConnectedAlertShown: {},
unconnectedAccountAlertShownOrigins: {},
}
)
this.store = new ObservableStore(state)
@ -54,9 +53,9 @@ export default class AlertController {
preferencesStore.subscribe(({ selectedAddress }) => {
const currentState = this.store.getState()
if (currentState.switchToConnectedAlertShown && this.selectedAddress !== selectedAddress) {
if (currentState.unconnectedAccountAlertShownOrigins && this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress
this.store.updateState({ switchToConnectedAlertShown: {} })
this.store.updateState({ unconnectedAccountAlertShownOrigins: {} })
}
})
}
@ -72,10 +71,10 @@ export default class AlertController {
* Sets the "switch to connected" alert as shown for the given origin
* @param {string} origin - The origin the alert has been shown for
*/
setSwitchToConnectedAlertShown (origin) {
let { switchToConnectedAlertShown } = this.store.getState()
switchToConnectedAlertShown = { ...switchToConnectedAlertShown }
switchToConnectedAlertShown[origin] = true
this.store.updateState({ switchToConnectedAlertShown })
setUnconnectedAccountAlertShown (origin) {
let { unconnectedAccountAlertShownOrigins } = this.store.getState()
unconnectedAccountAlertShownOrigins = { ...unconnectedAccountAlertShownOrigins }
unconnectedAccountAlertShownOrigins[origin] = true
this.store.updateState({ unconnectedAccountAlertShownOrigins })
}
}

@ -545,7 +545,7 @@ export default class MetamaskController extends EventEmitter {
// alert controller
setAlertEnabledness: nodeify(alertController.setAlertEnabledness, alertController),
setSwitchToConnectedAlertShown: nodeify(this.alertController.setSwitchToConnectedAlertShown, this.alertController),
setUnconnectedAccountAlertShown: nodeify(this.alertController.setUnconnectedAccountAlertShown, this.alertController),
// 3Box
setThreeBoxSyncingPermission: nodeify(threeBoxController.setThreeBoxSyncingPermission, threeBoxController),

@ -522,9 +522,6 @@
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {}
},
"switchToConnected": {
"state": "CLOSED"
},
"unconnectedAccount": {
"state": "CLOSED"
},

@ -473,9 +473,6 @@
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
"errors": {}
},
"switchToConnected": {
"state": "CLOSED"
},
"unconnectedAccount": {
"state": "CLOSED"
},

@ -1295,9 +1295,6 @@
"errors": {}
},
"confirmTransaction": {},
"switchToConnected": {
"state": "CLOSED"
},
"unconnectedAccount": {
"state": "CLOSED"
}

@ -2,22 +2,15 @@ import React from 'react'
import { useSelector } from 'react-redux'
import UnconnectedAccountAlert from './unconnected-account-alert'
import SwitchToConnectedAlert from './switch-to-connected-alert'
import { alertIsOpen as unconnectedAccountAlertIsOpen } from '../../../ducks/alerts/unconnected-account'
import { alertIsOpen as switchToConnectedAlertIsOpen } from '../../../ducks/alerts/switch-to-connected'
const Alerts = () => {
const _unconnectedAccountAlertIsOpen = useSelector(unconnectedAccountAlertIsOpen)
const _switchToConnectedAlertIsOpen = useSelector(switchToConnectedAlertIsOpen)
if (_unconnectedAccountAlertIsOpen) {
return (
<UnconnectedAccountAlert />
)
} else if (_switchToConnectedAlertIsOpen) {
return (
<SwitchToConnectedAlert />
)
}
return null

@ -1,3 +1 @@
@import './unconnected-account-alert/unconnected-account-alert';
@import './switch-to-connected-alert/switch-to-connected-alert';

@ -1 +0,0 @@
export { default } from './switch-to-connected-alert'

@ -1,121 +0,0 @@
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
ALERT_STATE,
switchToAccount,
dismissAlert,
dismissAndDisableAlert,
getAlertState,
} from '../../../../ducks/alerts/switch-to-connected'
import { getPermittedIdentitiesForCurrentTab } from '../../../../selectors'
import Popover from '../../../ui/popover'
import Button from '../../../ui/button'
import Dropdown from '../../../ui/dropdown'
import Checkbox from '../../../ui/check-box'
import Tooltip from '../../../ui/tooltip-v2'
import { useI18nContext } from '../../../../hooks/useI18nContext'
const {
ERROR,
LOADING,
} = ALERT_STATE
const SwitchToUnconnectedAccountAlert = () => {
const t = useI18nContext()
const dispatch = useDispatch()
const alertState = useSelector(getAlertState)
const connectedAccounts = useSelector(getPermittedIdentitiesForCurrentTab)
const [accountToSwitchTo, setAccountToSwitchTo] = useState(connectedAccounts[0].address)
const [dontShowThisAgain, setDontShowThisAgain] = useState(false)
const onClose = async () => {
return dontShowThisAgain
? await dispatch(dismissAndDisableAlert())
: dispatch(dismissAlert())
}
const options = connectedAccounts.map((account) => {
return { name: account.name, value: account.address }
})
return (
<Popover
contentClassName="switch-to-connected-alert__content"
footer={(
<>
{
alertState === ERROR
? (
<div className="switch-to-connected-alert__error">
{ t('failureMessage') }
</div>
)
: null
}
<div className="switch-to-connected-alert__footer-buttons">
<Button
disabled={alertState === LOADING}
onClick={onClose}
type="secondary"
>
{ t('dismiss') }
</Button>
<Button
disabled={alertState === LOADING || alertState === ERROR || dontShowThisAgain}
onClick={() => dispatch(switchToAccount(accountToSwitchTo))}
type="primary"
>
{ t('switchAccounts') }
</Button>
</div>
</>
)}
footerClassName="switch-to-connected-alert__footer"
onClose={onClose}
subtitle={
connectedAccounts.length > 1
? t('switchToConnectedAlertMultipleAccountsDescription')
: t('switchToConnectedAlertSingleAccountDescription', [connectedAccounts[0].name])
}
title={t('notConnected')}
>
{
connectedAccounts.length > 1
? (
<Dropdown
className="switch-to-connected-alert__dropdown"
title="Switch to account"
onChange={(address) => setAccountToSwitchTo(address)}
options={options}
selectedOption={accountToSwitchTo}
/>
)
: null
}
<div className="switch-to-connected-alert__checkbox-wrapper">
<Checkbox
id="switchToConnected_dontShowThisAgain"
checked={dontShowThisAgain}
className="switch-to-connected-alert__checkbox"
onClick={() => setDontShowThisAgain((checked) => !checked)}
/>
<label
className="switch-to-connected-alert__checkbox-label"
htmlFor="switchToConnected_dontShowThisAgain"
>
{ t('dontShowThisAgain') }
<Tooltip
position="top"
title={t('unconnectedAccountAlertDisableTooltip')}
wrapperClassName="switch-to-connected-alert__checkbox-label-tooltip"
>
<i className="fa fa-info-circle" />
</Tooltip>
</label>
</div>
</Popover>
)
}
export default SwitchToUnconnectedAccountAlert

@ -1,66 +0,0 @@
.switch-to-connected-alert {
&__footer {
flex-direction: column;
:only-child {
margin: 0;
}
}
&__footer-buttons {
display: flex;
flex-direction: row;
button:first-child {
margin-right: 24px;
}
button {
font-size: 14px;
line-height: 20px;
padding: 8px;
}
}
&__error {
margin-bottom: 16px;
padding: 16px;
font-size: 14px;
border: 1px solid #D73A49;
background: #F8EAE8;
border-radius: 3px;
}
&__content {
align-items: center;
padding: 0 24px 24px 24px;
}
&__dropdown {
background-color: white;
width: 100%;
margin-bottom: 24px;
}
&__checkbox-wrapper {
display: flex;
flex-direction: row;
width: 100%;
}
&__checkbox {
margin-right: 8px;
}
&__checkbox-label {
font-size: 14px;
margin-top: auto;
margin-bottom: auto;
color: $Grey-500;
display: flex;
}
&__checkbox-label-tooltip {
margin-left: 8px;
}
}

@ -7,12 +7,20 @@ import {
dismissAlert,
dismissAndDisableAlert,
getAlertState,
switchToAccount,
} from '../../../../ducks/alerts/unconnected-account'
import {
getOriginOfCurrentTab,
getPermittedIdentitiesForCurrentTab,
getSelectedAddress,
getSelectedIdentity,
} from '../../../../selectors'
import { isExtensionUrl } from '../../../../helpers/utils/util'
import Popover from '../../../ui/popover'
import Button from '../../../ui/button'
import Checkbox from '../../../ui/check-box'
import Tooltip from '../../../ui/tooltip-v2'
import { getSelectedIdentity, getOriginOfCurrentTab } from '../../../../selectors'
import ConnectedAccountsList from '../../connected-accounts-list'
import { useI18nContext } from '../../../../hooks/useI18nContext'
const {
@ -20,12 +28,14 @@ const {
LOADING,
} = ALERT_STATE
const SwitchToUnconnectedAccountAlert = () => {
const UnconnectedAccountAlert = () => {
const t = useI18nContext()
const dispatch = useDispatch()
const alertState = useSelector(getAlertState)
const connectedAccounts = useSelector(getPermittedIdentitiesForCurrentTab)
const origin = useSelector(getOriginOfCurrentTab)
const selectedIdentity = useSelector(getSelectedIdentity)
const selectedAddress = useSelector(getSelectedAddress)
const [dontShowThisAgain, setDontShowThisAgain] = useState(false)
const onClose = async () => {
@ -34,16 +44,7 @@ const SwitchToUnconnectedAccountAlert = () => {
: dispatch(dismissAlert())
}
const accountName = selectedIdentity?.name || t('thisAccount')
const siteName = origin || t('thisSite')
return (
<Popover
contentClassName="unconnected-account-alert__content"
title={t('notConnected')}
subtitle={t('unconnectedAccountAlertDescription', [ accountName, siteName ])}
onClose={onClose}
footer={(
const footer = (
<>
{
alertState === ERROR
@ -54,26 +55,8 @@ const SwitchToUnconnectedAccountAlert = () => {
)
: null
}
<div className="unconnected-account-alert__footer-buttons">
<Button
disabled={alertState === LOADING}
onClick={onClose}
type="secondary"
>
{ t('dismiss') }
</Button>
<Button
disabled={alertState === LOADING || alertState === ERROR || dontShowThisAgain }
onClick={() => dispatch(connectAccount())}
type="primary"
>
{ t('connect') }
</Button>
</div>
</>
)}
footerClassName="unconnected-account-alert__footer"
>
<div className="unconnected-account-alert__footer-row">
<div className="unconnected-account-alert__checkbox-wrapper">
<Checkbox
id="unconnectedAccount_dontShowThisAgain"
checked={dontShowThisAgain}
@ -87,14 +70,44 @@ const SwitchToUnconnectedAccountAlert = () => {
{ t('dontShowThisAgain') }
<Tooltip
position="top"
title={t('unconnectedAccountAlertDisableTooltip')}
title={t('alertDisableTooltip')}
wrapperClassName="unconnected-account-alert__checkbox-label-tooltip"
>
<i className="fa fa-info-circle" />
</Tooltip>
</label>
</div>
<Button
disabled={alertState === LOADING}
onClick={onClose}
type="secondary"
className="unconnected-account-alert__dismiss-button"
>
{ t('dismiss') }
</Button>
</div>
</>
)
return (
<Popover
title={isExtensionUrl(origin) ? t('currentExtension') : new URL(origin).host}
subtitle={t('currentAccountNotConnected')}
onClose={onClose}
contentClassName="unconnected-account-alert__content"
footerClassName="unconnected-account-alert__footer"
footer={footer}
>
<ConnectedAccountsList
accountToConnect={selectedIdentity}
connectAccount={() => dispatch(connectAccount(selectedAddress))}
connectedAccounts={connectedAccounts}
selectedAddress={selectedAddress}
setSelectedAddress={(address) => dispatch(switchToAccount(address))}
shouldRenderListOptions={false}
/>
</Popover>
)
}
export default SwitchToUnconnectedAccountAlert
export default UnconnectedAccountAlert

@ -1,25 +1,28 @@
.unconnected-account-alert {
&__content {
border-radius: 0;
}
&__footer {
flex-direction: column;
:only-child {
> :only-child {
margin: 0;
}
}
&__footer-buttons {
&__footer-row {
display: flex;
flex-direction: row;
button:first-child {
margin-right: 24px;
}
button {
font-size: 14px;
line-height: 20px;
padding: 8px;
}
&__dismiss-button {
background: #037DD6;
color: white;
height: 40px;
width: 100px;
border: 0;
border-radius: 100px;
}
&__error {
@ -31,21 +34,24 @@
border-radius: 3px;
}
&__content {
&__checkbox-wrapper {
width: 100%;
display: flex;
flex-direction: row;
padding: 0 24px 24px 24px;
align-items: center;
}
&__checkbox {
margin-right: 8px;
padding-top: 1px; // better alignment with rest of content
}
&__checkbox-label {
font-size: 14px;
display: flex;
font-size: 12px;
margin-top: auto;
margin-bottom: auto;
color: $Grey-500;
display: flex;
}
&__checkbox-label-tooltip {

@ -12,13 +12,15 @@ export default class ConnectedAccountsListItem extends PureComponent {
address: PropTypes.string.isRequired,
className: PropTypes.string,
name: PropTypes.node.isRequired,
status: PropTypes.node.isRequired,
status: PropTypes.string,
action: PropTypes.node,
options: PropTypes.node,
}
static defaultProps = {
className: null,
options: null,
action: null,
}
render () {
@ -27,6 +29,7 @@ export default class ConnectedAccountsListItem extends PureComponent {
className,
name,
status,
action,
options,
} = this.props
@ -39,18 +42,20 @@ export default class ConnectedAccountsListItem extends PureComponent {
diameter={32}
/>
<div>
<p>
<strong className="connected-accounts-list__account-name">{name}</strong>
<p className="connected-accounts-list__account-name">
<strong>{name}</strong>
</p>
{
status
? (
<p className="connected-accounts-list__account-status">
&nbsp;&nbsp;
{status}
</p>
)
: null
}
{action}
</div>
</div>
{options}

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

@ -1,7 +1,5 @@
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 { MenuItem } from '../../ui/menu'
@ -13,7 +11,6 @@ export default class ConnectedAccountsList extends PureComponent {
static defaultProps = {
accountToConnect: null,
permissions: undefined,
}
static propTypes = {
@ -26,31 +23,35 @@ export default class ConnectedAccountsList extends PureComponent {
name: PropTypes.string.isRequired,
lastActive: PropTypes.number,
})).isRequired,
permissions: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
})),
connectAccount: PropTypes.func.isRequired,
selectedAddress: PropTypes.string.isRequired,
addPermittedAccount: PropTypes.func.isRequired,
removePermittedAccount: PropTypes.func.isRequired,
removePermittedAccount: PropTypes.func,
setSelectedAddress: PropTypes.func.isRequired,
shouldRenderListOptions: (props, propName, componentName) => {
if (typeof props[propName] !== 'boolean') {
return new Error(
`Warning: Failed prop type: '${propName}' of component '${componentName}' must be a boolean. Received: ${typeof props[propName]}`
)
} else if (props[propName] && !props['removePermittedAccount']) {
return new Error(
`Warning: Failed prop type: '${propName}' of component '${componentName}' requires prop 'removePermittedAccount'.`
)
}
},
}
state = {
accountWithOptionsShown: null,
}
connectAccount = () => {
this.props.addPermittedAccount(this.props.accountToConnect?.address)
}
disconnectAccount = () => {
this.hideAccountOptions()
this.props.removePermittedAccount(this.state.accountWithOptionsShown)
}
switchAccount = () => {
switchAccount = (address) => {
this.hideAccountOptions()
this.props.setSelectedAddress(this.state.accountWithOptionsShown)
this.props.setSelectedAddress(address)
}
hideAccountOptions = () => {
@ -62,7 +63,7 @@ export default class ConnectedAccountsList extends PureComponent {
}
renderUnconnectedAccount () {
const { accountToConnect } = this.props
const { accountToConnect, connectAccount } = this.props
const { t } = this.context
if (!accountToConnect) {
@ -75,59 +76,29 @@ export default class ConnectedAccountsList extends PureComponent {
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}>
status={t('statusNotConnected')}
action={(
<a
className="connected-accounts-list__account-status-link"
onClick={() => connectAccount(accountToConnect.address)}
>
{t('connect')}
</a>
</>
)}
/>
)
}
render () {
const { connectedAccounts, permissions, selectedAddress } = this.props
renderListItemOptions (address) {
const { accountWithOptionsShown } = this.state
const { t } = this.context
return (
<>
<main className="connected-accounts-list">
{this.renderUnconnectedAccount()}
{
connectedAccounts.map(({ address, name, lastActive }, index) => {
let status
if (index === 0) {
status = t('primary')
} else if (lastActive) {
status = `${t('lastActive')}: ${DateTime.fromMillis(lastActive).toISODate()}`
}
return (
<ConnectedAccountsListItem
key={address}
address={address}
name={`${name} (…${address.substr(-4, 4)})`}
status={status}
options={(
<ConnectedAccountsListOptions
onHideOptions={this.hideAccountOptions}
onShowOptions={this.showAccountOptions.bind(null, address)}
show={accountWithOptionsShown === address}
>
{
address === selectedAddress ? null : (
<MenuItem
iconClassName="fas fa-random"
onClick={this.switchAccount}
>
{t('switchToThisAccount')}
</MenuItem>
)
}
<MenuItem
iconClassName="disconnect-icon"
onClick={this.disconnectAccount}
@ -135,13 +106,57 @@ export default class ConnectedAccountsList extends PureComponent {
{t('disconnectThisAccount')}
</MenuItem>
</ConnectedAccountsListOptions>
)}
)
}
renderListItemAction (address) {
const { t } = this.context
return (
<a
className="connected-accounts-list__account-status-link"
onClick={() => this.switchAccount(address)}
>
{t('switchToThisAccount')}
</a>
)
}
render () {
const {
connectedAccounts,
selectedAddress,
shouldRenderListOptions,
} = this.props
const { t } = this.context
return (
<>
<main className="connected-accounts-list">
{this.renderUnconnectedAccount()}
{
connectedAccounts.map(({ address, name }, index) => {
return (
<ConnectedAccountsListItem
key={address}
address={address}
name={`${name} (…${address.substr(-4, 4)})`}
status={index === 0 ? t('active') : null}
options={
shouldRenderListOptions
? this.renderListItemOptions(address)
: null
}
action={
address !== selectedAddress
? this.renderListItemAction(address)
: null
}
/>
)
})
}
</main>
<ConnectedAccountsListPermissions permissions={permissions} />
</>
)
}

@ -8,28 +8,34 @@
}
&__account-name {
display: inline;
font-weight: bold;
font-size: 14px;
line-height: 20px;
}
%account-status-typography {
font-size: 12px;
line-height: 17px;
padding-top: 4px;
}
&__account-status {
@extend %account-status-typography;
display: inline;
color: $Grey-500;
}
&__account-status-link {
@extend %account-status-typography;
display: block;
&, &:hover {
color: $curious-blue;
cursor: pointer;
}
}
&__account-status {
font-size: 12px;
line-height: 17px;
padding-top: 4px;
}
&__row {
display: flex;
flex-direction: row;
@ -40,10 +46,6 @@
border-top: 1px solid $geyser;
&:last-of-type {
border-bottom: 1px solid $geyser;
}
&--highlight {
background-color: $warning-light-yellow;
border: 1px solid $warning-yellow;
@ -70,72 +72,6 @@
}
}
.connected-accounts-permissions {
display: flex;
flex-direction: column;
padding: 24px;
font-size: 12px;
line-height: 17px;
color: $Grey-500;
strong {
font-weight: bold;
}
p + p {
padding-top: 8px;
}
&__header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
cursor: pointer;
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 {
display: flex;
i {
display: block;
padding-right: 8px;
font-size: 18px;
color: $Grey-800;
}
}
&__list-container {
max-height: 0px;
overflow: hidden;
height: auto;
transition: max-height 0.8s cubic-bezier(0.4, 0.0, 0.2, 1);
&--expanded {
// arbitrarily set hard coded value for effect to work
max-height: 100px;
}
}
}
.tippy-tooltip.none-theme {
background: none;
padding: 0;

@ -2,7 +2,7 @@ import classnames from 'classnames'
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
export default class ConnectedAccountsListPermissions extends PureComponent {
export default class ConnectedAccountsPermissions extends PureComponent {
static contextTypes = {
t: PropTypes.func.isRequired,
}

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

@ -0,0 +1,64 @@
.connected-accounts-permissions {
display: flex;
flex-direction: column;
font-size: 12px;
line-height: 17px;
color: $Grey-500;
strong {
font-weight: bold;
}
p + p {
padding-top: 8px;
}
&__header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
cursor: pointer;
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 {
display: flex;
i {
display: block;
padding-right: 8px;
font-size: 18px;
color: $Grey-800;
}
}
&__list-container {
max-height: 0px;
overflow: hidden;
height: auto;
transition: max-height 0.8s cubic-bezier(0.4, 0.0, 0.2, 1);
&--expanded {
// arbitrarily set hard coded value for effect to work
max-height: 100px;
}
}
}

@ -88,6 +88,8 @@
@import 'connected-accounts-list/index';
@import 'connected-accounts-permissions/index';
@import '../ui/icon-with-fallback/index';
@import '../ui/icon/index';

@ -107,7 +107,7 @@
border-top: 1px solid #D2D8DD;
padding: 16px 24px 24px;
& :only-child {
> :only-child {
margin: 0 auto;
}
}

@ -1,2 +1 @@
export { default as switchToConnected } from './switch-to-connected'
export { default as unconnectedAccount } from './unconnected-account'

@ -1,111 +0,0 @@
import { createSlice } from '@reduxjs/toolkit'
import { captureException } from '@sentry/browser'
import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert'
import * as actionConstants from '../../store/actionConstants'
import { setAlertEnabledness, setSelectedAddress } from '../../store/actions'
// Constants
export const ALERT_STATE = {
CLOSED: 'CLOSED',
ERROR: 'ERROR',
LOADING: 'LOADING',
OPEN: 'OPEN',
}
const name = ALERT_TYPES.switchToConnected
const initialState = {
state: ALERT_STATE.CLOSED,
}
// Slice (reducer plus auto-generated actions and action creators)
const slice = createSlice({
name,
initialState,
reducers: {
disableAlertFailed: (state) => {
state.state = ALERT_STATE.ERROR
},
disableAlertRequested: (state) => {
state.state = ALERT_STATE.LOADING
},
disableAlertSucceeded: (state) => {
state.state = ALERT_STATE.CLOSED
},
dismissAlert: (state) => {
state.state = ALERT_STATE.CLOSED
},
switchAccountFailed: (state) => {
state.state = ALERT_STATE.ERROR
},
switchAccountRequested: (state) => {
state.state = ALERT_STATE.LOADING
},
switchAccountSucceeded: (state) => {
state.state = ALERT_STATE.CLOSED
},
},
extraReducers: {
[actionConstants.SELECTED_ADDRESS_CHANGED]: (state) => {
// close the alert if the account is switched while it's open
if (state.state === ALERT_STATE.OPEN) {
state.state = ALERT_STATE.CLOSED
}
},
},
})
const { actions, reducer } = slice
export default reducer
// Selectors
export const getAlertState = (state) => state[name].state
export const alertIsOpen = (state) => state[name].state !== ALERT_STATE.CLOSED
// Actions / action-creators
const {
disableAlertFailed,
disableAlertRequested,
disableAlertSucceeded,
dismissAlert,
switchAccountFailed,
switchAccountRequested,
switchAccountSucceeded,
} = actions
export { dismissAlert }
export const dismissAndDisableAlert = () => {
return async (dispatch) => {
try {
await dispatch(disableAlertRequested())
await dispatch(setAlertEnabledness(name, false))
await dispatch(disableAlertSucceeded())
} catch (error) {
console.error(error)
captureException(error)
await dispatch(disableAlertFailed())
}
}
}
export const switchToAccount = (address) => {
return async (dispatch) => {
try {
await dispatch(switchAccountRequested())
await dispatch(setSelectedAddress(address))
await dispatch(switchAccountSucceeded())
} catch (error) {
console.error(error)
captureException(error)
await dispatch(switchAccountFailed())
}
}
}

@ -3,7 +3,11 @@ import { captureException } from '@sentry/browser'
import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert'
import * as actionConstants from '../../store/actionConstants'
import { addPermittedAccount, setAlertEnabledness } from '../../store/actions'
import {
addPermittedAccount,
setAlertEnabledness,
setSelectedAddress,
} from '../../store/actions'
import {
getOriginOfCurrentTab,
getSelectedAddress,
@ -39,9 +43,6 @@ const slice = createSlice({
connectAccountSucceeded: (state) => {
state.state = ALERT_STATE.CLOSED
},
dismissAlert: (state) => {
state.state = ALERT_STATE.CLOSED
},
disableAlertFailed: (state) => {
state.state = ALERT_STATE.ERROR
},
@ -51,6 +52,18 @@ const slice = createSlice({
disableAlertSucceeded: (state) => {
state.state = ALERT_STATE.CLOSED
},
dismissAlert: (state) => {
state.state = ALERT_STATE.CLOSED
},
switchAccountFailed: (state) => {
state.state = ALERT_STATE.ERROR
},
switchAccountRequested: (state) => {
state.state = ALERT_STATE.LOADING
},
switchAccountSucceeded: (state) => {
state.state = ALERT_STATE.CLOSED
},
switchedToUnconnectedAccount: (state) => {
state.state = ALERT_STATE.OPEN
},
@ -81,10 +94,13 @@ const {
connectAccountFailed,
connectAccountRequested,
connectAccountSucceeded,
dismissAlert,
disableAlertFailed,
disableAlertRequested,
disableAlertSucceeded,
dismissAlert,
switchAccountFailed,
switchAccountRequested,
switchAccountSucceeded,
switchedToUnconnectedAccount,
} = actions
@ -104,6 +120,20 @@ export const dismissAndDisableAlert = () => {
}
}
export const switchToAccount = (address) => {
return async (dispatch) => {
try {
await dispatch(switchAccountRequested())
await dispatch(setSelectedAddress(address))
await dispatch(switchAccountSucceeded())
} catch (error) {
console.error(error)
captureException(error)
await dispatch(switchAccountFailed())
}
}
}
export const connectAccount = () => {
return async (dispatch, getState) => {
const state = getState()

@ -5,12 +5,11 @@ import sendReducer from './send/send.duck'
import appStateReducer from './app/app'
import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck'
import gasReducer from './gas/gas.duck'
import { switchToConnected, unconnectedAccount } from './alerts'
import { unconnectedAccount } from './alerts'
import historyReducer from './history/history'
import { ALERT_TYPES } from '../../../app/scripts/controllers/alert'
export default combineReducers({
[ALERT_TYPES.switchToConnected]: switchToConnected,
[ALERT_TYPES.unconnectedAccount]: unconnectedAccount,
activeTab: (s) => (s === undefined ? null : s),
metamask: metamaskReducer,

@ -371,10 +371,8 @@ export const getCurrentLocale = (state) => state.metamask.currentLocale
export const getAlertEnabledness = (state) => state.metamask.alertEnabledness
export const getSwitchToConnectedAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.switchToConnected]
export const getUnconnectedAccountAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.unconnectedAccount]
export const getSwitchToConnectedAlertShown = (state) => state.metamask.switchToConnectedAlertShown
export const getUnconnectedAccountAlertShown = (state) => state.metamask.unconnectedAccountAlertShownOrigins
export const getTokens = (state) => state.metamask.tokens

@ -309,3 +309,27 @@ export function getAccountByAddress (accounts = [], targetAddress) {
export function stripHttpSchemes (urlString) {
return urlString.replace(/^https?:\/\//u, '')
}
/**
* Checks whether a URL-like value (object or string) is an extension URL.
*
* @param {string | URL | object} urlLike - The URL-like value to test.
* @returns {boolean} Whether the URL-like value is an extension URL.
*/
export function isExtensionUrl (urlLike) {
const EXT_PROTOCOLS = ['chrome-extension:', 'moz-extension:']
if (typeof urlLike === 'string') {
for (const protocol of EXT_PROTOCOLS) {
if (urlLike.startsWith(protocol)) {
return true
}
}
}
if (urlLike?.protocol) {
return EXT_PROTOCOLS.includes(urlLike.protocol)
}
return false
}

@ -1,8 +1,8 @@
import PropTypes from 'prop-types'
import React, { PureComponent } from 'react'
import { CONNECTED_ROUTE } from '../../helpers/constants/routes'
import Popover from '../../components/ui/popover'
import ConnectedAccountsList from '../../components/app/connected-accounts-list'
import ConnectedAccountsPermissions from '../../components/app/connected-accounts-permissions'
export default class ConnectedAccounts extends PureComponent {
static contextTypes = {
@ -17,7 +17,7 @@ export default class ConnectedAccounts extends PureComponent {
static propTypes = {
accountToConnect: PropTypes.object,
activeTabOrigin: PropTypes.string.isRequired,
addPermittedAccount: PropTypes.func.isRequired,
connectAccount: PropTypes.func.isRequired,
connectedAccounts: PropTypes.array.isRequired,
mostRecentOverviewPage: PropTypes.string.isRequired,
permissions: PropTypes.array,
@ -28,16 +28,12 @@ export default class ConnectedAccounts extends PureComponent {
history: PropTypes.object.isRequired,
}
viewConnectedSites = () => {
this.props.history.push(CONNECTED_ROUTE)
}
render () {
const {
accountToConnect,
activeTabOrigin,
isActiveTabExtension,
addPermittedAccount,
connectAccount,
connectedAccounts,
history,
mostRecentOverviewPage,
@ -58,15 +54,16 @@ export default class ConnectedAccounts extends PureComponent {
subtitle={connectedAccounts.length ? connectedAccountsDescription : t('connectedAccountsEmptyDescription')}
onClose={() => history.push(mostRecentOverviewPage)}
footerClassName="connected-accounts__footer"
footer={<ConnectedAccountsPermissions permissions={permissions} />}
>
<ConnectedAccountsList
accountToConnect={accountToConnect}
addPermittedAccount={addPermittedAccount}
connectAccount={connectAccount}
connectedAccounts={connectedAccounts}
permissions={permissions}
selectedAddress={selectedAddress}
removePermittedAccount={removePermittedAccount}
setSelectedAddress={setSelectedAddress}
shouldRenderListOptions
/>
</Popover>
)

@ -6,11 +6,10 @@ import {
getPermissionsForActiveTab,
getSelectedAddress,
} from '../../selectors'
import { isExtensionUrl } from '../../helpers/utils/util'
import { addPermittedAccount, removePermittedAccount, setSelectedAddress } from '../../store/actions'
import { getMostRecentOverviewPage } from '../../ducks/history/history'
const EXT_PROTOCOLS = ['chrome-extension:', 'moz-extension:']
const mapStateToProps = (state) => {
const { activeTab } = state
const accountToConnect = getAccountToConnectToActiveTab(state)
@ -18,7 +17,7 @@ const mapStateToProps = (state) => {
const permissions = getPermissionsForActiveTab(state)
const selectedAddress = getSelectedAddress(state)
const isActiveTabExtension = EXT_PROTOCOLS.includes(activeTab.protocol)
const isActiveTabExtension = isExtensionUrl(activeTab)
return {
accountToConnect,
isActiveTabExtension,
@ -39,14 +38,14 @@ const mapDispatchToProps = (dispatch) => {
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { activeTabOrigin: origin } = stateProps
const { activeTabOrigin } = stateProps
return {
...ownProps,
...stateProps,
...dispatchProps,
addPermittedAccount: (address) => dispatchProps.addPermittedAccount(origin, address),
removePermittedAccount: (address) => dispatchProps.removePermittedAccount(origin, address),
connectAccount: (address) => dispatchProps.addPermittedAccount(activeTabOrigin, address),
removePermittedAccount: (address) => dispatchProps.removePermittedAccount(activeTabOrigin, address),
}
}

@ -46,10 +46,6 @@ const AlertsTab = () => {
const t = useI18nContext()
const alertConfig = {
[ALERT_TYPES.switchToConnected]: {
title: t('alertSettingsSwitchToConnected'),
description: t('alertSettingsSwitchToConnectedDescription'),
},
[ALERT_TYPES.unconnectedAccount]: {
title: t('alertSettingsUnconnectedAccount'),
description: t('alertSettingsUnconnectedAccountDescription'),

@ -1187,7 +1187,7 @@ export function showAccountDetail (address) {
log.debug(`background.setSelectedAddress`)
const state = getState()
const unconnectedAccountAlertIsEnabled = getUnconnectedAccountAlertEnabledness(state)
const unconnectedAccountAccountAlertIsEnabled = getUnconnectedAccountAlertEnabledness(state)
const activeTabOrigin = state.activeTab.origin
const selectedAddress = getSelectedAddress(state)
const permittedAccountsForCurrentTab = getPermittedAccountsForCurrentTab(state)
@ -1207,9 +1207,9 @@ export function showAccountDetail (address) {
type: actionConstants.SHOW_ACCOUNT_DETAIL,
value: address,
})
if (unconnectedAccountAlertIsEnabled && switchingToUnconnectedAddress) {
if (unconnectedAccountAccountAlertIsEnabled && switchingToUnconnectedAddress) {
dispatch(switchedToUnconnectedAccount())
await setSwitchToConnectedAlertShown(activeTabOrigin)
await setUnconnectedAccountAlertShown(activeTabOrigin)
}
}
}
@ -2139,8 +2139,8 @@ export function setAlertEnabledness (alertId, enabledness) {
}
}
export async function setSwitchToConnectedAlertShown (origin) {
await promisifiedBackground.setSwitchToConnectedAlertShown(origin)
export async function setUnconnectedAccountAlertShown (origin) {
await promisifiedBackground.setUnconnectedAccountAlertShown(origin)
}
export function loadingMethodDataStarted () {

@ -13,10 +13,10 @@ import { ENVIRONMENT_TYPE_POPUP } from '../app/scripts/lib/enums'
import { fetchLocale } from './app/helpers/utils/i18n-helper'
import switchDirection from './app/helpers/utils/switch-direction'
import { getPermittedAccountsForCurrentTab, getSelectedAddress } from './app/selectors'
import { ALERT_STATE } from './app/ducks/alerts/switch-to-connected'
import { ALERT_STATE } from './app/ducks/alerts/unconnected-account'
import {
getSwitchToConnectedAlertEnabledness,
getSwitchToConnectedAlertShown,
getUnconnectedAccountAlertEnabledness,
getUnconnectedAccountAlertShown,
} from './app/ducks/metamask/metamask'
log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn')
@ -71,18 +71,18 @@ async function startApp (metamaskState, backgroundConnection, opts) {
const origin = draftInitialState.activeTab.origin
const permittedAccountsForCurrentTab = getPermittedAccountsForCurrentTab(draftInitialState)
const selectedAddress = getSelectedAddress(draftInitialState)
const switchToConnectedAlertShown = getSwitchToConnectedAlertShown(draftInitialState)
const switchToConnectedAlertIsEnabled = getSwitchToConnectedAlertEnabledness(draftInitialState)
const unconnectedAccountAlertShownOrigins = getUnconnectedAccountAlertShown(draftInitialState)
const unconnectedAccountAlertIsEnabled = getUnconnectedAccountAlertEnabledness(draftInitialState)
if (
origin &&
switchToConnectedAlertIsEnabled &&
!switchToConnectedAlertShown[origin] &&
unconnectedAccountAlertIsEnabled &&
!unconnectedAccountAlertShownOrigins[origin] &&
permittedAccountsForCurrentTab.length > 0 &&
!permittedAccountsForCurrentTab.includes(selectedAddress)
) {
draftInitialState[ALERT_TYPES.switchToConnected] = { state: ALERT_STATE.OPEN }
actions.setSwitchToConnectedAlertShown(origin)
draftInitialState[ALERT_TYPES.unconnectedAccount] = { state: ALERT_STATE.OPEN }
actions.setUnconnectedAccountAlertShown(origin)
}
}

Loading…
Cancel
Save