-
-
-
- Only connect with sites you trust.
-
-
-
-
-
-
diff --git a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx
index e720bb4781fc..089d80fa225a 100644
--- a/ui/pages/permissions-connect/connect-page/connect-page.test.tsx
+++ b/ui/pages/permissions-connect/connect-page/connect-page.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { fireEvent } from '@testing-library/react';
import {
Caip25CaveatType,
Caip25EndowmentPermissionName,
@@ -15,6 +16,14 @@ import { ConnectPage, ConnectPageProps } from './connect-page';
const mockTestDappUrl = 'https://test.dapp';
+const mockTargetSubjectMetadata = {
+ extensionId: null,
+ iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
+ name: 'E2E Test Dapp',
+ origin: 'https://metamask.github.io',
+ subjectType: 'website',
+};
+
const render = (
options: {
props?: ConnectPageProps;
@@ -31,6 +40,7 @@ const render = (
rejectPermissionsRequest: jest.fn(),
approveConnection: jest.fn(),
activeTabOrigin: mockTestDappUrl,
+ targetSubjectMetadata: mockTargetSubjectMetadata,
},
state,
} = options;
@@ -62,13 +72,139 @@ describe('ConnectPage', () => {
expect(container).toMatchSnapshot();
});
+ it('should render image icon correctly', () => {
+ const { getAllByAltText } = render();
+
+ const images = getAllByAltText('github.io logo');
+ expect(images.length).toBe(2);
+ expect(images[0]).toHaveAttribute(
+ 'src',
+ 'https://metamask.github.io/test-dapp/metamask-fox.svg',
+ );
+ expect(images[1]).toHaveAttribute(
+ 'src',
+ 'https://metamask.github.io/test-dapp/metamask-fox.svg',
+ );
+ });
+
+ it('should render fallback icon correctly', () => {
+ const { container } = render({
+ props: {
+ request: {
+ id: '1',
+ origin: mockTestDappUrl,
+ },
+ permissionsRequestId: '1',
+ rejectPermissionsRequest: jest.fn(),
+ approveConnection: jest.fn(),
+ activeTabOrigin: mockTestDappUrl,
+ targetSubjectMetadata: {
+ ...mockTargetSubjectMetadata,
+ iconUrl: null,
+ },
+ },
+ });
+
+ const divElement = container.querySelector('div.mm-avatar-base--size-lg');
+ expect(divElement).toHaveTextContent('g');
+ });
+
+ it('should render fallback icon correctly for IP address as an origin', () => {
+ const { container } = render({
+ props: {
+ request: {
+ id: '1',
+ origin: 'http://127.0.0.1/test-dapp',
+ },
+ permissionsRequestId: '1',
+ rejectPermissionsRequest: jest.fn(),
+ approveConnection: jest.fn(),
+ activeTabOrigin: mockTestDappUrl,
+ targetSubjectMetadata: {
+ ...mockTargetSubjectMetadata,
+ iconUrl: null,
+ origin: 'http://127.0.0.1/test-dapp',
+ },
+ },
+ });
+
+ const divElement = container.querySelector('div.mm-avatar-base--size-lg');
+ expect(divElement).toHaveTextContent('?');
+ });
+
it('should render title correctly', () => {
const { getByText } = render();
- expect(getByText('Connect with MetaMask')).toBeDefined();
+ expect(getByText('github.io')).toBeDefined();
+ });
+
+ it('should render subtitle correctly', () => {
+ const { getByText } = render();
+ expect(getByText('Connect this website with MetaMask.')).toBeDefined();
+ });
+
+ it('should render learn more link correctly', () => {
+ const { getByText } = render();
+ expect(getByText('Learn more')).toBeDefined();
+ });
+
+ it('should render accounts tab correctly', () => {
+ const { getByText, queryAllByText } = render();
+
+ expect(getByText('Accounts')).toBeDefined();
+ expect(getByText('Test Account')).toBeDefined();
+ expect(getByText('0x0DCD5...3E7bc')).toBeDefined();
+
+ const valueElements = queryAllByText('966.988');
+ expect(valueElements[0]).toBeDefined();
+ expect(getByText('Edit accounts')).toBeDefined();
+ });
+
+ it('should render edit accounts modal', () => {
+ const { getByText, queryAllByText } = render();
+ const editAccountsButton = getByText('Edit accounts');
+ fireEvent.click(editAccountsButton);
+
+ expect(getByText('Update')).toBeDefined();
+ expect(getByText('Select all')).toBeDefined();
+ expect(getByText('New account')).toBeDefined();
+
+ const accountElements = queryAllByText('Test Account');
+
+ expect(accountElements.length).toBe(2);
+ expect(accountElements[0].textContent).toBe('Test Account');
+ expect(accountElements[1].textContent).toBe('Test Account');
+ });
+
+ it('should render empty accounts state correctly', () => {
+ const { getByText, queryAllByText, getByTestId } = render();
+ const editAccountsButton = getByText('Edit accounts');
+ fireEvent.click(editAccountsButton);
+
+ const accountElements = queryAllByText('Test Account');
+ fireEvent.click(accountElements[1]);
+
+ const disconnectButton = getByText('Disconnect');
+ fireEvent.click(disconnectButton);
+
+ expect(getByText('Select an account to connect')).toBeDefined();
+
+ const confirmButton = getByTestId('confirm-btn');
+ expect(confirmButton).toBeDisabled();
+
+ const selectAnAccountToConnectButton = getByText(
+ 'Select an account to connect',
+ );
+ fireEvent.click(selectAnAccountToConnectButton);
+
+ expect(getByText('Select all')).toBeDefined();
+ expect(getByText('New account')).toBeDefined();
});
it('should render account connectionListItem', () => {
const { getByText } = render();
+ const permissionsTab = getByText('Permissions');
+ fireEvent.click(permissionsTab);
+
expect(
getByText('See your accounts and suggest transactions'),
).toBeDefined();
@@ -76,6 +212,9 @@ describe('ConnectPage', () => {
it('should render network connectionListItem', () => {
const { getByText } = render();
+ const permissionsTab = getByText('Permissions');
+ fireEvent.click(permissionsTab);
+
expect(getByText('Use your enabled networks')).toBeDefined();
});
@@ -118,6 +257,7 @@ describe('ConnectPage', () => {
rejectPermissionsRequest: jest.fn(),
approveConnection: jest.fn(),
activeTabOrigin: mockTestDappUrl,
+ targetSubjectMetadata: mockTargetSubjectMetadata,
},
});
expect(container).toMatchSnapshot();
@@ -139,7 +279,7 @@ describe('ConnectPage', () => {
const confirmButton = getByText('Connect');
const cancelButton = getByText('Cancel');
// The currently selected account is a Bitcoin account, the "connecting account list" would be
- // empty by default and thus, we cannot confirm without explictly select an EVM account.
+ // empty by default and thus, we cannot confirm without explicitly select an EVM account.
expect(confirmButton).toBeDisabled();
expect(cancelButton).toBeDefined();
});
diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx
index d39a95871189..8449104a6be0 100644
--- a/ui/pages/permissions-connect/connect-page/connect-page.tsx
+++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from 'react';
+import React, { useContext, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { InternalAccount } from '@metamask/keyring-internal-api';
import { isEvmAccountType } from '@metamask/keyring-api';
@@ -13,8 +13,14 @@ import {
} from '../../../selectors';
import { getNetworkConfigurationsByChainId } from '../../../../shared/modules/selectors/networks';
import {
+ AvatarBase,
+ AvatarBaseSize,
+ AvatarFavicon,
+ AvatarFaviconSize,
Box,
Button,
+ ButtonLink,
+ ButtonLinkSize,
ButtonSize,
ButtonVariant,
Text,
@@ -27,18 +33,37 @@ import {
} from '../../../components/multichain/pages/page';
import { SiteCell } from '../../../components/multichain/pages/review-permissions-page/site-cell/site-cell';
import {
+ AlignItems,
BackgroundColor,
BlockSize,
+ BorderRadius,
Display,
FlexDirection,
+ JustifyContent,
+ TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
import { TEST_CHAINS } from '../../../../shared/constants/network';
-import PermissionsConnectFooter from '../../../components/app/permissions-connect-footer';
import { getMultichainNetwork } from '../../../selectors/multichain';
+import { Tab, Tabs } from '../../../components/ui/tabs';
+import {
+ AccountListItem,
+ EditAccountsModal,
+} from '../../../components/multichain';
+import {
+ getAvatarFallbackLetter,
+ isIpAddress,
+ transformOriginToTitle,
+} from '../../../helpers/utils/util';
+import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
+import {
+ MetaMetricsEventCategory,
+ MetaMetricsEventName,
+} from '../../../../shared/constants/metametrics';
+import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
- getRequestedSessionScopes,
getCaip25PermissionsResponse,
+ getRequestedSessionScopes,
PermissionsRequest,
} from './utils';
@@ -54,6 +79,13 @@ export type ConnectPageProps = {
rejectPermissionsRequest: (id: string) => void;
approveConnection: (request: ConnectPageRequest) => void;
activeTabOrigin: string;
+ targetSubjectMetadata: {
+ extensionId: string | null;
+ iconUrl: string | null;
+ name: string;
+ origin: string;
+ subjectType: string;
+ };
};
export const ConnectPage: React.FC
= ({
@@ -61,8 +93,10 @@ export const ConnectPage: React.FC = ({
permissionsRequestId,
rejectPermissionsRequest,
approveConnection,
+ targetSubjectMetadata,
}) => {
const t = useI18nContext();
+ const trackEvent = useContext(MetaMetricsContext);
const requestedSessionsScopes = getRequestedSessionScopes(
request.permissions,
@@ -84,6 +118,8 @@ export const ConnectPage: React.FC = ({
[networkConfigurations],
);
+ const [showEditAccountsModal, setShowEditAccountsModal] = useState(false);
+
// By default, if a non test network is the globally selected network. We will only show non test networks as default selected.
const currentlySelectedNetwork = useSelector(getMultichainNetwork);
const currentlySelectedNetworkChainId =
@@ -152,6 +188,26 @@ export const ConnectPage: React.FC = ({
approveConnection(_request);
};
+ const selectedAccounts = accounts.filter(({ address }) =>
+ selectedAccountAddresses.some((selectedAccountAddress) =>
+ isEqualCaseInsensitive(selectedAccountAddress, address),
+ ),
+ );
+
+ const title = transformOriginToTitle(targetSubjectMetadata.origin);
+
+ const handleOpenAccountsModal = () => {
+ setShowEditAccountsModal(true);
+ trackEvent({
+ category: MetaMetricsEventCategory.Navigation,
+ event: MetaMetricsEventName.ViewPermissionedAccounts,
+ properties: {
+ location:
+ 'Connect view (accounts tab), Permissions toast, Permissions (dapp)',
+ },
+ });
+ };
+
return (
= ({
backgroundColor={BackgroundColor.backgroundAlternative}
>
- {t('connectWithMetaMask')}
- {t('connectionDescription')}:
+
+ {targetSubjectMetadata.iconUrl ? (
+ <>
+
+
+
+
+ >
+ ) : (
+
+ {isIpAddress(title) ? '?' : getAvatarFallbackLetter(title)}
+
+ )}
+
+
+ {title}
+
+
+ {t('connectionDescription')}
+ {
+ global.platform.openTab({
+ url: ZENDESK_URLS.USER_GUIDE_DAPPS,
+ });
+ }}
+ >
+ {t('learnMoreUpperCase')}
+
+
-
-
+
+ null}
+ backgroundColor={BackgroundColor.transparent}
+ justifyContent={JustifyContent.center}
+ defaultActiveTabKey="accounts"
+ tabListProps={{
+ backgroundColor: BackgroundColor.transparent,
+ }}
+ >
+
+
+
+ {selectedAccounts.map((account) => (
+
+ ))}
+ {selectedAccounts.length === 0 && (
+
+ handleOpenAccountsModal()}
+ data-testid="edit"
+ >
+ {t('selectAccountToConnect')}
+
+
+ )}
+
+ {selectedAccounts.length > 0 && (
+
+ handleOpenAccountsModal()}
+ data-testid="edit"
+ >
+ {t('editAccounts')}
+
+
+ )}
+ {showEditAccountsModal && (
+ setShowEditAccountsModal(false)}
+ onSubmit={setSelectedAccountAddresses}
+ />
+ )}
+
+
+
+
+
+
+
+