From 8fbd1e30e88abcbcc1c0634b44b2a28c0877ca99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Wed, 29 Mar 2023 14:49:45 +0100 Subject: [PATCH] [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 --- app/_locales/en/messages.json | 36 ++++++ .../compliance-settings.test.js.snap | 98 ++++++++++++++++ .../compliance-settings.js | 98 ++++++++++++++++ .../compliance-settings.stories.js | 35 ++++++ .../compliance-settings.test.js | 67 +++++++++++ .../compliance-settings/index.js | 1 + .../compliance-settings/index.scss | 27 +++++ .../compliance-feature-page.js | 95 ++++++++++++++++ .../compliance-feature-page.stories.js | 35 ++++++ .../compliance-feature-page.test.js | 107 ++++++++++++++++++ .../compliance-feature-page/index.js | 1 + .../compliance-feature-page/index.scss | 29 +++++ 12 files changed, 629 insertions(+) create mode 100644 ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap create mode 100644 ui/components/institutional/compliance-settings/compliance-settings.js create mode 100644 ui/components/institutional/compliance-settings/compliance-settings.stories.js create mode 100644 ui/components/institutional/compliance-settings/compliance-settings.test.js create mode 100644 ui/components/institutional/compliance-settings/index.js create mode 100644 ui/components/institutional/compliance-settings/index.scss create mode 100644 ui/pages/institutional/compliance-feature-page/compliance-feature-page.js create mode 100644 ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js create mode 100644 ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js create mode 100644 ui/pages/institutional/compliance-feature-page/index.js create mode 100644 ui/pages/institutional/compliance-feature-page/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index cf2ebbab5..b194112a5 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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." diff --git a/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap b/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap new file mode 100644 index 000000000..bddb1ef05 --- /dev/null +++ b/ui/components/institutional/compliance-settings/__snapshots__/compliance-settings.test.js.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Compliance Settings shows disconnect when Compliance is activated 1`] = ` +
+
+
+ Change your settings or view reports by opening up Codefi Compliance or disconnect below. +
+
+ + +
+
+
+`; + +exports[`Compliance Settings shows start btn when Compliance its not activated 1`] = ` +
+
+
+

+ DeFi raises AML/CFT risk for institutions, given the decentralised pools and pseudonymous counterparties. +

+

+ 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. +

+

+ Steps to enable AML/CFT Compliance: +

+
    +
  1. + Sign up to Codefi Compliance below +
  2. +
  3. + Create an organisation +
  4. +
  5. + Create a project +
  6. +
  7. + Set your compliance settings +
  8. +
  9. + Click the "Enable Compliance in MMI" button +
  10. +
+
+
+
+ +
+
+
+
+`; diff --git a/ui/components/institutional/compliance-settings/compliance-settings.js b/ui/components/institutional/compliance-settings/compliance-settings.js new file mode 100644 index 000000000..aca08e198 --- /dev/null +++ b/ui/components/institutional/compliance-settings/compliance-settings.js @@ -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 ( + + ); + }; + + const renderLinkButton = () => { + return ( + + ); + }; + + return complianceActivated ? ( + + + {t('complianceSettingsExplanation')} + +
+ {renderDisconnect()} + {renderLinkButton()} +
+
+ ) : ( + + + {t('complianceBlurb0')} + {t('complianceBlurb1')} + {t('complianceBlurpStep0')} +
    +
  1. {t('complianceBlurbStep1')}
  2. +
  3. {t('complianceBlurbStep2')}
  4. +
  5. {t('complianceBlurbStep3')}
  6. +
  7. {t('complianceBlurbStep4')}
  8. +
  9. {t('complianceBlurbStep5')}
  10. +
+
+ +
+ {renderLinkButton()} +
+
+
+ ); +}; + +export default ComplianceSettings; diff --git a/ui/components/institutional/compliance-settings/compliance-settings.stories.js b/ui/components/institutional/compliance-settings/compliance-settings.stories.js new file mode 100644 index 000000000..c3f73a8a3 --- /dev/null +++ b/ui/components/institutional/compliance-settings/compliance-settings.stories.js @@ -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) => {story()}], + component: ComplianceSettings, + args: { + onClick: () => { + action('onClick'); + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'ComplianceSettings'; diff --git a/ui/components/institutional/compliance-settings/compliance-settings.test.js b/ui/components/institutional/compliance-settings/compliance-settings.test.js new file mode 100644 index 000000000..b6d3e95b1 --- /dev/null +++ b/ui/components/institutional/compliance-settings/compliance-settings.test.js @@ -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( + , + 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( + , + store, + ); + + expect(getByTestId('disconnect-compliance')).toBeVisible(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/components/institutional/compliance-settings/index.js b/ui/components/institutional/compliance-settings/index.js new file mode 100644 index 000000000..5105b8bea --- /dev/null +++ b/ui/components/institutional/compliance-settings/index.js @@ -0,0 +1 @@ +export { default } from './compliance-settings'; diff --git a/ui/components/institutional/compliance-settings/index.scss b/ui/components/institutional/compliance-settings/index.scss new file mode 100644 index 000000000..df59c0c06 --- /dev/null +++ b/ui/components/institutional/compliance-settings/index.scss @@ -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; + } + } +} diff --git a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js new file mode 100644 index 000000000..0b050ad4c --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.js @@ -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 ( + + <> + + history.push(DEFAULT_ROUTE)} + display={[DISPLAY.FLEX]} + /> + + + Codefi Compliance + {t('codefiCompliance')} + {complianceActivated && ( + + {t('activated')} + + )} + + + + + + + ); +}; + +export default ComplianceFeaturePage; diff --git a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js new file mode 100644 index 000000000..a70991369 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.stories.js @@ -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) => {story()}], + component: ComplianceFeaturePage, + args: { + onClick: () => { + action('onClick'); + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'ComplianceFeaturePage'; diff --git a/ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js new file mode 100644 index 000000000..9b75b9107 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/compliance-feature-page.test.js @@ -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( + , + 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( + , + 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( + , + 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( + , + store, + ); + + const disconnectBtn = getByTestId('disconnect-compliance'); + fireEvent.click(disconnectBtn); + expect(mockedDeleteComplianceAuthData).toHaveBeenCalled(); + }); +}); diff --git a/ui/pages/institutional/compliance-feature-page/index.js b/ui/pages/institutional/compliance-feature-page/index.js new file mode 100644 index 000000000..e105bee40 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/index.js @@ -0,0 +1 @@ +export { default } from './compliance-feature-page'; diff --git a/ui/pages/institutional/compliance-feature-page/index.scss b/ui/pages/institutional/compliance-feature-page/index.scss new file mode 100644 index 000000000..61166d988 --- /dev/null +++ b/ui/pages/institutional/compliance-feature-page/index.scss @@ -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; + } + } + } +}