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.
feature/default_network_editable
Brad Decker 5 years ago committed by GitHub
parent 1f8a7a72c9
commit a4e5fc934d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/_locales/en/messages.json
  2. 1
      ui/app/components/app/transaction-icon/index.js
  3. 58
      ui/app/components/app/transaction-icon/transaction-icon.js
  4. 19
      ui/app/components/app/transaction-list-item/index.scss
  5. 74
      ui/app/components/app/transaction-list-item/transaction-list-item.component.js
  6. 62
      ui/app/components/app/transaction-status/index.scss
  7. 69
      ui/app/components/app/transaction-status/tests/transaction-status.component.test.js
  8. 92
      ui/app/components/app/transaction-status/transaction-status.component.js

@ -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"

@ -0,0 +1 @@
export { default } from './transaction-icon'

@ -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;
}
&--failed {
color: $Red-500;
}
&--approved, &--submitted {
background-color: #FFF2DB;
color: #CA810A;
.transaction-status__transaction-count {
border: 1px solid #CA810A;
}
&--cancelled {
color: $Red-500;
}
&--failed {
background: lighten($monzo, 56%);
color: $monzo;
.transaction-status__transaction-count {
border: 1px solid $monzo;
}
&--dropped {
color: $Red-500;
}
&--rejected {
color: $Red-500;
}
&--pending {
color: $Orange-500;
}
&__pending-spinner {
height: 16px;
width: 16px;
margin-right: 6px;
&--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,
}
const statusText = statusKey === CONFIRMED_STATUS ? date : t(statusKey)
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)
return (
<span>
<Tooltip
position="top"
title={tooltipText}
wrapperClassName={classnames('transaction-status', className, statusToClassNameHash[statusKey])}
>
{ statusText }
</Tooltip>
{' · '}
</span>
)
}
return (
<div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
{ statusToTextHash[statusKey] === 'pending' ? <Spinner className="transaction-status__pending-spinner" /> : null }
<Tooltip
position="top"
title={title}
>
{ statusText }
</Tooltip>
</div>
)
}
TransactionStatus.propTypes = {
status: PropTypes.string,
className: PropTypes.string,
date: PropTypes.string,
error: PropTypes.object,
isEarliestNonce: PropTypes.bool,
}

Loading…
Cancel
Save