From 4c9bf40688b1d352340002607a54c86dc412ba3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Oliv=C3=A9?= Date: Thu, 27 Apr 2023 10:45:37 +0200 Subject: [PATCH] [MMI] Add interactive-replacement-token-page (#18683) * Initial commit * converted the component from class-based to functional. * Refactored component * Improved code and tests * Finished adding tests * Fixed eslint problems * Added back custodyLabels component * Fixed eslint problems * fixed ts lint problem * Fixed eslint problems * Added more tests and improved code * Added comments * Fixed eslint problems * Fixed eslint problems --- app/_locales/en/messages.json | 15 + ui/components/app/app-components.scss | 2 +- ui/css/index.scss | 2 +- ui/helpers/constants/routes.ts | 13 +- ...active-replacement-token-page.test.js.snap | 119 ++++++ .../index.js | 3 + .../index.scss | 23 ++ .../interactive-replacement-token-page.js | 349 ++++++++++++++++++ ...eractive-replacement-token-page.stories.js | 71 ++++ ...interactive-replacement-token-page.test.js | 225 +++++++++++ ui/pages/pages.scss | 3 +- .../institutional/institution-background.ts | 34 +- 12 files changed, 834 insertions(+), 25 deletions(-) create mode 100644 ui/pages/institutional/interactive-replacement-token-page/__snapshots__/interactive-replacement-token-page.test.js.snap create mode 100644 ui/pages/institutional/interactive-replacement-token-page/index.js create mode 100644 ui/pages/institutional/interactive-replacement-token-page/index.scss create mode 100644 ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.js create mode 100644 ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.stories.js create mode 100644 ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.js 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`] = ` +
+
+
+
+ Replace custodian token + + +
+
+ This is will replace the custodian token for the following address: +
+
+
+
+
+
+
+ +
+
+`; + +exports[`Interactive Replacement Token Page should reject if there are errors 2`] = ` +
+
+
+
+ Replace custodian token + + failed +
+
+
+
+

+ 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: