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:
parent
215430236e
commit
6a17d76efc
27
app/_locales/en/messages.json
generated
27
app/_locales/en/messages.json
generated
@ -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"
|
||||
},
|
||||
|
14
ui/components/multichain/app-footer/app-footer-actions.tsx
Normal file
14
ui/components/multichain/app-footer/app-footer-actions.tsx
Normal 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,
|
||||
};
|
||||
}
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
`;
|
@ -0,0 +1 @@
|
||||
export { SelectActionModalItem } from './select-action-modal-item';
|
@ -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();
|
||||
});
|
||||
});
|
@ -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,
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
.select-action-modal-item {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: var(--color-background-default-hover);
|
||||
}
|
||||
}
|
@ -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';
|
1
ui/components/multichain/select-action-modal/index.js
Normal file
1
ui/components/multichain/select-action-modal/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { SelectActionModal } from './select-action-modal';
|
@ -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,
|
||||
};
|
@ -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';
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user