Skip to content

Commit

Permalink
feat: Connections List Item Network Badges (#23397)
Browse files Browse the repository at this point in the history
## **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
<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

[![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**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->
<img width="357" alt="image"
src="https://github.com/MetaMask/metamask-extension/assets/10986371/c54a4b60-9998-4b06-9ece-60a65806c7b4">

## **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.
  • Loading branch information
vthomas13 authored Mar 21, 2024
1 parent c6f4e61 commit a246dc3
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,16 @@ exports[`All Connections render renders correctly 1`] = `
<div
class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right"
>
<span
class="mm-box mm-icon mm-icon--size-xs mm-box--display-inline-block mm-box--color-icon-default mm-box--border-color-background-default box--border-style-solid box--border-width-1"
style="mask-image: url('./images/icons/global.svg');"
/>
<div
class="mm-box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-text-default mm-box--background-color-background-alternative mm-box--rounded-full mm-box--border-color-background-default mm-box--border-width-1 box--border-style-solid"
data-testid="connection-list-item__avatar-network-badge"
>
<img
alt="Ethereum Mainnet logo"
class="mm-avatar-network__network-image"
src="./images/eth_logo.svg"
/>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
AvatarFavicon,
AvatarNetwork,
AvatarNetworkSize,
BadgeWrapper,
Box,
Icon,
Expand Down Expand Up @@ -60,10 +62,12 @@ export const ConnectionListItem = ({ connection, onClick }) => {
) : (
<BadgeWrapper
badge={
<Icon
name={IconName.Global}
color={IconColor.iconDefault}
size={IconSize.Xs}
<AvatarNetwork
data-testid="connection-list-item__avatar-network-badge"
size={AvatarNetworkSize.Xs}
name={connection.networkName}
src={connection.networkIconUrl}
borderWidth={1}
borderColor={BackgroundColor.backgroundDefault}
/>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -29,6 +30,8 @@ export default {
'0xaaaF07C80ce267F3132cE7e6048B66E6E669365B': 'TestAddress1',
'0xbbbD671F1Fcc94bCF0ebC6Ec4790Da35E8d5e1E1': 'TestAddress2',
},
networkIconUrl: ETH_TOKEN_IMAGE_URL,
networkName: 'Test Dapp Network',
},
onClick: () => console.log('clicked'),
},
Expand Down Expand Up @@ -82,6 +85,8 @@ ChaosStory.args = {
'0x777F07C80ce267F3132cE7e6048B66E6E669365B': 'TestAddress9',
'0x666D671F1Fcc94bCF0ebC6Ec4790Da35E8d5e1E1': 'TestAddress10',
},
networkIconUrl: ETH_TOKEN_IMAGE_URL,
networkName: 'Test Dapp Network',
},
onClick: () => {
console.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
<ConnectionListItem connection={mockConnection2} onClick={onClickMock} />,
store,
);

expect(
getByTestId('connection-list-item__avatar-network-badge'),
).toBeInTheDocument();

expect(
document
.querySelector('.mm-avatar-network__network-image')
.getAttribute('src'),
).toBe(mockConnection2.networkIconUrl);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
import {
getOnboardedInThisUISession,
getShowPermissionsTour,
getConnectedSitesList,
getConnectedSitesListWithNetworkInfo,
getConnectedSnapsList,
} from '../../../../selectors';
import { Tab, Tabs } from '../../../ui/tabs';
Expand All @@ -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);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
20 changes: 19 additions & 1 deletion ui/selectors/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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] =
Expand All @@ -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;
},
);
Expand Down
122 changes: 122 additions & 0 deletions ui/selectors/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});
});
});

0 comments on commit a246dc3

Please sign in to comment.