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

New network info popup (#13319)

This commit is contained in:
Filip Sekulic 2022-08-23 17:04:07 +02:00 committed by GitHub
parent 4dab986ad2
commit 365bf11fdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 526 additions and 0 deletions

View File

@ -369,6 +369,9 @@
"assets": {
"message": "Assets"
},
"attemptSendingAssets": {
"message": "If you attempt to send assets directly from one network to another, this may result in permanent asset loss. Make sure to use a bridge."
},
"attemptToCancel": {
"message": "Attempt to cancel?"
},
@ -583,6 +586,9 @@
"message": "Click here to connect your Ledger via WebHID",
"description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid"
},
"clickToManuallyAdd": {
"message": "Click here to manually add the tokens."
},
"clickToRevealSeed": {
"message": "Click here to reveal secret words"
},
@ -2041,6 +2047,10 @@
"name": {
"message": "Name"
},
"nativeToken": {
"message": "The native token on this network is $1. It is the token used for gas fees.",
"description": "$1 represents the name of the native token on the current network"
},
"needCryptoInWallet": {
"message": "To interact with decentralized applications using MetaMask, youll need $1 in your wallet.",
"description": "$1 represents the cypto symbol to be purchased"
@ -3762,6 +3772,9 @@
"switchToThisAccount": {
"message": "Switch to this account"
},
"switchedTo": {
"message": "You have switched to"
},
"switchingNetworksCancelsPendingConfirmations": {
"message": "Switching networks will cancel all pending confirmations"
},
@ -3828,6 +3841,9 @@
"themeDescription": {
"message": "Choose your preferred MetaMask theme."
},
"thingsToKeep": {
"message": "Things to keep in mind:"
},
"thisWillCreate": {
"message": "This will create a new wallet and Secret Recovery Phrase"
},
@ -3890,6 +3906,9 @@
"tokenScamSecurityRisk": {
"message": "token scams and security risks"
},
"tokenShowUp": {
"message": "Your tokens may not automatically show up in your wallet."
},
"tokenSymbol": {
"message": "Token symbol"
},

View File

@ -37,6 +37,14 @@ export default class AppStateController extends EventEmitter {
...initState,
qrHardware: {},
collectiblesDropdownState: {},
usedNetworks: {
'0x1': true,
'0x2a': true,
'0x3': true,
'0x4': true,
'0x5': true,
'0x539': true,
},
});
this.timer = null;
@ -294,4 +302,18 @@ export default class AppStateController extends EventEmitter {
collectiblesDropdownState,
});
}
/**
* Updates the array of the first time used networks
*
* @param chainId
* @returns {void}
*/
setFirstTimeUsedNetwork(chainId) {
const currentState = this.store.getState();
const { usedNetworks } = currentState;
usedNetworks[chainId] = true;
this.store.updateState({ usedNetworks });
}
}

View File

@ -1739,6 +1739,8 @@ export default class MetamaskController extends EventEmitter {
appStateController.updateCollectibleDropDownState.bind(
appStateController,
),
setFirstTimeUsedNetwork:
appStateController.setFirstTimeUsedNetwork.bind(appStateController),
// EnsController
tryReverseResolveAddress:
ensController.reverseResolveAddress.bind(ensController),

View File

@ -36,3 +36,6 @@ export const STATIC_MAINNET_TOKEN_LIST = Object.keys(contractMap).reduce(
},
{},
);
export const TOKEN_API_METASWAP_CODEFI_URL =
'https://token-api.metaswap.codefi.network/tokens/';

View File

@ -0,0 +1 @@
export { default } from './new-network-info';

View File

@ -0,0 +1,48 @@
.new-network-info {
&__wrapper {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.214);
border-radius: 8px;
.popover-footer {
border-top: none;
}
.popover-header {
padding-bottom: 1px;
}
.fa-question-circle,
.popover-header__button {
color: var(--color-icon-default);
}
}
&__token-box {
align-self: center;
margin-top: 8px;
max-width: 245px;
}
&__bullet-paragraph {
border-bottom: 1px solid var(--color-border-default);
}
&__token-show-up {
display: inline;
}
&__button {
display: initial;
padding: 0;
}
&__manually-add-tokens {
display: inline;
}
}
.chip--with-left-icon {
padding-left: 8px;
padding-top: 8px;
padding-bottom: 8px;
}

View File

@ -0,0 +1,225 @@
import React, { useContext, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { I18nContext } from '../../../contexts/i18n';
import Popover from '../popover';
import Button from '../button';
import Identicon from '../identicon/identicon.component';
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
import Box from '../box';
import {
ALIGN_ITEMS,
COLORS,
DISPLAY,
FONT_WEIGHT,
TEXT_ALIGN,
TYPOGRAPHY,
} from '../../../helpers/constants/design-system';
import Typography from '../typography';
import { TOKEN_API_METASWAP_CODEFI_URL } from '../../../../shared/constants/tokens';
import fetchWithCache from '../../../helpers/utils/fetch-with-cache';
import {
getNativeCurrencyImage,
getProvider,
getUseTokenDetection,
} from '../../../selectors';
import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes';
import Chip from '../chip/chip';
import { setFirstTimeUsedNetwork } from '../../../store/actions';
const NewNetworkInfo = () => {
const t = useContext(I18nContext);
const history = useHistory();
const [tokenDetectionSupported, setTokenDetectionSupported] = useState(false);
const [showPopup, setShowPopup] = useState(true);
const autoDetectToken = useSelector(getUseTokenDetection);
const primaryTokenImage = useSelector(getNativeCurrencyImage);
const currentProvider = useSelector(getProvider);
const onCloseClick = () => {
setShowPopup(false);
setFirstTimeUsedNetwork(currentProvider.chainId);
};
const addTokenManually = () => {
history.push(IMPORT_TOKEN_ROUTE);
setShowPopup(false);
setFirstTimeUsedNetwork(currentProvider.chainId);
};
const getIsTokenDetectionSupported = async () => {
const fetchedTokenData = await fetchWithCache(
`${TOKEN_API_METASWAP_CODEFI_URL}${currentProvider.chainId}`,
);
return !fetchedTokenData.error;
};
const checkTokenDetection = async () => {
const fetchedData = await getIsTokenDetectionSupported();
setTokenDetectionSupported(fetchedData);
};
useEffect(() => {
checkTokenDetection();
});
if (!showPopup) {
return null;
}
return (
<Popover
onClose={onCloseClick}
className="new-network-info__wrapper"
footer={
<Button type="primary" onClick={onCloseClick}>
{t('recoveryPhraseReminderConfirm')}
</Button>
}
>
<Typography
variant={TYPOGRAPHY.H4}
color={COLORS.TEXT_DEFAULT}
fontWeight={FONT_WEIGHT.BOLD}
align={TEXT_ALIGN.CENTER}
>
{t('switchedTo')}
</Typography>
<Chip
className="new-network-info__token-box"
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
maxContent={false}
label={
currentProvider.type === NETWORK_TYPE_RPC
? currentProvider.nickname ?? t('privateNetwork')
: t(currentProvider.type)
}
labelProps={{
color: COLORS.TEXT_DEFAULT,
}}
leftIcon={
primaryTokenImage ? (
<Identicon image={primaryTokenImage} diameter={14} />
) : (
<i className="fa fa-question-circle" />
)
}
/>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_DEFAULT}
fontWeight={FONT_WEIGHT.BOLD}
align={TEXT_ALIGN.CENTER}
margin={[8, 0, 0, 0]}
>
{t('thingsToKeep')}
</Typography>
<Box marginRight={4} marginLeft={5} marginTop={6}>
{currentProvider.ticker ? (
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
marginBottom={2}
paddingBottom={2}
className="new-network-info__bullet-paragraph"
>
<Box marginRight={4} color={COLORS.TEXT_DEFAULT}>
&bull;
</Box>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_DEFAULT}
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
key="nativeTokenInfo"
>
{t('nativeToken', [
<Typography
variant={TYPOGRAPHY.H7}
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
fontWeight={FONT_WEIGHT.BOLD}
key="ticker"
>
{currentProvider.ticker}
</Typography>,
])}
</Typography>
</Box>
) : null}
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
marginBottom={2}
paddingBottom={2}
className={
!autoDetectToken || !tokenDetectionSupported
? 'new-network-info__bullet-paragraph'
: null
}
>
<Box marginRight={4} color={COLORS.TEXT_DEFAULT}>
&bull;
</Box>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_DEFAULT}
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
className="new-network-info__bullet-paragraph__text"
>
{t('attemptSendingAssets')}{' '}
<a
href="https://metamask.zendesk.com/hc/en-us/articles/4404424659995"
target="_blank"
rel="noreferrer"
>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.INFO_DEFAULT}
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
>
{t('learnMoreUpperCase')}
</Typography>
</a>
</Typography>
</Box>
{!autoDetectToken || !tokenDetectionSupported ? (
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
marginBottom={2}
paddingBottom={2}
>
<Box marginRight={4} color={COLORS.TEXT_DEFAULT}>
&bull;
</Box>
<Box>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.TEXT_DEFAULT}
className="new-network-info__token-show-up"
>
{t('tokenShowUp')}{' '}
<Button
type="link"
onClick={addTokenManually}
className="new-network-info__button"
>
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.INFO_DEFAULT}
className="new-network-info__manually-add-tokens"
>
{t('clickToManuallyAdd')}
</Typography>
</Button>
</Typography>
</Box>
</Box>
) : null}
</Box>
</Popover>
);
};
export default NewNetworkInfo;

View File

@ -0,0 +1,171 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import nock from 'nock';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import NewNetworkInfo from './new-network-info';
const fetchWithCache =
require('../../../helpers/utils/fetch-with-cache').default;
const state = {
metamask: {
provider: {
ticker: 'ETH',
nickname: '',
chainId: '0x1',
type: 'mainnet',
},
useTokenDetection: false,
nativeCurrency: 'ETH',
},
};
describe('NewNetworkInfo', () => {
afterEach(() => {
nock.cleanAll();
});
it('should render title', async () => {
nock('https://token-api.metaswap.codefi.network')
.get('/tokens/0x1')
.reply(
200,
'[{"address":"0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f","symbol":"SNX","decimals":18,"name":"Synthetix Network Token","iconUrl":"https://assets.coingecko.com/coins/images/3406/large/SNX.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","synthetix","zapper","zerion","zeroEx"],"occurrences":12},{"address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","symbol":"UNI","decimals":18,"name":"Uniswap","iconUrl":"https://images.prismic.io/token-price-prod/d0352dd9-5de8-4633-839d-bc3422c44d9c_UNI%404x.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","zapper","zerion","zeroEx"],"occurrences":11}]',
);
const updateTokenDetectionSupportStatus = await fetchWithCache(
'https://token-api.metaswap.codefi.network/tokens/0x1',
);
const store = configureMockStore()(
state,
updateTokenDetectionSupportStatus,
);
const { getByText } = renderWithProvider(<NewNetworkInfo />, store);
expect(getByText('You have switched to')).toBeInTheDocument();
});
it('should render a question mark icon image', async () => {
nock('https://token-api.metaswap.codefi.network')
.get('/tokens/0x1')
.reply(
200,
'[{"address":"0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f","symbol":"SNX","decimals":18,"name":"Synthetix Network Token","iconUrl":"https://assets.coingecko.com/coins/images/3406/large/SNX.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","synthetix","zapper","zerion","zeroEx"],"occurrences":12},{"address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","symbol":"UNI","decimals":18,"name":"Uniswap","iconUrl":"https://images.prismic.io/token-price-prod/d0352dd9-5de8-4633-839d-bc3422c44d9c_UNI%404x.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","zapper","zerion","zeroEx"],"occurrences":11}]',
);
const updateTokenDetectionSupportStatus = await fetchWithCache(
'https://token-api.metaswap.codefi.network/tokens/0x1',
);
state.metamask.nativeCurrency = '';
const store = configureMockStore()(
state,
updateTokenDetectionSupportStatus,
);
const { container } = renderWithProvider(<NewNetworkInfo />, store);
const questionMark = container.querySelector('.fa fa-question-circle');
expect(questionMark).toBeDefined();
});
it('should render Ethereum Mainnet caption', async () => {
nock('https://token-api.metaswap.codefi.network')
.get('/tokens/0x1')
.reply(
200,
'[{"address":"0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f","symbol":"SNX","decimals":18,"name":"Synthetix Network Token","iconUrl":"https://assets.coingecko.com/coins/images/3406/large/SNX.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","synthetix","zapper","zerion","zeroEx"],"occurrences":12},{"address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","symbol":"UNI","decimals":18,"name":"Uniswap","iconUrl":"https://images.prismic.io/token-price-prod/d0352dd9-5de8-4633-839d-bc3422c44d9c_UNI%404x.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","zapper","zerion","zeroEx"],"occurrences":11}]',
);
const updateTokenDetectionSupportStatus = await fetchWithCache(
'https://token-api.metaswap.codefi.network/tokens/0x1',
);
const store = configureMockStore()(
state,
updateTokenDetectionSupportStatus,
);
const { getByText } = renderWithProvider(<NewNetworkInfo />, store);
expect(getByText('Ethereum Mainnet')).toBeInTheDocument();
});
it('should render things to keep in mind text', async () => {
nock('https://token-api.metaswap.codefi.network')
.get('/tokens/0x1')
.reply(
200,
'[{"address":"0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f","symbol":"SNX","decimals":18,"name":"Synthetix Network Token","iconUrl":"https://assets.coingecko.com/coins/images/3406/large/SNX.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","synthetix","zapper","zerion","zeroEx"],"occurrences":12},{"address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","symbol":"UNI","decimals":18,"name":"Uniswap","iconUrl":"https://images.prismic.io/token-price-prod/d0352dd9-5de8-4633-839d-bc3422c44d9c_UNI%404x.png","aggregators":["aave","bancor","cmc","cryptocom","coinGecko","oneInch","paraswap","pmm","zapper","zerion","zeroEx"],"occurrences":11}]',
);
const updateTokenDetectionSupportStatus = await fetchWithCache(
'https://token-api.metaswap.codefi.network/tokens/0x1',
);
const store = configureMockStore()(
state,
updateTokenDetectionSupportStatus,
);
const { getByText } = renderWithProvider(<NewNetworkInfo />, store);
expect(getByText('Things to keep in mind:')).toBeInTheDocument();
});
it('should render things to keep in mind text when token detection support is not available', async () => {
nock('https://token-api.metaswap.codefi.network')
.get('/tokens/0x3')
.reply(200, '{"error":"ChainId 0x3 is not supported"}');
const updateTokenDetectionSupportStatus = await fetchWithCache(
'https://token-api.metaswap.codefi.network/tokens/0x3',
);
const store = configureMockStore()(
state,
updateTokenDetectionSupportStatus,
);
const { getByText } = renderWithProvider(<NewNetworkInfo />, store);
expect(getByText('Things to keep in mind:')).toBeInTheDocument();
});
it('should not render first bullet when provider ticker is null', async () => {
nock('https://token-api.metaswap.codefi.network')
.get('/tokens/0x3')
.reply(200, '{"error":"ChainId 0x3 is not supported"}');
const updateTokenDetectionSupportStatus = await fetchWithCache(
'https://token-api.metaswap.codefi.network/tokens/0x3',
);
state.metamask.provider.ticker = null;
const store = configureMockStore()(
state,
updateTokenDetectionSupportStatus,
);
const { container } = renderWithProvider(<NewNetworkInfo />, store);
const firstBox = container.querySelector('new-network-info__content-box-1');
expect(firstBox).toBeNull();
});
it('should render click to manually add link', async () => {
nock('https://token-api.metaswap.codefi.network')
.get('/tokens/0x3')
.reply(200, '{"error":"ChainId 0x3 is not supported"}');
const updateTokenDetectionSupportStatus = await fetchWithCache(
'https://token-api.metaswap.codefi.network/tokens/0x3',
);
const store = configureMockStore()(
state,
updateTokenDetectionSupportStatus,
);
const { getByText } = renderWithProvider(<NewNetworkInfo />, store);
expect(getByText('Click here to manually add the tokens.')).toBeDefined();
});
});

View File

@ -33,6 +33,7 @@
@import 'logo/logo-coinbasepay.scss';
@import 'loading-screen/index';
@import 'menu/menu';
@import 'new-network-info/index';
@import 'numeric-input/numeric-input';
@import 'nickname-popover/index';
@import 'form-field/index';

View File

@ -77,6 +77,7 @@ import OnboardingFlow from '../onboarding-flow/onboarding-flow';
import QRHardwarePopover from '../../components/app/qr-hardware-popover';
import { SEND_STAGES } from '../../ducks/send';
import { THEME_TYPE } from '../settings/experimental-tab/experimental-tab.constant';
import NewNetworkInfo from '../../components/ui/new-network-info/new-network-info';
export default class Routes extends Component {
static propTypes = {
@ -104,6 +105,8 @@ export default class Routes extends Component {
browserEnvironmentBrowser: PropTypes.string,
theme: PropTypes.string,
sendStage: PropTypes.string,
isNetworkUsed: PropTypes.bool,
hasAnAccountWithNoFundsOnNetwork: PropTypes.bool,
};
static contextTypes = {
@ -358,11 +361,17 @@ export default class Routes extends Component {
isMouseUser,
browserEnvironmentOs: os,
browserEnvironmentBrowser: browser,
isNetworkUsed,
hasAnAccountWithNoFundsOnNetwork,
} = this.props;
const loadMessage =
loadingMessage || isNetworkLoading
? this.getConnectingLabel(loadingMessage)
: null;
const shouldShowNetworkInfo =
isUnlocked && !isNetworkUsed && hasAnAccountWithNoFundsOnNetwork;
return (
<div
className={classnames('app', {
@ -378,6 +387,7 @@ export default class Routes extends Component {
}
}}
>
{shouldShowNetworkInfo && <NewNetworkInfo />}
<QRHardwarePopover />
<Modal />
<Alert visible={this.props.alertOpen} msg={alertMessage} />

View File

@ -2,6 +2,8 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { compose } from 'redux';
import {
getHasAnyAccountWithNoFundsOnNetwork,
getIsNetworkUsed,
getNetworkIdentifier,
getPreferences,
isNetworkLoading,
@ -40,6 +42,9 @@ function mapStateToProps(state) {
providerType: state.metamask.provider?.type,
theme: getTheme(state),
sendStage: getSendStage(state),
isNetworkUsed: getIsNetworkUsed(state),
hasAnAccountWithNoFundsOnNetwork:
getHasAnyAccountWithNoFundsOnNetwork(state),
};
}

View File

@ -1170,3 +1170,18 @@ export function getBlockExplorerLinkText(
return blockExplorerLinkText;
}
export function getIsNetworkUsed(state) {
const chainId = getCurrentChainId(state);
const { usedNetworks } = state.metamask;
return Boolean(usedNetworks[chainId]);
}
export function getHasAnyAccountWithNoFundsOnNetwork(state) {
const balances = getMetaMaskCachedBalances(state) ?? {};
const hasAnAccountWithNoFundsOnNetwork =
Object.values(balances).indexOf('0x0');
return hasAnAccountWithNoFundsOnNetwork !== -1;
}

View File

@ -3785,6 +3785,10 @@ export function setCustomNetworkListEnabled(customNetworkListEnabled) {
};
}
export function setFirstTimeUsedNetwork(chainId) {
return promisifiedBackground.setFirstTimeUsedNetwork(chainId);
}
// QR Hardware Wallets
export async function submitQRHardwareCryptoHDKey(cbor) {
await promisifiedBackground.submitQRHardwareCryptoHDKey(cbor);