diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index fb45872eb..cd00fe2c9 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -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 project’s 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"
},
diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts
index f448f07bc..6f045a369 100644
--- a/ui/helpers/constants/routes.ts
+++ b/ui/helpers/constants/routes.ts
@@ -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,
diff --git a/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap
new file mode 100644
index 000000000..2d68b5531
--- /dev/null
+++ b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Confirm Add Institutional Feature opens confirm institutional sucessfully 1`] = `
+
+
+
+
+
+ Project Name
+
+
+ projectName
+
+
+ Id
+ :
+ projectId
+
+
+
+
+
+
+`;
diff --git a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js
new file mode 100644
index 000000000..25a52fc16
--- /dev/null
+++ b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js
@@ -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 (
+
+
+
+ {t('institutionalFeatures')}
+
+
+ {t('mmiAuthenticate', [connectRequest.origin, serviceLabel.value])}
+
+
+
+
+ {t('projectName')}
+
+
+ {connectRequest?.token?.projectName}
+
+
+ {t('id')}: {connectRequest?.token?.projectId}
+
+
+ {connectError && (
+
+ {connectError}
+
+ )}
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+ConfirmAddInstitutionalFeature.propTypes = {
+ history: PropTypes.object,
+};
diff --git a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.stories.js b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.stories.js
new file mode 100644
index 000000000..07299393f
--- /dev/null
+++ b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.stories.js
@@ -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) => {story()}],
+ component: ConfirmAddInstitutionalFeature,
+ args: {
+ history: {
+ push: () => {
+ /**/
+ },
+ },
+ },
+};
+
+export const DefaultStory = (args) => (
+
+);
+
+DefaultStory.storyName = 'ConfirmAddInstitutionalFeature';
diff --git a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.test.js b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.test.js
new file mode 100644
index 000000000..141e27156
--- /dev/null
+++ b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.test.js
@@ -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(
+ ,
+ 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();
+ });
+});
diff --git a/ui/pages/institutional/confirm-add-institutional-feature/index.js b/ui/pages/institutional/confirm-add-institutional-feature/index.js
new file mode 100644
index 000000000..062300085
--- /dev/null
+++ b/ui/pages/institutional/confirm-add-institutional-feature/index.js
@@ -0,0 +1,3 @@
+import ConfirmAddInstitutionalFeature from './confirm-add-institutional-feature';
+
+export default ConfirmAddInstitutionalFeature;