mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
restore status tooltip (#8745)
initially set out to add the failed tooltip back to the transaction list, but in the process rediscovered the transaction-status component which illuminated a fair number of statuses that were not properly handled by the refactor of the list. These statuses were discussed with UX and engineering team members to come up with a definitive list of statuses that should be reflected in the UI Changes: 1. normalized the color of status labels to use Red-500 and Orange-500 where applicable 2. added a new color of icon for pending transactions -- grey 3. added support for dropped and rejected labels 4. failed, dropped, rejected and cancelled all have red icons now. 5. cancelled transactions will reflect a change in the user's balance 6. tooltip displayed for failed transactions 7. Icon logic isolated to a new component.
This commit is contained in:
parent
1f8a7a72c9
commit
a4e5fc934d
@ -1090,7 +1090,7 @@
|
||||
"description": "For importing an account from a private key"
|
||||
},
|
||||
"pending": {
|
||||
"message": "pending"
|
||||
"message": "Pending"
|
||||
},
|
||||
"permissionCheckedIconDescription": {
|
||||
"message": "You have approved this permission"
|
||||
|
1
ui/app/components/app/transaction-icon/index.js
Normal file
1
ui/app/components/app/transaction-icon/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-icon'
|
58
ui/app/components/app/transaction-icon/transaction-icon.js
Normal file
58
ui/app/components/app/transaction-icon/transaction-icon.js
Normal file
@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Approve from '../../ui/icon/approve-icon.component'
|
||||
import Interaction from '../../ui/icon/interaction-icon.component'
|
||||
import Receive from '../../ui/icon/receive-icon.component'
|
||||
import Send from '../../ui/icon/send-icon.component'
|
||||
import Sign from '../../ui/icon/sign-icon.component'
|
||||
import {
|
||||
TRANSACTION_CATEGORY_APPROVAL,
|
||||
TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
|
||||
TRANSACTION_CATEGORY_INTERACTION,
|
||||
TRANSACTION_CATEGORY_SEND,
|
||||
TRANSACTION_CATEGORY_RECEIVE,
|
||||
UNAPPROVED_STATUS,
|
||||
FAILED_STATUS,
|
||||
REJECTED_STATUS,
|
||||
CANCELLED_STATUS,
|
||||
DROPPED_STATUS,
|
||||
SUBMITTED_STATUS,
|
||||
APPROVED_STATUS,
|
||||
} from '../../../helpers/constants/transactions'
|
||||
|
||||
|
||||
const ICON_MAP = {
|
||||
[TRANSACTION_CATEGORY_APPROVAL]: Approve,
|
||||
[TRANSACTION_CATEGORY_INTERACTION]: Interaction,
|
||||
[TRANSACTION_CATEGORY_SEND]: Send,
|
||||
[TRANSACTION_CATEGORY_SIGNATURE_REQUEST]: Sign,
|
||||
[TRANSACTION_CATEGORY_RECEIVE]: Receive,
|
||||
}
|
||||
|
||||
const FAIL_COLOR = '#D73A49'
|
||||
const PENDING_COLOR = '#6A737D'
|
||||
const OK_COLOR = '#2F80ED'
|
||||
|
||||
const COLOR_MAP = {
|
||||
[SUBMITTED_STATUS]: PENDING_COLOR,
|
||||
[UNAPPROVED_STATUS]: PENDING_COLOR,
|
||||
[APPROVED_STATUS]: PENDING_COLOR,
|
||||
[FAILED_STATUS]: FAIL_COLOR,
|
||||
[REJECTED_STATUS]: FAIL_COLOR,
|
||||
[CANCELLED_STATUS]: FAIL_COLOR,
|
||||
[DROPPED_STATUS]: FAIL_COLOR,
|
||||
}
|
||||
|
||||
export default function TransactionIcon ({ status, category }) {
|
||||
|
||||
const color = COLOR_MAP[status] || OK_COLOR
|
||||
|
||||
const Icon = ICON_MAP[category]
|
||||
|
||||
return <Icon color={color} size={28} />
|
||||
}
|
||||
|
||||
TransactionIcon.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
}
|
@ -15,29 +15,14 @@
|
||||
color: $Grey-500;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
&--unconfirmed {
|
||||
color: $Grey-500;
|
||||
}
|
||||
|
||||
&--pending &__primary-currency {
|
||||
&--unconfirmed &__primary-currency {
|
||||
color: $Grey-500;
|
||||
}
|
||||
|
||||
&__status {
|
||||
&--unapproved {
|
||||
color: $flamingo;
|
||||
}
|
||||
&--failed {
|
||||
color: $valencia;
|
||||
}
|
||||
&--cancelled {
|
||||
color: $valencia;
|
||||
}
|
||||
&--queued {
|
||||
color: $Grey-500;
|
||||
}
|
||||
}
|
||||
|
||||
&__pending-actions {
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
|
@ -3,11 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import ListItem from '../../ui/list-item'
|
||||
import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData'
|
||||
import Approve from '../../ui/icon/approve-icon.component'
|
||||
import Interaction from '../../ui/icon/interaction-icon.component'
|
||||
import Receive from '../../ui/icon/receive-icon.component'
|
||||
import Preloader from '../../ui/icon/preloader'
|
||||
import Send from '../../ui/icon/send-icon.component'
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext'
|
||||
import { useCancelTransaction } from '../../../hooks/useCancelTransaction'
|
||||
import { useRetryTransaction } from '../../../hooks/useRetryTransaction'
|
||||
@ -17,17 +13,15 @@ import TransactionListItemDetails from '../transaction-list-item-details'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'
|
||||
import {
|
||||
TRANSACTION_CATEGORY_APPROVAL,
|
||||
TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
|
||||
TRANSACTION_CATEGORY_INTERACTION,
|
||||
TRANSACTION_CATEGORY_SEND,
|
||||
TRANSACTION_CATEGORY_RECEIVE,
|
||||
UNAPPROVED_STATUS,
|
||||
FAILED_STATUS,
|
||||
CANCELLED_STATUS,
|
||||
DROPPED_STATUS,
|
||||
REJECTED_STATUS,
|
||||
} from '../../../helpers/constants/transactions'
|
||||
import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp'
|
||||
import Sign from '../../ui/icon/sign-icon.component'
|
||||
import TransactionStatus from '../transaction-status/transaction-status.component'
|
||||
import TransactionIcon from '../transaction-icon'
|
||||
|
||||
|
||||
export default function TransactionListItem ({ transactionGroup, isEarliestNonce = false }) {
|
||||
@ -36,7 +30,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
const { hasCancelled } = transactionGroup
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const { initialTransaction: { id } } = transactionGroup
|
||||
const { initialTransaction: { id }, primaryTransaction } = transactionGroup
|
||||
|
||||
const [cancelEnabled, cancelTransaction] = useCancelTransaction(transactionGroup)
|
||||
const retryTransaction = useRetryTransaction(transactionGroup)
|
||||
@ -55,50 +49,12 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
senderAddress,
|
||||
} = useTransactionDisplayData(transactionGroup)
|
||||
|
||||
const isApprove = category === TRANSACTION_CATEGORY_APPROVAL
|
||||
const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST
|
||||
const isInteraction = category === TRANSACTION_CATEGORY_INTERACTION
|
||||
const isSend = category === TRANSACTION_CATEGORY_SEND
|
||||
const isReceive = category === TRANSACTION_CATEGORY_RECEIVE
|
||||
const isUnapproved = status === UNAPPROVED_STATUS
|
||||
const isFailed = status === FAILED_STATUS
|
||||
const isCancelled = status === CANCELLED_STATUS
|
||||
|
||||
const color = isFailed ? '#D73A49' : '#2F80ED'
|
||||
|
||||
let Icon
|
||||
if (isApprove) {
|
||||
Icon = Approve
|
||||
} else if (isSend) {
|
||||
Icon = Send
|
||||
} else if (isReceive) {
|
||||
Icon = Receive
|
||||
} else if (isInteraction) {
|
||||
Icon = Interaction
|
||||
} else if (isSignatureReq) {
|
||||
Icon = Sign
|
||||
}
|
||||
|
||||
let subtitleStatus = <span><span className="transaction-list-item__date">{date}</span> · </span>
|
||||
if (isUnapproved) {
|
||||
subtitleStatus = (
|
||||
<span><span className="transaction-list-item__status--unapproved">{t('unapproved')}</span> · </span>
|
||||
)
|
||||
} else if (isFailed) {
|
||||
subtitleStatus = (
|
||||
<span><span className="transaction-list-item__status--failed">{t('failed')}</span> · </span>
|
||||
)
|
||||
} else if (isCancelled) {
|
||||
subtitleStatus = (
|
||||
<span><span className="transaction-list-item__status--cancelled">{t('cancelled')}</span> · </span>
|
||||
)
|
||||
} else if (isPending && !isEarliestNonce) {
|
||||
subtitleStatus = (
|
||||
<span><span className="transaction-list-item__status--queued">{t('queued')}</span> · </span>
|
||||
)
|
||||
}
|
||||
|
||||
const className = classnames('transaction-list-item', { 'transaction-list-item--pending': isPending })
|
||||
const className = classnames('transaction-list-item', {
|
||||
'transaction-list-item--unconfirmed': isPending || [FAILED_STATUS, DROPPED_STATUS, REJECTED_STATUS].includes(status),
|
||||
})
|
||||
|
||||
const toggleShowDetails = useCallback(() => {
|
||||
if (isUnapproved) {
|
||||
@ -161,9 +117,17 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
color="#D73A49"
|
||||
/>
|
||||
)}
|
||||
icon={<Icon color={color} size={28} />}
|
||||
icon={<TransactionIcon category={category} status={status} />}
|
||||
subtitle={subtitle}
|
||||
subtitleStatus={subtitleStatus}
|
||||
subtitleStatus={(
|
||||
<TransactionStatus
|
||||
isPending={isPending}
|
||||
isEarliestNonce={isEarliestNonce}
|
||||
error={primaryTransaction.err}
|
||||
date={date}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
rightContent={!isSignatureReq && (
|
||||
<>
|
||||
<h2 className="transaction-list-item__primary-currency">{primaryCurrency}</h2>
|
||||
@ -184,7 +148,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
senderAddress={senderAddress}
|
||||
recipientAddress={recipientAddress}
|
||||
onRetry={retryTransaction}
|
||||
showRetry={isFailed}
|
||||
showRetry={status === FAILED_STATUS}
|
||||
showSpeedUp={shouldShowSpeedUp}
|
||||
isEarliestNonce={isEarliestNonce}
|
||||
onCancel={cancelTransaction}
|
||||
|
@ -1,52 +1,24 @@
|
||||
.transaction-status {
|
||||
height: 26px;
|
||||
width: 84px;
|
||||
border-radius: 4px;
|
||||
background-color: #f0f0f0;
|
||||
color: #5e6064;
|
||||
font-size: .625rem;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
height: 16px;
|
||||
min-width: 72px;
|
||||
font-size: 10px;
|
||||
padding: 0 12px;
|
||||
display: inline;
|
||||
&--unapproved {
|
||||
color: $Orange-500;
|
||||
}
|
||||
|
||||
&--confirmed {
|
||||
background-color: #eafad7;
|
||||
color: #609a1c;
|
||||
|
||||
.transaction-status__transaction-count {
|
||||
border: 1px solid #609a1c;
|
||||
}
|
||||
}
|
||||
|
||||
&--approved, &--submitted {
|
||||
background-color: #FFF2DB;
|
||||
color: #CA810A;
|
||||
|
||||
.transaction-status__transaction-count {
|
||||
border: 1px solid #CA810A;
|
||||
}
|
||||
}
|
||||
|
||||
&--failed {
|
||||
background: lighten($monzo, 56%);
|
||||
color: $monzo;
|
||||
|
||||
.transaction-status__transaction-count {
|
||||
border: 1px solid $monzo;
|
||||
color: $Red-500;
|
||||
}
|
||||
&--cancelled {
|
||||
color: $Red-500;
|
||||
}
|
||||
|
||||
&__pending-spinner {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 6px;
|
||||
&--dropped {
|
||||
color: $Red-500;
|
||||
}
|
||||
&--rejected {
|
||||
color: $Red-500;
|
||||
}
|
||||
&--pending {
|
||||
color: $Orange-500;
|
||||
}
|
||||
&--queued {
|
||||
color: $Grey-500;
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,80 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { mount } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import * as i18nHook from '../../../../hooks/useI18nContext'
|
||||
import TransactionStatus from '../transaction-status.component'
|
||||
import Tooltip from '../../../ui/tooltip-v2'
|
||||
|
||||
describe('TransactionStatus Component', function () {
|
||||
it('should render APPROVED properly', function () {
|
||||
before(function () {
|
||||
sinon.stub(i18nHook, 'useI18nContext').returns((str) => str.toUpperCase())
|
||||
})
|
||||
|
||||
it('should render CONFIRMED properly', function () {
|
||||
const wrapper = mount(
|
||||
<TransactionStatus
|
||||
statusKey="approved"
|
||||
title="test-title"
|
||||
/>,
|
||||
{ context: { t: (str) => str.toUpperCase() } }
|
||||
status="confirmed"
|
||||
date="June 1"
|
||||
/>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.text(), 'APPROVED')
|
||||
assert.equal(wrapper.text(), 'June 1 · ')
|
||||
})
|
||||
|
||||
it('should render PENDING properly when status is APPROVED', function () {
|
||||
const wrapper = mount(
|
||||
<TransactionStatus
|
||||
status="approved"
|
||||
isEarliestNonce
|
||||
error={{ message: 'test-title' }}
|
||||
/>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.text(), 'PENDING · ')
|
||||
assert.equal(wrapper.find(Tooltip).props().title, 'test-title')
|
||||
})
|
||||
|
||||
it('should render SUBMITTED properly', function () {
|
||||
it('should render PENDING properly', function () {
|
||||
const wrapper = mount(
|
||||
<TransactionStatus
|
||||
statusKey="submitted"
|
||||
/>,
|
||||
{ context: { t: (str) => str.toUpperCase() } }
|
||||
date="June 1"
|
||||
status="submitted"
|
||||
isEarliestNonce
|
||||
/>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.text(), 'PENDING')
|
||||
assert.equal(wrapper.text(), 'PENDING · ')
|
||||
})
|
||||
|
||||
it('should render QUEUED properly', function () {
|
||||
const wrapper = mount(
|
||||
<TransactionStatus
|
||||
status="queued"
|
||||
/>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.ok(wrapper.find('.transaction-status--queued').length, 'queued className not found')
|
||||
assert.equal(wrapper.text(), 'QUEUED · ')
|
||||
})
|
||||
|
||||
it('should render UNAPPROVED properly', function () {
|
||||
const wrapper = mount(
|
||||
<TransactionStatus
|
||||
status="unapproved"
|
||||
/>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.ok(wrapper.find('.transaction-status--unapproved').length, 'unapproved className not found')
|
||||
assert.equal(wrapper.text(), 'UNAPPROVED · ')
|
||||
})
|
||||
|
||||
after(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
})
|
||||
|
@ -1,66 +1,78 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Tooltip from '../../ui/tooltip-v2'
|
||||
import Spinner from '../../ui/spinner'
|
||||
|
||||
import {
|
||||
UNAPPROVED_STATUS,
|
||||
REJECTED_STATUS,
|
||||
APPROVED_STATUS,
|
||||
SIGNED_STATUS,
|
||||
SUBMITTED_STATUS,
|
||||
CONFIRMED_STATUS,
|
||||
FAILED_STATUS,
|
||||
DROPPED_STATUS,
|
||||
CANCELLED_STATUS,
|
||||
APPROVED_STATUS,
|
||||
SIGNED_STATUS,
|
||||
} from '../../../helpers/constants/transactions'
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext'
|
||||
|
||||
const QUEUED_PSEUDO_STATUS = 'queued'
|
||||
const PENDING_PSEUDO_STATUS = 'pending'
|
||||
|
||||
/**
|
||||
* A note about status logic for this component:
|
||||
* Approved, Signed and Submitted statuses are all treated, effectively
|
||||
* as pending. Transactions are only approved or signed for less than a
|
||||
* second, usually, and ultimately should be rendered in the UI no
|
||||
* differently than a pending transaction.
|
||||
*
|
||||
* Confirmed transactions are not especially highlighted except that their
|
||||
* status label will be the date the transaction was finalized.
|
||||
*/
|
||||
const pendingStatusHash = {
|
||||
[SUBMITTED_STATUS]: PENDING_PSEUDO_STATUS,
|
||||
[APPROVED_STATUS]: PENDING_PSEUDO_STATUS,
|
||||
[SIGNED_STATUS]: PENDING_PSEUDO_STATUS,
|
||||
}
|
||||
|
||||
const statusToClassNameHash = {
|
||||
[UNAPPROVED_STATUS]: 'transaction-status--unapproved',
|
||||
[REJECTED_STATUS]: 'transaction-status--rejected',
|
||||
[APPROVED_STATUS]: 'transaction-status--approved',
|
||||
[SIGNED_STATUS]: 'transaction-status--signed',
|
||||
[SUBMITTED_STATUS]: 'transaction-status--submitted',
|
||||
[CONFIRMED_STATUS]: 'transaction-status--confirmed',
|
||||
[FAILED_STATUS]: 'transaction-status--failed',
|
||||
[DROPPED_STATUS]: 'transaction-status--dropped',
|
||||
[CANCELLED_STATUS]: 'transaction-status--failed',
|
||||
[CANCELLED_STATUS]: 'transaction-status--cancelled',
|
||||
[QUEUED_PSEUDO_STATUS]: 'transaction-status--queued',
|
||||
[PENDING_PSEUDO_STATUS]: 'transaction-status--pending',
|
||||
}
|
||||
|
||||
const statusToTextHash = {
|
||||
[SUBMITTED_STATUS]: 'pending',
|
||||
}
|
||||
|
||||
export default class TransactionStatus extends PureComponent {
|
||||
static defaultProps = {
|
||||
title: null,
|
||||
export default function TransactionStatus ({ status, date, error, isEarliestNonce, className }) {
|
||||
const t = useI18nContext()
|
||||
const tooltipText = error?.rpc?.message || error?.message
|
||||
let statusKey = status
|
||||
if (pendingStatusHash[status]) {
|
||||
statusKey = isEarliestNonce ? PENDING_PSEUDO_STATUS : QUEUED_PSEUDO_STATUS
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
statusKey: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, statusKey, title } = this.props
|
||||
const statusText = this.context.t(statusToTextHash[statusKey] || statusKey)
|
||||
const statusText = statusKey === CONFIRMED_STATUS ? date : t(statusKey)
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
|
||||
{ statusToTextHash[statusKey] === 'pending' ? <Spinner className="transaction-status__pending-spinner" /> : null }
|
||||
<span>
|
||||
<Tooltip
|
||||
position="top"
|
||||
title={title}
|
||||
title={tooltipText}
|
||||
wrapperClassName={classnames('transaction-status', className, statusToClassNameHash[statusKey])}
|
||||
>
|
||||
{ statusText }
|
||||
</Tooltip>
|
||||
</div>
|
||||
{' · '}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TransactionStatus.propTypes = {
|
||||
status: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
date: PropTypes.string,
|
||||
error: PropTypes.object,
|
||||
isEarliestNonce: PropTypes.bool,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user