Commit 24b0f112 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-1035

parents d3fc4002 3504d596
......@@ -29,5 +29,5 @@ jobs:
cleanup_docker_image:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master
with:
dockerImage: prerelease-$GITHUB_REF_NAME_SLUG
dockerImage: review-$GITHUB_REF_NAME_SLUG
secrets: inherit
......@@ -31,6 +31,7 @@ const moduleExports = {
},
);
config.resolve.fallback = { fs: false, net: false, tls: false };
config.externals.push('pino-pretty', 'lokijs', 'encoding');
return config;
},
......
......@@ -2,6 +2,8 @@ import type CspDev from 'csp-dev';
import config from 'configs/app';
import { KEY_WORDS } from '../utils';
export function walletConnect(): CspDev.DirectiveDescriptor {
if (!config.features.blockchainInteraction.isEnabled) {
return {};
......@@ -9,11 +11,13 @@ export function walletConnect(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
'*.web3modal.com',
'*.walletconnect.com',
'wss://relay.walletconnect.com',
'wss://www.walletlink.org',
],
'img-src': [
KEY_WORDS.BLOB,
'*.walletconnect.com',
],
};
......
......@@ -4,15 +4,26 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
const feature = config.features.marketplace;
const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/apps">
<>
<PageTitle title="DAppscout"/>
<PageTitle
title="DAppscout"
contentAfter={ feature.isEnabled && (
<LinkExternal href={ feature.submitFormUrl } variant="subtle" fontSize="sm" lineHeight={ 5 } ml="auto">
Submit app
</LinkExternal>
) }
/>
<Marketplace/>
</>
</PageNextJs>
......
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { w3mProvider } from '@web3modal/ethereum';
import { createWeb3Modal, defaultWagmiConfig } from '@web3modal/wagmi/react';
import React from 'react';
import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import { WagmiConfig } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import type { Props as PageProps } from 'nextjs/getServerSideProps';
......@@ -33,17 +33,18 @@ const defaultAppContext = {
};
// >>> Web3 stuff
const { publicClient } = configureChains(
[ mainnet ],
[
w3mProvider({ projectId: '' }),
],
);
const chains = [ mainnet ];
const WALLET_CONNECT_PROJECT_ID = 'PROJECT_ID';
const wagmiConfig = createConfig({
autoConnect: false,
connectors: [ ],
publicClient,
const wagmiConfig = defaultWagmiConfig({
chains,
projectId: WALLET_CONNECT_PROJECT_ID,
});
createWeb3Modal({
wagmiConfig,
projectId: WALLET_CONNECT_PROJECT_ID,
chains,
});
// <<<<
......
import { Alert, Button, Flex } from '@chakra-ui/react';
import { useWeb3Modal } from '@web3modal/react';
import { useWeb3Modal, useWeb3ModalState } from '@web3modal/wagmi/react';
import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
......@@ -8,7 +8,8 @@ import * as mixpanel from 'lib/mixpanel/index';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
const ContractConnectWallet = () => {
const { open, isOpen } = useWeb3Modal();
const { open } = useWeb3Modal();
const { open: isOpen } = useWeb3ModalState();
const { disconnect } = useDisconnect();
const isMobile = useIsMobile();
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
......
import { Alert, Box, Button, chakra, Flex, Link, Radio, RadioGroup } from '@chakra-ui/react';
import { useWeb3Modal } from '@web3modal/react';
import { useWeb3Modal } from '@web3modal/wagmi/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
......
......@@ -15,8 +15,8 @@ import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts';
import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent';
import StatsGasPrices from './StatsGasPrices';
import StatsItem from './StatsItem';
const hasGasTracker = config.UI.homepage.showGasTracker;
......@@ -52,7 +52,7 @@ const Stats = () => {
!data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++;
const isOdd = Boolean(itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
const gasLabel = hasGasTracker && data.gas_prices ? <GasInfoTooltipContent gasPrices={ data.gas_prices }/> : null;
content = (
<>
......
import { Box, Icon, Link, Skeleton } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import PlusIcon from 'icons/plus.svg';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
......@@ -91,29 +90,6 @@ const Marketplace = () => {
appId={ selectedApp.id }
/>
) }
<Skeleton
isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }}
display="inline-block"
>
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
href={ feature.submitFormUrl }
isExternal
>
<Icon
as={ PlusIcon }
w={ 3 }
h={ 3 }
mr={ 2 }
/>
Submit an app
</Link>
</Skeleton>
</>
);
};
......
......@@ -3,7 +3,7 @@ import React from 'react';
import type { GasPrices } from 'types/api/stats';
const StatsGasPrices = ({ gasPrices }: {gasPrices: GasPrices}) => {
const GasInfoTooltipContent = ({ gasPrices }: {gasPrices: GasPrices}) => {
const nameStyleProps = {
color: useColorModeValue('blue.100', 'blue.600'),
};
......@@ -20,4 +20,4 @@ const StatsGasPrices = ({ gasPrices }: {gasPrices: GasPrices}) => {
);
};
export default StatsGasPrices;
export default React.memo(GasInfoTooltipContent);
import { useColorModeValue, useToken } from '@chakra-ui/react';
import { useColorMode } from '@chakra-ui/react';
import { jsonRpcProvider } from '@wagmi/core/providers/jsonRpc';
import { EthereumClient, w3mConnectors } from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react';
import { createWeb3Modal, useWeb3ModalTheme, defaultWagmiConfig } from '@web3modal/wagmi/react';
import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import { configureChains, WagmiConfig } from 'wagmi';
import config from 'configs/app';
import colors from 'theme/foundations/colors';
import { BODY_TYPEFACE } from 'theme/foundations/typography';
import zIndices from 'theme/foundations/zIndices';
const feature = config.features.blockchainInteraction;
......@@ -41,58 +43,71 @@ const getConfig = () => {
},
};
const chains = [ currentChain ];
const { publicClient } = configureChains(chains, [
jsonRpcProvider({
rpc: () => ({
http: config.chain.rpcUrl || '',
const { chains } = configureChains(
[ currentChain ],
[
jsonRpcProvider({
rpc: () => ({
http: config.chain.rpcUrl || '',
}),
}),
}),
]);
const wagmiConfig = createConfig({
autoConnect: true,
connectors: w3mConnectors({ projectId: feature.walletConnect.projectId, chains }),
publicClient,
],
);
const wagmiConfig = defaultWagmiConfig({
chains,
projectId: feature.walletConnect.projectId,
});
createWeb3Modal({
wagmiConfig,
projectId: feature.walletConnect.projectId,
chains,
themeVariables: {
'--w3m-font-family': `${ BODY_TYPEFACE }, sans-serif`,
'--w3m-accent': colors.blue[600],
'--w3m-border-radius-master': '2px',
'--w3m-z-index': zIndices.modal,
},
});
const ethereumClient = new EthereumClient(wagmiConfig, chains);
return { wagmiConfig, ethereumClient };
return { wagmiConfig };
} catch (error) {
return { wagmiConfig: undefined, ethereumClient: undefined };
return { };
}
};
const { wagmiConfig, ethereumClient } = getConfig();
const { wagmiConfig } = getConfig();
interface Props {
children: React.ReactNode;
fallback?: JSX.Element | (() => JSX.Element);
}
const Web3ModalProvider = ({ children, fallback }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal');
const web3ModalTheme = useColorModeValue('light', 'dark');
const Fallback = ({ children, fallback }: Props) => {
return typeof fallback === 'function' ? fallback() : (fallback || <>{ children }</>); // eslint-disable-line react/jsx-no-useless-fragment
};
const Provider = ({ children, fallback }: Props) => {
const { colorMode } = useColorMode();
const { setThemeMode } = useWeb3ModalTheme();
if (!wagmiConfig || !ethereumClient || !feature.isEnabled) {
return typeof fallback === 'function' ? fallback() : (fallback || <>{ children }</>); // eslint-disable-line react/jsx-no-useless-fragment
React.useEffect(() => {
setThemeMode(colorMode);
}, [ colorMode, setThemeMode ]);
// not really necessary, but we have to make typescript happy
if (!wagmiConfig || !feature.isEnabled) {
return <Fallback fallback={ fallback }>{ children }</Fallback>;
}
return (
<>
<WagmiConfig config={ wagmiConfig }>
{ children }
</WagmiConfig>
<Web3Modal
projectId={ feature.walletConnect.projectId }
ethereumClient={ ethereumClient }
themeMode={ web3ModalTheme }
themeVariables={{
'--w3m-z-index': modalZIndex,
}}
/>
</>
<WagmiConfig config={ wagmiConfig }>
{ children }
</WagmiConfig>
);
};
const Web3ModalProvider = wagmiConfig && feature.isEnabled ? Provider : Fallback;
export default Web3ModalProvider;
......@@ -12,10 +12,11 @@ import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => {
return (
<Layout.Container>
<Layout.TopRow/>
<HeaderMobile/>
<Layout.MainArea>
<Layout.MainColumn
paddingTop={{ base: '138px', lg: 6 }}
paddingTop={{ base: 16, lg: 6 }}
paddingX={{ base: 4, lg: 6 }}
>
<HeaderAlert/>
......
......@@ -76,7 +76,7 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => {
textAlign="center"
padding={ 2 }
isDisabled={ hasMenu }
openDelay={ 300 }
openDelay={ 500 }
>
<Box>
<PopoverTrigger>
......
......@@ -19,7 +19,9 @@ test('default view +@dark-mode +@mobile', async({ mount, page }) => {
</TestApp>,
);
await component.getByLabel('color mode switch').click();
await component.getByText(/gwei/i).hover();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
await component.getByLabel('color mode switch').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
});
import { Flex, Skeleton } from '@chakra-ui/react';
import { Flex, LightMode, Link, Skeleton, Tooltip, chakra, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent';
import TextSeparator from 'ui/shared/TextSeparator';
const TopBarStats = () => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onToggle();
}, [ onToggle ]);
const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
......@@ -26,14 +35,34 @@ const TopBarStats = () => {
>
{ data?.coin_price && (
<Skeleton isLoaded={ !isPlaceholderData }>
<span>{ config.chain.governanceToken.symbol || config.chain.currency.symbol }: </span>
<chakra.span color="text_secondary">{ config.chain.governanceToken.symbol || config.chain.currency.symbol } </chakra.span>
<span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton>
) }
{ data?.coin_price && config.UI.homepage.showGasTracker && <TextSeparator color="divider"/> }
{ data?.gas_prices && config.UI.homepage.showGasTracker && (
<Skeleton isLoaded={ !isPlaceholderData }>
<span>Gas: { data.gas_prices.average } Gwei</span>
<chakra.span color="text_secondary">Gas </chakra.span>
<LightMode>
<Tooltip
label={ <GasInfoTooltipContent gasPrices={ data.gas_prices }/> }
hasArrow={ false }
borderRadius="md"
offset={ [ 0, 16 ] }
bgColor="blackAlpha.900"
p={ 0 }
isOpen={ isOpen }
>
<Link
_hover={{ textDecoration: 'none', color: 'link_hovered' }}
onClick={ handleClick }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
>
{ data.gas_prices.average } Gwei
</Link>
</Tooltip>
</LightMode>
</Skeleton>
) }
</Flex>
......
......@@ -2,6 +2,7 @@ import type { ButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import HashStringShorten from 'ui/shared/HashStringShorten';
import useWallet from 'ui/snippets/walletMenu/useWallet';
......@@ -18,6 +19,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const isMobile = useIsMobile();
const variant = React.useMemo(() => {
if (isWalletConnected) {
......@@ -55,7 +57,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
isOpen={ isPopoverOpen }
onClose={ setIsPopoverOpen.off }
>
<WalletTooltip isDisabled={ isWalletConnected }>
<WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || isMobile }>
<Box ml={ 2 }>
<PopoverTrigger>
<Button
......
......@@ -2,6 +2,7 @@ import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconBu
import React from 'react';
import walletIcon from 'icons/wallet.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
......@@ -13,10 +14,11 @@ const WalletMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const isMobile = useIsMobile();
return (
<>
<WalletTooltip isDisabled={ isWalletConnected } isMobile>
<WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || !isMobile } isMobile>
<IconButton
aria-label="wallet menu"
icon={ isWalletConnected ?
......
import { Tooltip, useBoolean } from '@chakra-ui/react';
import { Tooltip, useBoolean, useOutsideClick } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
type Props = {
......@@ -8,27 +9,41 @@ type Props = {
};
const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => {
const router = useRouter();
const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false);
const ref = React.useRef(null);
useOutsideClick({ ref, handler: setIsTooltipShown.off });
const { defaultLabel, label, localStorageKey } = React.useMemo(() => {
const isAppPage = router.pathname === '/apps/[id]';
const defaultLabel = <span>Your wallet is used to interact with<br/>apps and contracts in the explorer</span>;
const label = isAppPage ?
<span>Connect once to use your wallet with<br/>all apps in the DAppscout marketplace!</span> :
defaultLabel;
const localStorageKey = `${ isAppPage ? 'dapp-' : '' }wallet-connect-tooltip-shown`;
return { defaultLabel, label, localStorageKey };
}, [ router.pathname ]);
React.useEffect(() => {
const key = `wallet-connect-tooltip-shown-${ isMobile ? 'mobile' : 'desktop' }`;
const wasShown = window.localStorage.getItem(key);
if (!wasShown) {
const wasShown = window.localStorage.getItem(localStorageKey);
if (!isDisabled && !wasShown) {
setIsTooltipShown.on();
window.localStorage.setItem(key, 'true');
window.localStorage.setItem(localStorageKey, 'true');
setTimeout(() => setIsTooltipShown.off(), 3000);
}
}, [ setIsTooltipShown, isMobile ]);
}, [ setIsTooltipShown, localStorageKey, isDisabled ]);
return (
<Tooltip
label={ <span>Your wallet is used to interact with<br/>apps and contracts in the explorer</span> }
label={ isTooltipShown ? label : defaultLabel }
textAlign="center"
padding={ 2 }
isDisabled={ isDisabled }
openDelay={ 300 }
openDelay={ 500 }
isOpen={ isTooltipShown || (isMobile ? false : undefined) }
onClose={ setIsTooltipShown.off }
display={ isMobile ? { base: 'flex', lg: 'none' } : { base: 'none', lg: 'flex' } }
ref={ ref }
>
{ children }
</Tooltip>
......
import { useWeb3Modal } from '@web3modal/react';
import { useWeb3Modal, useWeb3ModalState } from '@web3modal/wagmi/react';
import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
import * as mixpanel from 'lib/mixpanel/index';
export default function useWallet() {
const { open, isOpen } = useWeb3Modal();
const { open } = useWeb3Modal();
const { open: isOpen } = useWeb3ModalState();
const { disconnect } = useDisconnect();
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
......
import { Flex, Skeleton } from '@chakra-ui/react';
import { Grid, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
......@@ -72,16 +72,31 @@ const TokenTransferListItem = ({
/>
</Flex>
{ valueStr && (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Flex columnGap={ 2 } w="100%">
<Grid gap={ 2 } templateColumns={ `1fr auto auto${ usd ? ' auto' : '' }` }>
<Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }>
Value
</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<Skeleton
isLoaded={ !isLoading }
color="text_secondary"
wordBreak="break-all"
overflow="hidden"
flexGrow={ 1 }
>
<span>{ valueStr }</span>
</Skeleton>
{ token.symbol && <TruncatedValue isLoading={ isLoading } value={ token.symbol }/> }
{ usd && <Skeleton isLoaded={ !isLoading } color="text_secondary"><span>(${ usd })</span></Skeleton> }
</Flex>
{ usd && (
<Skeleton
isLoaded={ !isLoading }
color="text_secondary"
wordBreak="break-all"
overflow="hidden"
>
<span>(${ usd })</span>
</Skeleton>
) }
</Grid>
) }
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<NftEntity
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment