diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 0bad7b154..093174158 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -1081,8 +1081,10 @@ const slice = createSlice({ updateGasLimit: (state, action) => { const draftTransaction = state.draftTransactions[state.currentTransactionUUID]; - draftTransaction.gas.gasLimit = addHexPrefix(action.payload); - slice.caseReducers.calculateGasTotal(state); + if (draftTransaction) { + draftTransaction.gas.gasLimit = addHexPrefix(action.payload); + slice.caseReducers.calculateGasTotal(state); + } }, /** * sets the layer 1 fees total (for a multi-layer fee network) @@ -2334,6 +2336,19 @@ export function getCurrentDraftTransaction(state) { return state[name].draftTransactions[getCurrentTransactionUUID(state)] ?? {}; } +/** + * Selector that returns true if a draft transaction exists. + * + * @type {Selector} + */ +export function getDraftTransactionExists(state) { + const draftTransaction = getCurrentDraftTransaction(state); + if (Object.keys(draftTransaction).length === 0) { + return false; + } + return true; +} + // Gas selectors /** diff --git a/ui/pages/send/send-header/send-header.component.js b/ui/pages/send/send-header/send-header.component.js index f4528a6c4..d71b6ef99 100644 --- a/ui/pages/send/send-header/send-header.component.js +++ b/ui/pages/send/send-header/send-header.component.js @@ -5,6 +5,7 @@ import PageContainerHeader from '../../../components/ui/page-container/page-cont import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { + getDraftTransactionExists, getSendAsset, getSendStage, resetSendState, @@ -19,15 +20,18 @@ export default function SendHeader() { const stage = useSelector(getSendStage); const asset = useSelector(getSendAsset); const t = useI18nContext(); - + const draftTransactionExists = useSelector(getDraftTransactionExists); const onClose = () => { dispatch(resetSendState()); history.push(mostRecentOverviewPage); }; - let title = asset.type === ASSET_TYPES.NATIVE ? t('send') : t('sendTokens'); + let title = asset?.type === ASSET_TYPES.NATIVE ? t('send') : t('sendTokens'); - if (stage === SEND_STAGES.ADD_RECIPIENT || stage === SEND_STAGES.INACTIVE) { + if ( + draftTransactionExists === false || + [SEND_STAGES.ADD_RECIPIENT, SEND_STAGES.INACTIVE].includes(stage) + ) { title = t('sendTo'); } else if (stage === SEND_STAGES.EDIT) { title = t('edit'); diff --git a/ui/pages/send/send.js b/ui/pages/send/send.js index 060dbfee8..ad10615a7 100644 --- a/ui/pages/send/send.js +++ b/ui/pages/send/send.js @@ -1,8 +1,9 @@ -import React, { useEffect, useCallback, useContext } from 'react'; +import React, { useEffect, useCallback, useContext, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; import { addHistoryEntry, + getDraftTransactionExists, getIsUsingMyAccountForRecipientSearch, getRecipient, getRecipientUserInput, @@ -10,6 +11,7 @@ import { resetRecipientInput, resetSendState, SEND_STAGES, + startNewDraftTransaction, updateRecipient, updateRecipientUserInput, } from '../../ducks/send'; @@ -18,6 +20,7 @@ import { getSendHexDataFeatureFlagState } from '../../ducks/metamask/metamask'; import { showQrScanner } from '../../store/actions'; import { MetaMetricsContext } from '../../contexts/metametrics'; import { EVENT } from '../../../shared/constants/metametrics'; +import { ASSET_TYPES } from '../../../shared/constants/transaction'; import SendHeader from './send-header'; import AddRecipient from './send-content/add-recipient'; import SendContent from './send-content'; @@ -29,6 +32,7 @@ const sendSliceIsCustomPriceExcessive = (state) => export default function SendTransactionScreen() { const history = useHistory(); + const startedNewDraftTransaction = useRef(false); const stage = useSelector(getSendStage); const gasIsExcessive = useSelector(sendSliceIsCustomPriceExcessive); const isUsingMyAccountsForRecipientSearch = useSelector( @@ -37,6 +41,7 @@ export default function SendTransactionScreen() { const recipient = useSelector(getRecipient); const showHexData = useSelector(getSendHexDataFeatureFlagState); const userInput = useSelector(getRecipientUserInput); + const draftTransactionExists = useSelector(getDraftTransactionExists); const location = useLocation(); const trackEvent = useContext(MetaMetricsContext); @@ -46,6 +51,23 @@ export default function SendTransactionScreen() { dispatch(resetSendState()); }, [dispatch]); + /** + * It is possible to route to this page directly, either by typing in the url + * or by clicking the browser back button after progressing to the confirm + * screen. In the case where a draft transaction does not yet exist, this + * hook is responsible for creating it. We will assume that this is a native + * asset send. + */ + useEffect(() => { + if ( + draftTransactionExists === false && + startedNewDraftTransaction.current === false + ) { + startedNewDraftTransaction.current = true; + dispatch(startNewDraftTransaction({ type: ASSET_TYPES.NATIVE })); + } + }, [draftTransactionExists, dispatch]); + useEffect(() => { window.addEventListener('beforeunload', cleanup); }, [cleanup]); @@ -70,7 +92,10 @@ export default function SendTransactionScreen() { let content; - if ([SEND_STAGES.EDIT, SEND_STAGES.DRAFT].includes(stage)) { + if ( + draftTransactionExists && + [SEND_STAGES.EDIT, SEND_STAGES.DRAFT].includes(stage) + ) { content = ( <> { + const original = jest.requireActual('../../ducks/send/send'); + return { + ...original, + // We don't really need to start a draft transaction, and the mock store + // does not update as a result of action calls so instead we just ensure + // that the action WOULD be called. + startNewDraftTransaction: jest.fn(() => ({ + type: 'TEST_START_NEW_DRAFT', + payload: null, + })), + }; +}); + jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); return { @@ -163,6 +177,25 @@ describe('Send Page', () => { const { queryByText } = renderWithProvider(, store); expect(queryByText('Next')).toBeNull(); }); + + it('should render correctly even when a draftTransaction does not exist', () => { + const modifiedStore = { + ...baseStore, + send: { + ...baseStore.send, + currentTransactionUUID: null, + }, + }; + const store = configureMockStore(middleware)(modifiedStore); + const { getByPlaceholderText } = renderWithProvider(, store); + // Ensure that the send flow renders on the add recipient screen when + // there is no draft transaction. + expect( + getByPlaceholderText('Search, public address (0x), or ENS'), + ).toBeTruthy(); + // Ensure we start a new draft transaction when its missing. + expect(startNewDraftTransaction).toHaveBeenCalledTimes(1); + }); }); describe('Send and Edit Flow (draft)', () => {