1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +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:
António Regadas 2023-04-14 11:35:58 +01:00 committed by GitHub
parent 3eefe874a8
commit 8fdbd07c91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 357 additions and 17 deletions

View File

@ -929,6 +929,21 @@
"custodianAccount": {
"message": "Custodian account"
},
"custodyRefreshTokenModalDescription": {
"message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again."
},
"custodyRefreshTokenModalDescription1": {
"message": "Your custodian issues a token that authenticates the MetaMask Institutional extension, allowing you to connect your accounts."
},
"custodyRefreshTokenModalDescription2": {
"message": "This token expires after a certain period for security reasons. This requires you to reconnect to MMI."
},
"custodyRefreshTokenModalSubtitle": {
"message": "Why am I seeing this?"
},
"custodyRefreshTokenModalTitle": {
"message": "Your custodian session has expired"
},
"custom": {
"message": "Advanced"
},

View File

@ -0,0 +1 @@
export { default } from './interactive-replacement-token-modal';

View File

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

View File

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

View File

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

View File

@ -42,26 +42,28 @@ exports[`Confirm Add Institutional Feature opens confirm institutional sucessful
</p>
</div>
<div
class="box page-container__footer box--flex-direction-row"
<footer
class="box page-container__footer box--padding-4 box--flex-direction-row"
>
<footer>
<div
class="box box--display-flex box--gap-4 box--flex-direction-row"
>
<button
class="button btn--rounded btn-default btn--large"
class="button btn--rounded btn-secondary"
role="button"
tabindex="0"
>
Cancel
</button>
<button
class="button btn--rounded btn-primary btn--large"
class="button btn--rounded btn-primary"
role="button"
tabindex="0"
>
Confirm
</button>
</footer>
</div>
</div>
</footer>
</div>
</div>
`;

View File

@ -7,12 +7,17 @@ import PulseLoader from '../../../components/ui/pulse-loader';
import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { Text } from '../../../components/component-library';
import {
Text,
BUTTON_SIZES,
BUTTON_TYPES,
} from '../../../components/component-library';
import {
TextColor,
TextVariant,
OVERFLOW_WRAP,
TEXT_ALIGN,
DISPLAY,
} from '../../../helpers/constants/design-system';
import Box from '../../../components/ui/box';
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
@ -169,16 +174,15 @@ export default function ConfirmAddInstitutionalFeature({ history }) {
</Text>
)}
<Box className="page-container__footer">
<Box as="footer" className="page-container__footer" padding={4}>
{isLoading ? (
<footer>
<PulseLoader />
</footer>
<PulseLoader />
) : (
<footer>
<Box display={DISPLAY.FLEX} gap={4}>
<Button
type="default"
large
block
type={BUTTON_TYPES.SECONDARY}
size={BUTTON_SIZES.LG}
onClick={() => {
removeConnectInstitutionalFeature({
actions: 'Institutional feature RPC cancel',
@ -191,12 +195,13 @@ export default function ConfirmAddInstitutionalFeature({ history }) {
</Button>
<Button
type="primary"
large
block
size={BUTTON_SIZES.LG}
onClick={confirmAddInstitutionalFeature}
>
{t('confirm')}
</Button>
</footer>
</Box>
)}
</Box>
</Box>