1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 09:23:21 +01:00

[MMI] compliance feature page (#18320)

* MMI-2657 adds the compliance settings screen

* MMI-2657 adds stories file

* wip compliance feature page

* adds stories and fixes imports

* adds storybook and tests

* lint fix
This commit is contained in:
António Regadas 2023-03-29 14:49:45 +01:00 committed by GitHub
parent 12bd9b0b94
commit 8fbd1e30e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 629 additions and 0 deletions

View File

@ -150,6 +150,9 @@
"accountSelectionRequired": {
"message": "You need to select an account!"
},
"activated": {
"message": "Active"
},
"active": {
"message": "Active"
},
@ -639,9 +642,39 @@
"close": {
"message": "Close"
},
"codefiCompliance": {
"message": "Codefi Compliance"
},
"coingecko": {
"message": "CoinGecko"
},
"complianceBlurb0": {
"message": "DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties."
},
"complianceBlurb1": {
"message": "Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting."
},
"complianceBlurbStep1": {
"message": "Sign up to Codefi Compliance below"
},
"complianceBlurbStep2": {
"message": "Create an organisation"
},
"complianceBlurbStep3": {
"message": "Create a project"
},
"complianceBlurbStep4": {
"message": "Set your compliance settings"
},
"complianceBlurbStep5": {
"message": "Click the \"Enable Compliance in MMI\" button"
},
"complianceBlurpStep0": {
"message": "Steps to enable AML/CFT Compliance:"
},
"complianceSettingsExplanation": {
"message": "Change your settings or view reports by opening up Codefi Compliance or disconnect below."
},
"confirm": {
"message": "Confirm"
},
@ -2676,6 +2709,9 @@
"onlyConnectTrust": {
"message": "Only connect with sites you trust."
},
"openCodefiCompliance": {
"message": "Open Codefi Compliance"
},
"openFullScreenForLedgerWebHid": {
"message": "Open MetaMask in full screen to connect your ledger via WebHID.",
"description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid."

View File

@ -0,0 +1,98 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Compliance Settings shows disconnect when Compliance is activated 1`] = `
<div>
<div
class="box box--flex-direction-row"
>
<div
class="box institutional-feature__content box--flex-direction-row"
>
Change your settings or view reports by opening up Codefi Compliance or disconnect below.
</div>
<footer
class="institutional-feature__footer"
>
<button
class="button btn--rounded btn-default btn--large"
data-testid="disconnect-compliance"
role="button"
tabindex="0"
>
Disconnect
</button>
<button
class="button btn--rounded btn-primary"
data-testid="start-compliance"
role="button"
tabindex="0"
>
Open Codefi Compliance
</button>
</footer>
</div>
</div>
`;
exports[`Compliance Settings shows start btn when Compliance its not activated 1`] = `
<div>
<div
class="box box--sm:padding-6 box--flex-direction-row box--color-text-alternative"
data-testid="institutional-content"
>
<div
class="box institutional-feature__content box--flex-direction-row"
>
<p
class="box mm-text mm-text--body-md mm-text--color-text-default box--padding-bottom-3 box--flex-direction-row"
>
DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties.
</p>
<p
class="box mm-text mm-text--body-md mm-text--color-text-default box--padding-bottom-3 box--flex-direction-row"
>
Codefi Compliance is the only product capable of running AML/CFT analysis on DeFi pools. This allows you to identify and avoid pools and counterparties that fail your risk setting.
</p>
<p
class="box mm-text mm-text--body-md mm-text--color-text-default box--padding-bottom-3 box--flex-direction-row"
>
Steps to enable AML/CFT Compliance:
</p>
<ol>
<li>
Sign up to Codefi Compliance below
</li>
<li>
Create an organisation
</li>
<li>
Create a project
</li>
<li>
Set your compliance settings
</li>
<li>
Click the "Enable Compliance in MMI" button
</li>
</ol>
</div>
<div
class="box box--display-flex box--flex-direction-row box--justify-content-center"
>
<footer
class="institutional-feature__footer"
padding="4,6"
>
<button
class="button btn--rounded btn-primary"
data-testid="start-compliance"
role="button"
tabindex="0"
>
Open Codefi Compliance
</button>
</footer>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,98 @@
import React, { useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
JustifyContent,
DISPLAY,
TextColor,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
import { I18nContext } from '../../../contexts/i18n';
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
import { Text } from '../../component-library';
import Box from '../../ui/box';
import Button from '../../ui/button';
const ComplianceSettings = () => {
const t = useContext(I18nContext);
const dispatch = useDispatch();
const mmiActions = mmiActionsFactory();
const complianceActivated = useSelector((state) =>
Boolean(state.metamask.institutionalFeatures?.complianceProjectId),
);
const disconnectFromCompliance = async () => {
await dispatch(mmiActions.deleteComplianceAuthData());
};
const renderDisconnect = () => {
return (
<Button
type="default"
large
onClick={disconnectFromCompliance}
data-testid="disconnect-compliance"
>
{t('disconnect')}
</Button>
);
};
const renderLinkButton = () => {
return (
<Button
type="primary"
data-testid="start-compliance"
onClick={() => {
global.platform.openTab({
url: 'https://start.compliance.codefi.network/',
});
}}
>
{t('openCodefiCompliance')}
</Button>
);
};
return complianceActivated ? (
<Box>
<Box className="institutional-feature__content">
{t('complianceSettingsExplanation')}
</Box>
<footer className="institutional-feature__footer">
{renderDisconnect()}
{renderLinkButton()}
</footer>
</Box>
) : (
<Box
padding={[0, 6]}
color={TextColor.textAlternative}
data-testid="institutional-content"
>
<Box className="institutional-feature__content">
<Text paddingBottom={3}>{t('complianceBlurb0')}</Text>
<Text paddingBottom={3}>{t('complianceBlurb1')}</Text>
<Text paddingBottom={3}>{t('complianceBlurpStep0')}</Text>
<ol>
<li>{t('complianceBlurbStep1')}</li>
<li>{t('complianceBlurbStep2')}</li>
<li>{t('complianceBlurbStep3')}</li>
<li>{t('complianceBlurbStep4')}</li>
<li>{t('complianceBlurbStep5')}</li>
</ol>
</Box>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JustifyContent.center}
>
<footer padding={[4, 6]} className="institutional-feature__footer">
{renderLinkButton()}
</footer>
</Box>
</Box>
);
};
export default ComplianceSettings;

View File

@ -0,0 +1,35 @@
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 ComplianceSettings from '.';
const customData = {
...testData,
metamask: {
...testData.metamask,
institutionalFeatures: {
complianceProjectId: '',
complianceClientId: '',
reportsInProgress: {},
},
},
};
const store = configureStore(customData);
export default {
title: 'Components/Institutional/ComplianceSettings',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
component: ComplianceSettings,
args: {
onClick: () => {
action('onClick');
},
},
};
export const DefaultStory = (args) => <ComplianceSettings {...args} />;
DefaultStory.storyName = 'ComplianceSettings';

View File

@ -0,0 +1,67 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import ComplianceSettings from '.';
const mockedDeleteComplianceAuthData = jest
.fn()
.mockReturnValue({ type: 'TYPE' });
jest.mock('../../../store/institutional/institution-background', () => ({
mmiActionsFactory: () => ({
deleteComplianceAuthData: mockedDeleteComplianceAuthData,
}),
}));
const mockStore = {
metamask: {
provider: {
type: 'test',
},
institutionalFeatures: {
complianceProjectId: '',
complianceClientId: '',
reportsInProgress: {},
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
},
};
describe('Compliance Settings', () => {
it('shows start btn when Compliance its not activated', () => {
const store = configureMockStore()(mockStore);
const { container, getByTestId } = renderWithProvider(
<ComplianceSettings />,
store,
);
expect(getByTestId('start-compliance')).toBeVisible();
expect(container).toMatchSnapshot();
});
it('shows disconnect when Compliance is activated', () => {
const customMockStore = {
...mockStore,
metamask: {
...mockStore.metamask,
institutionalFeatures: {
complianceProjectId: '123',
complianceClientId: '123',
reportsInProgress: {},
},
},
};
const store = configureMockStore()(customMockStore);
const { container, getByTestId } = renderWithProvider(
<ComplianceSettings />,
store,
);
expect(getByTestId('disconnect-compliance')).toBeVisible();
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1 @@
export { default } from './compliance-settings';

View File

@ -0,0 +1,27 @@
.institutional-feature {
&__footer {
border-top: 1px solid var(--color-text-muted);
padding: 16px 30px;
flex: 0 0 auto;
button {
min-width: 0;
margin-right: 16px;
&:last-of-type {
margin-right: 0;
}
}
}
&__content {
@include H6;
line-height: 22px;
ol {
list-style: decimal;
list-style-position: inside;
}
}
}

View File

@ -0,0 +1,95 @@
import React, { useContext } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { I18nContext } from '../../../contexts/i18n';
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import {
JustifyContent,
DISPLAY,
AlignItems,
TextColor,
TEXT_ALIGN,
BackgroundColor,
Color,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
import {
ButtonIcon,
ICON_NAMES,
ICON_SIZES,
Text,
} from '../../../components/component-library';
import Box from '../../../components/ui/box';
import ComplianceSettings from '../../../components/institutional/compliance-settings';
const ComplianceFeaturePage = () => {
const t = useContext(I18nContext);
const history = useHistory();
const complianceActivated = useSelector((state) =>
Boolean(state.metamask.institutionalFeatures?.complianceProjectId),
);
return (
<Box
className="institutional-entity"
backgroundColor={BackgroundColor.backgroundDefault}
>
<>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
padding={[0, 6, 6]}
className="feature-connect__header"
>
<ButtonIcon
ariaLabel={t('back')}
iconName={ICON_NAMES.ARROW_LEFT}
size={ICON_SIZES.SM}
className="settings-page__back-button"
color={Color.iconDefault}
onClick={() => history.push(DEFAULT_ROUTE)}
display={[DISPLAY.FLEX]}
/>
<Text
as="h4"
marginTop={4}
marginBottom={4}
className="feature-connect__header__title"
>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
color={TextColor.textDefault}
>
<img
className="feature-connect__list__list-item__img"
src="images/compliance-logo-small.svg"
alt="Codefi Compliance"
/>
{t('codefiCompliance')}
{complianceActivated && (
<Text
as="h6"
margin={[2, 2, 0, 2]}
color={TextColor.textMuted}
display={DISPLAY.FLEX}
textAlign={TEXT_ALIGN.LEFT}
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JustifyContent.center}
className="feature-connect__label__text feature-connect__label__text--activated"
data-testid="activated-label"
>
{t('activated')}
</Text>
)}
</Box>
</Text>
</Box>
<ComplianceSettings />
</>
</Box>
);
};
export default ComplianceFeaturePage;

View File

@ -0,0 +1,35 @@
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 ComplianceFeaturePage from '.';
const customData = {
...testData,
metamask: {
...testData.metamask,
institutionalFeatures: {
complianceProjectId: '',
complianceClientId: '',
reportsInProgress: {},
},
},
};
const store = configureStore(customData);
export default {
title: 'Pages/Institutional/ComplianceFeaturePage',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
component: ComplianceFeaturePage,
args: {
onClick: () => {
action('onClick');
},
},
};
export const DefaultStory = (args) => <ComplianceFeaturePage {...args} />;
DefaultStory.storyName = 'ComplianceFeaturePage';

View File

@ -0,0 +1,107 @@
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 ComplianceFeaturePage from '.';
const mockedDeleteComplianceAuthData = jest
.fn()
.mockReturnValue({ type: 'TYPE' });
jest.mock('../../../store/institutional/institution-background', () => ({
mmiActionsFactory: () => ({
deleteComplianceAuthData: mockedDeleteComplianceAuthData,
}),
}));
describe('Compliance Feature, connect', function () {
const mockStore = {
metamask: {
provider: {
type: 'test',
},
institutionalFeatures: {
complianceProjectId: '',
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
},
};
it('shows compliance feature button as activated', () => {
const customMockStore = {
...mockStore,
metamask: {
...mockStore.metamask,
institutionalFeatures: {
complianceProjectId: '123',
complianceClientId: '123',
reportsInProgress: {},
},
},
};
const store = configureMockStore()(customMockStore);
const { getByText, getByTestId } = renderWithProvider(
<ComplianceFeaturePage />,
store,
);
expect(getByTestId('activated-label')).toBeVisible();
expect(getByText('Active')).toBeInTheDocument();
});
it('shows ComplianceSettings when feature is not activated', () => {
const store = configureMockStore()(mockStore);
const { getByTestId } = renderWithProvider(
<ComplianceFeaturePage />,
store,
);
expect(getByTestId('institutional-content')).toBeVisible();
});
it('opens new tab on Open Codefi Compliance click', async () => {
global.platform = { openTab: sinon.spy() };
const store = configureMockStore()(mockStore);
const { queryByTestId } = renderWithProvider(
<ComplianceFeaturePage />,
store,
);
const startBtn = queryByTestId('start-compliance');
fireEvent.click(startBtn);
await waitFor(() => {
expect(global.platform.openTab.calledOnce).toStrictEqual(true);
});
});
it('calls deleteComplianceAuthData on disconnect click', async () => {
const customMockStore = {
...mockStore,
metamask: {
...mockStore.metamask,
institutionalFeatures: {
complianceProjectId: '123',
complianceClientId: '123',
reportsInProgress: {},
},
},
};
const store = configureMockStore()(customMockStore);
const { getByTestId } = renderWithProvider(
<ComplianceFeaturePage />,
store,
);
const disconnectBtn = getByTestId('disconnect-compliance');
fireEvent.click(disconnectBtn);
expect(mockedDeleteComplianceAuthData).toHaveBeenCalled();
});
});

View File

@ -0,0 +1 @@
export { default } from './compliance-feature-page';

View File

@ -0,0 +1,29 @@
.institutional-entity {
box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08);
}
.feature-connect {
&__list {
&__list-item {
&__img {
height: 32px;
width: 32px;
margin: 0 10px 0 0;
}
}
}
&__label {
&__text {
font-size: 0.6rem;
&--activated {
color: var(--color-text-default);
background: var(--color-primary-default);
padding: 3px 10px;
border-radius: 10px;
margin-top: 0;
}
}
}
}