1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

UX: Multichain: App header (#18363)

* UX: Multichain: App header

* Export app header, provide required information, put feature flag in place

* Provide available data

* Implement account picker -- centered and opens account popover

* Remove backgrounds, use isUnlocked

* Fix placement of the global menu

* Show logo when unlocked

* Add selector for getting current network, provide props to AvatarNetwork and PickerNetwork

* Wire up the network menu to the header

* fixed ui for all the screens

* updated story for header

* fixed import and header settings

* updated lint error

* fixed tests

* updated header

* removed test

* updated snapshot test

* updated network menu

* updated changes

* removed comment from menu bar

* updated css

* updated test for network list menu

* updated stylesheet

* updated ButtonIcon import

---------

Co-authored-by: NidhiKJha <menidhikjha@gmail.com>
This commit is contained in:
David Walsh 2023-04-13 11:54:03 -05:00 committed by GitHub
parent 643a89f24d
commit eb51460cae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 778 additions and 37 deletions

View File

@ -208,6 +208,7 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
* @property {boolean} isInitialized - Whether the first vault has been created.
* @property {boolean} isUnlocked - Whether the vault is currently decrypted and accounts are available for selection.
* @property {boolean} isAccountMenuOpen - Represents whether the main account selection UI is currently displayed.
* @property {boolean} isNetworkMenuOpen - Represents whether the main network selection UI is currently displayed.
* @property {object} identities - An object matching lower-case hex addresses to Identity objects with "address" and "name" (nickname) keys.
* @property {object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions.
* @property {object} networkConfigurations - A list of network configurations, containing RPC provider details (eg chainId, rpcUrl, rpcPreferences).

View File

@ -16,7 +16,6 @@ import { getOriginOfCurrentTab } from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { ButtonIcon } from '../../component-library/button-icon/deprecated';
import { ICON_NAMES } from '../../component-library/icon/deprecated';
import { GlobalMenu } from '../../multichain/global-menu';
import AccountOptionsMenu from './account-options-menu';
export default function MenuBar() {
@ -34,7 +33,7 @@ export default function MenuBar() {
return (
<div className="menu-bar">
{showStatus ? ( // TODO: Move the connection status menu icon to the correct position in header once we implement the new header
{showStatus ? (
<ConnectedStatusIndicator
onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)}
/>
@ -58,18 +57,12 @@ export default function MenuBar() {
}}
/>
</span>
{accountOptionsMenuOpen &&
(process.env.MULTICHAIN ? (
<GlobalMenu
anchorElement={ref.current}
closeMenu={() => setAccountOptionsMenuOpen(false)}
/>
) : (
<AccountOptionsMenu
anchorElement={ref.current}
onClose={() => setAccountOptionsMenuOpen(false)}
/>
))}
{accountOptionsMenuOpen && (
<AccountOptionsMenu
anchorElement={ref.current}
onClose={() => setAccountOptionsMenuOpen(false)}
/>
)}
</div>
);
}

View File

@ -103,7 +103,6 @@ export const AccountListMenu = ({ onClose }) => {
},
});
dispatch(setSelectedAccount(account.address));
onClose();
}}
identity={account}
key={account.address}

View File

@ -0,0 +1,224 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App Header should match snapshot 1`] = `
<div>
<div
class="box multichain-app-header multichain-app-header-shadow box--display-flex box--flex-direction-row box--align-items-center box--width-full box--background-color-background-default"
>
<div
class="box multichain-app-header__lock-contents box--padding-2 box--display-flex box--gap-2 box--flex-direction-row box--justify-content-space-between box--align-items-center box--width-full box--background-color-background-default"
>
<button
class="box mm-picker-network box--padding-right-4 box--padding-left-2 box--display-flex box--gap-2 box--flex-direction-row box--align-items-center box--background-color-background-alternative box--rounded-pill"
>
<div
class="box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-picker-network__avatar-network mm-text--body-xs mm-text--text-transform-uppercase box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-background-alternative box--rounded-full box--border-color-transparent box--border-style-solid box--border-width-1"
>
G
</div>
<p
class="box mm-text mm-text--body-sm mm-text--ellipsis box--flex-direction-row box--color-text-default"
>
Goerli
</p>
<span
class="box mm-picker-network__arrow-down-icon mm-icon mm-icon--size-xs box--display-inline-block box--flex-direction-row box--color-icon-default"
style="mask-image: url('./images/icons/arrow-down.svg');"
/>
</button>
<div
class="app-header__logo-container app-header__logo-container--clickable"
data-testid="app-header-logo"
>
<svg
class="app-header__metafox-logo--horizontal"
height="30"
viewBox="0 0 1311 242"
width="162"
xmlns="http://www.w3.org/2000/svg"
>
<g
fill="none"
>
<g
fill="var(--color-text-default)"
transform="translate(361 61)"
>
<path
d="m796.7 60.9c-6.8-4.5-14.3-7.7-21.4-11.7-4.6-2.6-9.5-4.9-13.5-8.2-6.8-5.6-5.4-16.6 1.7-21.4 10.2-6.8 27.1-3 28.9 10.9 0 .3.3.5.6.5h15.4c.4 0 .7-.3.6-.7-.8-9.6-4.5-17.6-11.3-22.7-6.5-4.9-13.9-7.5-21.8-7.5-40.7 0-44.4 43.1-22.5 56.7 2.5 1.6 24 12.4 31.6 17.1s10 13.3 6.7 20.1c-3 6.2-10.8 10.5-18.6 10-8.5-.5-15.1-5.1-17.4-12.3-.4-1.3-.6-3.8-.6-4.9 0-.3-.3-.6-.6-.6h-16.7c-.3 0-.6.3-.6.6 0 12.1 3 18.8 11.2 24.9 7.7 5.8 16.1 8.2 24.8 8.2 22.8 0 34.6-12.9 37-26.3 2.1-13.1-1.8-24.9-13.5-32.7z"
/>
<path
d="m71.6 2.3h-7.4-8.1c-.3 0-.5.2-.6.4l-13.7 45.2c-.2.6-1 .6-1.2 0l-13.7-45.2c-.1-.3-.3-.4-.6-.4h-8.1-7.4-10c-.3 0-.6.3-.6.6v115.4c0 .3.3.6.6.6h16.7c.3 0 .6-.3.6-.6v-87.7c0-.7 1-.8 1.2-.2l13.8 45.5 1 3.2c.1.3.3.4.6.4h12.8c.3 0 .5-.2.6-.4l1-3.2 13.8-45.5c.2-.7 1.2-.5 1.2.2v87.7c0 .3.3.6.6.6h16.7c.3 0 .6-.3.6-.6v-115.4c0-.3-.3-.6-.6-.6z"
/>
<path
d="m541 2.3c-.3 0-.5.2-.6.4l-13.7 45.2c-.2.6-1 .6-1.2 0l-13.7-45.2c-.1-.3-.3-.4-.6-.4h-25.4c-.3 0-.6.3-.6.6v115.4c0 .3.3.6.6.6h16.7c.3 0 .6-.3.6-.6v-87.7c0-.7 1-.8 1.2-.2l13.8 45.5 1 3.2c.1.3.3.4.6.4h12.8c.3 0 .5-.2.6-.4l1-3.2 13.8-45.5c.2-.7 1.2-.5 1.2.2v87.7c0 .3.3.6.6.6h16.7c.3 0 .6-.3.6-.6v-115.4c0-.3-.3-.6-.6-.6z"
/>
<path
d="m325.6 2.3h-31.1-16.7-31.1c-.3 0-.6.3-.6.6v14.4c0 .3.3.6.6.6h30.5v100.4c0 .3.3.6.6.6h16.7c.3 0 .6-.3.6-.6v-100.4h30.5c.3 0 .6-.3.6-.6v-14.4c0-.3-.2-.6-.6-.6z"
/>
<path
d="m424.1 118.9h15.2c.4 0 .7-.4.6-.8l-31.4-115.8c-.1-.3-.3-.4-.6-.4h-5.8-10.2-5.8c-.3 0-.5.2-.6.4l-31.4 115.8c-.1.4.2.8.6.8h15.2c.3 0 .5-.2.6-.4l9.1-33.7c.1-.3.3-.4.6-.4h33.6c.3 0 .5.2.6.4l9.1 33.7c.1.2.4.4.6.4zm-39.9-51 12.2-45.1c.2-.6 1-.6 1.2 0l12.2 45.1c.1.4-.2.8-.6.8h-24.4c-.4 0-.7-.4-.6-.8z"
/>
<path
d="m683.3 118.9h15.2c.4 0 .7-.4.6-.8l-31.4-115.8c-.1-.3-.3-.4-.6-.4h-5.8-10.2-5.8c-.3 0-.5.2-.6.4l-31.4 115.8c-.1.4.2.8.6.8h15.2c.3 0 .5-.2.6-.4l9.1-33.7c.1-.3.3-.4.6-.4h33.6c.3 0 .5.2.6.4l9.1 33.7c.1.2.3.4.6.4zm-39.9-51 12.2-45.1c.2-.6 1-.6 1.2 0l12.2 45.1c.1.4-.2.8-.6.8h-24.4c-.4 0-.7-.4-.6-.8z"
/>
<path
d="m149.8 101.8v-35.8c0-.3.3-.6.6-.6h44.5c.3 0 .6-.3.6-.6v-14.4c0-.3-.3-.6-.6-.6h-44.5c-.3 0-.6-.3-.6-.6v-30.6c0-.3.3-.6.6-.6h50.6c.3 0 .6-.3.6-.6v-14.4c0-.3-.3-.6-.6-.6h-51.2-17.3c-.3 0-.6.3-.6.6v15 31.9 15.6 37 15.8c0 .3.3.6.6.6h17.3 53.3c.3 0 .6-.3.6-.6v-15.2c0-.3-.3-.6-.6-.6h-52.8c-.3-.1-.5-.3-.5-.7z"
/>
<path
d="m949.3 117.9-57.8-59.7c-.2-.2-.2-.6 0-.8l52-54c.4-.4.1-1-.4-1h-21.3c-.2 0-.3.1-.4.2l-44.1 45.8c-.4.4-1 .1-1-.4v-45c0-.3-.3-.6-.6-.6h-16.7c-.3 0-.6.3-.6.6v115.4c0 .3.3.6.6.6h16.7c.3 0 .6-.3.6-.6v-50.8c0-.5.7-.8 1-.4l50 51.6c.1.1.3.2.4.2h21.3c.4-.1.7-.8.3-1.1z"
/>
</g>
<g
stroke-linecap="round"
stroke-linejoin="round"
transform="translate(1 1)"
>
<path
d="m246.1.2-101.1 75 18.8-44.2z"
fill="#e17726"
stroke="#e17726"
/>
<g
fill="#e27625"
stroke="#e27625"
transform="translate(2)"
>
<path
d="m10.9.2 100.2 75.7-17.9-44.9z"
/>
<path
d="m207.7 174.1-26.9 41.2 57.6 15.9 16.5-56.2z"
/>
<path
d="m.2 175 16.4 56.2 57.5-15.9-26.8-41.2z"
/>
<path
d="m71 104.5-16 24.2 57 2.6-1.9-61.5z"
/>
<path
d="m184 104.5-39.7-35.4-1.3 62.2 57-2.6z"
/>
<path
d="m74.1 215.3 34.5-16.7-29.7-23.2z"
/>
<path
d="m146.4 198.6 34.4 16.7-4.7-39.9z"
/>
</g>
<g
fill="#d5bfb2"
stroke="#d5bfb2"
transform="translate(76 198)"
>
<path
d="m106.8 17.3-34.4-16.7 2.8 22.4-.3 9.5z"
/>
<path
d="m.1 17.3 32 15.2-.2-9.5 2.7-22.4z"
/>
</g>
<path
d="m108.7 160.6-28.6-8.4 20.2-9.3z"
fill="#233447"
stroke="#233447"
/>
<path
d="m150.3 160.6 8.4-17.7 20.3 9.3z"
fill="#233447"
stroke="#233447"
/>
<g
fill="#cc6228"
stroke="#cc6228"
transform="translate(49 128)"
>
<path
d="m27.1 87.3 5-41.2-31.8.9z"
/>
<path
d="m128.9 46.1 4.9 41.2 26.9-40.3z"
/>
<path
d="m153 .7-57 2.6 5.3 29.3 8.4-17.7 20.3 9.3z"
/>
<path
d="m31.1 24.2 20.2-9.3 8.4 17.7 5.3-29.3-57-2.6z"
/>
</g>
<g
fill="#e27525"
stroke="#e27525"
transform="translate(57 128)"
>
<path
d="m0 .7 23.9 46.7-.8-23.2z"
/>
<path
d="m122 24.2-.9 23.2 23.9-46.7z"
/>
<path
d="m57 3.3-5.3 29.3 6.7 34.6 1.5-45.6z"
/>
<path
d="m88 3.3-2.8 18.2 1.4 45.7 6.7-34.6z"
/>
</g>
<path
d="m150.3 160.6-6.7 34.6 4.8 3.4 29.7-23.2.9-23.2z"
fill="#f5841f"
stroke="#f5841f"
/>
<path
d="m80.1 152.2.8 23.2 29.7 23.2 4.8-3.4-6.7-34.6z"
fill="#f5841f"
stroke="#f5841f"
/>
<path
d="m150.9 230.5.3-9.5-2.6-2.2h-38.2l-2.5 2.2.2 9.5-32-15.2 11.2 9.2 22.7 15.7h38.9l22.8-15.7 11.1-9.2z"
fill="#c0ac9d"
stroke="#c0ac9d"
/>
<path
d="m148.4 198.6-4.8-3.4h-28.2l-4.8 3.4-2.7 22.4 2.5-2.2h38.2l2.6 2.2z"
fill="#161616"
stroke="#161616"
/>
<g
fill="#763e1a"
stroke="#763e1a"
>
<path
d="m250.4 80.1 8.5-41.4-12.8-38.5-97.7 72.5 37.6 31.8 53.1 15.5 11.7-13.7-5.1-3.7 8.1-7.4-6.2-4.8 8.1-6.2z"
/>
<path
d="m.1 38.7 8.6 41.4-5.5 4.1 8.2 6.2-6.2 4.8 8.1 7.4-5.1 3.7 11.7 13.7 53.1-15.5 37.6-31.8-97.7-72.5z"
/>
</g>
<g
fill="#f5841f"
stroke="#f5841f"
>
<path
d="m239.1 120-53.1-15.5 16 24.2-23.9 46.7 31.6-.4h47.2z"
/>
<path
d="m73 104.5-53.1 15.5-17.7 55h47.1l31.6.4-23.9-46.7z"
/>
<path
d="m145 131.3 3.4-58.6 15.4-41.7h-68.6l15.4 41.7 3.4 58.6 1.3 18.4.1 45.5h28.2l.1-45.5z"
/>
</g>
</g>
</g>
</svg>
<img
alt=""
class="app-header__metafox-logo--icon"
src="./images/logo/metamask-fox.svg"
/>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,215 @@
import React, { useContext, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import browser from 'webextension-polyfill';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import {
CONNECTED_ACCOUNTS_ROUTE,
DEFAULT_ROUTE,
} from '../../../helpers/constants/routes';
import {
AlignItems,
BackgroundColor,
BLOCK_SIZES,
DISPLAY,
JustifyContent,
Size,
} from '../../../helpers/constants/design-system';
import { AvatarNetwork, Button, PickerNetwork } from '../../component-library';
import { ButtonIcon } from '../../component-library/button-icon/deprecated';
import { ICON_NAMES } from '../../component-library/icon/deprecated';
import {
getCurrentNetwork,
getOriginOfCurrentTab,
getSelectedIdentity,
} from '../../../selectors';
import { GlobalMenu, AccountPicker } from '..';
import Box from '../../ui/box/box';
import { toggleAccountMenu, toggleNetworkMenu } from '../../../store/actions';
import MetafoxLogo from '../../ui/metafox-logo';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import ConnectedStatusIndicator from '../../app/connected-status-indicator';
export const AppHeader = ({ onClick }) => {
const trackEvent = useContext(MetaMetricsContext);
const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false);
const menuRef = useRef(false);
const origin = useSelector(getOriginOfCurrentTab);
const history = useHistory();
const isUnlocked = useSelector((state) => state.metamask.isUnlocked);
// Used for account picker
const identity = useSelector(getSelectedIdentity);
const dispatch = useDispatch();
// Used for network icon / dropdown
const currentNetwork = useSelector(getCurrentNetwork);
// used to get the environment and connection status
const popupStatus = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP;
const showStatus =
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP &&
origin &&
origin !== browser.runtime.id;
return (
<>
{isUnlocked && !popupStatus ? (
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
margin={2}
className="multichain-app-header-logo"
data-testid="app-header-logo"
justifyContent={JustifyContent.center}
>
<MetafoxLogo
unsetIconHeight
onClick={async () => {
if (onClick) {
await onClick();
}
history.push(DEFAULT_ROUTE);
}}
/>
</Box>
) : null}
<Box
display={DISPLAY.FLEX}
className={`multichain-app-header ${
!isUnlocked || popupStatus ? 'multichain-app-header-shadow' : ''
}`}
alignItems={AlignItems.center}
width={BLOCK_SIZES.FULL}
backgroundColor={
!isUnlocked || popupStatus
? BackgroundColor.backgroundDefault
: BackgroundColor.backgroundAlternative
}
>
<>
{isUnlocked ? (
<Box
className={`multichain-app-header__contents ${
isUnlocked && !popupStatus ? 'multichain-app-header-shadow' : ''
}`}
alignItems={AlignItems.center}
width={BLOCK_SIZES.FULL}
backgroundColor={BackgroundColor.backgroundDefault}
padding={2}
gap={2}
>
{popupStatus ? (
<Button
className="multichain-app-header__contents--avatar-network"
justifyContent={JustifyContent.flexStart}
>
<AvatarNetwork
name={currentNetwork?.nickname}
src={currentNetwork?.rpcPrefs?.imageUrl}
size={Size.SM}
onClick={() => dispatch(toggleNetworkMenu())}
/>
</Button>
) : (
<PickerNetwork
label={currentNetwork?.nickname}
src={currentNetwork?.rpcPrefs?.imageUrl}
onClick={() => dispatch(toggleNetworkMenu())}
/>
)}
<AccountPicker
address={identity.address}
name={identity.name}
onClick={() => dispatch(toggleAccountMenu())}
/>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
>
{showStatus ? (
<ConnectedStatusIndicator
onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)}
/>
) : null}
<Box
ref={menuRef}
display={DISPLAY.FLEX}
justifyContent={JustifyContent.flexEnd}
width={BLOCK_SIZES.FULL}
>
<ButtonIcon
iconName={ICON_NAMES.MORE_VERTICAL}
data-testid="account-options-menu-button"
ariaLabel="NEEDS NEW TRANSLATED LABEL" // TODO: Update the label
onClick={() => {
trackEvent({
event: MetaMetricsEventName.NavAccountMenuOpened,
category: MetaMetricsEventCategory.Navigation,
properties: {
location: 'Home',
},
});
setAccountOptionsMenuOpen(true);
}}
/>
</Box>
</Box>
{accountOptionsMenuOpen ? (
<GlobalMenu
anchorElement={menuRef.current}
closeMenu={() => setAccountOptionsMenuOpen(false)}
/>
) : null}
</Box>
) : (
<Box
display={DISPLAY.FLEX}
className={`multichain-app-header__lock-contents ${
isUnlocked && !popupStatus ? 'multichain-app-header-shadow' : ''
}`}
alignItems={AlignItems.center}
width={BLOCK_SIZES.FULL}
justifyContent={JustifyContent.spaceBetween}
backgroundColor={BackgroundColor.backgroundDefault}
padding={2}
gap={2}
>
<PickerNetwork
label={currentNetwork?.nickname}
src={currentNetwork?.rpcPrefs?.imageUrl}
onClick={() => dispatch(toggleNetworkMenu())}
/>
<MetafoxLogo
unsetIconHeight
onClick={async () => {
if (onClick) {
await onClick();
}
history.push(DEFAULT_ROUTE);
}}
/>
</Box>
)}
</>
</Box>
</>
);
};
AppHeader.propTypes = {
/**
* The onClick handler to be passed to the MetaMask Logo in the App Header
*/
onClick: PropTypes.func,
};

View File

@ -0,0 +1,74 @@
.multichain-app-header {
$height-screen-sm-max: 100%;
$width-screen-sm-min: 85vw;
$width-screen-md-min: 80vw;
$width-screen-lg-min: 62vw;
flex-flow: column nowrap;
z-index: 55;
min-height: 64px;
&__contents {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
height: 64px;
@include screen-sm-max {
height: $height-screen-sm-max;
}
@include screen-sm-min {
width: $width-screen-sm-min;
}
@include screen-md-min {
width: $width-screen-md-min;
}
@include screen-lg-min {
width: $width-screen-lg-min;
}
&--avatar-network {
background-color: transparent;
width: min-content;
padding: 8px;
&:hover,
&:active {
box-shadow: none;
background: transparent;
}
}
}
&__lock-contents {
flex-flow: row nowrap;
height: 64px;
@include screen-sm-max {
height: $height-screen-sm-max;
}
@include screen-sm-min {
width: $width-screen-sm-min;
}
@include screen-md-min {
width: $width-screen-md-min;
}
@include screen-lg-min {
width: $width-screen-lg-min;
}
}
}
.multichain-app-header-shadow {
box-shadow: var(--shadow-size-md) var(--color-shadow-default);
}
.multichain-app-header-logo {
height: 75px;
flex: 0 0 auto;
}

View File

@ -0,0 +1,71 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
import testData from '../../../../.storybook/test-data';
import { AppHeader } from '.';
const store = configureStore(testData);
export default {
title: 'Components/Multichain/AppHeader',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
component: AppHeader,
argTypes: {
onClick: {
action: 'onClick',
},
},
};
const customNetworkUnlockedData = {
...testData,
metamask: {
...testData.metamask,
preferences: {
showTestNetworks: true,
},
isUnlocked: true,
networkConfigurations: {
...testData.metamask.networkConfigurations,
},
},
};
const customNetworkUnlockedStore = configureStore(customNetworkUnlockedData);
const customNetworkLockedData = {
...testData,
metamask: {
...testData.metamask,
preferences: {
showTestNetworks: true,
},
isUnlocked: false,
networkConfigurations: {
...testData.metamask.networkConfigurations,
},
},
};
const customNetworkLockedStore = configureStore(customNetworkLockedData);
const Template = (args) => {
return <AppHeader {...args} />;
};
export const FullScreenAndUnlockedStory = Template.bind({});
FullScreenAndUnlockedStory.decorators = [
(Story) => (
<Provider store={customNetworkUnlockedStore}>
<Story />
</Provider>
),
];
export const FullScreenAndLockedStory = Template.bind({});
FullScreenAndLockedStory.decorators = [
(Story) => (
<Provider store={customNetworkLockedStore}>
<Story />
</Provider>
),
];

View File

@ -0,0 +1,107 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import { CHAIN_IDS } from '../../../../shared/constants/network';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { AppHeader } from '.';
describe('App Header', () => {
it('should match snapshot', () => {
const mockState = {
activeTab: {
title: 'Eth Sign Tests',
origin: 'https://remix.ethereum.org',
protocol: 'https:',
url: 'https://remix.ethereum.org/',
},
metamask: {
provider: {
chainId: CHAIN_IDS.GOERLI,
},
accounts: {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': {
address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d',
},
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
},
},
preferences: {
showTestNetworks: true,
},
cachedBalances: {},
subjects: {
'https://remix.ethereum.org': {
permissions: {
eth_accounts: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
],
},
],
date: 1586359844177,
id: '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
invoker: 'https://remix.ethereum.org',
parentCapability: 'eth_accounts',
},
},
},
'peepeth.com': {
permissions: {
eth_accounts: {
caveats: [
{
type: 'restrictReturnedAccounts',
value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'],
},
],
date: 1585676177970,
id: '840d72a0-925f-449f-830a-1aa1dd5ce151',
invoker: 'peepeth.com',
parentCapability: 'eth_accounts',
},
},
},
},
identities: {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': {
address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d',
name: 'Really Long Name That Should Be Truncated',
},
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': {
address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
lastSelected: 1586359844192,
name: 'Account 1',
},
},
keyrings: [
{
accounts: [
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
],
},
],
permissionHistory: {
'https://remix.ethereum.org': {
eth_accounts: {
accounts: {
'0x7250739de134d33ec7ab1ee592711e15098c9d2d': 1586359844192,
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': 1586359844192,
},
lastApproved: 1586359844192,
},
},
},
},
};
const mockStore = configureStore();
const store = mockStore(mockState);
const { container } = renderWithProvider(<AppHeader />, store);
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1 @@
export { AppHeader } from './app-header';

View File

@ -2,6 +2,7 @@ export { AccountListItem } from './account-list-item';
export { AccountListItemMenu } from './account-list-item-menu';
export { AccountListMenu } from './account-list-menu';
export { AccountPicker } from './account-picker';
export { AppHeader } from './app-header';
export { DetectedTokensBanner } from './detected-token-banner';
export { GlobalMenu } from './global-menu';
export { MultichainImportTokenLink } from './multichain-import-token-link';

View File

@ -8,6 +8,7 @@
@import 'account-list-item/index';
@import 'account-list-menu/index';
@import 'account-picker/index';
@import 'app-header/app-header';
@import 'multichain-connected-site-menu/index';
@import 'account-list-menu/';
@import 'multichain-token-list-item/multichain-token-list-item';

View File

@ -10,6 +10,7 @@ import {
showModal,
setShowTestNetworks,
setProviderType,
toggleNetworkMenu,
} from '../../../store/actions';
import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network';
import {
@ -30,7 +31,7 @@ import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
const UNREMOVABLE_CHAIN_IDS = [CHAIN_IDS.MAINNET, ...TEST_CHAINS];
export const NetworkListMenu = ({ closeMenu }) => {
export const NetworkListMenu = ({ onClose }) => {
const t = useI18nContext();
const networks = useSelector(getAllNetworks);
const showTestNetworks = useSelector(getShowTestNetworks);
@ -42,7 +43,7 @@ export const NetworkListMenu = ({ closeMenu }) => {
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
return (
<Popover onClose={closeMenu} centerTitle title={t('networkMenuHeading')}>
<Popover onClose={onClose} centerTitle title={t('networkMenuHeading')}>
<>
<Box className="multichain-network-list-menu">
{networks.map((network) => {
@ -58,16 +59,17 @@ export const NetworkListMenu = ({ closeMenu }) => {
key={network.id || network.chainId}
selected={isCurrentNetwork}
onClick={() => {
dispatch(toggleNetworkMenu());
if (network.providerType) {
dispatch(setProviderType(network.providerType));
} else {
dispatch(setActiveNetwork(network.id));
}
closeMenu();
}}
onDeleteClick={
canDeleteNetwork
? () => {
dispatch(toggleNetworkMenu());
dispatch(
showModal({
name: 'CONFIRM_DELETE_NETWORK',
@ -75,7 +77,6 @@ export const NetworkListMenu = ({ closeMenu }) => {
onConfirm: () => undefined,
}),
);
closeMenu();
}
: null
}
@ -104,6 +105,7 @@ export const NetworkListMenu = ({ closeMenu }) => {
: global.platform.openExtensionInBrowser(
ADD_POPULAR_CUSTOM_NETWORK,
);
dispatch(toggleNetworkMenu());
}}
>
{t('addNetwork')}
@ -118,5 +120,5 @@ NetworkListMenu.propTypes = {
/**
* Executes when the menu should be closed
*/
closeMenu: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

View File

@ -42,8 +42,8 @@ export default {
title: 'Components/Multichain/NetworkListMenu',
component: NetworkListMenu,
argTypes: {
closeMenu: {
action: 'closeMenu',
onClose: {
action: 'onClose',
},
},
};

View File

@ -11,9 +11,11 @@ import { NetworkListMenu } from '.';
const mockSetShowTestNetworks = jest.fn();
const mockSetProviderType = jest.fn();
const mockToggleNetworkMenu = jest.fn();
jest.mock('../../../store/actions.ts', () => ({
setShowTestNetworks: () => mockSetShowTestNetworks,
setProviderType: () => mockSetProviderType,
toggleNetworkMenu: () => mockToggleNetworkMenu,
}));
const render = (showTestNetworks = false) => {
@ -25,7 +27,7 @@ const render = (showTestNetworks = false) => {
},
},
});
return renderWithProvider(<NetworkListMenu closeMenu={jest.fn()} />, store);
return renderWithProvider(<NetworkListMenu onClose={jest.fn()} />, store);
};
describe('NetworkListMenu', () => {
@ -56,6 +58,7 @@ describe('NetworkListMenu', () => {
it('switches networks when an item is clicked', () => {
const { getByText } = render();
fireEvent.click(getByText(MAINNET_DISPLAY_NAME));
expect(mockToggleNetworkMenu).toHaveBeenCalled();
expect(mockSetProviderType).toHaveBeenCalled();
});
});

View File

@ -26,6 +26,7 @@ const initialState = {
isInitialized: false,
isUnlocked: false,
isAccountMenuOpen: false,
isNetworkMenuOpen: false,
identities: {},
unapprovedTxs: {},
networkConfigurations: {},
@ -102,6 +103,12 @@ export default function reduceMetamask(state = initialState, action) {
isAccountMenuOpen: !metamaskState.isAccountMenuOpen,
};
case actionConstants.TOGGLE_NETWORK_MENU:
return {
...metamaskState,
isNetworkMenuOpen: !metamaskState.isNetworkMenuOpen,
};
case actionConstants.UPDATE_TRANSACTION_PARAMS: {
const { id: txId, value } = action;
let { currentNetworkTxList } = metamaskState;

View File

@ -157,6 +157,17 @@ describe('MetaMask Reducers', () => {
expect(state.isAccountMenuOpen).toStrictEqual(true);
});
it('toggles network menu', () => {
const state = reduceMetamask(
{},
{
type: actionConstants.TOGGLE_NETWORK_MENU,
},
);
expect(state.isNetworkMenuOpen).toStrictEqual(true);
});
it('updates value of tx by id', () => {
const oldState = {
currentNetworkTxList: [

View File

@ -641,7 +641,7 @@ export default class Home extends PureComponent {
? this.renderPopover()
: null}
<div className="home__main-view">
<MenuBar />
{process.env.MULTICHAIN ? null : <MenuBar />}
<div className="home__balance-wrapper">
<EthOverview />
</div>

View File

@ -31,6 +31,11 @@ import AccountMenu from '../../components/app/account-menu';
import { Modal } from '../../components/app/modals';
import Alert from '../../components/ui/alert';
import AppHeader from '../../components/app/app-header';
import {
AppHeader as MultichainAppHeader,
AccountListMenu,
NetworkListMenu,
} from '../../components/multichain';
import UnlockPage from '../unlock-page';
import Alerts from '../../components/app/alerts';
import Asset from '../asset';
@ -90,7 +95,6 @@ import { SEND_STAGES } from '../../ducks/send';
import DeprecatedTestNetworks from '../../components/ui/deprecated-test-networks/deprecated-test-networks';
import NewNetworkInfo from '../../components/ui/new-network-info/new-network-info';
import { ThemeType } from '../../../shared/constants/preferences';
import { AccountListMenu } from '../../components/multichain';
export default class Routes extends Component {
static propTypes = {
@ -128,6 +132,8 @@ export default class Routes extends Component {
completedOnboarding: PropTypes.bool,
isAccountMenuOpen: PropTypes.bool,
toggleAccountMenu: PropTypes.func,
isNetworkMenuOpen: PropTypes.bool,
toggleNetworkMenu: PropTypes.func,
};
static contextTypes = {
@ -436,6 +442,8 @@ export default class Routes extends Component {
completedOnboarding,
isAccountMenuOpen,
toggleAccountMenu,
isNetworkMenuOpen,
toggleNetworkMenu,
} = this.props;
const loadMessage =
loadingMessage || isNetworkLoading
@ -478,24 +486,30 @@ export default class Routes extends Component {
<QRHardwarePopover />
<Modal />
<Alert visible={this.props.alertOpen} msg={alertMessage} />
{!this.hideAppHeader() && (
<AppHeader
hideNetworkIndicator={this.onInitializationUnlockPage()}
disableNetworkIndicator={this.onSwapsPage()}
onClick={this.onAppHeaderClick}
disabled={
this.onConfirmPage() ||
this.onEditTransactionPage() ||
(this.onSwapsPage() && !this.onSwapsBuildQuotePage())
}
/>
)}
{!this.hideAppHeader() &&
(process.env.MULTICHAIN ? (
<MultichainAppHeader />
) : (
<AppHeader
hideNetworkIndicator={this.onInitializationUnlockPage()}
disableNetworkIndicator={this.onSwapsPage()}
onClick={this.onAppHeaderClick}
disabled={
this.onConfirmPage() ||
this.onEditTransactionPage() ||
(this.onSwapsPage() && !this.onSwapsBuildQuotePage())
}
/>
))}
{this.showOnboardingHeader() && <OnboardingAppHeader />}
{completedOnboarding ? <NetworkDropdown /> : null}
{process.env.MULTICHAIN ? null : <AccountMenu />}
{process.env.MULTICHAIN && isAccountMenuOpen ? (
<AccountListMenu onClose={() => toggleAccountMenu()} />
) : null}
{process.env.MULTICHAIN && isNetworkMenuOpen ? (
<NetworkListMenu onClose={() => toggleNetworkMenu()} />
) : null}
<div className="main-container-wrapper">
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}

View File

@ -19,6 +19,7 @@ import {
setLastActiveTime,
setMouseUserState,
toggleAccountMenu,
toggleNetworkMenu,
} from '../../store/actions';
import { pageChanged } from '../../ducks/history/history';
import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps';
@ -57,6 +58,7 @@ function mapStateToProps(state) {
isCurrentProviderCustom: isCurrentProviderCustom(state),
completedOnboarding,
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
};
}
@ -70,6 +72,7 @@ function mapDispatchToProps(dispatch) {
pageChanged: (path) => dispatch(pageChanged(path)),
prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()),
toggleAccountMenu: () => dispatch(toggleAccountMenu()),
toggleNetworkMenu: () => dispatch(toggleNetworkMenu()),
};
}

View File

@ -1111,6 +1111,13 @@ export function getNetworkConfigurations(state) {
return state.metamask.networkConfigurations;
}
export function getCurrentNetwork(state) {
const allNetworks = getAllNetworks(state);
const currentChainId = getCurrentChainId(state);
return allNetworks.find((network) => network.chainId === currentChainId);
}
export function getAllNetworks(state) {
const networkConfigurations = getNetworkConfigurations(state) || {};
const showTestnetNetworks = getShowTestNetworks(state);

View File

@ -48,6 +48,7 @@ export const SHOW_LOADING = 'SHOW_LOADING_INDICATION';
export const HIDE_LOADING = 'HIDE_LOADING_INDICATION';
export const TOGGLE_ACCOUNT_MENU = 'TOGGLE_ACCOUNT_MENU';
export const TOGGLE_NETWORK_MENU = 'TOGGLE_NETWORK_MENU';
// preferences
export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE';

View File

@ -3122,6 +3122,12 @@ export function toggleAccountMenu() {
};
}
export function toggleNetworkMenu() {
return {
type: actionConstants.TOGGLE_NETWORK_MENU,
};
}
export function setParticipateInMetaMetrics(
participationPreference: boolean,
): ThunkAction<