mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
[MMI] Interactive replacement token modal (#18523)
* updates styles for confirm-add-institutional-feature * initial component * adds tests and storybook file * clean up styles and adds MM design system * prettier * locale update * lint * snapshot update --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com>
This commit is contained in:
parent
3eefe874a8
commit
8fdbd07c91
@ -929,6 +929,21 @@
|
|||||||
"custodianAccount": {
|
"custodianAccount": {
|
||||||
"message": "Custodian account"
|
"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": {
|
"custom": {
|
||||||
"message": "Advanced"
|
"message": "Advanced"
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './interactive-replacement-token-modal';
|
@ -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 = (
|
||||||
|
<Box
|
||||||
|
display={DISPLAY.FLEX}
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
alignItems={AlignItems.center}
|
||||||
|
paddingTop={5}
|
||||||
|
>
|
||||||
|
<Box display={DISPLAY.BLOCK} textAlign={TEXT_ALIGN.CENTER}>
|
||||||
|
<img
|
||||||
|
src={custodian.iconUrl}
|
||||||
|
width={45}
|
||||||
|
alt={custodian.displayName}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
img = (
|
||||||
|
<Box display={DISPLAY.BLOCK} textAlign={TEXT_ALIGN.CENTER}>
|
||||||
|
<Text>{custodian.displayName}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{img}
|
||||||
|
<Text
|
||||||
|
as="h4"
|
||||||
|
paddingTop={4}
|
||||||
|
textAlign={TEXT_ALIGN.CENTER}
|
||||||
|
fontWeight={FONT_WEIGHT.BOLD}
|
||||||
|
>
|
||||||
|
{t('custodyRefreshTokenModalTitle')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
paddingTop={4}
|
||||||
|
paddingBottom={6}
|
||||||
|
textAlign={TEXT_ALIGN.LEFT}
|
||||||
|
>
|
||||||
|
{t('custodyRefreshTokenModalDescription', [custodian.displayName])}
|
||||||
|
</Text>
|
||||||
|
<Text as="p" fontWeight={FONT_WEIGHT.MEDIUM}>
|
||||||
|
{t('custodyRefreshTokenModalSubtitle')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
paddingTop={4}
|
||||||
|
paddingBottom={6}
|
||||||
|
textAlign={TEXT_ALIGN.LEFT}
|
||||||
|
>
|
||||||
|
{t('custodyRefreshTokenModalDescription1')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
marginTop={4}
|
||||||
|
paddingTop={4}
|
||||||
|
paddingBottom={6}
|
||||||
|
textAlign={TEXT_ALIGN.LEFT}
|
||||||
|
>
|
||||||
|
{t('custodyRefreshTokenModalDescription2')}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
global.platform.openTab({
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
|
trackEvent({
|
||||||
|
category: 'MMI',
|
||||||
|
event: 'User clicked refresh token link',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dispatch(hideModal());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onCancel={handleClose}
|
||||||
|
onClose={handleClose}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitText={custodian.displayName || 'Custodian'}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width={BLOCK_SIZES.FULL}
|
||||||
|
backgroundColor={BackgroundColor.backgroundDefault}
|
||||||
|
display={DISPLAY.FLEX}
|
||||||
|
flexWrap={FLEX_WRAP.WRAP}
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
borderRadius={BorderRadius.SM}
|
||||||
|
data-testid="interactive-replacement-token-modal"
|
||||||
|
>
|
||||||
|
{renderCustodyInfo(custodian)}
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InteractiveReplacementTokenModal;
|
@ -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) => <Provider store={store}>{story()}</Provider>],
|
||||||
|
component: InteractiveReplacementTokenModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultStory = (args) => (
|
||||||
|
<InteractiveReplacementTokenModal {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'InteractiveReplacementTokenModal';
|
@ -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(
|
||||||
|
<InteractiveReplacementTokenModal />,
|
||||||
|
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(
|
||||||
|
<InteractiveReplacementTokenModal />,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = container.getElementsByClassName('btn-primary')[0];
|
||||||
|
|
||||||
|
fireEvent.click(button);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(global.platform.openTab.calledOnce).toStrictEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -42,26 +42,28 @@ exports[`Confirm Add Institutional Feature opens confirm institutional sucessful
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<footer
|
||||||
class="box page-container__footer box--flex-direction-row"
|
class="box page-container__footer box--padding-4 box--flex-direction-row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="box box--display-flex box--gap-4 box--flex-direction-row"
|
||||||
>
|
>
|
||||||
<footer>
|
|
||||||
<button
|
<button
|
||||||
class="button btn--rounded btn-default btn--large"
|
class="button btn--rounded btn-secondary"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button btn--rounded btn-primary btn--large"
|
class="button btn--rounded btn-primary"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
@ -7,12 +7,17 @@ import PulseLoader from '../../../components/ui/pulse-loader';
|
|||||||
import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes';
|
import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes';
|
||||||
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||||
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
||||||
import { Text } from '../../../components/component-library';
|
import {
|
||||||
|
Text,
|
||||||
|
BUTTON_SIZES,
|
||||||
|
BUTTON_TYPES,
|
||||||
|
} from '../../../components/component-library';
|
||||||
import {
|
import {
|
||||||
TextColor,
|
TextColor,
|
||||||
TextVariant,
|
TextVariant,
|
||||||
OVERFLOW_WRAP,
|
OVERFLOW_WRAP,
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
|
DISPLAY,
|
||||||
} from '../../../helpers/constants/design-system';
|
} from '../../../helpers/constants/design-system';
|
||||||
import Box from '../../../components/ui/box';
|
import Box from '../../../components/ui/box';
|
||||||
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
|
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
|
||||||
@ -169,16 +174,15 @@ export default function ConfirmAddInstitutionalFeature({ history }) {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box className="page-container__footer">
|
<Box as="footer" className="page-container__footer" padding={4}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<footer>
|
|
||||||
<PulseLoader />
|
<PulseLoader />
|
||||||
</footer>
|
|
||||||
) : (
|
) : (
|
||||||
<footer>
|
<Box display={DISPLAY.FLEX} gap={4}>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
block
|
||||||
large
|
type={BUTTON_TYPES.SECONDARY}
|
||||||
|
size={BUTTON_SIZES.LG}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeConnectInstitutionalFeature({
|
removeConnectInstitutionalFeature({
|
||||||
actions: 'Institutional feature RPC cancel',
|
actions: 'Institutional feature RPC cancel',
|
||||||
@ -191,12 +195,13 @@ export default function ConfirmAddInstitutionalFeature({ history }) {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
large
|
block
|
||||||
|
size={BUTTON_SIZES.LG}
|
||||||
onClick={confirmAddInstitutionalFeature}
|
onClick={confirmAddInstitutionalFeature}
|
||||||
>
|
>
|
||||||
{t('confirm')}
|
{t('confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user