diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 2eba88ef0..9a4cec433 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -935,6 +935,21 @@
"custodianAccount": {
"message": "Custodian account"
},
+ "custodianReplaceRefreshTokenChangedFailed": {
+ "message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again."
+ },
+ "custodianReplaceRefreshTokenChangedSubtitle": {
+ "message": "You can now use your custodian accounts in MetaMask Institutional."
+ },
+ "custodianReplaceRefreshTokenChangedTitle": {
+ "message": "Your custodian token has been refreshed"
+ },
+ "custodianReplaceRefreshTokenSubtitle": {
+ "message": "This is will replace the custodian token for the following address:"
+ },
+ "custodianReplaceRefreshTokenTitle": {
+ "message": "Replace custodian token"
+ },
"custodyDeeplinkDescription": {
"message": "Approve the transaction in the $1 app. Once all required custody approvals have been performed the transaction will complete. Check your $1 app for status."
},
diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss
index 81ab85809..fd7334459 100644
--- a/ui/components/app/app-components.scss
+++ b/ui/components/app/app-components.scss
@@ -105,7 +105,7 @@
@import 'network-account-balance-header/index';
@import 'approve-content-card/index';
@import 'transaction-alerts/transaction-alerts';
-///: BEGIN:ONLY_INCLUDE_IN(mmi)
+///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
@import '../institutional/custody-confirm-link-modal/index';
@import '../institutional/transaction-failed-modal/index';
///: END:ONLY_INCLUDE_IN
diff --git a/ui/css/index.scss b/ui/css/index.scss
index c97d9573a..1033eec1e 100644
--- a/ui/css/index.scss
+++ b/ui/css/index.scss
@@ -11,7 +11,7 @@
@import '../components/component-library/component-library-components.scss';
@import '../components/app/app-components';
@import '../components/ui/ui-components';
-///: BEGIN:ONLY_INCLUDE_IN(mmi)
+///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
@import '../components/institutional/institutional-components';
///: END:ONLY_INCLUDE_IN
@import '../components/multichain/multichain-components.scss';
diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts
index d905819b8..13e888ceb 100644
--- a/ui/helpers/constants/routes.ts
+++ b/ui/helpers/constants/routes.ts
@@ -22,6 +22,7 @@ const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact';
const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody';
+const INSTITUTIONAL_FEATURES_DONE_ROUTE = '/institutional-features/done';
///: END:ONLY_INCLUDE_IN
const REVEAL_SEED_ROUTE = '/seed';
const RESTORE_VAULT_ROUTE = '/restore-vault';
@@ -31,9 +32,6 @@ const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token';
const NEW_ACCOUNT_ROUTE = '/new-account';
const IMPORT_ACCOUNT_ROUTE = '/new-account/import';
const CONNECT_HARDWARE_ROUTE = '/new-account/connect';
-///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
-const INSTITUTIONAL_FEATURES_DONE_ROUTE = '/institutional-features/done';
-///: END:ONLY_INCLUDE_IN
const SEND_ROUTE = '/send';
const TOKEN_DETAILS = '/token-details';
const CONNECT_ROUTE = '/connect';
@@ -127,7 +125,7 @@ const PATH_NAME_MAP = {
[IMPORT_ACCOUNT_ROUTE]: 'Import Account Page',
[CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page',
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
- [INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional features done',
+ [INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional Features Done Page',
///: END:ONLY_INCLUDE_IN
[SEND_ROUTE]: 'Send Page',
[`${TOKEN_DETAILS}/:address`]: 'Token Details Page',
@@ -162,6 +160,9 @@ const PATH_NAME_MAP = {
'Decrypt Message Request Page',
[`${CONFIRM_TRANSACTION_ROUTE}/:id${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`]:
'Encryption Public Key Request Page',
+ ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
+ [INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional Features Done Page',
+ ///: END:ONLY_INCLUDE_IN
[BUILD_QUOTE_ROUTE]: 'Swaps Build Quote Page',
[VIEW_QUOTE_ROUTE]: 'Swaps View Quotes Page',
[LOADING_QUOTES_ROUTE]: 'Swaps Loading Quotes Page',
@@ -184,9 +185,6 @@ export {
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
CONNECT_HARDWARE_ROUTE,
- ///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
- INSTITUTIONAL_FEATURES_DONE_ROUTE,
- ///: END:ONLY_INCLUDE_IN
SEND_ROUTE,
TOKEN_DETAILS,
CONFIRM_TRANSACTION_ROUTE,
@@ -215,6 +213,7 @@ export {
CONTACT_VIEW_ROUTE,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
CUSTODY_ACCOUNT_ROUTE,
+ INSTITUTIONAL_FEATURES_DONE_ROUTE,
///: END:ONLY_INCLUDE_IN
NETWORKS_ROUTE,
NETWORKS_FORM_ROUTE,
diff --git a/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.js.snap b/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.js.snap
new file mode 100644
index 000000000..9d57ea9b9
--- /dev/null
+++ b/ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.js.snap
@@ -0,0 +1,119 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Interactive Replacement Token Page should reject if there are errors 1`] = `
+
+`;
+
+exports[`Interactive Replacement Token Page should reject if there are errors 2`] = `
+
+
+
+
+
+
+ Please go to displayName and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again.
+
+
+
+
+
+
+
+`;
diff --git a/ui/pages/institutional/interactive-replacement-token-page/index.js b/ui/pages/institutional/interactive-replacement-token-page/index.js
new file mode 100644
index 000000000..0d0241f73
--- /dev/null
+++ b/ui/pages/institutional/interactive-replacement-token-page/index.js
@@ -0,0 +1,3 @@
+import InteractiveReplacementTokenPage from './interactive-replacement-token-page';
+
+export default InteractiveReplacementTokenPage;
diff --git a/ui/pages/institutional/interactive-replacement-token-page/index.scss b/ui/pages/institutional/interactive-replacement-token-page/index.scss
new file mode 100644
index 000000000..a1d39a9c9
--- /dev/null
+++ b/ui/pages/institutional/interactive-replacement-token-page/index.scss
@@ -0,0 +1,23 @@
+.interactive-replacement-token-page {
+ opacity: 0.8;
+
+ &__item {
+ border-bottom: 1px solid --color-border-default;
+
+ &-clipboard {
+ background-color: transparent;
+ }
+ }
+
+ &__item:first-child {
+ margin-top: 12px;
+ }
+
+ &__item:last-child {
+ border: 0;
+ }
+
+ &__item:hover {
+ background-color: rgba(0, 0, 0, 0.03);
+ }
+}
diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js
new file mode 100644
index 000000000..751168841
--- /dev/null
+++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js
@@ -0,0 +1,349 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import PropTypes from 'prop-types';
+import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
+import { getMostRecentOverviewPage } from '../../../ducks/history/history';
+import { getMetaMaskAccounts } from '../../../selectors';
+import Button from '../../../components/ui/button';
+import CustodyLabels from '../../../components/institutional/custody-labels/custody-labels';
+import PulseLoader from '../../../components/ui/pulse-loader';
+import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes';
+import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
+import { shortenAddress } from '../../../helpers/utils/util';
+import Tooltip from '../../../components/ui/tooltip';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import {
+ mmiActionsFactory,
+ showInteractiveReplacementTokenBanner,
+} from '../../../store/institutional/institution-background';
+import Box from '../../../components/ui/box';
+import {
+ Text,
+ Label,
+ Icon,
+ ButtonLink,
+ IconName,
+ IconSize,
+} from '../../../components/component-library';
+import {
+ OVERFLOW_WRAP,
+ TextColor,
+ JustifyContent,
+ BLOCK_SIZES,
+ DISPLAY,
+ FLEX_DIRECTION,
+ IconColor,
+} from '../../../helpers/constants/design-system';
+import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
+
+const getButtonLinkHref = ({ address }) => {
+ const url = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET];
+ return `${url}address/${address}`;
+};
+
+export default function InteractiveReplacementTokenPage({ history }) {
+ const dispatch = useDispatch();
+ const isMountedRef = useRef(false);
+ const mmiActions = mmiActionsFactory();
+ const { address } = useSelector((state) => state.metamask.modal.props);
+ const {
+ selectedAddress,
+ custodyAccountDetails,
+ interactiveReplacementToken,
+ mmiConfiguration,
+ } = useSelector((state) => state.metamask);
+ const { custodianName } =
+ custodyAccountDetails[toChecksumHexAddress(address || selectedAddress)] ||
+ {};
+ const { url } = interactiveReplacementToken || {};
+ const { custodians } = mmiConfiguration;
+ const custodian =
+ custodians.find((item) => item.name === custodianName) || {};
+ const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
+ const metaMaskAccounts = useSelector(getMetaMaskAccounts);
+ const connectRequests = useSelector(
+ (state) => state.metamask.institutionalFeatures?.connectRequests,
+ );
+ const [isLoading, setIsLoading] = useState(false);
+ const [tokenAccounts, setTokenAccounts] = useState([]);
+ const [error, setError] = useState(false);
+ const t = useI18nContext();
+ const [copied, handleCopy] = useCopyToClipboard();
+ const {
+ removeAddTokenConnectRequest,
+ setCustodianNewRefreshToken,
+ getCustodianAccounts,
+ } = mmiActions;
+ const connectRequest = connectRequests ? connectRequests[0] : undefined;
+
+ useEffect(() => {
+ isMountedRef.current = true;
+ return () => (isMountedRef.current = false);
+ }, []);
+
+ useEffect(() => {
+ const getTokenAccounts = async () => {
+ if (!connectRequest) {
+ history.push(mostRecentOverviewPage);
+ return;
+ }
+
+ try {
+ const custodianAccounts = await dispatch(
+ getCustodianAccounts(
+ connectRequest.token,
+ connectRequest.apiUrl,
+ connectRequest.service,
+ false,
+ ),
+ );
+
+ const filteredAccounts = custodianAccounts.filter(
+ (account) => metaMaskAccounts[account.address.toLowerCase()],
+ );
+
+ const mappedAccounts = filteredAccounts.map((account) => ({
+ address: account.address,
+ name: account.name,
+ labels: account.labels,
+ balance:
+ metaMaskAccounts[account.address.toLowerCase()]?.balance || 0,
+ }));
+
+ if (isMountedRef.current) {
+ setTokenAccounts(mappedAccounts);
+ setIsLoading(false);
+ }
+ } catch (e) {
+ setError(true);
+ setIsLoading(false);
+ }
+ };
+
+ getTokenAccounts();
+ // We just want to get the accounts in the render of the component
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ if (!connectRequest) {
+ history.push(mostRecentOverviewPage);
+ return null;
+ }
+
+ const onRemoveAddTokenConnectRequest = ({ origin, apiUrl, token }) => {
+ dispatch(
+ removeAddTokenConnectRequest({
+ origin,
+ apiUrl,
+ token,
+ }),
+ );
+ };
+
+ const handleReject = () => {
+ onRemoveAddTokenConnectRequest(connectRequest);
+ history.push(mostRecentOverviewPage);
+ };
+
+ const handleApprove = async () => {
+ if (error) {
+ global.platform.openTab({
+ url,
+ });
+ handleReject();
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ await Promise.all(
+ tokenAccounts.map(async (account) => {
+ await dispatch(
+ setCustodianNewRefreshToken({
+ address: account.address,
+ newAuthDetails: {
+ refreshToken: connectRequest.token,
+ refreshTokenUrl: connectRequest.apiUrl,
+ },
+ }),
+ );
+ }),
+ );
+
+ dispatch(showInteractiveReplacementTokenBanner({}));
+
+ onRemoveAddTokenConnectRequest(connectRequest);
+
+ history.push({
+ pathname: INSTITUTIONAL_FEATURES_DONE_ROUTE,
+ state: {
+ imgSrc: custodian?.iconUrl,
+ title: t('custodianReplaceRefreshTokenChangedTitle'),
+ description: t('custodianReplaceRefreshTokenChangedSubtitle'),
+ },
+ });
+
+ if (isMountedRef.current) {
+ setIsLoading(false);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ return (
+
+
+
+ {t('custodianReplaceRefreshTokenTitle')}{' '}
+ {error ? t('failed').toLowerCase() : ''}
+
+ {!error && (
+
+ {t('custodianReplaceRefreshTokenSubtitle')}
+
+ )}
+
+
+
+ {error ? (
+
+ {t('custodianReplaceRefreshTokenChangedFailed', [
+ custodian.displayName || 'Custodian',
+ ])}
+
+ ) : null}
+
+ {tokenAccounts.map((account, idx) => {
+ return (
+
+
+
+
+
+ {account.labels && (
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+InteractiveReplacementTokenPage.propTypes = {
+ history: PropTypes.object,
+};
diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.stories.js b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.stories.js
new file mode 100644
index 000000000..ed51dbc78
--- /dev/null
+++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.stories.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { action } from '@storybook/addon-actions';
+import configureStore from '../../../store/store';
+import testData from '../../../../.storybook/test-data';
+import InteractiveReplacementTokenPage from '.';
+
+const address = '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F';
+const customData = {
+ ...testData,
+ metamask: {
+ ...testData.metamask,
+ modal: { props: address },
+ selectedAddress: address,
+ interactiveReplacementToken: {
+ url: 'https://saturn-custody-ui.codefi.network/',
+ },
+ custodyAccountDetails: {
+ [address]: { balance: '0x', custodianName: 'Jupiter' },
+ },
+ mmiConfiguration: {
+ custodians: [
+ {
+ production: true,
+ name: 'Jupiter',
+ type: 'Jupiter',
+ iconUrl: 'iconUrl',
+ displayName: 'displayName',
+ },
+ ],
+ },
+ institutionalFeatures: {
+ complianceProjectId: '',
+ connectRequests: [
+ {
+ labels: [
+ {
+ key: 'service',
+ value: 'test',
+ },
+ ],
+ origin: 'origin',
+ token: 'testToken',
+ feature: 'custodian',
+ service: 'Jupiter',
+ apiUrl: 'https://',
+ environment: 'Jupiter',
+ },
+ ],
+ },
+ },
+};
+
+const store = configureStore(customData);
+
+export default {
+ title: 'Pages/Institutional/InteractiveReplacementTokenPage',
+ decorators: [(story) => {story()}],
+ component: InteractiveReplacementTokenPage,
+ args: {
+ history: {
+ push: action('history.push()'),
+ },
+ },
+};
+
+export const DefaultStory = (args) => (
+
+);
+
+DefaultStory.storyName = 'InteractiveReplacementTokenPage';
diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js
new file mode 100644
index 000000000..d462f4a7a
--- /dev/null
+++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js
@@ -0,0 +1,225 @@
+import React from 'react';
+import { screen, act, fireEvent, waitFor } from '@testing-library/react';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { renderWithProvider } from '../../../../test/lib/render-helpers';
+import mockState from '../../../../test/data/mock-state.json';
+import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
+import { shortenAddress } from '../../../helpers/utils/util';
+import InteractiveReplacementTokenPage from '.';
+
+const custodianAccounts = [
+ {
+ address: '0x9d0ba4ddac06032527b140912ec808ab9451b788',
+ balance: '0x',
+ name: 'Jupiter',
+ labels: [
+ {
+ key: 'service',
+ value: 'Label test 1',
+ },
+ ],
+ },
+ {
+ address: '0xeb9e64b93097bc15f01f13eae97015c57ab64823',
+ balance: '0x',
+ name: 'Jupiter',
+ labels: [
+ {
+ key: 'service',
+ value: 'Label test 2',
+ },
+ ],
+ },
+];
+
+const mockedShowInteractiveReplacementTokenBanner = jest.fn();
+
+const mockedRemoveAddTokenConnectRequest = jest
+ .fn()
+ .mockReturnValue({ type: 'TYPE' });
+const mockedSetCustodianNewRefreshToken = jest
+ .fn()
+ .mockReturnValue({ type: 'TYPE' });
+let mockedGetCustodianConnectRequest = jest
+ .fn()
+ .mockReturnValue(async () => await custodianAccounts);
+
+jest.mock('../../../store/institutional/institution-background', () => ({
+ mmiActionsFactory: () => ({
+ removeAddTokenConnectRequest: mockedRemoveAddTokenConnectRequest,
+ setCustodianNewRefreshToken: mockedSetCustodianNewRefreshToken,
+ getCustodianAccounts: mockedGetCustodianConnectRequest,
+ }),
+ showInteractiveReplacementTokenBanner: () =>
+ mockedShowInteractiveReplacementTokenBanner,
+}));
+
+const address = '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F';
+const custodianAddress = '0xeb9e64b93097bc15f01f13eae97015c57ab64823';
+const accountName = 'Jupiter';
+const labels = [
+ {
+ key: 'service',
+ value: 'label test',
+ },
+];
+const connectRequests = [
+ {
+ labels,
+ origin: 'origin',
+ apiUrl: 'apiUrl',
+ token: {
+ projectName: 'projectName',
+ projectId: 'projectId',
+ clientId: 'clientId',
+ },
+ },
+];
+
+const props = {
+ history: {
+ push: jest.fn(),
+ },
+};
+
+const render = ({ newState } = {}) => {
+ const state = {
+ ...mockState,
+ metamask: {
+ ...mockState.metamask,
+ modal: { props: address },
+ selectedAddress: address,
+ interactiveReplacementToken: {
+ url: 'https://saturn-custody-ui.codefi.network/',
+ },
+ custodyAccountDetails: {
+ [address]: { balance: '0x', custodianName: 'Jupiter' },
+ },
+ mmiConfiguration: {
+ custodians: [
+ {
+ production: true,
+ name: 'Jupiter',
+ type: 'Jupiter',
+ iconUrl: 'iconUrl',
+ displayName: 'displayName',
+ },
+ ],
+ },
+ institutionalFeatures: {
+ complianceProjectId: '',
+ connectRequests,
+ },
+ ...newState,
+ },
+ };
+ const middlewares = [thunk];
+ const mockStore = configureMockStore(middlewares);
+ const store = mockStore(state);
+
+ return renderWithProvider(
+ ,
+ store,
+ );
+};
+
+describe('Interactive Replacement Token Page', function () {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render all the accounts correctly', async () => {
+ const expectedHref = `${
+ SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]
+ }address/${custodianAddress}`;
+
+ await act(async () => await render());
+
+ expect(screen.getByText(accountName)).toBeInTheDocument();
+ const link = screen.getByRole('link', {
+ name: shortenAddress(custodianAddress),
+ });
+
+ expect(link).toHaveAttribute('href', expectedHref);
+ expect(
+ screen.getByText(shortenAddress(custodianAddress)),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(custodianAccounts[1].labels[0].value),
+ ).toBeInTheDocument();
+ });
+
+ it('should not render if connectRequests is empty', async () => {
+ const newState = {
+ institutionalFeatures: {
+ connectRequests: [],
+ },
+ };
+
+ const { queryByTestId } = render({ newState });
+
+ expect(
+ queryByTestId('interactive-replacement-token'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should call onRemoveAddTokenConnectRequest and navigate to mostRecentOverviewPage when handleReject is called', () => {
+ const mostRecentOverviewPage = '/mostRecentOverviewPage';
+
+ const { getByText } = render();
+
+ fireEvent.click(getByText('Reject'));
+
+ expect(mockedRemoveAddTokenConnectRequest).toHaveBeenCalled();
+ expect(mockedRemoveAddTokenConnectRequest).toHaveBeenCalledWith({
+ origin: connectRequests[0].origin,
+ apiUrl: connectRequests[0].apiUrl,
+ token: connectRequests[0].token,
+ });
+ expect(props.history.push).toHaveBeenCalled();
+ expect(props.history.push).toHaveBeenCalledWith(mostRecentOverviewPage);
+ });
+
+ it('should call onRemoveAddTokenConnectRequest, setCustodianNewRefreshToken, and dispatch showInteractiveReplacementTokenBanner when handleApprove is called', async () => {
+ const mostRecentOverviewPage = {
+ pathname: '/institutional-features/done',
+ state: {
+ description:
+ 'You can now use your custodian accounts in MetaMask Institutional.',
+ imgSrc: 'iconUrl',
+ title: 'Your custodian token has been refreshed',
+ },
+ };
+
+ await act(async () => {
+ const { getByText } = await render();
+ fireEvent.click(getByText('Approve'));
+ });
+
+ expect(mockedShowInteractiveReplacementTokenBanner).toHaveBeenCalled();
+ expect(mockedRemoveAddTokenConnectRequest).toHaveBeenCalled();
+ expect(mockedRemoveAddTokenConnectRequest).toHaveBeenCalledWith({
+ origin: connectRequests[0].origin,
+ apiUrl: connectRequests[0].apiUrl,
+ token: connectRequests[0].token,
+ });
+ expect(props.history.push).toHaveBeenCalled();
+ expect(props.history.push).toHaveBeenCalledWith(mostRecentOverviewPage);
+ });
+
+ it('should reject if there are errors', async () => {
+ mockedGetCustodianConnectRequest = jest.fn().mockReturnValue(async () => {
+ throw new Error();
+ });
+
+ await act(async () => {
+ const { getByText, container } = await render();
+ fireEvent.click(getByText('Approve'));
+ await waitFor(() => {
+ expect(container).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss
index 925feceb0..9d926ccd9 100644
--- a/ui/pages/pages.scss
+++ b/ui/pages/pages.scss
@@ -12,8 +12,9 @@
@import 'connected-accounts/index';
@import 'connected-sites/index';
@import 'create-account/index';
-///: BEGIN:ONLY_INCLUDE_IN(mmi)
+///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
@import "create-account/institutional/connect-custody/index";
+@import "institutional/interactive-replacement-token-page/index";
///: END:ONLY_INCLUDE_IN
@import 'error/index';
@import 'send/gas-display/index';
diff --git a/ui/store/institutional/institution-background.ts b/ui/store/institutional/institution-background.ts
index 21a1432db..7defb2abe 100644
--- a/ui/store/institutional/institution-background.ts
+++ b/ui/store/institutional/institution-background.ts
@@ -14,23 +14,27 @@ import {
import { MetaMaskReduxState } from '../store';
import { isErrorWithMessage } from '../../../shared/modules/error';
-export function showInteractiveReplacementTokenBanner(
- url: string,
- oldRefreshToken: string,
-) {
- return () => {
- callBackgroundMethod(
- 'showInteractiveReplacementTokenBanner',
- [url, oldRefreshToken],
- (err) => {
- if (isErrorWithMessage(err)) {
- throw new Error(err.message);
- }
- },
- );
+export function showInteractiveReplacementTokenBanner({
+ url,
+ oldRefreshToken,
+}: {
+ url: string;
+ oldRefreshToken: string;
+}): ThunkAction {
+ return async (dispatch) => {
+ try {
+ await submitRequestToBackground('showInteractiveReplacementTokenBanner', [
+ url,
+ oldRefreshToken,
+ ]);
+ } catch (err: any) {
+ if (err) {
+ dispatch(displayWarning(err.message));
+ throw new Error(err.message);
+ }
+ }
};
}
-
/**
* A factory that contains all MMI actions ready to use
* Example usage: