feat(governance): eth chain should not inhibit usage (#3568)

This commit is contained in:
Sam Keen 2023-05-02 13:31:16 +01:00 committed by GitHub
parent 8a59722f09
commit 8d42481130
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 144 additions and 24 deletions

View File

@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/react';
import { DisconnectedNotice } from './disconnected-notice';
describe('DisconnectedNotice', () => {
it('renders Notification when isDisconnected is true and correctNetworkChainId is valid', () => {
render(
<DisconnectedNotice isDisconnected={true} correctNetworkChainId={'1'} />
);
const disconnectedNotice = screen.getByTestId('disconnected-notice');
expect(disconnectedNotice).toBeInTheDocument();
});
it("doesn't render Notification when isDisconnected is false", () => {
render(
<DisconnectedNotice isDisconnected={false} correctNetworkChainId={'1'} />
);
const disconnectedNotice = screen.queryByTestId('disconnected-notice');
expect(disconnectedNotice).not.toBeInTheDocument();
});
it("doesn't render Notification when correctNetworkChainId is undefined", () => {
render(
<DisconnectedNotice
isDisconnected={true}
correctNetworkChainId={undefined}
/>
);
const disconnectedNotice = screen.queryByTestId('disconnected-notice');
expect(disconnectedNotice).not.toBeInTheDocument();
});
it("doesn't render Notification when correctNetworkChainId is null", () => {
render(
<DisconnectedNotice isDisconnected={true} correctNetworkChainId={null} />
);
const disconnectedNotice = screen.queryByTestId('disconnected-notice');
expect(disconnectedNotice).not.toBeInTheDocument();
});
});

View File

@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next';
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
interface DisconnectedNoticeProps {
isDisconnected: boolean;
correctNetworkChainId?: string | null;
}
export const DisconnectedNotice = ({
isDisconnected,
correctNetworkChainId,
}: DisconnectedNoticeProps) => {
const { t } = useTranslation();
if (
!isDisconnected ||
correctNetworkChainId === undefined ||
correctNetworkChainId === null
) {
return null;
}
return (
<div className="col-span-full" data-testid="disconnected-notice">
<Notification
message={t('disconnectedNotice', {
correctNetwork: correctNetworkChainId,
})}
intent={Intent.Danger}
/>
</div>
);
};

View File

@ -0,0 +1 @@
export * from './disconnected-notice';

View File

@ -3,6 +3,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { DisconnectedNotice } from '../disconnected-notice';
import { import {
AppStateActionType, AppStateActionType,
@ -26,7 +27,8 @@ import {
import { Loader } from '@vegaprotocol/ui-toolkit'; import { Loader } from '@vegaprotocol/ui-toolkit';
import colors from 'tailwindcss/colors'; import colors from 'tailwindcss/colors';
import { useBalances } from '../../lib/balances/balances-store'; import { useBalances } from '../../lib/balances/balances-store';
import { useWeb3Disconnect } from '@vegaprotocol/web3'; import { useEthereumConfig, useWeb3Disconnect } from '@vegaprotocol/web3';
import { getChainName } from '@vegaprotocol/web3';
const removeLeadingAddressSymbol = (key: string) => { const removeLeadingAddressSymbol = (key: string) => {
if (key && key.length > 2 && key.slice(0, 2) === '0x') { if (key && key.length > 2 && key.slice(0, 2) === '0x') {
@ -182,16 +184,21 @@ const ConnectedKey = () => {
export const EthWallet = () => { export const EthWallet = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appDispatch } = useAppState(); const { appDispatch, appState } = useAppState();
const { account, connector } = useWeb3React(); const { account, connector } = useWeb3React();
const pendingTxs = usePendingTransactions(); const pendingTxs = usePendingTransactions();
const disconnect = useWeb3Disconnect(connector); const disconnect = useWeb3Disconnect(connector);
const { config } = useEthereumConfig();
return ( return (
<WalletCard> <WalletCard>
<section data-testid="ethereum-wallet"> <section data-testid="ethereum-wallet">
<WalletCardHeader> <WalletCardHeader>
<h1 className="m-0 uppercase">{t('ethereumKey')}</h1> <h1 className="m-0 uppercase">{t('ethereumKey')}</h1>
<DisconnectedNotice
isDisconnected={appState.disconnectNotice}
correctNetworkChainId={getChainName(Number(config?.chain_id))}
/>
{account && ( {account && (
<div className="place-self-end font-mono"> <div className="place-self-end font-mono">
<div <div

View File

@ -1,13 +1,12 @@
import { Button, Splash } from '@vegaprotocol/ui-toolkit'; import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { import {
getChainName,
useWeb3ConnectStore, useWeb3ConnectStore,
useWeb3Disconnect, useWeb3Disconnect,
Web3ConnectDialog, Web3ConnectDialog,
} from '@vegaprotocol/web3'; } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
AppStateActionType, AppStateActionType,
useAppState, useAppState,
@ -36,9 +35,7 @@ export function Web3Connector({
const appChainId = Number(chainId); const appChainId = Number(chainId);
return ( return (
<> <>
<Web3Content appChainId={appChainId} setDialogOpen={setDialogOpen}> <Web3Content appChainId={appChainId}>{children}</Web3Content>
{children}
</Web3Content>
<Web3ConnectDialog <Web3ConnectDialog
connectors={connectors} connectors={connectors}
dialogOpen={appState.ethConnectOverlay} dialogOpen={appState.ethConnectOverlay}
@ -52,23 +49,59 @@ export function Web3Connector({
interface Web3ContentProps { interface Web3ContentProps {
children: ReactElement; children: ReactElement;
appChainId: number; appChainId: number;
setDialogOpen: (isOpen: boolean) => void;
} }
export const Web3Content = ({ children, appChainId }: Web3ContentProps) => { export const Web3Content = ({ children, appChainId }: Web3ContentProps) => {
const { appState, appDispatch } = useAppState();
const { connector, chainId } = useWeb3React(); const { connector, chainId } = useWeb3React();
const [previousChainId, setPreviousChainId] = useState(chainId);
const error = useWeb3ConnectStore((store) => store.error); const error = useWeb3ConnectStore((store) => store.error);
const disconnect = useWeb3Disconnect(connector); const disconnect = useWeb3Disconnect(connector);
const showDisconnectNotice = useCallback(
(isVisible: boolean) =>
appDispatch({
type: AppStateActionType.SET_DISCONNECT_NOTICE,
isVisible,
}),
[appDispatch]
);
useEffect(() => { useEffect(() => {
if (connector?.connectEagerly) { if (connector?.connectEagerly) {
connector.connectEagerly(); connector.connectEagerly();
} }
// wallet connect doesnt handle connectEagerly being called when connector is also in the // wallet connect doesn't handle connectEagerly being called when connector is also in the
// deps array. // deps array.
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
if (chainId !== undefined) {
// We use this to detect when the user switches networks.
setPreviousChainId(chainId);
if (chainId !== appChainId) {
disconnect();
// If the user was previously connected, show the disconnect explanation notice.
if (previousChainId !== undefined && !appState.disconnectNotice) {
showDisconnectNotice(true);
}
} else if (appState.disconnectNotice) {
showDisconnectNotice(false);
}
}
}, [
appChainId,
appDispatch,
appState.disconnectNotice,
chainId,
disconnect,
previousChainId,
showDisconnectNotice,
]);
if (error) { if (error) {
return ( return (
<Splash> <Splash>
@ -80,18 +113,5 @@ export const Web3Content = ({ children, appChainId }: Web3ContentProps) => {
); );
} }
if (chainId !== undefined && chainId !== appChainId) {
return (
<Splash>
<div className="flex flex-col items-center gap-12">
<p className="text-white">
This app only works on {getChainName(appChainId)}
</p>
<Button onClick={() => disconnect()}>Disconnect</Button>
</div>
</Splash>
);
}
return children; return children;
}; };

View File

@ -43,6 +43,11 @@ export interface AppState {
* Message to display in a banner at the top of the screen, currently always shown as a warning/error * Message to display in a banner at the top of the screen, currently always shown as a warning/error
*/ */
bannerMessage: string; bannerMessage: string;
/**
* Displays a notice to the user that they have been disconnected because they've changed their
* ethereum network to an incompatible one.
*/
disconnectNotice: boolean;
} }
export enum AppStateActionType { export enum AppStateActionType {
@ -58,6 +63,7 @@ export enum AppStateActionType {
SET_ASSOCIATION_BREAKDOWN, SET_ASSOCIATION_BREAKDOWN,
SET_TRANSACTION_OVERLAY, SET_TRANSACTION_OVERLAY,
SET_BANNER_MESSAGE, SET_BANNER_MESSAGE,
SET_DISCONNECT_NOTICE,
} }
export type AppStateAction = export type AppStateAction =
@ -90,6 +96,10 @@ export type AppStateAction =
| { | {
type: AppStateActionType.SET_BANNER_MESSAGE; type: AppStateActionType.SET_BANNER_MESSAGE;
message: string; message: string;
}
| {
type: AppStateActionType.SET_DISCONNECT_NOTICE;
isVisible: boolean;
}; };
type AppStateContextShape = { type AppStateContextShape = {

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import { AppStateActionType, AppStateContext } from './app-state-context';
import type { AppState, AppStateAction } from './app-state-context'; import type { AppState, AppStateAction } from './app-state-context';
import { AppStateActionType, AppStateContext } from './app-state-context';
interface AppStateProviderProps { interface AppStateProviderProps {
children: React.ReactNode; children: React.ReactNode;
@ -19,6 +19,7 @@ const initialAppState: AppState = {
ethConnectOverlay: false, ethConnectOverlay: false,
transactionOverlay: false, transactionOverlay: false,
bannerMessage: '', bannerMessage: '',
disconnectNotice: false,
}; };
function appStateReducer(state: AppState, action: AppStateAction): AppState { function appStateReducer(state: AppState, action: AppStateAction): AppState {
@ -68,6 +69,12 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
bannerMessage: action.message, bannerMessage: action.message,
}; };
} }
case AppStateActionType.SET_DISCONNECT_NOTICE: {
return {
...state,
disconnectNotice: action.isVisible,
};
}
} }
} }

View File

@ -790,5 +790,6 @@
"approval (% validator voting power)": "approval (% validator voting power)", "approval (% validator voting power)": "approval (% validator voting power)",
"67% voting power required": "67% voting power required", "67% voting power required": "67% voting power required",
"Token": "Token", "Token": "Token",
"associateVegaNow": "Associate $VEGA now" "associateVegaNow": "Associate $VEGA now",
"disconnectedNotice": "You have been disconnected. Connect your ETH wallet to the {{correctNetwork}} network to use this app."
} }

View File

@ -56,6 +56,7 @@ const mockAppState: AppState = {
ethConnectOverlay: false, ethConnectOverlay: false,
transactionOverlay: false, transactionOverlay: false,
bannerMessage: '', bannerMessage: '',
disconnectNotice: false,
}; };
jest.mock('../../../contexts/app-state/app-state-context', () => ({ jest.mock('../../../contexts/app-state/app-state-context', () => ({

View File

@ -19,6 +19,7 @@ const mockAppState: AppState = {
ethConnectOverlay: false, ethConnectOverlay: false,
transactionOverlay: false, transactionOverlay: false,
bannerMessage: '', bannerMessage: '',
disconnectNotice: false,
}; };
jest.mock('../../../contexts/app-state/app-state-context', () => ({ jest.mock('../../../contexts/app-state/app-state-context', () => ({