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
parent
1f8a7a72c9
commit
a4e5fc934d
@ -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, |
||||
} |
@ -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…
Reference in new issue