diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f86498776..beef994aa 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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" diff --git a/ui/app/components/app/transaction-icon/index.js b/ui/app/components/app/transaction-icon/index.js new file mode 100644 index 000000000..d41970a54 --- /dev/null +++ b/ui/app/components/app/transaction-icon/index.js @@ -0,0 +1 @@ +export { default } from './transaction-icon' diff --git a/ui/app/components/app/transaction-icon/transaction-icon.js b/ui/app/components/app/transaction-icon/transaction-icon.js new file mode 100644 index 000000000..24f4f4715 --- /dev/null +++ b/ui/app/components/app/transaction-icon/transaction-icon.js @@ -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 +} + +TransactionIcon.propTypes = { + status: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, +} diff --git a/ui/app/components/app/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss index 3f7af9586..330f731ec 100644 --- a/ui/app/components/app/transaction-list-item/index.scss +++ b/ui/app/components/app/transaction-list-item/index.scss @@ -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; diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index bbeb777e8..b42d8c7b6 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -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 = {date} · - if (isUnapproved) { - subtitleStatus = ( - {t('unapproved')} · - ) - } else if (isFailed) { - subtitleStatus = ( - {t('failed')} · - ) - } else if (isCancelled) { - subtitleStatus = ( - {t('cancelled')} · - ) - } else if (isPending && !isEarliestNonce) { - subtitleStatus = ( - {t('queued')} · - ) - } - - 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={} subtitle={subtitle} - subtitleStatus={subtitleStatus} + subtitleStatus={( + + )} rightContent={!isSignatureReq && ( <>

{primaryCurrency}

@@ -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} diff --git a/ui/app/components/app/transaction-status/index.scss b/ui/app/components/app/transaction-status/index.scss index 99884d28c..13dcd6c30 100644 --- a/ui/app/components/app/transaction-status/index.scss +++ b/ui/app/components/app/transaction-status/index.scss @@ -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; } } diff --git a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js index 510950248..939090875 100644 --- a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js +++ b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js @@ -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( , - { 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( + + ) + + 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( , - { 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( + + ) + + 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( + + ) + + assert.ok(wrapper) + assert.ok(wrapper.find('.transaction-status--unapproved').length, 'unapproved className not found') + assert.equal(wrapper.text(), 'UNAPPROVED · ') + }) + + after(function () { + sinon.restore() }) }) diff --git a/ui/app/components/app/transaction-status/transaction-status.component.js b/ui/app/components/app/transaction-status/transaction-status.component.js index a97b79bde..c525f7484 100644 --- a/ui/app/components/app/transaction-status/transaction-status.component.js +++ b/ui/app/components/app/transaction-status/transaction-status.component.js @@ -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 ( + + + { statusText } + + {' · '} + + ) +} - return ( -
- { statusToTextHash[statusKey] === 'pending' ? : null } - - { statusText } - -
- ) - } +TransactionStatus.propTypes = { + status: PropTypes.string, + className: PropTypes.string, + date: PropTypes.string, + error: PropTypes.object, + isEarliestNonce: PropTypes.bool, }