diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7de8d265f..3441e3e80 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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" }, diff --git a/ui/components/multichain/app-footer/app-footer-actions.tsx b/ui/components/multichain/app-footer/app-footer-actions.tsx new file mode 100644 index 000000000..ab5cba45b --- /dev/null +++ b/ui/components/multichain/app-footer/app-footer-actions.tsx @@ -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, + }; +} diff --git a/ui/components/multichain/app-footer/app-footer.js b/ui/components/multichain/app-footer/app-footer.js index b0defc078..b4862ced7 100644 --- a/ui/components/multichain/app-footer/app-footer.js +++ b/ui/components/multichain/app-footer/app-footer.js @@ -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 = () => { { backgroundColor={BackgroundColor.primaryDefault} size={ButtonIconSize.Lg} borderRadius={BorderRadius.full} + onClick={() => dispatch(showSelectActionModal())} /> + + + + + + + + + + Buy + + + + + Buy crypto with MetaMask + + + + +`; diff --git a/ui/components/multichain/select-action-modal-item/index.js b/ui/components/multichain/select-action-modal-item/index.js new file mode 100644 index 000000000..5246bae93 --- /dev/null +++ b/ui/components/multichain/select-action-modal-item/index.js @@ -0,0 +1 @@ +export { SelectActionModalItem } from './select-action-modal-item'; diff --git a/ui/components/multichain/select-action-modal-item/select-action-item.test.js b/ui/components/multichain/select-action-modal-item/select-action-item.test.js new file mode 100644 index 000000000..0febb1abc --- /dev/null +++ b/ui/components/multichain/select-action-modal-item/select-action-item.test.js @@ -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( + , + ); + expect(container).toMatchSnapshot(); + expect(getByTestId('select-action-modal-item')).toBeDefined(); + }); +}); diff --git a/ui/components/multichain/select-action-modal-item/select-action-modal-item.js b/ui/components/multichain/select-action-modal-item/select-action-modal-item.js new file mode 100644 index 000000000..dee286277 --- /dev/null +++ b/ui/components/multichain/select-action-modal-item/select-action-modal-item.js @@ -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 ( + { + e.preventDefault(); + onClick(); + }} + className="select-action-modal-item" + data-testid="select-action-modal-item" + width={BlockSize.Full} + > + + + + + + + {primaryText} + + {showIcon && ( + + )} + + + {secondaryText} + + + + ); +}; + +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, +}; diff --git a/ui/components/multichain/select-action-modal-item/select-action-modal-item.scss b/ui/components/multichain/select-action-modal-item/select-action-modal-item.scss new file mode 100644 index 000000000..a65f55e4c --- /dev/null +++ b/ui/components/multichain/select-action-modal-item/select-action-modal-item.scss @@ -0,0 +1,6 @@ +.select-action-modal-item { + &:hover, + &:focus-within { + background-color: var(--color-background-default-hover); + } +} diff --git a/ui/components/multichain/select-action-modal-item/select-action-modal-item.stories.js b/ui/components/multichain/select-action-modal-item/select-action-modal-item.stories.js new file mode 100644 index 000000000..a0b8aa431 --- /dev/null +++ b/ui/components/multichain/select-action-modal-item/select-action-modal-item.stories.js @@ -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) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/select-action-modal/index.js b/ui/components/multichain/select-action-modal/index.js new file mode 100644 index 000000000..84119c0ca --- /dev/null +++ b/ui/components/multichain/select-action-modal/index.js @@ -0,0 +1 @@ +export { SelectActionModal } from './select-action-modal'; diff --git a/ui/components/multichain/select-action-modal/select-action-modal.js b/ui/components/multichain/select-action-modal/select-action-modal.js new file mode 100644 index 000000000..410768db0 --- /dev/null +++ b/ui/components/multichain/select-action-modal/select-action-modal.js @@ -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 ( + + + + {t('selectAnAction')} + + { + ///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask) + { + 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) + { + stakingEvent(); + global.platform.openTab({ + url: MMI_STAKE_WEBSITE, + }); + onClose(); + }} + /> + ///: END:ONLY_INCLUDE_IN + } + { + ///: 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(); + }} + /> + { + 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) + { + 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 + } + + + + ); +}; + +SelectActionModal.propTypes = { + /** + * onClose handler for Modal + */ + onClose: PropTypes.func, +}; diff --git a/ui/components/multichain/select-action-modal/select-action-modal.stories.js b/ui/components/multichain/select-action-modal/select-action-modal.stories.js new file mode 100644 index 000000000..300db40d4 --- /dev/null +++ b/ui/components/multichain/select-action-modal/select-action-modal.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { SelectActionModal } from '.'; + +export default { + title: 'Components/Multichain/SelectActionModal', + component: SelectActionModal, +}; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/select-action-modal/select-action-modal.test.js b/ui/components/multichain/select-action-modal/select-action-modal.test.js new file mode 100644 index 000000000..e951a59bb --- /dev/null +++ b/ui/components/multichain/select-action-modal/select-action-modal.test.js @@ -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(, 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( + , + 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( + , + 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( + , + 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( + , + 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( + , + 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( + , + 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( + , + 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( + , + mockedStore, + ); + expect(queryByText('Send')).toBeInTheDocument(); + }); +}); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index 0ad619c0a..4a440031c 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -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 { diff --git a/ui/helpers/constants/common.ts b/ui/helpers/constants/common.ts index eb1b49467..f59f45166 100644 --- a/ui/helpers/constants/common.ts +++ b/ui/helpers/constants/common.ts @@ -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 diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 1fb4f4aad..bb384a17e 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -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 ? ( hideImportTokensModal()} /> ) : null} + {isSelectActionModalOpen ? ( + hideSelectActionModal()} /> + ) : null} {isLoading ? : null} {!isLoading && isNetworkLoading ? : null} diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index d964dfc54..22fa69790 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -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()), }; } diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 0a48192b5..1c94d42e5 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -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';
+ Buy +
+ Buy crypto with MetaMask +