1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 09:23:21 +01:00

UX Multichain: Added Select an action Modal (#20559)

* added select-action-modal skeleton

* added select action modal item

* replaced stake link with constant

* added route for open and close of modal

* updated lint errors

* lint fix

* updated tests

* revert unnecessary changes

* fixed lint errors

* added suggestions

* lint fix

* updated test

* nit fix

* updated select action item to use button

* removed unused fragments

* moved onClose command to bottom

* moved select action modal on footer click

* changed isDisabled to disabled

* added hover and updated test

* nit fix
This commit is contained in:
Nidhi Kumari 2023-08-29 22:15:30 +05:30 committed by GitHub
parent 215430236e
commit 6a17d76efc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 824 additions and 3 deletions

View File

@ -596,6 +596,12 @@
"bridge": {
"message": "Bridge"
},
"bridgeDescription": {
"message": "Transfer tokens from different networks"
},
"bridgeDisabled": {
"message": "Bridge is not available in this network"
},
"browserNotSupported": {
"message": "Your browser is not supported..."
},
@ -615,6 +621,12 @@
"message": "Buy $1",
"description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase"
},
"buyDescription": {
"message": "Hold up your crypto and earn potential profits"
},
"buyDisabled": {
"message": "Buy is not available in this network"
},
"buyMoreAsset": {
"message": "Buy more $1",
"description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase"
@ -3753,6 +3765,9 @@
"selectAnAccountHelp": {
"message": "Select the custodian accounts to use in MetaMask Institutional."
},
"selectAnAction": {
"message": "Select an action"
},
"selectHdPath": {
"message": "Select HD path"
},
@ -3780,6 +3795,9 @@
"sendBugReport": {
"message": "Send us a bug report."
},
"sendDescription": {
"message": "Send crypto to any account"
},
"sendSpecifiedTokens": {
"message": "Send $1",
"description": "Symbol of the specified token"
@ -4226,6 +4244,9 @@
"stake": {
"message": "Stake"
},
"stakeDescription": {
"message": "Hold up your crypto and earn potential profits"
},
"stateLogError": {
"message": "Error in retrieving state logs."
},
@ -4417,9 +4438,15 @@
"swapDecentralizedExchange": {
"message": "Decentralized exchange"
},
"swapDescription": {
"message": "Swap and trade your tokens"
},
"swapDirectContract": {
"message": "Direct contract"
},
"swapDisabled": {
"message": "Swap is not available in this network"
},
"swapEditLimit": {
"message": "Edit limit"
},

View File

@ -0,0 +1,14 @@
import { Action } from 'redux';
import * as actionConstants from '../../../store/actionConstants';
export function showSelectActionModal(): Action {
return {
type: actionConstants.SELECT_ACTION_MODAL_OPEN,
};
}
export function hideSelectActionModal(): Action {
return {
type: actionConstants.SELECT_ACTION_MODAL_CLOSE,
};
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import {
CONNECTED_ROUTE,
DEFAULT_ROUTE,
@ -26,10 +27,12 @@ import {
TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
import { showSelectActionModal } from './app-footer-actions';
export const AppFooter = () => {
const t = useI18nContext();
const location = useLocation();
const dispatch = useDispatch();
const walletRoute = `#${DEFAULT_ROUTE}`;
const connectedRoute = `#${CONNECTED_ROUTE}`;
@ -78,7 +81,7 @@ export const AppFooter = () => {
</Text>
</Box>
<Box
as="a"
as="button"
width={BlockSize.OneThird}
padding={2}
display={Display.Flex}
@ -96,6 +99,7 @@ export const AppFooter = () => {
backgroundColor={BackgroundColor.primaryDefault}
size={ButtonIconSize.Lg}
borderRadius={BorderRadius.full}
onClick={() => dispatch(showSelectActionModal())}
/>
</Box>
<Box

View File

@ -21,3 +21,5 @@ export { ImportAccount } from './import-account';
export { ImportNftsModal } from './import-nfts-modal';
export { AccountDetailsMenuItem, ViewExplorerMenuItem } from './menu-items';
export { ImportTokensModal } from './import-tokens-modal';
export { SelectActionModal } from './select-action-modal';
export { SelectActionModalItem } from './select-action-modal-item';

View File

@ -21,4 +21,4 @@
@import 'product-tour-popover/product-tour-popover';
@import 'nft-item/nft-item';
@import 'import-tokens-modal/import-tokens-modal';
@import 'select-action-modal-item/select-action-modal-item';

View File

@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectActionModalItem should render correctly 1`] = `
<div>
<button
class="mm-box select-action-modal-item mm-box--padding-top-4 mm-box--padding-bottom-4 mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-row mm-box--width-full mm-box--background-color-transparent"
data-testid="select-action-modal-item"
>
<div
class="mm-box"
>
<div
class="mm-box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-icon select-action-modal-item__avatar mm-text--body-sm mm-text--text-transform-uppercase mm-box--display-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-full mm-box--border-color-transparent box--border-style-solid box--border-width-1"
>
<span
class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-inherit"
style="mask-image: url('./images/icons/add.svg');"
/>
</div>
</div>
<div
class="mm-box mm-box--display-flex mm-box--flex-direction-column"
>
<div
class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-direction-row mm-box--align-items-center"
>
<p
class="mm-box mm-text mm-text--body-lg-medium mm-text--text-align-left mm-box--color-text-default"
>
Buy
</p>
<span
class="mm-box mm-icon mm-icon--size-xs mm-box--display-inline-block mm-box--color-icon-alternative"
style="mask-image: url('./images/icons/export.svg');"
/>
</div>
<p
class="mm-box mm-text mm-text--body-md mm-text--text-align-left mm-box--color-text-default"
>
Buy crypto with MetaMask
</p>
</div>
</button>
</div>
`;

View File

@ -0,0 +1 @@
export { SelectActionModalItem } from './select-action-modal-item';

View File

@ -0,0 +1,21 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IconName } from '../../component-library';
import { SelectActionModalItem } from '.';
describe('SelectActionModalItem', () => {
it('should render correctly', () => {
const props = {
showIcon: true,
primaryText: 'Buy',
secondaryText: 'Buy crypto with MetaMask',
actionIcon: IconName.Add,
};
const { container, getByTestId } = render(
<SelectActionModalItem {...props} />,
);
expect(container).toMatchSnapshot();
expect(getByTestId('select-action-modal-item')).toBeDefined();
});
});

View File

@ -0,0 +1,111 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
AlignItems,
BackgroundColor,
BlockSize,
Display,
FlexDirection,
IconColor,
TextAlign,
TextVariant,
} from '../../../helpers/constants/design-system';
import {
AvatarIcon,
AvatarIconSize,
Box,
Icon,
IconName,
IconSize,
Text,
} from '../../component-library';
export const SelectActionModalItem = ({
actionIcon,
onClick,
showIcon,
primaryText,
secondaryText,
disabled,
}) => {
if (disabled) {
return null;
}
return (
<Box
paddingTop={4}
paddingBottom={4}
gap={4}
display={Display.Flex}
flexDirection={FlexDirection.Row}
as="button"
backgroundColor={BackgroundColor.transparent}
onClick={(e) => {
e.preventDefault();
onClick();
}}
className="select-action-modal-item"
data-testid="select-action-modal-item"
width={BlockSize.Full}
>
<Box>
<AvatarIcon
iconName={actionIcon}
color={IconColor.primaryInverse}
backgroundColor={BackgroundColor.primaryDefault}
size={AvatarIconSize.Md}
className="select-action-modal-item__avatar"
/>
</Box>
<Box display={Display.Flex} flexDirection={FlexDirection.Column}>
<Box
display={Display.Flex}
flexDirection={FlexDirection.Row}
gap={2}
alignItems={AlignItems.center}
>
<Text variant={TextVariant.bodyLgMedium} textAlign={TextAlign.Left}>
{primaryText}
</Text>
{showIcon && (
<Icon
name={IconName.Export}
size={IconSize.Xs}
color={IconColor.iconAlternative}
/>
)}
</Box>
<Text variant={TextVariant.bodyMd} textAlign={TextAlign.Left}>
{secondaryText}
</Text>
</Box>
</Box>
);
};
SelectActionModalItem.propTypes = {
/**
* Show link icon with text
*/
showIcon: PropTypes.bool,
/**
* onClick handler for each action
*/
onClick: PropTypes.func.isRequired,
/**
* Icon for each action Item
*/
actionIcon: PropTypes.string.isRequired,
/**
* Title for each action Item
*/
primaryText: PropTypes.string.isRequired,
/**
* Description for each action Item
*/
secondaryText: PropTypes.string.isRequired,
/**
* Disable bridge and swap for selected networks
*/
disabled: PropTypes.bool,
};

View File

@ -0,0 +1,6 @@
.select-action-modal-item {
&:hover,
&:focus-within {
background-color: var(--color-background-default-hover);
}
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import { IconName } from '../../component-library';
import { SelectActionModalItem } from '.';
export default {
title: 'Components/Multichain/SelectActionModalItem',
component: SelectActionModalItem,
argTypes: {
showIcon: {
control: 'boolean',
},
primaryText: {
control: 'text',
},
actionIcon: {
control: 'text',
},
secondaryText: {
control: 'text',
},
onClick: {
action: 'onClick',
},
},
args: {
showIcon: true,
primaryText: 'Buy',
secondaryText: 'Buy crypto with MetaMask',
actionIcon: IconName.Add,
},
};
export const DefaultStory = (args) => <SelectActionModalItem {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1 @@
export { SelectActionModal } from './select-action-modal';

View File

@ -0,0 +1,249 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import {
useHistory,
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
useLocation,
///: END:ONLY_INCLUDE_IN
} from 'react-router-dom';
import {
Box,
IconName,
Modal,
ModalContent,
ModalHeader,
ModalOverlay,
} from '../../component-library';
import { SelectActionModalItem } from '../select-action-modal-item';
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
import useRamps from '../../../hooks/experiences/useRamps';
import { getPortfolioUrl } from '../../../helpers/utils/portfolio';
///: END:ONLY_INCLUDE_IN
import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
MetaMetricsSwapsEventSource,
///: END:ONLY_INCLUDE_IN
} from '../../../../shared/constants/metametrics';
import {
getCurrentChainId,
getIsSwapsChain,
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
getSwapsDefaultToken,
getCurrentKeyring,
getIsBridgeChain,
getIsBuyableChain,
getMetaMetricsId,
///: END:ONLY_INCLUDE_IN
} from '../../../selectors';
import {
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
BUILD_QUOTE_ROUTE,
///: END:ONLY_INCLUDE_IN
SEND_ROUTE,
} from '../../../helpers/constants/routes';
import { startNewDraftTransaction } from '../../../ducks/send';
import { I18nContext } from '../../../contexts/i18n';
import { AssetType } from '../../../../shared/constants/transaction';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import { MMI_SWAPS_URL } from '../../../../shared/constants/swaps';
import { MMI_STAKE_WEBSITE } from '../../../helpers/constants/common';
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
///: END:ONLY_INCLUDE_IN
export const SelectActionModal = ({ onClose }) => {
const dispatch = useDispatch();
const t = useContext(I18nContext);
const trackEvent = useContext(MetaMetricsContext);
const history = useHistory();
const chainId = useSelector(getCurrentChainId);
const isSwapsChain = useSelector(getIsSwapsChain);
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
const location = useLocation();
const { openBuyCryptoInPdapp } = useRamps();
const defaultSwapsToken = useSelector(getSwapsDefaultToken);
const keyring = useSelector(getCurrentKeyring);
const usingHardwareWallet = isHardwareKeyring(keyring?.type);
const isBridgeChain = useSelector(getIsBridgeChain);
const metaMetricsId = useSelector(getMetaMetricsId);
const isBuyableChain = useSelector(getIsBuyableChain);
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const stakingEvent = () => {
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.MMIPortfolioButtonClicked,
});
};
///: END:ONLY_INCLUDE_IN
return (
<Modal
isOpen
onClose={onClose}
className="select-action-modal"
data-testid="select-action-modal"
>
<ModalOverlay />
<ModalContent>
<ModalHeader onClose={onClose}>{t('selectAnAction')}</ModalHeader>
<Box className="select-action-modal__container" marginTop={6}>
{
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
<SelectActionModalItem
actionIcon={IconName.Add}
showIcon
primaryText={t('buy')}
secondaryText={t('buyDescription')}
disabled={!isBuyableChain}
tooltipTitle={t('buyDisabled')}
onClick={() => {
openBuyCryptoInPdapp();
trackEvent({
event: MetaMetricsEventName.NavBuyButtonClicked,
category: MetaMetricsEventCategory.Navigation,
properties: {
location: 'Home',
text: 'Buy',
chain_id: chainId,
token_symbol: defaultSwapsToken,
},
});
onClose();
}}
/>
///: END:ONLY_INCLUDE_IN
}
{
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
<SelectActionModalItem
actionIcon={IconName.Stake}
showIcon
primaryText={t('stake')}
secondaryText={t('stakeDescription')}
onClick={() => {
stakingEvent();
global.platform.openTab({
url: MMI_STAKE_WEBSITE,
});
onClose();
}}
/>
///: END:ONLY_INCLUDE_IN
}
<SelectActionModalItem
actionIcon={IconName.SwapHorizontal}
primaryText={t('swap')}
secondaryText={t('swapDescription')}
disabled={!isSwapsChain}
tooltipTitle={t('swapDisabled')}
onClick={() => {
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
global.platform.openTab({
url: MMI_SWAPS_URL,
});
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
if (isSwapsChain) {
trackEvent({
event: MetaMetricsEventName.NavSwapButtonClicked,
category: MetaMetricsEventCategory.Swaps,
properties: {
token_symbol: 'ETH',
location: MetaMetricsSwapsEventSource.MainView,
text: 'Swap',
chain_id: chainId,
},
});
dispatch(setSwapsFromToken(defaultSwapsToken));
if (usingHardwareWallet) {
global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE);
} else {
history.push(BUILD_QUOTE_ROUTE);
}
}
///: END:ONLY_INCLUDE_IN
onClose();
}}
/>
<SelectActionModalItem
actionIcon={IconName.Arrow2UpRight}
primaryText={t('send')}
secondaryText={t('sendDescription')}
onClick={async () => {
trackEvent({
event: MetaMetricsEventName.NavSendButtonClicked,
category: MetaMetricsEventCategory.Navigation,
properties: {
token_symbol: 'ETH',
location: 'Home',
text: 'Send',
chain_id: chainId,
},
});
await dispatch(
startNewDraftTransaction({ type: AssetType.native }),
);
history.push(SEND_ROUTE);
onClose();
}}
/>
{
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
<SelectActionModalItem
actionIcon={IconName.Arrow2UpRight}
showIcon
primaryText={t('bridge')}
secondaryText={t('bridgeDescription')}
disabled={!isBridgeChain}
tooltipTitle={t('bridgeDisabled')}
onClick={() => {
if (isBridgeChain) {
const portfolioUrl = getPortfolioUrl(
'bridge',
'ext_bridge_button',
metaMetricsId,
);
global.platform.openTab({
url: `${portfolioUrl}${
location.pathname.includes('asset') ? '&token=native' : ''
}`,
});
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.BridgeLinkClicked,
properties: {
location: 'Home',
text: 'Bridge',
chain_id: chainId,
token_symbol: 'ETH',
},
});
}
onClose();
}}
/>
///: END:ONLY_INCLUDE_IN
}
</Box>
</ModalContent>
</Modal>
);
};
SelectActionModal.propTypes = {
/**
* onClose handler for Modal
*/
onClose: PropTypes.func,
};

View File

@ -0,0 +1,11 @@
import React from 'react';
import { SelectActionModal } from '.';
export default {
title: 'Components/Multichain/SelectActionModal',
component: SelectActionModal,
};
export const DefaultStory = () => <SelectActionModal />;
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,265 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fireEvent, waitFor } from '@testing-library/react';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/jest/rendering';
import { KeyringType } from '../../../../shared/constants/keyring';
import { CHAIN_IDS } from '../../../../shared/constants/network';
import { SelectActionModal } from '.';
// Mock BUYABLE_CHAINS_MAP
jest.mock('../../../../shared/constants/network', () => ({
...jest.requireActual('../../../../shared/constants/network'),
BUYABLE_CHAINS_MAP: {
// MAINNET
'0x1': {
nativeCurrency: 'ETH',
network: 'ethereum',
},
// POLYGON
'0x89': {
nativeCurrency: 'MATIC',
network: 'polygon',
},
},
}));
let openTabSpy;
describe('Select Action Modal', () => {
beforeAll(() => {
jest.clearAllMocks();
Object.defineProperty(global, 'platform', {
value: {
openTab: jest.fn(),
},
});
openTabSpy = jest.spyOn(global.platform, 'openTab');
});
beforeEach(() => {
openTabSpy.mockClear();
});
const mockStore = {
metamask: {
providerConfig: {
type: 'test',
chainId: CHAIN_IDS.MAINNET,
},
cachedBalances: {
'0x1': {
'0x1': '0x1F4',
},
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
useCurrencyRateCheck: true,
conversionRate: 2,
identities: {
'0x1': {
address: '0x1',
},
},
accounts: {
'0x1': {
address: '0x1',
balance: '0x1F4',
},
},
selectedAddress: '0x1',
keyrings: [
{
type: KeyringType.imported,
accounts: ['0x1', '0x2'],
},
{
type: KeyringType.ledger,
accounts: [],
},
],
contractExchangeRates: {},
},
};
const store = configureMockStore([thunk])(mockState);
it('should render correctly', () => {
const { getByTestId } = renderWithProvider(<SelectActionModal />, store);
expect(getByTestId('select-action-modal')).toBeDefined();
});
it('should have the Buy native token enabled if chain id is part of supported buyable chains', () => {
const mockedStoreWithBuyableChainId = {
metamask: {
...mockStore.metamask,
providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON },
},
};
const mockedStore = configureMockStore([thunk])(
mockedStoreWithBuyableChainId,
);
const { queryByText } = renderWithProvider(
<SelectActionModal />,
mockedStore,
);
expect(queryByText('Buy')).toBeInTheDocument();
});
it('should open the Buy native token URI when clicking on Buy button for a buyable chain ID', async () => {
const mockedStoreWithBuyableChainId = {
metamask: {
...mockStore.metamask,
providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON },
},
};
const mockedStore = configureMockStore([thunk])(
mockedStoreWithBuyableChainId,
);
const onClose = jest.fn();
const { queryByText } = renderWithProvider(
<SelectActionModal onClose={onClose} />,
mockedStore,
);
const buyButton = queryByText('Buy');
expect(buyButton).toBeInTheDocument();
expect(buyButton).not.toBeDisabled();
fireEvent.click(buyButton);
expect(onClose).toHaveBeenCalledTimes(1);
expect(openTabSpy).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(openTabSpy).toHaveBeenCalledWith({
url: expect.stringContaining(`/buy?metamaskEntry=ext_buy_button`),
}),
);
});
it('should not have the Buy native token button if chain id is not part of supported buyable chains', () => {
const mockedStoreWithUnbuyableChainId = {
metamask: {
...mockStore.metamask,
providerConfig: { type: 'test', chainId: CHAIN_IDS.FANTOM },
},
};
const mockedStore = configureMockStore([thunk])(
mockedStoreWithUnbuyableChainId,
);
const { queryByText } = renderWithProvider(
<SelectActionModal />,
mockedStore,
);
const buyButton = queryByText('Buy');
expect(buyButton).not.toBeInTheDocument();
});
it('should have the Bridge button if chain id is a part of supported chains', () => {
const mockedAvalancheStore = {
...mockStore,
metamask: {
...mockStore.metamask,
providerConfig: {
...mockStore.metamask.providerConfig,
chainId: '0xa86a',
},
},
};
const mockedStore = configureMockStore([thunk])(mockedAvalancheStore);
const { queryByText } = renderWithProvider(
<SelectActionModal />,
mockedStore,
);
const bridgeButton = queryByText('Bridge');
expect(bridgeButton).toBeInTheDocument();
});
it('should open the Bridge URI when clicking on Bridge button on supported network', async () => {
const onClose = jest.fn();
const mockedAvalancheStore = {
...mockStore,
metamask: {
...mockStore.metamask,
providerConfig: {
...mockStore.metamask.providerConfig,
chainId: '0xa86a',
},
},
};
const mockedStore = configureMockStore([thunk])(mockedAvalancheStore);
const { queryByText } = renderWithProvider(
<SelectActionModal onClose={onClose} />,
mockedStore,
);
const bridgeButton = queryByText('Bridge');
expect(bridgeButton).toBeInTheDocument();
fireEvent.click(bridgeButton);
expect(onClose).toHaveBeenCalledTimes(1);
expect(openTabSpy).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(openTabSpy).toHaveBeenCalledWith({
url: expect.stringContaining('/bridge?metamaskEntry=ext_bridge_button'),
}),
);
});
it('should not have the Bridge button if chain id is not part of supported chains', () => {
const mockedFantomStore = {
...mockStore,
metamask: {
...mockStore.metamask,
providerConfig: {
...mockStore.metamask.providerConfig,
chainId: '0xfa',
},
},
};
const mockedStore = configureMockStore([thunk])(mockedFantomStore);
const { queryByText } = renderWithProvider(
<SelectActionModal />,
mockedStore,
);
const buyButton = queryByText('Bridge');
expect(buyButton).not.toBeInTheDocument();
});
it('should have the Swap button if chain id is part of supported buyable chains', () => {
const mockedStoreWithSwapableChainId = {
metamask: {
...mockStore.metamask,
providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON },
},
};
const mockedStore = configureMockStore([thunk])(
mockedStoreWithSwapableChainId,
);
const { queryByText } = renderWithProvider(
<SelectActionModal />,
mockedStore,
);
expect(queryByText('Swap')).toBeInTheDocument();
});
it('should have the Send button if chain id is part of supported buyable chains', () => {
const mockedStoreWithSendChainId = {
metamask: {
...mockStore.metamask,
providerConfig: { type: 'test', chainId: CHAIN_IDS.POLYGON },
},
};
const mockedStore = configureMockStore([thunk])(mockedStoreWithSendChainId);
const { queryByText } = renderWithProvider(
<SelectActionModal />,
mockedStore,
);
expect(queryByText('Send')).toBeInTheDocument();
});
});

View File

@ -29,6 +29,7 @@ interface AppState {
importNftsModalOpen: boolean;
showIpfsModalOpen: boolean;
importTokensModalOpen: boolean;
showSelectActionModal: boolean;
accountDetail: {
subview?: string;
accountExport?: string;
@ -100,6 +101,7 @@ const initialState: AppState = {
importNftsModalOpen: false,
showIpfsModalOpen: false,
importTokensModalOpen: false,
showSelectActionModal: false,
accountDetail: {
privateKey: '',
},
@ -205,6 +207,18 @@ export default function reduceApp(
importTokensModalOpen: false,
};
case actionConstants.SELECT_ACTION_MODAL_OPEN:
return {
...appState,
showSelectActionModal: true,
};
case actionConstants.SELECT_ACTION_MODAL_CLOSE:
return {
...appState,
showSelectActionModal: false,
};
// alert methods
case actionConstants.ALERT_OPEN:
return {

View File

@ -7,6 +7,7 @@ const _contractAddressLink =
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const _mmiWebSite = 'https://metamask.io/institutions/';
export const MMI_WEB_SITE = _mmiWebSite;
export const MMI_STAKE_WEBSITE = 'https://metamask-institutional.io/stake';
///: END:ONLY_INCLUDE_IN
// eslint-disable-next-line prefer-destructuring

View File

@ -34,6 +34,7 @@ import {
AccountDetails,
ImportNftsModal,
ImportTokensModal,
SelectActionModal,
} from '../../components/multichain';
import UnlockPage from '../unlock-page';
import Alerts from '../../components/app/alerts';
@ -164,6 +165,8 @@ export default class Routes extends Component {
hideIpfsModal: PropTypes.func.isRequired,
isImportTokensModalOpen: PropTypes.bool.isRequired,
hideImportTokensModal: PropTypes.func.isRequired,
isSelectActionModalOpen: PropTypes.bool.isRequired,
hideSelectActionModal: PropTypes.func.isRequired,
};
static contextTypes = {
@ -511,12 +514,14 @@ export default class Routes extends Component {
toggleNetworkMenu,
accountDetailsAddress,
isImportTokensModalOpen,
isSelectActionModalOpen,
location,
isImportNftsModalOpen,
hideImportNftsModal,
isIpfsModalOpen,
hideIpfsModal,
hideImportTokensModal,
hideSelectActionModal,
} = this.props;
const loadMessage =
@ -585,6 +590,9 @@ export default class Routes extends Component {
{isImportTokensModalOpen ? (
<ImportTokensModal onClose={() => hideImportTokensModal()} />
) : null}
{isSelectActionModalOpen ? (
<SelectActionModal onClose={() => hideSelectActionModal()} />
) : null}
<Box className="main-container-wrapper">
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}

View File

@ -14,7 +14,6 @@ import {
isCurrentProviderCustom,
} from '../../selectors';
import {
hideImportTokensModal,
lockMetamask,
hideImportNftsModal,
hideIpfsModal,
@ -23,7 +22,9 @@ import {
setMouseUserState,
toggleAccountMenu,
toggleNetworkMenu,
hideImportTokensModal,
} from '../../store/actions';
import { hideSelectActionModal } from '../../components/multichain/app-footer/app-footer-actions';
import { pageChanged } from '../../ducks/history/history';
import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps';
import { getSendStage } from '../../ducks/send';
@ -69,6 +70,7 @@ function mapStateToProps(state) {
accountDetailsAddress: state.appState.accountDetailsAddress,
isImportNftsModalOpen: state.appState.importNftsModalOpen,
isIpfsModalOpen: state.appState.showIpfsModalOpen,
isSelectActionModalOpen: state.appState.showSelectActionModal,
};
}
@ -86,6 +88,7 @@ function mapDispatchToProps(dispatch) {
hideImportNftsModal: () => dispatch(hideImportNftsModal()),
hideIpfsModal: () => dispatch(hideIpfsModal()),
hideImportTokensModal: () => dispatch(hideImportTokensModal()),
hideSelectActionModal: () => dispatch(hideSelectActionModal()),
};
}

View File

@ -15,6 +15,9 @@ export const SHOW_IPFS_MODAL_OPEN = 'UI_IPFS_MODAL_OPEN';
export const SHOW_IPFS_MODAL_CLOSE = 'UI_IPFS_MODAL_CLOSE';
export const IMPORT_TOKENS_POPOVER_OPEN = 'UI_IMPORT_TOKENS_POPOVER_OPEN';
export const IMPORT_TOKENS_POPOVER_CLOSE = 'UI_IMPORT_TOKENS_POPOVER_CLOSE';
export const SELECT_ACTION_MODAL_OPEN = 'UI_SELECT_ACTION_MODAL_OPEN';
export const SELECT_ACTION_MODAL_CLOSE = 'UI_SELECT_ACTION_MODAL_CLOSE';
// remote state
export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE';
export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED';