Skip to content

Commit 5be6048

Browse files
authored
refactor(5563): migrate pages/swaps to react-router-dom-v5-compat (#37561)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Migrate swaps component to React Router v6 compatibility. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/37561?quickstart=1) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: MetaMask/MetaMask-planning#5563 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Migrate Swaps routing to react-router-dom v5-compat, replacing history-based navigation with navigate, updating routes/components, and aligning tests. > > - **Routing migration to v5-compat**: > - Replace `useHistory`, `Redirect`, `Switch`, and v5 `<Route render>` with `useNavigate`, `Navigate`, and `Routes` across `ui/pages/swaps/*`. > - Introduce `FeatureToggledRoute` using v5-compat `Navigate` and update consumers. > - Update swaps thunks in `ui/ducks/swaps/swaps.js` to accept/use `navigate` instead of `history` for redirects. > - In `routes.component.tsx`, add `createV5CompatNavigate` helper and wrap Swaps/Cross-chain Swap routes with `AuthenticatedV5Compat`, passing v5-compatible props. > - **Swaps pages refactor**: > - Refactor navigation/redirect logic in `awaiting-*`, `loading-swaps-quotes`, `notification-page`, `prepare-swap-page`, `review-quote`, `smart-transaction-status`, and `index` to v5-compat APIs; minor prop types added/adjusted. > - `Swap` now uses `useSafeNavigation`, v5-compat `Routes`, and `FeatureToggledRoute` for maintenance gating. > - **Tests**: > - Update tests to use `render-helpers-navigate` and v5-compat `useNavigate` mocks; remove legacy helpers. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8aecb26. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent ee62e1b commit 5be6048

File tree

20 files changed

+254
-260
lines changed

20 files changed

+254
-260
lines changed

ui/ducks/swaps/swaps.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -525,12 +525,12 @@ export {
525525
slice as swapsSlice,
526526
};
527527

528-
export const navigateBackToPrepareSwap = (history) => {
528+
export const navigateBackToPrepareSwap = (navigate) => {
529529
return async (dispatch) => {
530530
// TODO: Ensure any fetch in progress is cancelled
531531
await dispatch(setBackgroundSwapRouteState(''));
532532
dispatch(navigatedBackToBuildQuote());
533-
history.push(PREPARE_SWAP_ROUTE);
533+
navigate(PREPARE_SWAP_ROUTE);
534534
};
535535
};
536536

@@ -632,7 +632,7 @@ const isTokenAlreadyAdded = (tokenAddress, tokens) => {
632632
};
633633

634634
export const fetchQuotesAndSetQuoteState = (
635-
history,
635+
navigate,
636636
inputValue,
637637
maxSlippage,
638638
trackEvent,
@@ -658,7 +658,7 @@ export const fetchQuotesAndSetQuoteState = (
658658
await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
659659

660660
if (!swapsLivenessForNetwork.swapsFeatureIsLive) {
661-
await history.push(SWAPS_MAINTENANCE_ROUTE);
661+
await navigate(SWAPS_MAINTENANCE_ROUTE);
662662
return;
663663
}
664664

@@ -691,7 +691,7 @@ export const fetchQuotesAndSetQuoteState = (
691691
// In that case we just want to silently prefetch quotes without redirecting to the quotes loading page.
692692
if (!pageRedirectionDisabled) {
693693
await dispatch(setBackgroundSwapRouteState('loading'));
694-
history.push(LOADING_QUOTES_ROUTE);
694+
navigate(LOADING_QUOTES_ROUTE);
695695
}
696696
dispatch(setFetchingQuotes(true));
697697

@@ -938,7 +938,7 @@ export const fetchQuotesAndSetQuoteState = (
938938
export const signAndSendSwapsSmartTransaction = ({
939939
unsignedTransaction,
940940
trackEvent,
941-
history,
941+
navigate,
942942
additionalTrackingParams,
943943
}) => {
944944
return async (dispatch, getState) => {
@@ -1020,7 +1020,7 @@ export const signAndSendSwapsSmartTransaction = ({
10201020
},
10211021
});
10221022
await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR));
1023-
history.push(SWAPS_ERROR_ROUTE);
1023+
navigate(SWAPS_ERROR_ROUTE);
10241024
return;
10251025
}
10261026

@@ -1100,7 +1100,7 @@ export const signAndSendSwapsSmartTransaction = ({
11001100
}),
11011101
);
11021102
}
1103-
history.push(SMART_TRANSACTION_STATUS_ROUTE);
1103+
navigate(SMART_TRANSACTION_STATUS_ROUTE);
11041104
dispatch(setSwapsSTXSubmitLoading(false));
11051105
} catch (e) {
11061106
console.log('signAndSendSwapsSmartTransaction error', e);
Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
import React, { useMemo } from 'react';
1+
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import { Redirect, Route } from 'react-router-dom';
3+
import { Navigate } from 'react-router-dom-v5-compat';
44

5-
export default function FeatureToggledRoute({ flag, redirectRoute, ...props }) {
6-
const redirect = useMemo(
7-
() => ({ pathname: redirectRoute }),
8-
[redirectRoute],
9-
);
5+
export default function FeatureToggledRoute({ flag, redirectRoute, element }) {
106
if (flag) {
11-
return <Route {...props} />;
7+
return element;
128
}
13-
return <Redirect to={redirect} />;
9+
return <Navigate to={redirectRoute} replace />;
1410
}
1511

1612
FeatureToggledRoute.propTypes = {
1713
flag: PropTypes.bool.isRequired,
1814
redirectRoute: PropTypes.string.isRequired,
15+
element: PropTypes.node.isRequired,
1916
};

ui/pages/routes/routes.component.tsx

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,35 @@ import {
178178
showAppHeader,
179179
} from './utils';
180180

181+
// V5-compat navigate function type for bridging v5 routes with v5-compat components
182+
type V5CompatNavigate = (
183+
to: string | number,
184+
options?: {
185+
replace?: boolean;
186+
state?: Record<string, unknown>;
187+
},
188+
) => void;
189+
190+
/**
191+
* Creates a v5-compat navigate function from v5 history
192+
* Used to bridge v5 routes with components expecting v5-compat navigation
193+
*
194+
* @param history
195+
*/
196+
const createV5CompatNavigate = (
197+
history: RouteComponentProps['history'],
198+
): V5CompatNavigate => {
199+
return (to, options = {}) => {
200+
if (typeof to === 'number') {
201+
history.go(to);
202+
} else if (options.replace) {
203+
history.replace(to, options.state);
204+
} else {
205+
history.push(to, options.state);
206+
}
207+
};
208+
};
209+
181210
// TODO: Fix `as unknown as` casting once `mmLazy` is updated to handle named exports, wrapped components, and other React module types.
182211
// Casting is preferable over `@ts-expect-error` annotations in this case,
183212
// because it doesn't suppress competing error messages e.g. "Cannot find module..."
@@ -624,7 +653,19 @@ export default function Routes() {
624653
component={ConfirmTransaction}
625654
/>
626655
<Authenticated path={`${SEND_ROUTE}/:page?`} component={SendPage} />
627-
<Authenticated path={SWAPS_ROUTE} component={Swaps} />
656+
<Route path={SWAPS_ROUTE}>
657+
{(props: RouteComponentProps) => {
658+
const { location: v5Location } = props;
659+
const SwapsComponent = Swaps as React.ComponentType<{
660+
location: RouteComponentProps['location'];
661+
}>;
662+
return (
663+
<AuthenticatedV5Compat>
664+
<SwapsComponent location={v5Location} />
665+
</AuthenticatedV5Compat>
666+
);
667+
}}
668+
</Route>
628669
<Route
629670
path={`${CROSS_CHAIN_SWAP_TX_DETAILS_ROUTE}/:srcTxMetaId`}
630671
// v5 Route supports exact with render props, but TS types don't recognize it
@@ -634,24 +675,13 @@ export default function Routes() {
634675
>
635676
{(props: RouteComponentProps<{ srcTxMetaId: string }>) => {
636677
const { history: v5History, location: v5Location, match } = props;
637-
const navigate = (
638-
to: string | number,
639-
options: {
640-
replace?: boolean;
641-
state?: Record<string, unknown>;
642-
} = {},
643-
) => {
644-
if (typeof to === 'number') {
645-
v5History.go(to);
646-
} else if (options.replace) {
647-
v5History.replace(to, options.state);
648-
} else {
649-
v5History.push(to, options.state);
650-
}
651-
};
678+
const navigate = createV5CompatNavigate(v5History);
652679
const CrossChainSwapTxDetailsComponent =
653-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
654-
CrossChainSwapTxDetails as any;
680+
CrossChainSwapTxDetails as React.ComponentType<{
681+
location: RouteComponentProps['location'];
682+
navigate: V5CompatNavigate;
683+
params: { srcTxMetaId: string };
684+
}>;
655685
return (
656686
<AuthenticatedV5Compat>
657687
<CrossChainSwapTxDetailsComponent
@@ -666,8 +696,10 @@ export default function Routes() {
666696
<Route path={CROSS_CHAIN_SWAP_ROUTE}>
667697
{(props: RouteComponentProps) => {
668698
const { location: v5Location } = props;
669-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
670-
const CrossChainSwapComponent = CrossChainSwap as any;
699+
const CrossChainSwapComponent =
700+
CrossChainSwap as React.ComponentType<{
701+
location: RouteComponentProps['location'];
702+
}>;
671703
return (
672704
<AuthenticatedV5Compat>
673705
<CrossChainSwapComponent location={v5Location} />
@@ -701,20 +733,14 @@ export default function Routes() {
701733
match: v5Match,
702734
} = props;
703735

704-
// Create a navigate function compatible with v5-compat for the component
705-
const navigate = (
706-
to: string,
707-
options: { replace?: boolean } = {},
708-
) => {
709-
if (options.replace) {
710-
v5History.replace(to);
711-
} else {
712-
v5History.push(to);
713-
}
714-
};
736+
const navigate = createV5CompatNavigate(v5History);
715737

716-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
717-
const PermissionsConnectWithProps = PermissionsConnect as any;
738+
const PermissionsConnectWithProps =
739+
PermissionsConnect as React.ComponentType<{
740+
location: RouteComponentProps['location'];
741+
navigate: V5CompatNavigate;
742+
match: RouteComponentProps<{ id: string }>['match'];
743+
}>;
718744
return (
719745
<AuthenticatedV5Compat>
720746
<PermissionsConnectWithProps
@@ -728,8 +754,10 @@ export default function Routes() {
728754
</Route>
729755
<Route path={`${ASSET_ROUTE}/image/:asset/:id`}>
730756
{(props: RouteComponentProps<{ asset: string; id: string }>) => {
731-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
732-
const NftFullImageComponent = NftFullImage as any;
757+
const NftFullImageComponent =
758+
NftFullImage as React.ComponentType<{
759+
params: { asset: string; id: string };
760+
}>;
733761
return (
734762
<AuthenticatedV5Compat>
735763
<NftFullImageComponent params={props.match.params} />
@@ -745,8 +773,9 @@ export default function Routes() {
745773
id: string;
746774
}>,
747775
) => {
748-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
749-
const AssetComponent = Asset as any;
776+
const AssetComponent = Asset as React.ComponentType<{
777+
params: { chainId: string; asset: string; id: string };
778+
}>;
750779
return (
751780
<AuthenticatedV5Compat>
752781
<AssetComponent params={props.match.params} />
@@ -761,8 +790,9 @@ export default function Routes() {
761790
asset: string;
762791
}>,
763792
) => {
764-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
765-
const AssetComponent = Asset as any;
793+
const AssetComponent = Asset as React.ComponentType<{
794+
params: { chainId: string; asset: string };
795+
}>;
766796
return (
767797
<AuthenticatedV5Compat>
768798
<AssetComponent params={props.match.params} />
@@ -772,8 +802,9 @@ export default function Routes() {
772802
</Route>
773803
<Route path={`${ASSET_ROUTE}/:chainId`}>
774804
{(props: RouteComponentProps<{ chainId: string }>) => {
775-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
776-
const AssetComponent = Asset as any;
805+
const AssetComponent = Asset as React.ComponentType<{
806+
params: { chainId: string };
807+
}>;
777808
return (
778809
<AuthenticatedV5Compat>
779810
<AssetComponent params={props.match.params} />

ui/pages/swaps/awaiting-signatures/awaiting-signatures.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React, { useContext, useEffect } from 'react';
22
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
3-
import { useHistory } from 'react-router-dom';
3+
import { useNavigate } from 'react-router-dom-v5-compat';
44
import isEqual from 'lodash/isEqual';
5-
65
import { I18nContext } from '../../../contexts/i18n';
76
import {
87
getFetchParams,
@@ -18,10 +17,7 @@ import {
1817
getSmartTransactionsEnabled,
1918
getSmartTransactionsOptInStatusForMetrics,
2019
} from '../../../../shared/modules/selectors';
21-
import {
22-
DEFAULT_ROUTE,
23-
PREPARE_SWAP_ROUTE,
24-
} from '../../../helpers/constants/routes';
20+
import { PREPARE_SWAP_ROUTE } from '../../../helpers/constants/routes';
2521
import PulseLoader from '../../../components/ui/pulse-loader';
2622
import Box from '../../../components/ui/box';
2723
import {
@@ -39,7 +35,7 @@ import SwapStepIcon from './swap-step-icon';
3935

4036
export default function AwaitingSignatures() {
4137
const t = useContext(I18nContext);
42-
const history = useHistory();
38+
const navigate = useNavigate();
4339
const dispatch = useDispatch();
4440
const fetchParams = useSelector(getFetchParams, isEqual);
4541
const { destinationTokenInfo, sourceTokenInfo } = fetchParams?.metaData || {};
@@ -147,10 +143,8 @@ export default function AwaitingSignatures() {
147143
<SwapsFooter
148144
onSubmit={async () => {
149145
await dispatch(prepareToLeaveSwaps());
150-
// Go to the default route and then to the build quote route in order to clean up
151-
// the `inputValue` local state in `pages/swaps/index.js`
152-
history.push(DEFAULT_ROUTE);
153-
history.push(PREPARE_SWAP_ROUTE);
146+
// prepareToLeaveSwaps() clears all swaps state, so we can navigate directly
147+
navigate(PREPARE_SWAP_ROUTE);
154148
}}
155149
submitText={t('cancel')}
156150
hideCancel

ui/pages/swaps/awaiting-signatures/awaiting-signatures.test.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import React from 'react';
22
import configureMockStore from 'redux-mock-store';
3-
4-
import {
5-
renderWithProvider,
6-
createSwapsMockStore,
7-
} from '../../../../test/jest';
3+
import { createSwapsMockStore } from '../../../../test/jest';
4+
import { renderWithProvider } from '../../../../test/lib/render-helpers-navigate';
85
import AwaitingSignatures from '.';
96

107
describe('AwaitingSignatures', () => {

ui/pages/swaps/awaiting-swap/awaiting-swap.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import EventEmitter from 'events';
22
import React, { useContext, useRef, useState, useEffect } from 'react';
33
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
44
import PropTypes from 'prop-types';
5-
import { useHistory } from 'react-router-dom';
5+
import { useNavigate } from 'react-router-dom-v5-compat';
66
import isEqual from 'lodash/isEqual';
77
import { getBlockExplorerLink } from '@metamask/etherscan-link';
88
import { I18nContext } from '../../../contexts/i18n';
@@ -80,7 +80,7 @@ export default function AwaitingSwap({
8080
}) {
8181
const t = useContext(I18nContext);
8282
const trackEvent = useContext(MetaMetricsContext);
83-
const history = useHistory();
83+
const navigate = useNavigate();
8484
const dispatch = useDispatch();
8585
const hdEntropyIndex = useSelector(getHDEntropyIndex);
8686
const animationEventEmitter = useRef(new EventEmitter());
@@ -329,31 +329,31 @@ export default function AwaitingSwap({
329329
/* istanbul ignore next */
330330
if (errorKey === OFFLINE_FOR_MAINTENANCE) {
331331
await dispatch(prepareToLeaveSwaps());
332-
history.push(DEFAULT_ROUTE);
332+
navigate(DEFAULT_ROUTE);
333333
} else if (errorKey === QUOTES_EXPIRED_ERROR) {
334334
dispatch(prepareForRetryGetQuotes());
335335
await dispatch(
336336
fetchQuotesAndSetQuoteState(
337-
history,
337+
navigate,
338338
fromTokenInputValue,
339339
maxSlippage,
340340
trackEvent,
341341
),
342342
);
343343
} else if (errorKey) {
344-
await dispatch(navigateBackToPrepareSwap(history));
344+
await dispatch(navigateBackToPrepareSwap(navigate));
345345
} else if (
346346
isSwapsDefaultTokenSymbol(destinationTokenSymbol, chainId) ||
347347
swapComplete
348348
) {
349-
history.push(DEFAULT_ROUTE);
349+
navigate(DEFAULT_ROUTE);
350350
} else {
351351
await dispatch(setDefaultHomeActiveTabName('activity'));
352-
history.push(DEFAULT_ROUTE);
352+
navigate(DEFAULT_ROUTE);
353353
}
354354
}}
355355
onCancel={async () =>
356-
await dispatch(navigateBackToPrepareSwap(history))
356+
await dispatch(navigateBackToPrepareSwap(navigate))
357357
}
358358
submitText={submitText}
359359
disabled={submittingSwap}

ui/pages/swaps/awaiting-swap/awaiting-swap.test.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import configureMockStore from 'redux-mock-store';
33
import thunk from 'redux-thunk';
44

55
import { setBackgroundConnection } from '../../../store/background-connection';
6-
import {
7-
renderWithProvider,
8-
createSwapsMockStore,
9-
fireEvent,
10-
} from '../../../../test/jest';
6+
import { renderWithProvider } from '../../../../test/lib/render-helpers-navigate';
7+
import { createSwapsMockStore, fireEvent } from '../../../../test/jest';
118
import {
129
Slippage,
1310
QUOTES_EXPIRED_ERROR,

0 commit comments

Comments
 (0)