1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

enable direct routing to the send page (#15259)

This commit is contained in:
Brad Decker 2022-07-18 12:01:10 -05:00 committed by GitHub
parent d92936475a
commit 751c119d10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 8 deletions

View File

@ -1081,8 +1081,10 @@ const slice = createSlice({
updateGasLimit: (state, action) => { updateGasLimit: (state, action) => {
const draftTransaction = const draftTransaction =
state.draftTransactions[state.currentTransactionUUID]; state.draftTransactions[state.currentTransactionUUID];
draftTransaction.gas.gasLimit = addHexPrefix(action.payload); if (draftTransaction) {
slice.caseReducers.calculateGasTotal(state); draftTransaction.gas.gasLimit = addHexPrefix(action.payload);
slice.caseReducers.calculateGasTotal(state);
}
}, },
/** /**
* sets the layer 1 fees total (for a multi-layer fee network) * 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)] ?? {}; return state[name].draftTransactions[getCurrentTransactionUUID(state)] ?? {};
} }
/**
* Selector that returns true if a draft transaction exists.
*
* @type {Selector<boolean>}
*/
export function getDraftTransactionExists(state) {
const draftTransaction = getCurrentDraftTransaction(state);
if (Object.keys(draftTransaction).length === 0) {
return false;
}
return true;
}
// Gas selectors // Gas selectors
/** /**

View File

@ -5,6 +5,7 @@ import PageContainerHeader from '../../../components/ui/page-container/page-cont
import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { import {
getDraftTransactionExists,
getSendAsset, getSendAsset,
getSendStage, getSendStage,
resetSendState, resetSendState,
@ -19,15 +20,18 @@ export default function SendHeader() {
const stage = useSelector(getSendStage); const stage = useSelector(getSendStage);
const asset = useSelector(getSendAsset); const asset = useSelector(getSendAsset);
const t = useI18nContext(); const t = useI18nContext();
const draftTransactionExists = useSelector(getDraftTransactionExists);
const onClose = () => { const onClose = () => {
dispatch(resetSendState()); dispatch(resetSendState());
history.push(mostRecentOverviewPage); 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'); title = t('sendTo');
} else if (stage === SEND_STAGES.EDIT) { } else if (stage === SEND_STAGES.EDIT) {
title = t('edit'); title = t('edit');

View File

@ -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 { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { import {
addHistoryEntry, addHistoryEntry,
getDraftTransactionExists,
getIsUsingMyAccountForRecipientSearch, getIsUsingMyAccountForRecipientSearch,
getRecipient, getRecipient,
getRecipientUserInput, getRecipientUserInput,
@ -10,6 +11,7 @@ import {
resetRecipientInput, resetRecipientInput,
resetSendState, resetSendState,
SEND_STAGES, SEND_STAGES,
startNewDraftTransaction,
updateRecipient, updateRecipient,
updateRecipientUserInput, updateRecipientUserInput,
} from '../../ducks/send'; } from '../../ducks/send';
@ -18,6 +20,7 @@ import { getSendHexDataFeatureFlagState } from '../../ducks/metamask/metamask';
import { showQrScanner } from '../../store/actions'; import { showQrScanner } from '../../store/actions';
import { MetaMetricsContext } from '../../contexts/metametrics'; import { MetaMetricsContext } from '../../contexts/metametrics';
import { EVENT } from '../../../shared/constants/metametrics'; import { EVENT } from '../../../shared/constants/metametrics';
import { ASSET_TYPES } from '../../../shared/constants/transaction';
import SendHeader from './send-header'; import SendHeader from './send-header';
import AddRecipient from './send-content/add-recipient'; import AddRecipient from './send-content/add-recipient';
import SendContent from './send-content'; import SendContent from './send-content';
@ -29,6 +32,7 @@ const sendSliceIsCustomPriceExcessive = (state) =>
export default function SendTransactionScreen() { export default function SendTransactionScreen() {
const history = useHistory(); const history = useHistory();
const startedNewDraftTransaction = useRef(false);
const stage = useSelector(getSendStage); const stage = useSelector(getSendStage);
const gasIsExcessive = useSelector(sendSliceIsCustomPriceExcessive); const gasIsExcessive = useSelector(sendSliceIsCustomPriceExcessive);
const isUsingMyAccountsForRecipientSearch = useSelector( const isUsingMyAccountsForRecipientSearch = useSelector(
@ -37,6 +41,7 @@ export default function SendTransactionScreen() {
const recipient = useSelector(getRecipient); const recipient = useSelector(getRecipient);
const showHexData = useSelector(getSendHexDataFeatureFlagState); const showHexData = useSelector(getSendHexDataFeatureFlagState);
const userInput = useSelector(getRecipientUserInput); const userInput = useSelector(getRecipientUserInput);
const draftTransactionExists = useSelector(getDraftTransactionExists);
const location = useLocation(); const location = useLocation();
const trackEvent = useContext(MetaMetricsContext); const trackEvent = useContext(MetaMetricsContext);
@ -46,6 +51,23 @@ export default function SendTransactionScreen() {
dispatch(resetSendState()); dispatch(resetSendState());
}, [dispatch]); }, [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(() => { useEffect(() => {
window.addEventListener('beforeunload', cleanup); window.addEventListener('beforeunload', cleanup);
}, [cleanup]); }, [cleanup]);
@ -70,7 +92,10 @@ export default function SendTransactionScreen() {
let content; let content;
if ([SEND_STAGES.EDIT, SEND_STAGES.DRAFT].includes(stage)) { if (
draftTransactionExists &&
[SEND_STAGES.EDIT, SEND_STAGES.DRAFT].includes(stage)
) {
content = ( content = (
<> <>
<SendContent <SendContent

View File

@ -2,7 +2,7 @@ import React from 'react';
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { SEND_STAGES } from '../../ducks/send'; import { SEND_STAGES, startNewDraftTransaction } from '../../ducks/send';
import { ensInitialState } from '../../ducks/ens'; import { ensInitialState } from '../../ducks/ens';
import { renderWithProvider } from '../../../test/jest'; import { renderWithProvider } from '../../../test/jest';
import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network'; import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
@ -12,6 +12,20 @@ import Send from './send';
const middleware = [thunk]; const middleware = [thunk];
jest.mock('../../ducks/send/send', () => {
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', () => { jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom'); const original = jest.requireActual('react-router-dom');
return { return {
@ -163,6 +177,25 @@ describe('Send Page', () => {
const { queryByText } = renderWithProvider(<Send />, store); const { queryByText } = renderWithProvider(<Send />, store);
expect(queryByText('Next')).toBeNull(); 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(<Send />, 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)', () => { describe('Send and Edit Flow (draft)', () => {