diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 382dd314a..5d7a113e0 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -929,6 +929,21 @@
"custodianAccount": {
"message": "Custodian account"
},
+ "custodyRefreshTokenModalDescription": {
+ "message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again."
+ },
+ "custodyRefreshTokenModalDescription1": {
+ "message": "Your custodian issues a token that authenticates the MetaMask Institutional extension, allowing you to connect your accounts."
+ },
+ "custodyRefreshTokenModalDescription2": {
+ "message": "This token expires after a certain period for security reasons. This requires you to reconnect to MMI."
+ },
+ "custodyRefreshTokenModalSubtitle": {
+ "message": "Why am I seeing this?"
+ },
+ "custodyRefreshTokenModalTitle": {
+ "message": "Your custodian session has expired"
+ },
"custom": {
"message": "Advanced"
},
diff --git a/ui/components/institutional/interactive-replacement-token-modal/index.js b/ui/components/institutional/interactive-replacement-token-modal/index.js
new file mode 100644
index 000000000..1f1b5f7f6
--- /dev/null
+++ b/ui/components/institutional/interactive-replacement-token-modal/index.js
@@ -0,0 +1 @@
+export { default } from './interactive-replacement-token-modal';
diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js
new file mode 100644
index 000000000..0f40723d4
--- /dev/null
+++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js
@@ -0,0 +1,154 @@
+import React, { useContext } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import Modal from '../../app/modal';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { MetaMetricsContext } from '../../../contexts/metametrics';
+import { hideModal } from '../../../store/actions';
+import { getSelectedAddress } from '../../../selectors/selectors';
+import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
+import { Text } from '../../component-library';
+import Box from '../../ui/box';
+import {
+ BLOCK_SIZES,
+ BackgroundColor,
+ DISPLAY,
+ FLEX_WRAP,
+ FLEX_DIRECTION,
+ BorderRadius,
+ FONT_WEIGHT,
+ TEXT_ALIGN,
+ AlignItems,
+} from '../../../helpers/constants/design-system';
+
+const InteractiveReplacementTokenModal = () => {
+ const t = useI18nContext();
+ const trackEvent = useContext(MetaMetricsContext);
+ const dispatch = useDispatch();
+
+ const { url } = useSelector(
+ (state) => state.metamask.interactiveReplacementToken || {},
+ );
+
+ const { custodians } = useSelector(
+ (state) => state.metamask.mmiConfiguration,
+ );
+ const address = useSelector(getSelectedAddress);
+ const custodyAccountDetails = useSelector(
+ (state) =>
+ state.metamask.custodyAccountDetails[toChecksumHexAddress(address)],
+ );
+
+ const custodianName = custodyAccountDetails?.custodianName;
+ const custodian =
+ custodians.find((item) => item.name === custodianName) || {};
+
+ const renderCustodyInfo = () => {
+ let img;
+
+ if (custodian.iconUrl) {
+ img = (
+
+
+
+
+
+ );
+ } else {
+ img = (
+
+ {custodian.displayName}
+
+ );
+ }
+
+ return (
+ <>
+ {img}
+
+ {t('custodyRefreshTokenModalTitle')}
+
+
+ {t('custodyRefreshTokenModalDescription', [custodian.displayName])}
+
+
+ {t('custodyRefreshTokenModalSubtitle')}
+
+
+ {t('custodyRefreshTokenModalDescription1')}
+
+
+ {t('custodyRefreshTokenModalDescription2')}
+
+ >
+ );
+ };
+
+ const handleSubmit = () => {
+ global.platform.openTab({
+ url,
+ });
+
+ trackEvent({
+ category: 'MMI',
+ event: 'User clicked refresh token link',
+ });
+ };
+
+ const handleClose = () => {
+ dispatch(hideModal());
+ };
+
+ return (
+
+
+ {renderCustodyInfo(custodian)}
+
+
+ );
+};
+
+export default InteractiveReplacementTokenModal;
diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js
new file mode 100644
index 000000000..86c03504c
--- /dev/null
+++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import configureStore from '../../../store/store';
+import testData from '../../../../.storybook/test-data';
+import InteractiveReplacementTokenModal from '.';
+
+const customData = {
+ ...testData,
+ metamask: {
+ ...testData.metamask,
+ mmiConfiguration: {
+ portfolio: {
+ enabled: true,
+ url: 'https://dev.metamask-institutional.io/',
+ },
+ features: {
+ websocketApi: true,
+ },
+ custodians: [
+ {
+ refreshTokenUrl:
+ 'https://saturn-custody.dev.metamask-institutional.io/oauth/token',
+ name: 'saturn-dev',
+ displayName: 'Saturn Custody',
+ enabled: true,
+ mmiApiUrl: 'https://api.dev.metamask-institutional.io/v1',
+ websocketApiUrl:
+ 'wss://websocket.dev.metamask-institutional.io/v1/ws',
+ apiBaseUrl:
+ 'https://saturn-custody.dev.metamask-institutional.io/eth',
+ iconUrl:
+ 'https://saturn-custody-ui.dev.metamask-institutional.io/saturn.svg',
+ isNoteToTraderSupported: true,
+ },
+ ],
+ },
+ custodyAccountDetails: {
+ '0xAddress': {
+ address: '0xAddress',
+ details: 'details',
+ custodyType: 'testCustody - Saturn',
+ custodianName: 'saturn-dev',
+ },
+ },
+ provider: {
+ type: 'test',
+ },
+ selectedAddress: '0xAddress',
+ isUnlocked: true,
+ interactiveReplacementToken: {
+ oldRefreshToken: 'abc',
+ url: 'https://saturn-custody-ui.dev.metamask-institutional.io',
+ },
+ preferences: {
+ useNativeCurrencyAsPrimaryCurrency: true,
+ },
+ },
+};
+
+const store = configureStore(customData);
+
+export default {
+ title: 'Components/Institutional/InteractiveReplacementToken-Modal',
+ decorators: [(story) => {story()}],
+ component: InteractiveReplacementTokenModal,
+};
+
+export const DefaultStory = (args) => (
+
+);
+
+DefaultStory.storyName = 'InteractiveReplacementTokenModal';
diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js
new file mode 100644
index 000000000..4a78a2237
--- /dev/null
+++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js
@@ -0,0 +1,91 @@
+import React from 'react';
+import sinon from 'sinon';
+import configureMockStore from 'redux-mock-store';
+import { fireEvent, waitFor } from '@testing-library/react';
+import { renderWithProvider } from '../../../../test/lib/render-helpers';
+import testData from '../../../../.storybook/test-data';
+import InteractiveReplacementTokenModal from '.';
+
+describe('Interactive Replacement Token Modal', function () {
+ const mockStore = {
+ ...testData,
+ metamask: {
+ ...testData.metamask,
+ mmiConfiguration: {
+ portfolio: {
+ enabled: true,
+ url: 'https://dev.metamask-institutional.io/',
+ },
+ features: {
+ websocketApi: true,
+ },
+ custodians: [
+ {
+ refreshTokenUrl:
+ 'https://saturn-custody.dev.metamask-institutional.io/oauth/token',
+ name: 'saturn-dev',
+ displayName: 'Saturn Custody',
+ enabled: true,
+ mmiApiUrl: 'https://api.dev.metamask-institutional.io/v1',
+ websocketApiUrl:
+ 'wss://websocket.dev.metamask-institutional.io/v1/ws',
+ apiBaseUrl:
+ 'https://saturn-custody.dev.metamask-institutional.io/eth',
+ iconUrl:
+ 'https://saturn-custody-ui.dev.metamask-institutional.io/saturn.svg',
+ isNoteToTraderSupported: true,
+ },
+ ],
+ },
+ custodyAccountDetails: {
+ '0xAddress': {
+ address: '0xAddress',
+ details: 'details',
+ custodyType: 'testCustody - Saturn',
+ custodianName: 'saturn-dev',
+ },
+ },
+ provider: {
+ type: 'test',
+ },
+ selectedAddress: '0xAddress',
+ isUnlocked: true,
+ interactiveReplacementToken: {
+ oldRefreshToken: 'abc',
+ url: 'https://saturn-custody-ui.dev.metamask-institutional.io',
+ },
+ preferences: {
+ useNativeCurrencyAsPrimaryCurrency: true,
+ },
+ },
+ };
+
+ const store = configureMockStore()(mockStore);
+
+ it('should render the interactive-replacement-token-modal', () => {
+ const { getByText, getByTestId } = renderWithProvider(
+ ,
+ store,
+ );
+
+ expect(getByTestId('interactive-replacement-token-modal')).toBeVisible();
+ expect(getByText('Your custodian session has expired')).toBeInTheDocument();
+ });
+
+ it('opens new tab on Open Codefi Compliance click', async () => {
+ global.platform = { openTab: sinon.spy() };
+
+ const { container } = renderWithProvider(
+ ,
+ store,
+ );
+
+ const button = container.getElementsByClassName('btn-primary')[0];
+
+ fireEvent.click(button);
+
+ await waitFor(() => {
+ expect(global.platform.openTab.calledOnce).toStrictEqual(true);
+ });
+ });
+});
diff --git a/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap
index 2d68b5531..10ee36b9b 100644
--- a/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap
+++ b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap
@@ -42,26 +42,28 @@ exports[`Confirm Add Institutional Feature opens confirm institutional sucessful
-
+
`;
diff --git a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js
index 25a52fc16..0e89910e2 100644
--- a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js
+++ b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js
@@ -7,12 +7,17 @@ import PulseLoader from '../../../components/ui/pulse-loader';
import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
-import { Text } from '../../../components/component-library';
+import {
+ Text,
+ BUTTON_SIZES,
+ BUTTON_TYPES,
+} from '../../../components/component-library';
import {
TextColor,
TextVariant,
OVERFLOW_WRAP,
TEXT_ALIGN,
+ DISPLAY,
} from '../../../helpers/constants/design-system';
import Box from '../../../components/ui/box';
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
@@ -169,16 +174,15 @@ export default function ConfirmAddInstitutionalFeature({ history }) {
)}
-
+
{isLoading ? (
-
+
) : (
-
+
)}