1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

[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
This commit is contained in:
Albert Olivé 2023-04-27 10:45:37 +02:00 committed by GitHub
parent e0919d529e
commit 4c9bf40688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 834 additions and 25 deletions

View File

@ -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."
},

View File

@ -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

View File

@ -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';

View File

@ -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,

View File

@ -0,0 +1,119 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Interactive Replacement Token Page should reject if there are errors 1`] = `
<div>
<div
class="box page-container box--flex-direction-row"
data-testid="interactive-replacement-token"
>
<div
class="box page-container__header false box--flex-direction-row"
>
<div
class="box page-container__title box--flex-direction-row"
>
Replace custodian token
</div>
<div
class="box page-container__subtitle box--flex-direction-row"
>
This is will replace the custodian token for the following address:
</div>
</div>
<div
class="box page-container__content box--flex-direction-row"
>
<div
class="box interactive-replacement-token-page box--margin-right-7 box--margin-left-7 box--display-flex box--flex-direction-row box--color-text-alternative"
overflowwrap="break-word"
>
<div
class="box box--display-flex box--flex-direction-column box--width-full"
data-testid="interactive-replacement-token-page"
/>
</div>
</div>
<div
class="box page-container__footer box--flex-direction-row"
>
<footer>
<div
class="pulse-loader"
>
<div
class="pulse-loader__loading-dot-one"
/>
<div
class="pulse-loader__loading-dot-two"
/>
<div
class="pulse-loader__loading-dot-three"
/>
</div>
</footer>
</div>
</div>
</div>
`;
exports[`Interactive Replacement Token Page should reject if there are errors 2`] = `
<div>
<div
class="box page-container box--flex-direction-row"
data-testid="interactive-replacement-token"
>
<div
class="box page-container__header error box--flex-direction-row"
>
<div
class="box page-container__title box--flex-direction-row"
>
Replace custodian token
failed
</div>
</div>
<div
class="box page-container__content box--flex-direction-row"
>
<div
class="box interactive-replacement-token-page box--margin-right-7 box--margin-left-7 box--display-flex box--flex-direction-row box--color-text-alternative"
overflowwrap="break-word"
>
<p
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
data-testid="connect-error-message"
>
Please go to displayName and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again.
</p>
<div
class="box box--display-flex box--flex-direction-column box--width-full"
data-testid="interactive-replacement-token-page"
/>
</div>
</div>
<div
class="box page-container__footer box--flex-direction-row"
>
<footer>
<button
class="button btn--rounded btn-default btn--large page-container__footer-button"
role="button"
tabindex="0"
>
Reject
</button>
<button
class="button btn--rounded btn-primary btn--large page-container__footer-button"
role="button"
tabindex="0"
>
displayName
</button>
</footer>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,3 @@
import InteractiveReplacementTokenPage from './interactive-replacement-token-page';
export default InteractiveReplacementTokenPage;

View File

@ -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);
}
}

View File

@ -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 (
<Box className="page-container" data-testid="interactive-replacement-token">
<Box className={`page-container__header ${error && 'error'}`}>
<Box className="page-container__title">
{t('custodianReplaceRefreshTokenTitle')}{' '}
{error ? t('failed').toLowerCase() : ''}
</Box>
{!error && (
<Box className="page-container__subtitle">
{t('custodianReplaceRefreshTokenSubtitle')}
</Box>
)}
</Box>
<Box className="page-container__content">
<Box
display={DISPLAY.FLEX}
marginRight={7}
marginLeft={7}
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
color={TextColor.textAlternative}
className="interactive-replacement-token-page"
>
{error ? (
<Text data-testid="connect-error-message">
{t('custodianReplaceRefreshTokenChangedFailed', [
custodian.displayName || 'Custodian',
])}
</Text>
) : null}
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
width={BLOCK_SIZES.FULL}
data-testid="interactive-replacement-token-page"
>
{tokenAccounts.map((account, idx) => {
return (
<Box
display={DISPLAY.FLEX}
className="interactive-replacement-token-page__item"
key={account.address}
>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
width={BLOCK_SIZES.FULL}
>
<Label
marginTop={3}
marginRight={2}
htmlFor={`address-${idx}`}
>
<Text as="span" data-testid="account-name">
{account.name}
</Text>
</Label>
<Label
marginTop={1}
marginRight={2}
htmlFor={`address-${idx}`}
>
<Text
as="span"
display={DISPLAY.FLEX}
className="interactive-replacement-token-page__item__address"
>
<ButtonLink
href={getButtonLinkHref(account)}
target="_blank"
rel="noopener noreferrer"
>
{shortenAddress(account.address)}
<Icon
name={IconName.Export}
size={IconSize.Sm}
color={IconColor.primaryDefault}
marginLeft={1}
/>
</ButtonLink>
<Tooltip
position="bottom"
title={
copied
? t('copiedExclamation')
: t('copyToClipboard')
}
>
<button
className="interactive-replacement-token-page__item-clipboard"
onClick={() => handleCopy(account.address)}
>
<Icon
name={IconName.Copy}
size={IconSize.Xs}
color={IconColor.iconMuted}
/>
</button>
</Tooltip>
</Text>
</Label>
<Box
display={DISPLAY.FLEX}
justifyContent={JustifyContent.spaceBetween}
>
{account.labels && (
<CustodyLabels
labels={account.labels}
index={idx.toString()}
hideNetwork
/>
)}
</Box>
</Box>
</Box>
);
})}
</Box>
</Box>
</Box>
<Box className="page-container__footer">
{isLoading ? (
<footer>
<PulseLoader />
</footer>
) : (
<footer>
<Button
type="default"
large
className="page-container__footer-button"
onClick={handleReject}
>
{t('reject')}
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={handleApprove}
>
{error
? custodian.displayName || 'Custodian'
: t('approveButtonText')}
</Button>
</footer>
)}
</Box>
</Box>
);
}
InteractiveReplacementTokenPage.propTypes = {
history: PropTypes.object,
};

View File

@ -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) => <Provider store={store}>{story()}</Provider>],
component: InteractiveReplacementTokenPage,
args: {
history: {
push: action('history.push()'),
},
},
};
export const DefaultStory = (args) => (
<InteractiveReplacementTokenPage {...args} />
);
DefaultStory.storyName = 'InteractiveReplacementTokenPage';

View File

@ -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(
<InteractiveReplacementTokenPage {...props} />,
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();
});
});
});
});

View File

@ -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';

View File

@ -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)) {
export function showInteractiveReplacementTokenBanner({
url,
oldRefreshToken,
}: {
url: string;
oldRefreshToken: string;
}): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
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: