diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ffa4609d0..388649813 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -305,6 +305,9 @@ "cancel": { "message": "Cancel" }, + "cancelEdit": { + "message": "Cancel Edit" + }, "cancelPopoverTitle": { "message": "Cancel transaction" }, diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index e3008b577..3b5b8e0f3 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -1736,6 +1736,10 @@ export function getSendHexData(state) { return state[name].draftTransaction.userInputHexData; } +export function getDraftTransactionID(state) { + return state[name].draftTransaction.id; +} + export function sendAmountIsInError(state) { return Boolean(state[name].amount.error); } diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 807a936b9..9e272b9b3 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -51,6 +51,7 @@ import sendReducer, { getSendAmount, getIsBalanceInsufficient, getSendMaxModeState, + getDraftTransactionID, sendAmountIsInError, getSendHexData, getSendTo, @@ -2459,6 +2460,21 @@ describe('Send Slice', () => { ).toBe(true); }); + it('has a selector to get the draft transaction ID', () => { + expect(getDraftTransactionID({ send: initialState })).toBeNull(); + expect( + getDraftTransactionID({ + send: { + ...initialState, + draftTransaction: { + ...initialState.draftTransaction, + id: 'ID', + }, + }, + }), + ).toBe('ID'); + }); + it('has a selector to get the user entered hex data', () => { expect(getSendHexData({ send: initialState })).toBeNull(); expect( diff --git a/ui/pages/send/send-footer/send-footer.component.js b/ui/pages/send/send-footer/send-footer.component.js index 840146f5f..cde413e0a 100644 --- a/ui/pages/send/send-footer/send-footer.component.js +++ b/ui/pages/send/send-footer/send-footer.component.js @@ -2,7 +2,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { isEqual } from 'lodash'; import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'; -import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'; +import { + CONFIRM_TRANSACTION_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes'; +import { SEND_STAGES } from '../../../ducks/send'; export default class SendFooter extends Component { static propTypes = { @@ -13,9 +17,12 @@ export default class SendFooter extends Component { sign: PropTypes.func, to: PropTypes.string, toAccounts: PropTypes.array, + sendStage: PropTypes.string, sendErrors: PropTypes.object, gasEstimateType: PropTypes.string, mostRecentOverviewPage: PropTypes.string.isRequired, + cancelTx: PropTypes.func, + draftTransactionID: PropTypes.string, }; static contextTypes = { @@ -24,9 +31,21 @@ export default class SendFooter extends Component { }; onCancel() { - const { resetSendState, history, mostRecentOverviewPage } = this.props; + const { + cancelTx, + draftTransactionID, + history, + mostRecentOverviewPage, + resetSendState, + sendStage, + } = this.props; + + if (draftTransactionID) cancelTx({ id: draftTransactionID }); resetSendState(); - history.push(mostRecentOverviewPage); + + const nextRoute = + sendStage === SEND_STAGES.EDIT ? DEFAULT_ROUTE : mostRecentOverviewPage; + history.push(nextRoute); } async onSubmit(event) { @@ -85,11 +104,14 @@ export default class SendFooter extends Component { } render() { + const { t } = this.context; + const { sendStage } = this.props; return ( this.onCancel()} onSubmit={(e) => this.onSubmit(e)} disabled={this.props.disabled} + cancelText={sendStage === SEND_STAGES.EDIT ? t('reject') : t('cancel')} /> ); } diff --git a/ui/pages/send/send-footer/send-footer.component.test.js b/ui/pages/send/send-footer/send-footer.component.test.js index fcd4472d6..23bc565ac 100644 --- a/ui/pages/send/send-footer/send-footer.component.test.js +++ b/ui/pages/send/send-footer/send-footer.component.test.js @@ -1,8 +1,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'; +import { + CONFIRM_TRANSACTION_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes'; import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'; +import { renderWithProvider } from '../../../../test/jest'; import SendFooter from './send-footer.component'; describe('SendFooter Component', () => { @@ -10,6 +14,7 @@ describe('SendFooter Component', () => { const propsMethodSpies = { addToAddressBookIfNew: sinon.spy(), + cancelTx: sinon.spy(), resetSendState: sinon.spy(), sign: sinon.spy(), update: sinon.spy(), @@ -20,31 +25,40 @@ describe('SendFooter Component', () => { }; const MOCK_EVENT = { preventDefault: () => undefined }; + const renderShallow = (props) => { + return shallow( + , + { context: { t: (str) => str, metricsEvent: () => ({}) } }, + ); + }; + beforeAll(() => { sinon.spy(SendFooter.prototype, 'onCancel'); sinon.spy(SendFooter.prototype, 'onSubmit'); }); beforeEach(() => { - wrapper = shallow( - , - { context: { t: (str) => str, metricsEvent: () => ({}) } }, - ); + wrapper = renderShallow(); }); afterEach(() => { propsMethodSpies.resetSendState.resetHistory(); + propsMethodSpies.cancelTx.resetHistory(); propsMethodSpies.addToAddressBookIfNew.resetHistory(); propsMethodSpies.resetSendState.resetHistory(); propsMethodSpies.sign.resetHistory(); @@ -65,6 +79,15 @@ describe('SendFooter Component', () => { expect(propsMethodSpies.resetSendState.callCount).toStrictEqual(1); }); + it('should call cancelTx', () => { + expect(propsMethodSpies.cancelTx.callCount).toStrictEqual(0); + wrapper.instance().onCancel(); + expect(propsMethodSpies.cancelTx.callCount).toStrictEqual(1); + expect(propsMethodSpies.cancelTx.getCall(0).args[0]?.id).toStrictEqual( + 'ID', + ); + }); + it('should call history.push', () => { expect(historySpies.push.callCount).toStrictEqual(0); wrapper.instance().onCancel(); @@ -73,6 +96,14 @@ describe('SendFooter Component', () => { 'mostRecentOverviewPage', ); }); + + it('should call history.push with DEFAULT_ROUTE in edit stage', () => { + wrapper = renderShallow({ sendStage: 'EDIT' }); + expect(historySpies.push.callCount).toStrictEqual(0); + wrapper.instance().onCancel(); + expect(historySpies.push.callCount).toStrictEqual(1); + expect(historySpies.push.getCall(0).args[0]).toStrictEqual(DEFAULT_ROUTE); + }); }); describe('onSubmit', () => { @@ -107,7 +138,9 @@ describe('SendFooter Component', () => { addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew} amount="mockAmount" resetSendState={propsMethodSpies.resetSendState} + cancelTx={propsMethodSpies.cancelTx} disabled + draftTransactionID="ID" editingTransactionId="mockEditingTransactionId" errors={{}} from={{ address: 'mockAddress', balance: 'mockBalance' }} @@ -147,4 +180,28 @@ describe('SendFooter Component', () => { expect(SendFooter.prototype.onCancel.callCount).toStrictEqual(1); }); }); + + describe('Cancel Button', () => { + const renderFooter = (props) => + renderWithProvider( + , + ); + + it('has a cancel button in footer', () => { + const { getByText } = renderFooter(); + expect(getByText('Cancel')).toBeTruthy(); + }); + + it('has label changed to Reject in editing stage', () => { + const { getByText } = renderFooter({ sendStage: 'EDIT' }); + expect(getByText('Reject')).toBeTruthy(); + }); + }); }); diff --git a/ui/pages/send/send-footer/send-footer.container.js b/ui/pages/send/send-footer/send-footer.container.js index bcdb796e1..eba93e5d7 100644 --- a/ui/pages/send/send-footer/send-footer.container.js +++ b/ui/pages/send/send-footer/send-footer.container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { addToAddressBook } from '../../../store/actions'; +import { addToAddressBook, cancelTx } from '../../../store/actions'; import { getRenderableEstimateDataForSmallButtonsFromGWEI, getDefaultActiveButtonIndex, @@ -7,10 +7,12 @@ import { import { resetSendState, getGasPrice, + getSendStage, getSendTo, getSendErrors, isSendFormInvalid, signTransaction, + getDraftTransactionID, } from '../../../ducks/send'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; @@ -43,7 +45,9 @@ function mapStateToProps(state) { disabled: isSendFormInvalid(state), to: getSendTo(state), toAccounts: getSendToAccounts(state), + sendStage: getSendStage(state), sendErrors: getSendErrors(state), + draftTransactionID: getDraftTransactionID(state), gasEstimateType, mostRecentOverviewPage: getMostRecentOverviewPage(state), }; @@ -52,6 +56,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { resetSendState: () => dispatch(resetSendState()), + cancelTx: (t) => dispatch(cancelTx(t)), sign: () => dispatch(signTransaction()), addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => { const hexPrefixedAddress = addHexPrefix(newAddress); diff --git a/ui/pages/send/send-footer/send-footer.container.test.js b/ui/pages/send/send-footer/send-footer.container.test.js index 61c081719..d5eb9f8ab 100644 --- a/ui/pages/send/send-footer/send-footer.container.test.js +++ b/ui/pages/send/send-footer/send-footer.container.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; -import { addToAddressBook } from '../../../store/actions'; +import { addToAddressBook, cancelTx } from '../../../store/actions'; import { resetSendState, signTransaction } from '../../../ducks/send'; let mapDispatchToProps; @@ -14,6 +14,7 @@ jest.mock('react-redux', () => ({ jest.mock('../../../store/actions.js', () => ({ addToAddressBook: jest.fn(), + cancelTx: jest.fn(), })); jest.mock('../../../ducks/metamask/metamask', () => ({ @@ -24,6 +25,8 @@ jest.mock('../../../ducks/send', () => ({ getGasPrice: (s) => `mockGasPrice:${s}`, getSendTo: (s) => `mockTo:${s}`, getSendErrors: (s) => `mockSendErrors:${s}`, + getSendStage: (s) => `mockStage:${s}`, + getDraftTransaction: (s) => ({ id: `draftTransaction:${s}` }), resetSendState: jest.fn(), signTransaction: jest.fn(), })); @@ -53,6 +56,16 @@ describe('send-footer container', () => { }); }); + describe('cancelTx()', () => { + it('should dispatch an action', () => { + const draftTansaction = { id: 'ID' }; + mapDispatchToPropsObject.cancelTx(draftTansaction); + expect(dispatchSpy.calledOnce).toStrictEqual(true); + expect(cancelTx).toHaveBeenCalledTimes(1); + expect(cancelTx).toHaveBeenCalledWith(draftTansaction); + }); + }); + describe('sign()', () => { it('should dispatch a signTransaction action', () => { mapDispatchToPropsObject.sign(); diff --git a/ui/pages/send/send-header/send-header.component.js b/ui/pages/send/send-header/send-header.component.js index 8b5d54c51..5e8b7f106 100644 --- a/ui/pages/send/send-header/send-header.component.js +++ b/ui/pages/send/send-header/send-header.component.js @@ -38,7 +38,9 @@ export default function SendHeader() { className="send__header" onClose={onClose} title={title} - headerCloseText={t('cancel')} + headerCloseText={ + stage === SEND_STAGES.EDIT ? t('cancelEdit') : t('cancel') + } /> ); } diff --git a/ui/pages/send/send-header/send-header.component.test.js b/ui/pages/send/send-header/send-header.component.test.js index 6d61a1ea3..c718018cb 100644 --- a/ui/pages/send/send-header/send-header.component.test.js +++ b/ui/pages/send/send-header/send-header.component.test.js @@ -103,6 +103,18 @@ describe('SendHeader Component', () => { expect(getByText('Cancel')).toBeTruthy(); }); + it('has button label changed to Cancel Edit in editing stage', () => { + const { getByText } = renderWithProvider( + , + configureMockStore(middleware)({ + send: { ...initialState, stage: SEND_STAGES.EDIT }, + gas: { basicEstimateStatus: 'LOADING' }, + history: { mostRecentOverviewPage: 'activity' }, + }), + ); + expect(getByText('Cancel Edit')).toBeTruthy(); + }); + it('resets send state when clicked', () => { const store = configureMockStore(middleware)({ send: initialState, diff --git a/ui/pages/send/send.scss b/ui/pages/send/send.scss index 36d26e6b6..c47b21fe5 100644 --- a/ui/pages/send/send.scss +++ b/ui/pages/send/send.scss @@ -22,6 +22,7 @@ right: 1rem; width: min-content; font-size: 0.75rem; + white-space: nowrap; } }