diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index e02d7e061..841ddef6f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2402,6 +2402,9 @@ "noNFTs": { "message": "No NFTs yet" }, + "noReport": { + "message": "No Report" + }, "noSnaps": { "message": "You don't have any snaps installed." }, @@ -3245,6 +3248,12 @@ "replace": { "message": "replace" }, + "reportLastRun": { + "message": "Report last run" + }, + "reportLastRunTooltip": { + "message": "The date and time of when the last AML/CFT report was run" + }, "requestFailed": { "message": "Request failed" }, @@ -3374,9 +3383,18 @@ "revokeSpendingCapTooltipText": { "message": "This third party will be unable to spend any more of your current or future tokens." }, + "riskRating": { + "message": "Risk rating" + }, + "riskRatingTooltip": { + "message": "The risk rating of the address you are interacting with based on your risk settings" + }, "rpcUrl": { "message": "New RPC URL" }, + "runReport": { + "message": "Run report" + }, "safeTransferFrom": { "message": "Safe transfer from" }, @@ -3591,6 +3609,9 @@ "showPrivateKeys": { "message": "Show Private Keys" }, + "showReport": { + "message": "Show report" + }, "showTestnetNetworks": { "message": "Show test networks" }, diff --git a/ui/components/institutional/compliance-details/__snapshots__/compliance-details.test.js.snap b/ui/components/institutional/compliance-details/__snapshots__/compliance-details.test.js.snap new file mode 100644 index 000000000..2cd5c29ec --- /dev/null +++ b/ui/components/institutional/compliance-details/__snapshots__/compliance-details.test.js.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ComplianceDetails should render correctly 1`] = ` +
+
+
+

+ Address +

+

+ 0xAddress +

+
+
+
+

+ Risk rating +

+
+
+
+ + + +
+
+
+
+
+

+ low +

+
+
+
+
+

+ Report last run +

+
+
+
+ + + +
+
+
+
+

+

+
+ +
+
+
+`; diff --git a/ui/components/institutional/compliance-details/compliance-details.js b/ui/components/institutional/compliance-details/compliance-details.js new file mode 100644 index 000000000..c195dbd3c --- /dev/null +++ b/ui/components/institutional/compliance-details/compliance-details.js @@ -0,0 +1,159 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { I18nContext } from '../../../contexts/i18n'; +import InfoTooltip from '../../ui/info-tooltip'; +import SwapsFooter from '../../../pages/swaps/swaps-footer'; +import { + fetchHistoricalReports, + getComplianceHistoricalReportsByAddress, + getComplianceTenantSubdomain, +} from '../../../ducks/institutional/institutional'; +import { formatDate } from '../../../helpers/utils/util'; +import Box from '../../ui/box'; +import { Text } from '../../component-library'; +import { + TextColor, + TextVariant, + JustifyContent, + AlignItems, + BLOCK_SIZES, + DISPLAY, + FLEX_DIRECTION, +} from '../../../helpers/constants/design-system'; + +const ComplianceDetails = ({ address, onClose, onGenerate }) => { + const t = useContext(I18nContext); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchHistoricalReports(address)); + }, [address, dispatch]); + + const [lastReport, setLastReport] = useState(null); + const historicalReports = useSelector( + getComplianceHistoricalReportsByAddress(address), + ); + + useEffect(() => { + if (historicalReports && historicalReports.length) { + setLastReport( + historicalReports.reduce((prev, cur) => + prev.createTime > cur.createTime ? prev : cur, + ), + ); + } + }, [historicalReports]); + + const complianceTenantSubdomain = useSelector(getComplianceTenantSubdomain); + + return ( + + + {t('address')} + {address} + + + + {t('riskRating')} + {t('riskRatingTooltip')}} + /> + + + {lastReport ? lastReport.risk : t('noReport')} + + + + + {t('reportLastRun')} + {t('reportLastRunTooltip')}} + /> + + + {lastReport + ? formatDate(new Date(lastReport.createTime).getTime()) + : 'N/A'} + + + + { + onGenerate(address); + onClose(); + }} + submitText={t('runReport')} + onCancel={() => + global.platform.openTab({ + url: `https://${complianceTenantSubdomain}.compliance.codefi.network/app/kyt/addresses/${lastReport.address}/${lastReport.reportId}`, + }) + } + cancelText={t('showReport')} + hideCancel={!lastReport} + approveActive={lastReport} + showTopBorder + /> + + + ); +}; + +ComplianceDetails.propTypes = { + address: PropTypes.string, + onClose: PropTypes.func, + onGenerate: PropTypes.func, +}; + +export default ComplianceDetails; diff --git a/ui/components/institutional/compliance-details/compliance-details.stories.js b/ui/components/institutional/compliance-details/compliance-details.stories.js new file mode 100644 index 000000000..b4ead4450 --- /dev/null +++ b/ui/components/institutional/compliance-details/compliance-details.stories.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import ComplianceDetails from '.'; + +const customData = { + ...testData, + metamask: { + institutionalFeatures: { + complianceProjectId: '', + complianceClientId: '', + reportsInProgress: {}, + historicalReports: { + '0xAddress': [ + { + reportId: 'reportId', + address: '0xAddress', + risk: 'low', + creatTime: new Date(), + }, + ], + }, + }, + }, +}; + +const store = configureStore(customData); + +export default { + title: 'Components/Institutional/ComplianceDetails', + decorators: [(story) => {story()}], + component: ComplianceDetails, + args: { + address: '0xAddress', + onClose: () => undefined, + onGenerate: () => undefined, + }, + argTypes: { + onClick: { + action: 'onClick', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'ComplianceDetails'; diff --git a/ui/components/institutional/compliance-details/compliance-details.test.js b/ui/components/institutional/compliance-details/compliance-details.test.js new file mode 100644 index 000000000..670d8a1b4 --- /dev/null +++ b/ui/components/institutional/compliance-details/compliance-details.test.js @@ -0,0 +1,67 @@ +import React from 'react'; +import configureStore from 'redux-mock-store'; +import { fireEvent, screen } from '@testing-library/react'; +import thunk from 'redux-thunk'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import ComplianceDetails from './compliance-details'; + +const initState = { + metamask: { + institutionalFeatures: { + complianceProjectId: '', + complianceClientId: '', + reportsInProgress: {}, + historicalReports: { + '0xAddress': [ + { + reportId: 'reportId', + address: '0xAddress', + risk: 'low', + creatTime: new Date(), + }, + ], + }, + }, + }, +}; +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +describe('ComplianceDetails', () => { + const props = { + address: '0xAddress', + onClose: jest.fn(), + onGenerate: jest.fn(), + }; + + const store = mockStore(initState); + + it('should render correctly', () => { + const { container } = renderWithProvider( + , + store, + ); + + expect(container).toMatchSnapshot(); + }); + + it('runs onGenerate fuction', () => { + renderWithProvider( + , + store, + ); + + fireEvent.click(screen.queryByTestId('page-container-footer-next')); + + expect(props.onGenerate).toHaveBeenCalledTimes(1); + expect(props.onGenerate).toHaveBeenCalledWith(props.address); + }); +}); diff --git a/ui/components/institutional/compliance-details/index.js b/ui/components/institutional/compliance-details/index.js new file mode 100644 index 000000000..4cccdb1cd --- /dev/null +++ b/ui/components/institutional/compliance-details/index.js @@ -0,0 +1 @@ +export { default } from './compliance-details'; diff --git a/ui/components/institutional/compliance-details/index.scss b/ui/components/institutional/compliance-details/index.scss new file mode 100644 index 000000000..d342c36ff --- /dev/null +++ b/ui/components/institutional/compliance-details/index.scss @@ -0,0 +1,5 @@ +.compliance-details { + &__row { + border-top: 1px solid var(--color-border-muted); + } +} diff --git a/ui/components/institutional/institutional-components.scss b/ui/components/institutional/institutional-components.scss new file mode 100644 index 000000000..8ae612682 --- /dev/null +++ b/ui/components/institutional/institutional-components.scss @@ -0,0 +1,11 @@ +/** +* Please import your styles in order of atomicity. +* The most atomic styles should be imported first. +* This will help improve specificity and reduce the chance of +* unintended overrides. +**/ +@import 'compliance-details/index'; +@import 'compliance-settings/index'; +@import 'jwt-dropdown/jwt-dropdown'; +@import 'jwt-url-form/jwt-url-form'; +@import 'note-to-trader/index'; diff --git a/ui/css/index.scss b/ui/css/index.scss index a26c7ae09..c97d9573a 100644 --- a/ui/css/index.scss +++ b/ui/css/index.scss @@ -11,6 +11,9 @@ @import '../components/component-library/component-library-components.scss'; @import '../components/app/app-components'; @import '../components/ui/ui-components'; +///: BEGIN:ONLY_INCLUDE_IN(mmi) +@import '../components/institutional/institutional-components'; +///: END:ONLY_INCLUDE_IN @import '../components/multichain/multichain-components.scss'; @import '../pages/pages'; @import './errors.scss';