1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-28 23:06:37 +01:00

[MMI] Confirm-add-institutional-feature page (#18321)

* Added confirm add institutional feature page

* Finished implementing component

* Added all institutional ducks

* Fixed tests

* Removed ducks and console log

* Fixed snapshots

* Fixed messages json

* Changed method name and using useEffect

* Replace useEffect hook with a simple if statement to check if connectRequest exists and add null return statement to avoid warnings

* Remove unneeded dependency

* Added back useEffect and added a extra check to return null if connectRequest is false

* Fixed eslint problem

* Fixed all issues commented in the pr
This commit is contained in:
Albert Olivé 2023-04-05 10:50:30 +02:00 committed by GitHub
parent b9c5120332
commit c52d2131d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 490 additions and 0 deletions

View File

@ -654,6 +654,12 @@
"coingecko": {
"message": "CoinGecko"
},
"complianceActivatedDesc": {
"message": "You can now use compliance in MetaMask Institutional. Receiving AML/CFT analysis within the confirmation screen on all the addresses you interact with."
},
"complianceActivatedTitle": {
"message": "Your compliance feature is activated"
},
"complianceBlurb0": {
"message": "DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties."
},
@ -780,6 +786,9 @@
"connectingToSepolia": {
"message": "Connecting to Sepolia test network"
},
"connectionError": {
"message": "Connection error"
},
"contactUs": {
"message": "Contact us"
},
@ -1707,6 +1716,9 @@
"holdToRevealTitle": {
"message": "Keep your SRP safe"
},
"id": {
"message": "Id"
},
"ignoreAll": {
"message": "Ignore all"
},
@ -1812,6 +1824,9 @@
"install": {
"message": "Install"
},
"institutionalFeatures": {
"message": "Institutional Features"
},
"insufficientBalance": {
"message": "Insufficient balance."
},
@ -2141,6 +2156,9 @@
"mmiAddToken": {
"message": "The page at $1 would like to authorise the following custodian token in MetaMask Institutional"
},
"mmiAuthenticate": {
"message": "The page at $1 would like to authorise the following projects compliance settings in MetaMask Institutional"
},
"mobileSyncWarning": {
"message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet's secret phrase to then import your wallet into mobile."
},
@ -3065,6 +3083,12 @@
"proceedWithTransaction": {
"message": "I want to proceed anyway"
},
"projectIdInvalid": {
"message": "Provided Project ID is invalid"
},
"projectName": {
"message": "Project Name"
},
"proposedApprovalLimit": {
"message": "Proposed approval limit"
},

View File

@ -32,6 +32,9 @@ 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(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';
@ -124,6 +127,9 @@ const PATH_NAME_MAP = {
[NEW_ACCOUNT_ROUTE]: 'New Account Page',
[IMPORT_ACCOUNT_ROUTE]: 'Import Account Page',
[CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page',
///: BEGIN:ONLY_INCLUDE_IN(mmi)
[INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional features done',
///: END:ONLY_INCLUDE_IN
[SEND_ROUTE]: 'Send Page',
[`${TOKEN_DETAILS}/:address`]: 'Token Details Page',
[`${CONNECT_ROUTE}/:id`]: 'Connect To Site Confirmation Page',
@ -180,6 +186,9 @@ export {
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
CONNECT_HARDWARE_ROUTE,
///: BEGIN:ONLY_INCLUDE_IN(mmi)
INSTITUTIONAL_FEATURES_DONE_ROUTE,
///: END:ONLY_INCLUDE_IN
SEND_ROUTE,
TOKEN_DETAILS,
CONFIRM_TRANSACTION_ROUTE,

View File

@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Confirm Add Institutional Feature opens confirm institutional sucessfully 1`] = `
<div>
<div
class="box page-container box--flex-direction-row"
data-testid="confirm-add-institutional-feature"
>
<div
class="box page-container__header box--flex-direction-row"
>
<p
class="box mm-text page-container__title mm-text--body-md box--flex-direction-row box--color-text-default"
>
Institutional Features
</p>
<p
class="box mm-text page-container__subtitle mm-text--body-md box--flex-direction-row box--color-text-default"
>
The page at origin would like to authorise the following projects compliance settings in MetaMask Institutional
</p>
</div>
<div
class="box page-container__content box--flex-direction-row"
>
<p
class="box mm-text mm-text--body-sm box--margin-top-3 box--margin-right-8 box--margin-left-8 box--flex-direction-row box--color-text-default"
>
Project Name
</p>
<p
class="box mm-text mm-text--body-lg-medium mm-text--overflow-wrap-break-word box--margin-top-1 box--margin-right-8 box--margin-bottom-1 box--margin-left-8 box--flex-direction-row box--color-text-default"
>
projectName
</p>
<p
class="box mm-text mm-text--body-xs mm-text--overflow-wrap-break-word box--margin-right-8 box--margin-left-8 box--flex-direction-row box--color-text-muted"
>
Id
:
projectId
</p>
</div>
<div
class="box page-container__footer box--flex-direction-row"
>
<footer>
<button
class="button btn--rounded btn-default btn--large"
role="button"
tabindex="0"
>
Cancel
</button>
<button
class="button btn--rounded btn-primary btn--large"
role="button"
tabindex="0"
>
Confirm
</button>
</footer>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,208 @@
import React, { useState, useContext, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Button from '../../../components/ui/button';
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 {
TextColor,
TextVariant,
OVERFLOW_WRAP,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import Box from '../../../components/ui/box';
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
export default function ConfirmAddInstitutionalFeature({ history }) {
const t = useI18nContext();
const dispatch = useDispatch();
const mmiActions = mmiActionsFactory();
const [isLoading, setIsLoading] = useState(false);
const [connectError, setConnectError] = useState('');
const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
const connectRequests = useSelector(
(state) => state.metamask.institutionalFeatures?.connectRequests,
);
const trackEvent = useContext(MetaMetricsContext);
const connectRequest = connectRequests[0];
useEffect(() => {
if (!connectRequest) {
history.push(mostRecentOverviewPage);
}
}, [connectRequest, history, mostRecentOverviewPage]);
if (!connectRequest) {
return null;
}
const serviceLabel = connectRequest.labels.find(
(label) => label.key === 'service',
);
const sendEvent = ({ actions, service }) => {
trackEvent({
category: 'MMI',
event: 'Institutional feature connection',
properties: {
actions,
service,
},
});
};
const handleConnectError = ({ message }) => {
let error = message;
if (message.startsWith('401')) {
error = t('projectIdInvalid');
}
if (!error) {
error = t('connectionError');
}
setIsLoading(false);
setConnectError(error);
sendEvent({ actions: 'Institutional feature RPC error' });
};
const removeConnectInstitutionalFeature = ({ actions, service, push }) => {
dispatch(
mmiActions.removeConnectInstitutionalFeature({
origin: connectRequest.origin,
projectId: connectRequest.token.projectId,
}),
);
sendEvent({ actions, service });
history.push(push);
};
const confirmAddInstitutionalFeature = async () => {
setIsLoading(true);
setConnectError('');
try {
await dispatch(
mmiActions.setComplianceAuthData({
clientId: connectRequest.token.clientId,
projectId: connectRequest.token.projectId,
}),
);
removeConnectInstitutionalFeature({
actions: 'Institutional feature RPC confirm',
service: serviceLabel.value,
push: {
pathname: INSTITUTIONAL_FEATURES_DONE_ROUTE,
state: {
imgSrc: 'images/compliance-logo.png',
title: t('complianceActivatedTitle'),
description: t('complianceActivatedDesc'),
},
},
});
} catch (e) {
handleConnectError(e);
}
};
sendEvent({
actions: 'Institutional feature RPC request',
service: serviceLabel.value,
});
return (
<Box
className="page-container"
data-testid="confirm-add-institutional-feature"
>
<Box className="page-container__header">
<Text className="page-container__title">
{t('institutionalFeatures')}
</Text>
<Text className="page-container__subtitle">
{t('mmiAuthenticate', [connectRequest.origin, serviceLabel.value])}
</Text>
</Box>
<Box className="page-container__content">
<Text
variant={TextVariant.bodySm}
marginTop={3}
marginRight={8}
marginBottom={0}
marginLeft={8}
>
{t('projectName')}
</Text>
<Text
variant={TextVariant.bodyLgMedium}
color={TextColor.textDefault}
marginTop={1}
marginRight={8}
marginBottom={1}
marginLeft={8}
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
>
{connectRequest?.token?.projectName}
</Text>
<Text
variant={TextVariant.bodyXs}
marginRight={8}
marginLeft={8}
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
color={TextColor.textMuted}
>
{t('id')}: {connectRequest?.token?.projectId}
</Text>
</Box>
{connectError && (
<Text
textAlign={TEXT_ALIGN.CENTER}
marginTop={4}
data-testid="connect-error-message"
>
{connectError}
</Text>
)}
<Box className="page-container__footer">
{isLoading ? (
<footer>
<PulseLoader />
</footer>
) : (
<footer>
<Button
type="default"
large
onClick={() => {
removeConnectInstitutionalFeature({
actions: 'Institutional feature RPC cancel',
service: serviceLabel.value,
push: mostRecentOverviewPage,
});
}}
>
{t('cancel')}
</Button>
<Button
type="primary"
large
onClick={confirmAddInstitutionalFeature}
>
{t('confirm')}
</Button>
</footer>
)}
</Box>
</Box>
);
}
ConfirmAddInstitutionalFeature.propTypes = {
history: PropTypes.object,
};

View File

@ -0,0 +1,57 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
import testData from '../../../../.storybook/test-data';
import ConfirmAddInstitutionalFeature from '.';
const customData = {
...testData,
metamask: {
provider: {
type: 'test',
},
institutionalFeatures: {
complianceProjectId: '',
connectRequests: [
{
labels: [
{
key: 'service',
value: 'test',
},
],
origin: 'origin',
token: {
projectName: 'projectName',
projectId: 'projectId',
clientId: 'clientId',
},
},
],
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
},
};
const store = configureStore(customData);
export default {
title: 'Pages/Institutional/ConfirmAddInstitutionalFeature',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
component: ConfirmAddInstitutionalFeature,
args: {
history: {
push: () => {
/**/
},
},
},
};
export const DefaultStory = (args) => (
<ConfirmAddInstitutionalFeature {...args} />
);
DefaultStory.storyName = 'ConfirmAddInstitutionalFeature';

View File

@ -0,0 +1,122 @@
import React from 'react';
import { fireEvent, screen } 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 ConfirmAddInstitutionalFeature from '.';
const mockRemoveConnectInstitutionalFeature = jest
.fn()
.mockReturnValue({ type: 'TYPE' });
let mockSetComplianceAuthData = jest.fn().mockReturnValue({ type: 'TYPE' });
jest.mock('../../../store/institutional/institution-background', () => ({
mmiActionsFactory: () => ({
setComplianceAuthData: mockSetComplianceAuthData,
removeConnectInstitutionalFeature: mockRemoveConnectInstitutionalFeature,
}),
}));
const connectRequests = [
{
labels: [
{
key: 'service',
value: 'test',
},
],
origin: 'origin',
token: {
projectName: 'projectName',
projectId: 'projectId',
clientId: 'clientId',
},
},
];
const props = {
history: {
push: jest.fn(),
},
};
const render = ({ newState } = {}) => {
const state = {
...mockState,
metamask: {
provider: {
type: 'test',
},
institutionalFeatures: {
complianceProjectId: '',
connectRequests,
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
...newState,
},
};
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const store = mockStore(state);
return renderWithProvider(
<ConfirmAddInstitutionalFeature {...props} />,
store,
);
};
describe('Confirm Add Institutional Feature', function () {
it('opens confirm institutional sucessfully', () => {
const { container } = render();
expect(container).toMatchSnapshot();
expect(
screen.getByText(`Id: ${connectRequests[0].token.projectId}`),
).toBeInTheDocument();
});
it('runs removeConnectInstitutionalFeature on cancel click', () => {
render();
fireEvent.click(screen.queryByText('Cancel'));
expect(mockRemoveConnectInstitutionalFeature).toHaveBeenCalledTimes(1);
expect(mockRemoveConnectInstitutionalFeature).toHaveBeenCalledWith({
origin: connectRequests[0].origin,
projectId: connectRequests[0].token.projectId,
});
expect(props.history.push).toHaveBeenCalledTimes(1);
});
it('runs setComplianceAuthData on confirm click', () => {
render();
fireEvent.click(screen.queryByText('Confirm'));
expect(mockSetComplianceAuthData).toHaveBeenCalledTimes(1);
expect(mockSetComplianceAuthData).toHaveBeenCalledWith({
clientId: connectRequests[0].token.clientId,
projectId: connectRequests[0].token.projectId,
});
});
it('handles error', () => {
mockSetComplianceAuthData = jest
.fn()
.mockReturnValue(new Error('Async error message'));
const { queryByTestId } = render();
fireEvent.click(screen.queryByText('Confirm'));
expect(queryByTestId('connect-error-message')).toBeInTheDocument();
});
it('does not render without connectRequest', () => {
const newState = {
institutionalFeatures: {
connectRequests: [],
},
};
const { queryByTestId } = render({ newState });
expect(
queryByTestId('confirm-add-institutional-feature'),
).not.toBeInTheDocument();
});
});

View File

@ -0,0 +1,3 @@
import ConfirmAddInstitutionalFeature from './confirm-add-institutional-feature';
export default ConfirmAddInstitutionalFeature;