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:
parent
3eefe874a8
commit
8fdbd07c91
15
app/_locales/en/messages.json
generated
15
app/_locales/en/messages.json
generated
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
</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>
|
||||
`;
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user