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 { |
.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…
Reference in new issue