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" "description": "For importing an account from a private key"
}, },
"pending": { "pending": {
"message": "pending" "message": "Pending"
}, },
"permissionCheckedIconDescription": { "permissionCheckedIconDescription": {
"message": "You have approved this permission" "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; color: $Grey-500;
} }
&--pending { &--unconfirmed {
color: $Grey-500; color: $Grey-500;
} }
&--pending &__primary-currency { &--unconfirmed &__primary-currency {
color: $Grey-500; color: $Grey-500;
} }
&__status {
&--unapproved {
color: $flamingo;
}
&--failed {
color: $valencia;
}
&--cancelled {
color: $valencia;
}
&--queued {
color: $Grey-500;
}
}
&__pending-actions { &__pending-actions {
padding-top: 12px; padding-top: 12px;
display: flex; display: flex;

@ -3,11 +3,7 @@ import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import ListItem from '../../ui/list-item' import ListItem from '../../ui/list-item'
import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData' 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 Preloader from '../../ui/icon/preloader'
import Send from '../../ui/icon/send-icon.component'
import { useI18nContext } from '../../../hooks/useI18nContext' import { useI18nContext } from '../../../hooks/useI18nContext'
import { useCancelTransaction } from '../../../hooks/useCancelTransaction' import { useCancelTransaction } from '../../../hooks/useCancelTransaction'
import { useRetryTransaction } from '../../../hooks/useRetryTransaction' import { useRetryTransaction } from '../../../hooks/useRetryTransaction'
@ -17,17 +13,15 @@ import TransactionListItemDetails from '../transaction-list-item-details'
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes' import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'
import { import {
TRANSACTION_CATEGORY_APPROVAL,
TRANSACTION_CATEGORY_SIGNATURE_REQUEST, TRANSACTION_CATEGORY_SIGNATURE_REQUEST,
TRANSACTION_CATEGORY_INTERACTION,
TRANSACTION_CATEGORY_SEND,
TRANSACTION_CATEGORY_RECEIVE,
UNAPPROVED_STATUS, UNAPPROVED_STATUS,
FAILED_STATUS, FAILED_STATUS,
CANCELLED_STATUS, DROPPED_STATUS,
REJECTED_STATUS,
} from '../../../helpers/constants/transactions' } from '../../../helpers/constants/transactions'
import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp' 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 }) { export default function TransactionListItem ({ transactionGroup, isEarliestNonce = false }) {
@ -36,7 +30,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
const { hasCancelled } = transactionGroup const { hasCancelled } = transactionGroup
const [showDetails, setShowDetails] = useState(false) const [showDetails, setShowDetails] = useState(false)
const { initialTransaction: { id } } = transactionGroup const { initialTransaction: { id }, primaryTransaction } = transactionGroup
const [cancelEnabled, cancelTransaction] = useCancelTransaction(transactionGroup) const [cancelEnabled, cancelTransaction] = useCancelTransaction(transactionGroup)
const retryTransaction = useRetryTransaction(transactionGroup) const retryTransaction = useRetryTransaction(transactionGroup)
@ -55,50 +49,12 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
senderAddress, senderAddress,
} = useTransactionDisplayData(transactionGroup) } = useTransactionDisplayData(transactionGroup)
const isApprove = category === TRANSACTION_CATEGORY_APPROVAL
const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST 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 isUnapproved = status === UNAPPROVED_STATUS
const isFailed = status === FAILED_STATUS
const isCancelled = status === CANCELLED_STATUS
const color = isFailed ? '#D73A49' : '#2F80ED' const className = classnames('transaction-list-item', {
'transaction-list-item--unconfirmed': isPending || [FAILED_STATUS, DROPPED_STATUS, REJECTED_STATUS].includes(status),
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 toggleShowDetails = useCallback(() => { const toggleShowDetails = useCallback(() => {
if (isUnapproved) { if (isUnapproved) {
@ -161,9 +117,17 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
color="#D73A49" color="#D73A49"
/> />
)} )}
icon={<Icon color={color} size={28} />} icon={<TransactionIcon category={category} status={status} />}
subtitle={subtitle} subtitle={subtitle}
subtitleStatus={subtitleStatus} subtitleStatus={(
<TransactionStatus
isPending={isPending}
isEarliestNonce={isEarliestNonce}
error={primaryTransaction.err}
date={date}
status={status}
/>
)}
rightContent={!isSignatureReq && ( rightContent={!isSignatureReq && (
<> <>
<h2 className="transaction-list-item__primary-currency">{primaryCurrency}</h2> <h2 className="transaction-list-item__primary-currency">{primaryCurrency}</h2>
@ -184,7 +148,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
senderAddress={senderAddress} senderAddress={senderAddress}
recipientAddress={recipientAddress} recipientAddress={recipientAddress}
onRetry={retryTransaction} onRetry={retryTransaction}
showRetry={isFailed} showRetry={status === FAILED_STATUS}
showSpeedUp={shouldShowSpeedUp} showSpeedUp={shouldShowSpeedUp}
isEarliestNonce={isEarliestNonce} isEarliestNonce={isEarliestNonce}
onCancel={cancelTransaction} onCancel={cancelTransaction}

@ -1,52 +1,24 @@
.transaction-status { .transaction-status {
height: 26px; display: inline;
width: 84px; &--unapproved {
border-radius: 4px; color: $Orange-500;
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;
} }
&--failed {
&--confirmed { color: $Red-500;
background-color: #eafad7;
color: #609a1c;
.transaction-status__transaction-count {
border: 1px solid #609a1c;
}
} }
&--cancelled {
&--approved, &--submitted { color: $Red-500;
background-color: #FFF2DB;
color: #CA810A;
.transaction-status__transaction-count {
border: 1px solid #CA810A;
}
} }
&--dropped {
&--failed { color: $Red-500;
background: lighten($monzo, 56%); }
color: $monzo; &--rejected {
color: $Red-500;
.transaction-status__transaction-count { }
border: 1px solid $monzo; &--pending {
} color: $Orange-500;
} }
&--queued {
&__pending-spinner { color: $Grey-500;
height: 16px;
width: 16px;
margin-right: 6px;
} }
} }

@ -1,33 +1,80 @@
import React from 'react' import React from 'react'
import assert from 'assert' import assert from 'assert'
import { mount } from 'enzyme' import { mount } from 'enzyme'
import sinon from 'sinon'
import * as i18nHook from '../../../../hooks/useI18nContext'
import TransactionStatus from '../transaction-status.component' import TransactionStatus from '../transaction-status.component'
import Tooltip from '../../../ui/tooltip-v2' import Tooltip from '../../../ui/tooltip-v2'
describe('TransactionStatus Component', function () { 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( const wrapper = mount(
<TransactionStatus <TransactionStatus
statusKey="approved" status="confirmed"
title="test-title" date="June 1"
/>, />
{ context: { t: (str) => str.toUpperCase() } }
) )
assert.ok(wrapper) 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') assert.equal(wrapper.find(Tooltip).props().title, 'test-title')
}) })
it('should render SUBMITTED properly', function () { it('should render PENDING properly', function () {
const wrapper = mount( const wrapper = mount(
<TransactionStatus <TransactionStatus
statusKey="submitted" date="June 1"
/>, status="submitted"
{ context: { t: (str) => str.toUpperCase() } } isEarliestNonce
/>
) )
assert.ok(wrapper) 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 PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import Tooltip from '../../ui/tooltip-v2' import Tooltip from '../../ui/tooltip-v2'
import Spinner from '../../ui/spinner'
import { import {
UNAPPROVED_STATUS, UNAPPROVED_STATUS,
REJECTED_STATUS, REJECTED_STATUS,
APPROVED_STATUS,
SIGNED_STATUS,
SUBMITTED_STATUS, SUBMITTED_STATUS,
CONFIRMED_STATUS, CONFIRMED_STATUS,
FAILED_STATUS, FAILED_STATUS,
DROPPED_STATUS, DROPPED_STATUS,
CANCELLED_STATUS, CANCELLED_STATUS,
APPROVED_STATUS,
SIGNED_STATUS,
} from '../../../helpers/constants/transactions' } 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 = { const statusToClassNameHash = {
[UNAPPROVED_STATUS]: 'transaction-status--unapproved', [UNAPPROVED_STATUS]: 'transaction-status--unapproved',
[REJECTED_STATUS]: 'transaction-status--rejected', [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', [FAILED_STATUS]: 'transaction-status--failed',
[DROPPED_STATUS]: 'transaction-status--dropped', [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 = { export default function TransactionStatus ({ status, date, error, isEarliestNonce, className }) {
[SUBMITTED_STATUS]: 'pending', const t = useI18nContext()
} const tooltipText = error?.rpc?.message || error?.message
let statusKey = status
export default class TransactionStatus extends PureComponent { if (pendingStatusHash[status]) {
static defaultProps = { statusKey = isEarliestNonce ? PENDING_PSEUDO_STATUS : QUEUED_PSEUDO_STATUS
title: null,
} }
static contextTypes = { const statusText = statusKey === CONFIRMED_STATUS ? date : t(statusKey)
t: PropTypes.func,
}
static propTypes = { return (
statusKey: PropTypes.string, <span>
className: PropTypes.string, <Tooltip
title: PropTypes.string, position="top"
} title={tooltipText}
wrapperClassName={classnames('transaction-status', className, statusToClassNameHash[statusKey])}
render () { >
const { className, statusKey, title } = this.props { statusText }
const statusText = this.context.t(statusToTextHash[statusKey] || statusKey) </Tooltip>
{' · '}
</span>
)
}
return ( TransactionStatus.propTypes = {
<div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}> status: PropTypes.string,
{ statusToTextHash[statusKey] === 'pending' ? <Spinner className="transaction-status__pending-spinner" /> : null } className: PropTypes.string,
<Tooltip date: PropTypes.string,
position="top" error: PropTypes.object,
title={title} isEarliestNonce: PropTypes.bool,
>
{ statusText }
</Tooltip>
</div>
)
}
} }

Loading…
Cancel
Save