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:
parent
e0919d529e
commit
4c9bf40688
15
app/_locales/en/messages.json
generated
15
app/_locales/en/messages.json
generated
@ -935,6 +935,21 @@
|
|||||||
"custodianAccount": {
|
"custodianAccount": {
|
||||||
"message": "Custodian account"
|
"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": {
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
@import 'network-account-balance-header/index';
|
@import 'network-account-balance-header/index';
|
||||||
@import 'approve-content-card/index';
|
@import 'approve-content-card/index';
|
||||||
@import 'transaction-alerts/transaction-alerts';
|
@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/custody-confirm-link-modal/index';
|
||||||
@import '../institutional/transaction-failed-modal/index';
|
@import '../institutional/transaction-failed-modal/index';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
@import '../components/component-library/component-library-components.scss';
|
@import '../components/component-library/component-library-components.scss';
|
||||||
@import '../components/app/app-components';
|
@import '../components/app/app-components';
|
||||||
@import '../components/ui/ui-components';
|
@import '../components/ui/ui-components';
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(mmi)
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
||||||
@import '../components/institutional/institutional-components';
|
@import '../components/institutional/institutional-components';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
@import '../components/multichain/multichain-components.scss';
|
@import '../components/multichain/multichain-components.scss';
|
||||||
|
@ -22,6 +22,7 @@ const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact';
|
|||||||
const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact';
|
const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact';
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
||||||
const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody';
|
const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody';
|
||||||
|
const INSTITUTIONAL_FEATURES_DONE_ROUTE = '/institutional-features/done';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
const REVEAL_SEED_ROUTE = '/seed';
|
const REVEAL_SEED_ROUTE = '/seed';
|
||||||
const RESTORE_VAULT_ROUTE = '/restore-vault';
|
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 NEW_ACCOUNT_ROUTE = '/new-account';
|
||||||
const IMPORT_ACCOUNT_ROUTE = '/new-account/import';
|
const IMPORT_ACCOUNT_ROUTE = '/new-account/import';
|
||||||
const CONNECT_HARDWARE_ROUTE = '/new-account/connect';
|
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 SEND_ROUTE = '/send';
|
||||||
const TOKEN_DETAILS = '/token-details';
|
const TOKEN_DETAILS = '/token-details';
|
||||||
const CONNECT_ROUTE = '/connect';
|
const CONNECT_ROUTE = '/connect';
|
||||||
@ -127,7 +125,7 @@ const PATH_NAME_MAP = {
|
|||||||
[IMPORT_ACCOUNT_ROUTE]: 'Import Account Page',
|
[IMPORT_ACCOUNT_ROUTE]: 'Import Account Page',
|
||||||
[CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page',
|
[CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page',
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
///: 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
|
///: END:ONLY_INCLUDE_IN
|
||||||
[SEND_ROUTE]: 'Send Page',
|
[SEND_ROUTE]: 'Send Page',
|
||||||
[`${TOKEN_DETAILS}/:address`]: 'Token Details Page',
|
[`${TOKEN_DETAILS}/:address`]: 'Token Details Page',
|
||||||
@ -162,6 +160,9 @@ const PATH_NAME_MAP = {
|
|||||||
'Decrypt Message Request Page',
|
'Decrypt Message Request Page',
|
||||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`]:
|
[`${CONFIRM_TRANSACTION_ROUTE}/:id${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`]:
|
||||||
'Encryption Public Key Request Page',
|
'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',
|
[BUILD_QUOTE_ROUTE]: 'Swaps Build Quote Page',
|
||||||
[VIEW_QUOTE_ROUTE]: 'Swaps View Quotes Page',
|
[VIEW_QUOTE_ROUTE]: 'Swaps View Quotes Page',
|
||||||
[LOADING_QUOTES_ROUTE]: 'Swaps Loading Quotes Page',
|
[LOADING_QUOTES_ROUTE]: 'Swaps Loading Quotes Page',
|
||||||
@ -184,9 +185,6 @@ export {
|
|||||||
NEW_ACCOUNT_ROUTE,
|
NEW_ACCOUNT_ROUTE,
|
||||||
IMPORT_ACCOUNT_ROUTE,
|
IMPORT_ACCOUNT_ROUTE,
|
||||||
CONNECT_HARDWARE_ROUTE,
|
CONNECT_HARDWARE_ROUTE,
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
||||||
INSTITUTIONAL_FEATURES_DONE_ROUTE,
|
|
||||||
///: END:ONLY_INCLUDE_IN
|
|
||||||
SEND_ROUTE,
|
SEND_ROUTE,
|
||||||
TOKEN_DETAILS,
|
TOKEN_DETAILS,
|
||||||
CONFIRM_TRANSACTION_ROUTE,
|
CONFIRM_TRANSACTION_ROUTE,
|
||||||
@ -215,6 +213,7 @@ export {
|
|||||||
CONTACT_VIEW_ROUTE,
|
CONTACT_VIEW_ROUTE,
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
||||||
CUSTODY_ACCOUNT_ROUTE,
|
CUSTODY_ACCOUNT_ROUTE,
|
||||||
|
INSTITUTIONAL_FEATURES_DONE_ROUTE,
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
NETWORKS_ROUTE,
|
NETWORKS_ROUTE,
|
||||||
NETWORKS_FORM_ROUTE,
|
NETWORKS_FORM_ROUTE,
|
||||||
|
@ -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>
|
||||||
|
`;
|
@ -0,0 +1,3 @@
|
|||||||
|
import InteractiveReplacementTokenPage from './interactive-replacement-token-page';
|
||||||
|
|
||||||
|
export default InteractiveReplacementTokenPage;
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
};
|
@ -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';
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -12,8 +12,9 @@
|
|||||||
@import 'connected-accounts/index';
|
@import 'connected-accounts/index';
|
||||||
@import 'connected-sites/index';
|
@import 'connected-sites/index';
|
||||||
@import 'create-account/index';
|
@import 'create-account/index';
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(mmi)
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
||||||
@import "create-account/institutional/connect-custody/index";
|
@import "create-account/institutional/connect-custody/index";
|
||||||
|
@import "institutional/interactive-replacement-token-page/index";
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
@import 'error/index';
|
@import 'error/index';
|
||||||
@import 'send/gas-display/index';
|
@import 'send/gas-display/index';
|
||||||
|
@ -14,23 +14,27 @@ import {
|
|||||||
import { MetaMaskReduxState } from '../store';
|
import { MetaMaskReduxState } from '../store';
|
||||||
import { isErrorWithMessage } from '../../../shared/modules/error';
|
import { isErrorWithMessage } from '../../../shared/modules/error';
|
||||||
|
|
||||||
export function showInteractiveReplacementTokenBanner(
|
export function showInteractiveReplacementTokenBanner({
|
||||||
url: string,
|
url,
|
||||||
oldRefreshToken: string,
|
oldRefreshToken,
|
||||||
) {
|
}: {
|
||||||
return () => {
|
url: string;
|
||||||
callBackgroundMethod(
|
oldRefreshToken: string;
|
||||||
'showInteractiveReplacementTokenBanner',
|
}): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
|
||||||
[url, oldRefreshToken],
|
return async (dispatch) => {
|
||||||
(err) => {
|
try {
|
||||||
if (isErrorWithMessage(err)) {
|
await submitRequestToBackground('showInteractiveReplacementTokenBanner', [
|
||||||
throw new Error(err.message);
|
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
|
* A factory that contains all MMI actions ready to use
|
||||||
* Example usage:
|
* Example usage:
|
||||||
|
Loading…
Reference in New Issue
Block a user