From a246dc3022996bfc6f07132804c0ff983253ed09 Mon Sep 17 00:00:00 2001 From: Victor Thomas <10986371+vthomas13@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:42:41 -0400 Subject: [PATCH] feat: Connections List Item Network Badges (#23397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds Network badges to each item in the connection list. In future PRs, we'll make the onClick send to Nidhi's connect/disconnect flow TODO: fix storybook [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/23397?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/1978 ## **Manual testing steps** 1. Open Extension 2. Make sure that Request Queueing Setting is turned on in Experimental tab 3. Connect to some Dapps 4. Go to All Permissions page and verify that the network for each connection is accurate. ## **Screenshots/Recordings** ### **Before** ### **After** image ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've clearly explained what problem this PR is solving and how it is solved. - [x] I've linked related issues - [x] I've included manual testing steps - [x] I've included screenshots/recordings if applicable - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. - [x] I’ve properly set the pull request status: - [ ] In case it's not yet "ready for review", I've set it to "draft". - [x] In case it's "ready for review", I've changed it from "draft" to "non-draft". ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../permissions-page.test.js.snap | 14 +- .../permissions-page/connection-list-item.js | 12 +- .../connection-list-item.stories.js | 5 + .../connection-list-item.test.js | 34 +++++ .../permissions-page/permissions-page.js | 6 +- .../permissions-page.stories.js | 9 -- .../permissions-page/permissions-page.test.js | 8 ++ ui/selectors/selectors.js | 20 ++- ui/selectors/selectors.test.js | 122 ++++++++++++++++++ 9 files changed, 210 insertions(+), 20 deletions(-) delete mode 100644 ui/components/multichain/pages/permissions-page/permissions-page.stories.js diff --git a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap index 6cbc845c52d1..baf6e9450ad6 100644 --- a/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap +++ b/ui/components/multichain/pages/permissions-page/__snapshots__/permissions-page.test.js.snap @@ -76,10 +76,16 @@ exports[`All Connections render renders correctly 1`] = `
- +
+ Ethereum Mainnet logo +
diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.js b/ui/components/multichain/pages/permissions-page/connection-list-item.js index fd5dd75747ed..95b9cafd5a7e 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.js @@ -16,6 +16,8 @@ import { import { useI18nContext } from '../../../../hooks/useI18nContext'; import { AvatarFavicon, + AvatarNetwork, + AvatarNetworkSize, BadgeWrapper, Box, Icon, @@ -60,10 +62,12 @@ export const ConnectionListItem = ({ connection, onClick }) => { ) : ( } diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.stories.js b/ui/components/multichain/pages/permissions-page/connection-list-item.stories.js index 45718e887427..134fe683f630 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.stories.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.stories.js @@ -1,5 +1,6 @@ import React from 'react'; import { PERMISSIONS } from '../../../../helpers/constants/routes'; +import { ETH_TOKEN_IMAGE_URL } from '../../../../../shared/constants/network'; import { ConnectionListItem } from './connection-list-item'; export default { @@ -29,6 +30,8 @@ export default { '0xaaaF07C80ce267F3132cE7e6048B66E6E669365B': 'TestAddress1', '0xbbbD671F1Fcc94bCF0ebC6Ec4790Da35E8d5e1E1': 'TestAddress2', }, + networkIconUrl: ETH_TOKEN_IMAGE_URL, + networkName: 'Test Dapp Network', }, onClick: () => console.log('clicked'), }, @@ -82,6 +85,8 @@ ChaosStory.args = { '0x777F07C80ce267F3132cE7e6048B66E6E669365B': 'TestAddress9', '0x666D671F1Fcc94bCF0ebC6Ec4790Da35E8d5e1E1': 'TestAddress10', }, + networkIconUrl: ETH_TOKEN_IMAGE_URL, + networkName: 'Test Dapp Network', }, onClick: () => { console.log( diff --git a/ui/components/multichain/pages/permissions-page/connection-list-item.test.js b/ui/components/multichain/pages/permissions-page/connection-list-item.test.js index 30c1be8c5ee8..7e9205517cd5 100644 --- a/ui/components/multichain/pages/permissions-page/connection-list-item.test.js +++ b/ui/components/multichain/pages/permissions-page/connection-list-item.test.js @@ -35,6 +35,8 @@ describe('ConnectionListItem', () => { origin: 'https://metamask.github.io', subjectType: 'website', iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', + networkIconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', + networkName: 'Test Dapp Network', }; const { getByText, getByTestId } = renderWithProvider( @@ -68,4 +70,36 @@ describe('ConnectionListItem', () => { fireEvent.click(getByTestId('connection-list-item')); expect(onClickMock).toHaveBeenCalledTimes(1); }); + + it('renders badgewrapper correctly for non-Snap connection', () => { + const onClickMock = jest.fn(); + const mockConnection2 = { + extensionId: null, + iconUrl: 'https://metamask.github.io/test-dapp/metamask-fox.svg', + name: 'MM Test Dapp', + origin: 'https://metamask.github.io', + subjectType: 'website', + addresses: ['0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da'], + addressToNameMap: { + '0x0836f5ed6b62baf60706fe3adc0ff0fd1df833da': + 'Unreasonably long account name', + }, + networkIconUrl: './images/eth_logo.svg', + networkName: 'Ethereum Mainnet', + }; + const { getByTestId } = renderWithProvider( + , + store, + ); + + expect( + getByTestId('connection-list-item__avatar-network-badge'), + ).toBeInTheDocument(); + + expect( + document + .querySelector('.mm-avatar-network__network-image') + .getAttribute('src'), + ).toBe(mockConnection2.networkIconUrl); + }); }); diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.js b/ui/components/multichain/pages/permissions-page/permissions-page.js index d6f3b242aba5..56d33cce9fba 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.js @@ -29,7 +29,7 @@ import { import { getOnboardedInThisUISession, getShowPermissionsTour, - getConnectedSitesList, + getConnectedSitesListWithNetworkInfo, getConnectedSnapsList, } from '../../../../selectors'; import { Tab, Tabs } from '../../../ui/tabs'; @@ -45,7 +45,9 @@ export const PermissionsPage = () => { const history = useHistory(); const headerRef = useRef(); const [totalConnections, setTotalConnections] = useState(0); - const sitesConnectionsList = useSelector(getConnectedSitesList); + const sitesConnectionsList = useSelector( + getConnectedSitesListWithNetworkInfo, + ); const snapsConnectionsList = useSelector(getConnectedSnapsList); const showPermissionsTour = useSelector(getShowPermissionsTour); const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.stories.js b/ui/components/multichain/pages/permissions-page/permissions-page.stories.js deleted file mode 100644 index 9a016d81b760..000000000000 --- a/ui/components/multichain/pages/permissions-page/permissions-page.stories.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { PermissionsPage } from './permissions-page'; - -export default { - title: 'Components/Multichain/PermissionsPage', -}; -export const DefaultStory = () => ; - -DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.test.js b/ui/components/multichain/pages/permissions-page/permissions-page.test.js index 6b26ad5e089c..1440f8c389c8 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.test.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.test.js @@ -78,6 +78,14 @@ mockState.metamask.snaps = { }, }, }; + +mockState.metamask.domains = { + 'https://metamask.github.io': 'mainnet', + 'npm:@metamask/testSnap1': 'mainnet', + 'npm:@metamask/testSnap2': 'mainnet', + 'npm:@metamask/testSnap3': 'mainnet', +}; + let store = configureStore(mockState); describe('All Connections', () => { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 371a6d24c805..0e3935fd6a5b 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -473,6 +473,10 @@ export function getAllTokens(state) { return state.metamask.allTokens; } +export function getAllDomains(state) { + return state.metamask.domains; +} + export const getConfirmationExchangeRates = (state) => { return state.metamask.confirmationExchangeRates; }; @@ -1253,7 +1257,6 @@ export const getConnectedSitesList = createDeepEqualSelector( connectedAddresses.forEach((connectedAddress) => { connectedSubjectsForAllAddresses[connectedAddress].forEach((app) => { const siteKey = app.origin; - if (sitesList[siteKey]) { sitesList[siteKey].addresses.push(connectedAddress); sitesList[siteKey].addressToNameMap[connectedAddress] = @@ -1269,7 +1272,22 @@ export const getConnectedSitesList = createDeepEqualSelector( } }); }); + return sitesList; + }, +); +export const getConnectedSitesListWithNetworkInfo = createDeepEqualSelector( + getConnectedSitesList, + getAllDomains, + getAllNetworks, + (sitesList, domains, networks) => { + Object.keys(sitesList).forEach((siteKey) => { + const connectedNetwork = networks.find( + (network) => network.id === domains[siteKey], + ); + sitesList[siteKey].networkIconUrl = connectedNetwork.rpcPrefs.imageUrl; + sitesList[siteKey].networkName = connectedNetwork.nickname; + }); return sitesList; }, ); diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 036bb1d5301c..50ed0a8d875a 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1339,3 +1339,125 @@ describe('#getKeyringSnapAccounts', () => { ]); }); }); +describe('#getConnectedSitesListWithNetworkInfo', () => { + it('returns the sites list with network information', () => { + const sitesList = { + site1: { + id: 'site1', + }, + site2: { + id: 'site2', + }, + }; + + const domains = { + site1: 'network1', + site2: 'network2', + }; + + const networks = [ + { + id: 'network1', + rpcPrefs: { + imageUrl: 'network1-icon.png', + }, + nickname: 'Network 1', + }, + { + id: 'network2', + rpcPrefs: { + imageUrl: 'network2-icon.png', + }, + nickname: 'Network 2', + }, + ]; + + const expectedSitesList = { + site1: { + id: 'site1', + networkIconUrl: 'network1-icon.png', + networkName: 'Network 1', + }, + site2: { + id: 'site2', + networkIconUrl: 'network2-icon.png', + networkName: 'Network 2', + }, + }; + + const result = selectors.getConnectedSitesListWithNetworkInfo.resultFunc( + sitesList, + domains, + networks, + ); + + expect(result).toStrictEqual(expectedSitesList); + }); +}); +describe('#getConnectedSitesList', () => { + it('returns an empty object if there are no connected addresses', () => { + const connectedSubjectsForAllAddresses = {}; + const identities = {}; + const connectedAddresses = []; + + const result = selectors.getConnectedSitesList.resultFunc( + connectedSubjectsForAllAddresses, + identities, + connectedAddresses, + ); + + expect(result).toStrictEqual({}); + }); + + it('returns the correct sites list with addresses and name mappings', () => { + const connectedSubjectsForAllAddresses = { + '0x123': [ + { origin: 'site1', name: 'Site 1' }, + { origin: 'site2', name: 'Site 2' }, + ], + '0x456': [ + { origin: 'site1', name: 'Site 1' }, + { origin: 'site3', name: 'Site 3' }, + ], + }; + const identities = { + '0x123': { name: 'John Doe' }, + '0x456': { name: 'Jane Smith' }, + }; + const connectedAddresses = ['0x123', '0x456']; + + const result = selectors.getConnectedSitesList.resultFunc( + connectedSubjectsForAllAddresses, + identities, + connectedAddresses, + ); + + expect(result).toStrictEqual({ + site1: { + origin: 'site1', + addresses: ['0x123', '0x456'], + addressToNameMap: { + '0x123': 'John Doe', + '0x456': 'Jane Smith', + }, + name: 'Site 1', + }, + site2: { + origin: 'site2', + addresses: ['0x123'], + addressToNameMap: { + '0x123': 'John Doe', + }, + name: 'Site 2', + }, + site3: { + origin: 'site3', + addresses: ['0x456'], + addressToNameMap: { + '0x456': 'Jane Smith', + }, + name: 'Site 3', + }, + }); + }); +});