Commit d1f7e165 authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge pull request #1822 from blockscout/rework-marketplace-app-header

Rework marketplace app header and tooltips
parents 528ee6fa 7828a98d
import { useRouter } from 'next/router';
import React, { createContext, useContext, useEffect, useState, useMemo } from 'react';
type Props = {
children: React.ReactNode;
}
type TMarketplaceContext = {
isAutoConnectDisabled: boolean;
setIsAutoConnectDisabled: (isAutoConnectDisabled: boolean) => void;
}
const MarketplaceContext = createContext<TMarketplaceContext>({
isAutoConnectDisabled: false,
setIsAutoConnectDisabled: () => {},
});
export function MarketplaceContextProvider({ children }: Props) {
const router = useRouter();
const [ isAutoConnectDisabled, setIsAutoConnectDisabled ] = useState(false);
useEffect(() => {
const handleRouteChange = () => {
setIsAutoConnectDisabled(false);
};
router.events.on('routeChangeStart', handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange);
};
}, [ router.events ]);
const value = useMemo(() => ({
isAutoConnectDisabled,
setIsAutoConnectDisabled,
}), [ isAutoConnectDisabled, setIsAutoConnectDisabled ]);
return (
<MarketplaceContext.Provider value={ value }>
{ children }
</MarketplaceContext.Provider>
);
}
export function useMarketplaceContext() {
return useContext(MarketplaceContext);
}
...@@ -12,6 +12,7 @@ import config from 'configs/app'; ...@@ -12,6 +12,7 @@ import config from 'configs/app';
import useQueryClientConfig from 'lib/api/useQueryClientConfig'; import useQueryClientConfig from 'lib/api/useQueryClientConfig';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { ChakraProvider } from 'lib/contexts/chakra'; import { ChakraProvider } from 'lib/contexts/chakra';
import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { growthBook } from 'lib/growthbook/init'; import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
...@@ -67,7 +68,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -67,7 +68,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<GrowthBookProvider growthbook={ growthBook }> <GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider> <ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }> <SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
<MarketplaceContextProvider>
{ getLayout(<Component { ...pageProps }/>) } { getLayout(<Component { ...pageProps }/>) }
</MarketplaceContextProvider>
</SocketProvider> </SocketProvider>
</ScrollDirectionProvider> </ScrollDirectionProvider>
</GrowthBookProvider> </GrowthBookProvider>
......
...@@ -54,6 +54,7 @@ const AppSecurityReport = ({ id, securityReport, height, showContractList, isLoa ...@@ -54,6 +54,7 @@ const AppSecurityReport = ({ id, securityReport, height, showContractList, isLoa
onClick={ handleButtonClick } onClick={ handleButtonClick }
height={ height } height={ height }
onlyIcon={ onlyIcon } onlyIcon={ onlyIcon }
label="The security score is based on analysis of a DApp's smart contracts."
/> />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '328px' }}> <PopoverContent w={{ base: '100vw', lg: '328px' }}>
......
import { Alert } from '@chakra-ui/react';
import React from 'react';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
internalWallet: boolean | undefined;
isWalletConnected: boolean;
}
const MarketplaceAppAlert = ({ internalWallet, isWalletConnected }: Props) => {
const message = React.useMemo(() => {
let icon: IconName = 'wallet';
let text = 'Connect your wallet to Blockscout for full-featured access';
let status: 'warning' | 'success' = 'warning';
if (isWalletConnected && internalWallet) {
icon = 'integration/full';
text = 'Your wallet is connected with Blockscout';
status = 'success';
} else if (!internalWallet) {
icon = 'integration/partial';
text = 'Connect your wallet in the app below';
}
return { icon, text, status };
}, [ isWalletConnected, internalWallet ]);
return (
<Alert
status={ message.status }
borderRadius="base"
px={ 3 }
py={{ base: 3, md: 1.5 }}
fontSize="sm"
lineHeight={ 5 }
>
<IconSvg
name={ message.icon }
color={ message.status === 'success' ? 'green.600' : 'current' }
boxSize={ 5 }
flexShrink={ 0 }
mr={ 2 }
/>
{ message.text }
</Alert>
);
};
export default MarketplaceAppAlert;
import { chakra, Flex, Tooltip, Skeleton, useBoolean, Box } from '@chakra-ui/react'; import { chakra, Flex, Tooltip, Skeleton, useBoolean } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace'; import type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace';
...@@ -6,26 +6,28 @@ import { ContractListTypes } from 'types/client/marketplace'; ...@@ -6,26 +6,28 @@ import { ContractListTypes } from 'types/client/marketplace';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useFeatureValue from 'lib/growthbook/useFeatureValue'; import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
import AppSecurityReport from './AppSecurityReport'; import AppSecurityReport from './AppSecurityReport';
import ContractListModal from './ContractListModal'; import ContractListModal from './ContractListModal';
import MarketplaceAppAlert from './MarketplaceAppAlert';
import MarketplaceAppInfo from './MarketplaceAppInfo'; import MarketplaceAppInfo from './MarketplaceAppInfo';
type Props = { type Props = {
data: MarketplaceAppOverview | undefined; data: MarketplaceAppOverview | undefined;
isLoading: boolean; isLoading: boolean;
isWalletConnected: boolean;
securityReport?: MarketplaceAppSecurityReport; securityReport?: MarketplaceAppSecurityReport;
} }
const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityReport }: Props) => { const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
const [ showContractList, setShowContractList ] = useBoolean(false); const [ showContractList, setShowContractList ] = useBoolean(false);
const appProps = useAppContext(); const appProps = useAppContext();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -46,32 +48,14 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityRepo ...@@ -46,32 +48,14 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityRepo
return ( return (
<> <>
<Flex alignItems="center" flexWrap="wrap" mb={{ base: 6, md: 2 }} rowGap={ 3 } columnGap={ 2 }> <Flex alignItems="center" flexWrap="wrap" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
<Tooltip label="Back to dApps list" order={ 1 }> { !isMobile && <NetworkLogo isCollapsed/> }
<LinkInternal display="inline-flex" href={ goBackUrl } h="32px" isLoading={ isLoading }> <Tooltip label="Back to dApps list">
<LinkInternal display="inline-flex" href={ goBackUrl } h="32px" isLoading={ isLoading } ml={ isMobile ? 0 : 4 }>
<IconSvg name="arrows/east" boxSize={ 6 } transform="rotate(180deg)" margin="auto" color="gray.400"/> <IconSvg name="arrows/east" boxSize={ 6 } transform="rotate(180deg)" margin="auto" color="gray.400"/>
</LinkInternal> </LinkInternal>
</Tooltip> </Tooltip>
<Skeleton width={{ base: '100%', md: 'auto' }} order={{ base: 5, md: 2 }} isLoaded={ !isLoading }>
<MarketplaceAppAlert internalWallet={ data?.internalWallet } isWalletConnected={ isWalletConnected }/>
</Skeleton>
<Skeleton order={{ base: 2, md: 3 }} isLoaded={ !isLoading }>
<MarketplaceAppInfo data={ data }/>
</Skeleton>
{ (isExperiment && (securityReport || isLoading)) && (
<Box order={{ base: 3, md: 4 }}>
<AppSecurityReport
id={ data?.id || '' }
securityReport={ securityReport }
showContractList={ setShowContractList.on }
isLoading={ isLoading }
onlyIcon={ isMobile }
source="App page"
/>
</Box>
) }
<LinkExternal <LinkExternal
order={{ base: 4, md: 5 }}
href={ data?.url } href={ data?.url }
variant="subtle" variant="subtle"
fontSize="sm" fontSize="sm"
...@@ -85,6 +69,25 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityRepo ...@@ -85,6 +69,25 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityRepo
{ getHostname(data?.url) } { getHostname(data?.url) }
</chakra.span> </chakra.span>
</LinkExternal> </LinkExternal>
<Skeleton isLoaded={ !isLoading }>
<MarketplaceAppInfo data={ data }/>
</Skeleton>
{ (isExperiment && (securityReport || isLoading)) && (
<AppSecurityReport
id={ data?.id || '' }
securityReport={ securityReport }
showContractList={ setShowContractList.on }
isLoading={ isLoading }
onlyIcon={ isMobile }
source="App page"
/>
) }
{ !isMobile && (
<Flex flex="1" justifyContent="flex-end">
{ config.features.account.isEnabled && <ProfileMenuDesktop boxSize="32px" fallbackIconSize={ 16 }/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop size="sm"/> }
</Flex>
) }
</Flex> </Flex>
{ showContractList && ( { showContractList && (
<ContractListModal <ContractListModal
......
...@@ -11,6 +11,7 @@ import { route } from 'nextjs-routes'; ...@@ -11,6 +11,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import * as metadata from 'lib/metadata'; import * as metadata from 'lib/metadata';
...@@ -129,6 +130,7 @@ const MarketplaceApp = () => { ...@@ -129,6 +130,7 @@ const MarketplaceApp = () => {
enabled: feature.isEnabled, enabled: feature.isEnabled,
}); });
const { data, isPending } = query; const { data, isPending } = query;
const { setIsAutoConnectDisabled } = useMarketplaceContext();
useEffect(() => { useEffect(() => {
if (data) { if (data) {
...@@ -136,8 +138,9 @@ const MarketplaceApp = () => { ...@@ -136,8 +138,9 @@ const MarketplaceApp = () => {
{ pathname: '/apps/[id]', query: { id: data.id } }, { pathname: '/apps/[id]', query: { id: data.id } },
{ app_name: data.title }, { app_name: data.title },
); );
setIsAutoConnectDisabled(!data.internalWallet);
} }
}, [ data ]); }, [ data, setIsAutoConnectDisabled ]);
throwOnResourceLoadError(query); throwOnResourceLoadError(query);
...@@ -146,7 +149,6 @@ const MarketplaceApp = () => { ...@@ -146,7 +149,6 @@ const MarketplaceApp = () => {
<MarketplaceAppTopBar <MarketplaceAppTopBar
data={ data } data={ data }
isLoading={ isPending || isSecurityReportsLoading } isLoading={ isPending || isSecurityReportsLoading }
isWalletConnected={ Boolean(address) }
securityReport={ securityReports?.[id] } securityReport={ securityReports?.[id] }
/> />
<DappscoutIframeProvider <DappscoutIframeProvider
......
...@@ -8,9 +8,10 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -8,9 +8,10 @@ import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
size: number; size: number;
fallbackIconSize?: number;
} }
const UserAvatar = ({ size }: Props) => { const UserAvatar = ({ size, fallbackIconSize = 20 }: Props) => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies)); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies));
const [ isImageLoadError, setImageLoadError ] = React.useState(false); const [ isImageLoadError, setImageLoadError ] = React.useState(false);
...@@ -34,7 +35,7 @@ const UserAvatar = ({ size }: Props) => { ...@@ -34,7 +35,7 @@ const UserAvatar = ({ size }: Props) => {
boxSize={ `${ size }px` } boxSize={ `${ size }px` }
borderRadius="full" borderRadius="full"
overflow="hidden" overflow="hidden"
fallback={ isImageLoadError || !data?.avatar ? <IconSvg name="profile" boxSize={ 5 }/> : undefined } fallback={ isImageLoadError || !data?.avatar ? <IconSvg name="profile" boxSize={ `${ fallbackIconSize }px` }/> : undefined }
onError={ handleImageLoadError } onError={ handleImageLoadError }
/> />
); );
......
...@@ -3,26 +3,28 @@ import React from 'react'; ...@@ -3,26 +3,28 @@ import React from 'react';
import type { Props } from './types'; import type { Props } from './types';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
import HeaderMobile from 'ui/snippets/header/HeaderMobile'; import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import * as Layout from './components'; import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => { const LayoutDefault = ({ children }: Props) => {
return ( return (
<Layout.Container overflowY="hidden" height="100vh"> <Layout.Container
overflowY="hidden"
height={ [ '-webkit-fill-available', '100vh' ] }
display="flex"
flexDirection="column"
>
<Layout.TopRow/> <Layout.TopRow/>
<HeaderMobile/> <HeaderMobile hideSearchBar/>
<Layout.MainArea> <Layout.MainArea minH="auto" flex={ 1 }>
<Layout.MainColumn <Layout.MainColumn
paddingTop={{ base: 16, lg: 6 }} paddingTop={{ base: 0, lg: 0 }}
paddingBottom={ 0 } paddingBottom={ 0 }
paddingX={{ base: 4, lg: 6 }} paddingX={{ base: 4, lg: 6 }}
height={{ base: 'calc(100vh - 92px)', sm: 'auto' }} // 92px = Layout.TopRow + HeaderMobile
> >
<HeaderDesktop isMarketplaceAppPage/>
<AppErrorBoundary> <AppErrorBoundary>
<Layout.Content pt={{ base: 0, lg: 4 }} flexGrow={ 1 }> <Layout.Content pt={{ base: 0, lg: 2 }} flexGrow={ 1 }>
{ children } { children }
</Layout.Content> </Layout.Content>
</AppErrorBoundary> </AppErrorBoundary>
......
...@@ -12,7 +12,7 @@ const LayoutHome = ({ children }: Props) => { ...@@ -12,7 +12,7 @@ const LayoutHome = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Container>
<Layout.TopRow/> <Layout.TopRow/>
<HeaderMobile isHomePage/> <HeaderMobile hideSearchBar/>
<Layout.MainArea> <Layout.MainArea>
<Layout.SideBar/> <Layout.SideBar/>
<Layout.MainColumn <Layout.MainColumn
......
import { Flex } from '@chakra-ui/react'; import { Flex, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
className?: string;
} }
const MainArea = ({ children }: Props) => { const MainArea = ({ children, className }: Props) => {
return ( return (
<Flex w="100%" minH="calc(100vh - 36px)" alignItems="stretch"> <Flex className={ className } w="100%" minH="calc(100vh - 36px)" alignItems="stretch">
{ children } { children }
</Flex> </Flex>
); );
}; };
export default React.memo(MainArea); export default React.memo(chakra(MainArea));
...@@ -13,16 +13,17 @@ interface Props { ...@@ -13,16 +13,17 @@ interface Props {
height?: string; height?: string;
onlyIcon?: boolean; onlyIcon?: boolean;
onClick?: () => void; onClick?: () => void;
label?: string;
} }
const SolidityscanReportButton = ( const SolidityscanReportButton = (
{ className, score, isLoading, height = '32px', onlyIcon, onClick }: Props, { className, score, isLoading, height = '32px', onlyIcon, onClick, label = 'Security score' }: Props,
ref: React.ForwardedRef<HTMLButtonElement>, ref: React.ForwardedRef<HTMLButtonElement>,
) => { ) => {
const { scoreColor } = useScoreLevelAndColor(score); const { scoreColor } = useScoreLevelAndColor(score);
return ( return (
<PopoverTriggerTooltip label="Security score" isLoading={ isLoading } className={ className }> <PopoverTriggerTooltip label={ label } isLoading={ isLoading } className={ className }>
<Button <Button
ref={ ref } ref={ ref }
color={ scoreColor } color={ scoreColor }
......
...@@ -16,11 +16,11 @@ const LOGO_IMAGE_PROPS = { ...@@ -16,11 +16,11 @@ const LOGO_IMAGE_PROPS = {
}; };
type Props = { type Props = {
isHomePage?: boolean; hideSearchBar?: boolean;
renderSearchBar?: () => React.ReactNode; renderSearchBar?: () => React.ReactNode;
} }
const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => { const HeaderMobile = ({ hideSearchBar, renderSearchBar }: Props) => {
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
const { ref, inView } = useInView({ threshold: 1 }); const { ref, inView } = useInView({ threshold: 1 });
...@@ -57,7 +57,7 @@ const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => { ...@@ -57,7 +57,7 @@ const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => {
{ config.features.blockchainInteraction.isEnabled && <WalletMenuMobile/> } { config.features.blockchainInteraction.isEnabled && <WalletMenuMobile/> }
</Flex> </Flex>
</Flex> </Flex>
{ !isHomePage && searchBar } { !hideSearchBar && searchBar }
</Box> </Box>
); );
}; };
......
import type { IconButtonProps } from '@chakra-ui/react'; import type { IconButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, IconButton, Tooltip, Box } from '@chakra-ui/react'; import { Popover, PopoverContent, PopoverBody, PopoverTrigger, IconButton, Tooltip, Box, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
...@@ -12,9 +12,11 @@ import useMenuButtonColors from '../useMenuButtonColors'; ...@@ -12,9 +12,11 @@ import useMenuButtonColors from '../useMenuButtonColors';
type Props = { type Props = {
isHomePage?: boolean; isHomePage?: boolean;
className?: string;
fallbackIconSize?: number;
}; };
const ProfileMenuDesktop = ({ isHomePage }: Props) => { const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize }: Props) => {
const { data, error, isPending } = useFetchProfileInfo(); const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
...@@ -81,8 +83,9 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => { ...@@ -81,8 +83,9 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => {
<Box> <Box>
<PopoverTrigger> <PopoverTrigger>
<IconButton <IconButton
className={ className }
aria-label="profile menu" aria-label="profile menu"
icon={ <UserAvatar size={ 20 }/> } icon={ <UserAvatar size={ 20 } fallbackIconSize={ fallbackIconSize }/> }
variant={ variant } variant={ variant }
colorScheme="blue" colorScheme="blue"
boxSize="40px" boxSize="40px"
...@@ -104,4 +107,4 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => { ...@@ -104,4 +107,4 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => {
); );
}; };
export default ProfileMenuDesktop; export default chakra(ProfileMenuDesktop);
...@@ -2,8 +2,9 @@ import { useColorModeValue } from '@chakra-ui/react'; ...@@ -2,8 +2,9 @@ import { useColorModeValue } from '@chakra-ui/react';
export default function useMenuColors() { export default function useMenuColors() {
const themedBackground = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const themedBackground = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const themedBackgroundOrange = useColorModeValue('orange.100', 'orange.900');
const themedBorderColor = useColorModeValue('gray.300', 'gray.700'); const themedBorderColor = useColorModeValue('gray.300', 'gray.700');
const themedColor = useColorModeValue('blackAlpha.800', 'gray.400'); const themedColor = useColorModeValue('blackAlpha.800', 'gray.400');
return { themedBackground, themedBorderColor, themedColor }; return { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor };
} }
import { Box, Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import IconSvg from 'ui/shared/IconSvg';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
address: string;
isAutoConnectDisabled?: boolean;
className?: string;
};
const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) => {
const { themedBackgroundOrange } = useMenuButtonColors();
const isMobile = useIsMobile();
return (
<Box className={ className } position="relative">
<AddressIdenticon size={ 20 } hash={ address }/>
{ isAutoConnectDisabled && (
<Flex
alignItems="center"
justifyContent="center"
boxSize="14px"
position="absolute"
bottom={ isMobile ? '-3px' : '-1px' }
right={ isMobile ? '-4px' : '-5px' }
backgroundColor="rgba(16, 17, 18, 0.80)"
borderRadius="full"
border="1px solid"
borderColor={ themedBackgroundOrange }
>
<IconSvg
name="integration/partial"
color="white"
boxSize="8px"
/>
</Flex>
) }
</Box>
);
};
export default chakra(WalletIdenticon);
import { Box, Button, Text } from '@chakra-ui/react'; import { Box, Button, Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = { type Props = {
address?: string; address?: string;
disconnect?: () => void; disconnect?: () => void;
isAutoConnectDisabled?: boolean;
}; };
const WalletMenuContent = ({ address, disconnect }: Props) => { const WalletMenuContent = ({ address, disconnect, isAutoConnectDisabled }: Props) => {
const { themedBackgroundOrange } = useMenuButtonColors();
const onAddressClick = React.useCallback(() => { const onAddressClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Address click' }); mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Address click' });
}, []); }, []);
return ( return (
<Box> <Box>
{ isAutoConnectDisabled && (
<Flex
borderRadius="base"
p={ 3 }
mb={ 3 }
alignItems="center"
backgroundColor={ themedBackgroundOrange }
>
<IconSvg
name="integration/partial"
color="text"
boxSize={ 5 }
flexShrink={ 0 }
mr={ 2 }
/>
<Text fontSize="xs" lineHeight="16px">
Connect your wallet in the app below
</Text>
</Flex>
) }
<Text <Text
fontSize="sm" fontSize="sm"
fontWeight={ 600 } fontWeight={ 600 }
......
import type { ButtonProps } from '@chakra-ui/react'; import type { ButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean } from '@chakra-ui/react'; import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import useWallet from 'ui/snippets/walletMenu/useWallet'; import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent'; import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors'; import useMenuButtonColors from '../useMenuButtonColors';
import WalletIdenticon from './WalletIdenticon';
import WalletTooltip from './WalletTooltip'; import WalletTooltip from './WalletTooltip';
type Props = { type Props = {
isHomePage?: boolean; isHomePage?: boolean;
className?: string;
size?: 'sm' | 'md';
}; };
const WalletMenuDesktop = ({ isHomePage }: Props) => { const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet({ source: 'Header' }); const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet({ source: 'Header' });
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false); const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
const variant = React.useMemo(() => { const variant = React.useMemo(() => {
if (isWalletConnected) { if (isWalletConnected) {
...@@ -29,13 +33,16 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ...@@ -29,13 +33,16 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
return isHomePage ? 'solid' : 'outline'; return isHomePage ? 'solid' : 'outline';
}, [ isWalletConnected, isHomePage ]); }, [ isWalletConnected, isHomePage ]);
const themedColorForOrangeBg = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
let buttonStyles: Partial<ButtonProps> = {}; let buttonStyles: Partial<ButtonProps> = {};
if (isWalletConnected) { if (isWalletConnected) {
const backgroundColor = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground;
const color = isAutoConnectDisabled ? themedColorForOrangeBg : themedColor;
buttonStyles = { buttonStyles = {
bg: isHomePage ? 'blue.50' : themedBackground, bg: isHomePage ? 'blue.50' : backgroundColor,
color: isHomePage ? 'blackAlpha.800' : themedColor, color: isHomePage ? 'blackAlpha.800' : color,
_hover: { _hover: {
color: isHomePage ? 'blackAlpha.800' : themedColor, color: isHomePage ? 'blackAlpha.800' : color,
}, },
}; };
} else if (isHomePage) { } else if (isHomePage) {
...@@ -51,7 +58,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ...@@ -51,7 +58,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
const openPopover = React.useCallback(() => { const openPopover = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' }); mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' });
setIsPopoverOpen.on(); setIsPopoverOpen.toggle();
}, [ setIsPopoverOpen ]); }, [ setIsPopoverOpen ]);
return ( return (
...@@ -63,10 +70,15 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ...@@ -63,10 +70,15 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
isOpen={ isPopoverOpen } isOpen={ isPopoverOpen }
onClose={ setIsPopoverOpen.off } onClose={ setIsPopoverOpen.off }
> >
<WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || isMobile }>
<Box ml={ 2 }> <Box ml={ 2 }>
<PopoverTrigger> <PopoverTrigger>
<WalletTooltip
isDisabled={ isMobile === undefined || isMobile }
isWalletConnected={ isWalletConnected }
isAutoConnectDisabled={ isAutoConnectDisabled }
>
<Button <Button
className={ className }
variant={ variant } variant={ variant }
colorScheme="blue" colorScheme="blue"
flexShrink={ 0 } flexShrink={ 0 }
...@@ -74,24 +86,23 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ...@@ -74,24 +86,23 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
loadingText="Connect wallet" loadingText="Connect wallet"
onClick={ isWalletConnected ? openPopover : connect } onClick={ isWalletConnected ? openPopover : connect }
fontSize="sm" fontSize="sm"
size={ size }
{ ...buttonStyles } { ...buttonStyles }
> >
{ isWalletConnected ? ( { isWalletConnected ? (
<> <>
<Box mr={ 2 }> <WalletIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled } mr={ 2 }/>
<AddressIdenticon size={ 20 } hash={ address }/>
</Box>
<HashStringShorten hash={ address } isTooltipDisabled/> <HashStringShorten hash={ address } isTooltipDisabled/>
</> </>
) : 'Connect wallet' } ) : 'Connect wallet' }
</Button> </Button>
</WalletTooltip>
</PopoverTrigger> </PopoverTrigger>
</Box> </Box>
</WalletTooltip>
{ isWalletConnected && ( { isWalletConnected && (
<PopoverContent w="235px"> <PopoverContent w="235px">
<PopoverBody padding="24px 16px 16px 16px"> <PopoverBody padding="24px 16px 16px 16px">
<WalletMenuContent address={ address } disconnect={ disconnect }/> <WalletMenuContent address={ address } disconnect={ disconnect } isAutoConnectDisabled={ isAutoConnectDisabled }/>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
) } ) }
...@@ -99,4 +110,4 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ...@@ -99,4 +110,4 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
); );
}; };
export default WalletMenuDesktop; export default chakra(WalletMenuDesktop);
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react'; import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet'; import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent'; import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors'; import useMenuButtonColors from '../useMenuButtonColors';
import WalletIdenticon from './WalletIdenticon';
import WalletTooltip from './WalletTooltip'; import WalletTooltip from './WalletTooltip';
const WalletMenuMobile = () => { const WalletMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet({ source: 'Header' }); const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet({ source: 'Header' });
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
const openPopover = React.useCallback(() => { const openPopover = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' }); mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' });
onOpen(); onOpen();
}, [ onOpen ]); }, [ onOpen ]);
const themedBg = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground;
return ( return (
<> <>
<WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || !isMobile } isMobile> <WalletTooltip
isDisabled={ isMobile === undefined || !isMobile }
isMobile
isWalletConnected={ isWalletConnected }
isAutoConnectDisabled={ isAutoConnectDisabled }
>
<IconButton <IconButton
aria-label="wallet menu" aria-label="wallet menu"
icon={ isWalletConnected ? icon={ isWalletConnected ?
<AddressIdenticon size={ 20 } hash={ address }/> : <WalletIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled }/> :
<IconSvg name="wallet" boxSize={ 6 }/> <IconSvg name="wallet" boxSize={ 6 }/>
} }
variant={ isWalletConnected ? 'subtle' : 'outline' } variant={ isWalletConnected ? 'subtle' : 'outline' }
colorScheme="gray" colorScheme="gray"
boxSize="40px" boxSize="40px"
flexShrink={ 0 } flexShrink={ 0 }
bg={ isWalletConnected ? themedBackground : undefined } bg={ isWalletConnected ? themedBg : undefined }
color={ themedColor } color={ themedColor }
borderColor={ !isWalletConnected ? themedBorderColor : undefined } borderColor={ !isWalletConnected ? themedBorderColor : undefined }
onClick={ isWalletConnected ? openPopover : connect } onClick={ isWalletConnected ? openPopover : connect }
...@@ -52,7 +61,7 @@ const WalletMenuMobile = () => { ...@@ -52,7 +61,7 @@ const WalletMenuMobile = () => {
<DrawerOverlay/> <DrawerOverlay/>
<DrawerContent maxWidth="260px"> <DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }> <DrawerBody p={ 6 }>
<WalletMenuContent address={ address } disconnect={ disconnect }/> <WalletMenuContent address={ address } disconnect={ disconnect } isAutoConnectDisabled={ isAutoConnectDisabled }/>
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
......
import { Tooltip, useBoolean, useOutsideClick } from '@chakra-ui/react'; import { Tooltip, useBoolean, useOutsideClick, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,38 +9,46 @@ type Props = { ...@@ -9,38 +9,46 @@ type Props = {
children: React.ReactNode; children: React.ReactNode;
isDisabled?: boolean; isDisabled?: boolean;
isMobile?: boolean; isMobile?: boolean;
isWalletConnected?: boolean;
isAutoConnectDisabled?: boolean;
}; };
const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => { const LOCAL_STORAGE_KEY = 'wallet-connect-tooltip-shown';
const WalletTooltip = ({ children, isDisabled, isMobile, isWalletConnected, isAutoConnectDisabled }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
const router = useRouter(); const router = useRouter();
const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false); const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false);
const ref = React.useRef(null); const innerRef = React.useRef(null);
useOutsideClick({ ref, handler: setIsTooltipShown.off }); useOutsideClick({ ref: innerRef, handler: setIsTooltipShown.off });
const label = React.useMemo(() => {
if (isWalletConnected) {
if (isAutoConnectDisabled) {
return <span>Your wallet is not<br/>connected to this app.<br/>Connect your wallet<br/>in the app directly</span>;
}
return <span>Your wallet is connected<br/>with Blockscout</span>;
}
return <span>Connect your wallet<br/>to Blockscout for<br/>full-featured access</span>;
}, [ isWalletConnected, isAutoConnectDisabled ]);
const { defaultLabel, label, localStorageKey } = React.useMemo(() => {
const isAppPage = router.pathname === '/apps/[id]'; 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(() => { React.useEffect(() => {
const wasShown = window.localStorage.getItem(localStorageKey); const wasShown = window.localStorage.getItem(LOCAL_STORAGE_KEY);
const isMarketplacePage = [ '/apps', '/apps/[id]' ].includes(router.pathname); const isMarketplacePage = router.pathname === '/apps';
const isTooltipShowAction = router.query.action === 'tooltip'; const isTooltipShowAction = router.query.action === 'tooltip';
const isConnectWalletAction = router.query.action === 'connect'; const isConnectWalletAction = router.query.action === 'connect';
const needToShow = (!wasShown && !isConnectWalletAction) || isTooltipShowAction; const needToShow = (isAppPage && !isConnectWalletAction) || isTooltipShowAction || (!wasShown && isMarketplacePage);
let timer1: ReturnType<typeof setTimeout>; let timer1: ReturnType<typeof setTimeout>;
let timer2: ReturnType<typeof setTimeout>; let timer2: ReturnType<typeof setTimeout>;
if (!isDisabled && isMarketplacePage && needToShow) { if (!isDisabled && needToShow) {
timer1 = setTimeout(() => { timer1 = setTimeout(() => {
setIsTooltipShown.on(); setIsTooltipShown.on();
window.localStorage.setItem(localStorageKey, 'true');
timer2 = setTimeout(() => setIsTooltipShown.off(), 5 * SECOND); timer2 = setTimeout(() => setIsTooltipShown.off(), 5 * SECOND);
if (!wasShown && isMarketplacePage) {
window.localStorage.setItem(LOCAL_STORAGE_KEY, 'true');
}
if (isTooltipShowAction) { if (isTooltipShowAction) {
removeQueryParam(router, 'action'); removeQueryParam(router, 'action');
} }
...@@ -51,23 +59,25 @@ const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => { ...@@ -51,23 +59,25 @@ const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => {
clearTimeout(timer1); clearTimeout(timer1);
clearTimeout(timer2); clearTimeout(timer2);
}; };
}, [ setIsTooltipShown, localStorageKey, isDisabled, router ]); }, [ setIsTooltipShown, isDisabled, router, isAppPage ]);
return ( return (
<Box ref={ ref }>
<Tooltip <Tooltip
label={ isTooltipShown ? label : defaultLabel } label={ label }
textAlign="center" textAlign="center"
padding={ 2 } padding={ 2 }
isDisabled={ isDisabled } isDisabled={ isDisabled || (isWalletConnected && !isAppPage) }
openDelay={ 500 } openDelay={ 500 }
isOpen={ isTooltipShown || (isMobile ? false : undefined) } isOpen={ isTooltipShown || (isMobile ? false : undefined) }
onClose={ setIsTooltipShown.off } onClose={ setIsTooltipShown.off }
display={ isMobile ? { base: 'flex', lg: 'none' } : { base: 'none', lg: 'flex' } } display={ isMobile ? { base: 'flex', lg: 'none' } : { base: 'none', lg: 'flex' } }
ref={ ref } ref={ innerRef }
> >
{ children } { children }
</Tooltip> </Tooltip>
</Box>
); );
}; };
export default WalletTooltip; export default React.forwardRef(WalletTooltip);
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