diff --git a/lib/contexts/marketplace.tsx b/lib/contexts/marketplace.tsx new file mode 100644 index 0000000000..2aba76e39b --- /dev/null +++ b/lib/contexts/marketplace.tsx @@ -0,0 +1,48 @@ +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({ + 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 ( + + { children } + + ); +} + +export function useMarketplaceContext() { + return useContext(MarketplaceContext); +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 15fc5edd91..38b3c1d358 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -12,6 +12,7 @@ import config from 'configs/app'; import useQueryClientConfig from 'lib/api/useQueryClientConfig'; import { AppContextProvider } from 'lib/contexts/app'; import { ChakraProvider } from 'lib/contexts/chakra'; +import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { growthBook } from 'lib/growthbook/init'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; @@ -67,7 +68,9 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { - { getLayout() } + + { getLayout() } + diff --git a/ui/marketplace/AppSecurityReport.tsx b/ui/marketplace/AppSecurityReport.tsx index 35301af312..70a060f1b5 100644 --- a/ui/marketplace/AppSecurityReport.tsx +++ b/ui/marketplace/AppSecurityReport.tsx @@ -54,6 +54,7 @@ const AppSecurityReport = ({ id, securityReport, height, showContractList, isLoa onClick={ handleButtonClick } height={ height } onlyIcon={ onlyIcon } + label="The security score is based on analysis of a DApp's smart contracts." /> diff --git a/ui/marketplace/MarketplaceAppAlert.tsx b/ui/marketplace/MarketplaceAppAlert.tsx deleted file mode 100644 index 88436a603d..0000000000 --- a/ui/marketplace/MarketplaceAppAlert.tsx +++ /dev/null @@ -1,51 +0,0 @@ -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 ( - - - { message.text } - - ); -}; - -export default MarketplaceAppAlert; diff --git a/ui/marketplace/MarketplaceAppTopBar.tsx b/ui/marketplace/MarketplaceAppTopBar.tsx index 0b82faffeb..38654292c1 100644 --- a/ui/marketplace/MarketplaceAppTopBar.tsx +++ b/ui/marketplace/MarketplaceAppTopBar.tsx @@ -1,4 +1,4 @@ -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 type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace'; @@ -6,26 +6,28 @@ import { ContractListTypes } from 'types/client/marketplace'; import { route } from 'nextjs-routes'; +import config from 'configs/app'; import { useAppContext } from 'lib/contexts/app'; import useFeatureValue from 'lib/growthbook/useFeatureValue'; import useIsMobile from 'lib/hooks/useIsMobile'; import IconSvg from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/LinkExternal'; 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 ContractListModal from './ContractListModal'; -import MarketplaceAppAlert from './MarketplaceAppAlert'; import MarketplaceAppInfo from './MarketplaceAppInfo'; type Props = { data: MarketplaceAppOverview | undefined; isLoading: boolean; - isWalletConnected: boolean; securityReport?: MarketplaceAppSecurityReport; } -const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityReport }: Props) => { +const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => { const [ showContractList, setShowContractList ] = useBoolean(false); const appProps = useAppContext(); const isMobile = useIsMobile(); @@ -46,32 +48,14 @@ const MarketplaceAppTopBar = ({ data, isLoading, isWalletConnected, securityRepo return ( <> - - - + + { !isMobile && } + + - - - - - - - { (isExperiment && (securityReport || isLoading)) && ( - - - - ) } + + + + { (isExperiment && (securityReport || isLoading)) && ( + + ) } + { !isMobile && ( + + { config.features.account.isEnabled && } + { config.features.blockchainInteraction.isEnabled && } + + ) } { showContractList && ( { enabled: feature.isEnabled, }); const { data, isPending } = query; + const { setIsAutoConnectDisabled } = useMarketplaceContext(); useEffect(() => { if (data) { @@ -136,8 +138,9 @@ const MarketplaceApp = () => { { pathname: '/apps/[id]', query: { id: data.id } }, { app_name: data.title }, ); + setIsAutoConnectDisabled(!data.internalWallet); } - }, [ data ]); + }, [ data, setIsAutoConnectDisabled ]); throwOnResourceLoadError(query); @@ -146,7 +149,6 @@ const MarketplaceApp = () => { { +const UserAvatar = ({ size, fallbackIconSize = 20 }: Props) => { const appProps = useAppContext(); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies)); const [ isImageLoadError, setImageLoadError ] = React.useState(false); @@ -34,7 +35,7 @@ const UserAvatar = ({ size }: Props) => { boxSize={ `${ size }px` } borderRadius="full" overflow="hidden" - fallback={ isImageLoadError || !data?.avatar ? : undefined } + fallback={ isImageLoadError || !data?.avatar ? : undefined } onError={ handleImageLoadError } /> ); diff --git a/ui/shared/layout/LayoutApp.tsx b/ui/shared/layout/LayoutApp.tsx index c295c069a1..515c79bbcb 100644 --- a/ui/shared/layout/LayoutApp.tsx +++ b/ui/shared/layout/LayoutApp.tsx @@ -3,26 +3,28 @@ import React from 'react'; import type { Props } from './types'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; -import HeaderDesktop from 'ui/snippets/header/HeaderDesktop'; import HeaderMobile from 'ui/snippets/header/HeaderMobile'; import * as Layout from './components'; const LayoutDefault = ({ children }: Props) => { return ( - + - - + + - - + { children } diff --git a/ui/shared/layout/LayoutHome.tsx b/ui/shared/layout/LayoutHome.tsx index 6274afeeea..b6786f2339 100644 --- a/ui/shared/layout/LayoutHome.tsx +++ b/ui/shared/layout/LayoutHome.tsx @@ -12,7 +12,7 @@ const LayoutHome = ({ children }: Props) => { return ( - + { +const MainArea = ({ children, className }: Props) => { return ( - + { children } ); }; -export default React.memo(MainArea); +export default React.memo(chakra(MainArea)); diff --git a/ui/shared/solidityscanReport/SolidityscanReportButton.tsx b/ui/shared/solidityscanReport/SolidityscanReportButton.tsx index f8747185e0..d278c38ec8 100644 --- a/ui/shared/solidityscanReport/SolidityscanReportButton.tsx +++ b/ui/shared/solidityscanReport/SolidityscanReportButton.tsx @@ -13,16 +13,17 @@ interface Props { height?: string; onlyIcon?: boolean; onClick?: () => void; + label?: string; } const SolidityscanReportButton = ( - { className, score, isLoading, height = '32px', onlyIcon, onClick }: Props, + { className, score, isLoading, height = '32px', onlyIcon, onClick, label = 'Security score' }: Props, ref: React.ForwardedRef, ) => { const { scoreColor } = useScoreLevelAndColor(score); return ( - + - - - + + + { isWalletConnected && ( - + ) } @@ -99,4 +110,4 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ); }; -export default WalletMenuDesktop; +export default chakra(WalletMenuDesktop); diff --git a/ui/snippets/walletMenu/WalletMenuMobile.tsx b/ui/snippets/walletMenu/WalletMenuMobile.tsx index e8e6bcb48a..0711a107fd 100644 --- a/ui/snippets/walletMenu/WalletMenuMobile.tsx +++ b/ui/snippets/walletMenu/WalletMenuMobile.tsx @@ -1,41 +1,50 @@ import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react'; import React from 'react'; +import { useMarketplaceContext } from 'lib/contexts/marketplace'; import useIsMobile from 'lib/hooks/useIsMobile'; import * as mixpanel from 'lib/mixpanel/index'; -import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; import IconSvg from 'ui/shared/IconSvg'; import useWallet from 'ui/snippets/walletMenu/useWallet'; import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent'; import useMenuButtonColors from '../useMenuButtonColors'; +import WalletIdenticon from './WalletIdenticon'; import WalletTooltip from './WalletTooltip'; const WalletMenuMobile = () => { const { isOpen, onOpen, onClose } = useDisclosure(); 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 { isAutoConnectDisabled } = useMarketplaceContext(); const openPopover = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' }); onOpen(); }, [ onOpen ]); + const themedBg = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground; + return ( <> - + : + : } variant={ isWalletConnected ? 'subtle' : 'outline' } colorScheme="gray" boxSize="40px" flexShrink={ 0 } - bg={ isWalletConnected ? themedBackground : undefined } + bg={ isWalletConnected ? themedBg : undefined } color={ themedColor } borderColor={ !isWalletConnected ? themedBorderColor : undefined } onClick={ isWalletConnected ? openPopover : connect } @@ -52,7 +61,7 @@ const WalletMenuMobile = () => { - + diff --git a/ui/snippets/walletMenu/WalletTooltip.tsx b/ui/snippets/walletMenu/WalletTooltip.tsx index fc03e6dcee..d8ad0bcf79 100644 --- a/ui/snippets/walletMenu/WalletTooltip.tsx +++ b/ui/snippets/walletMenu/WalletTooltip.tsx @@ -1,4 +1,4 @@ -import { Tooltip, useBoolean, useOutsideClick } from '@chakra-ui/react'; +import { Tooltip, useBoolean, useOutsideClick, Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; @@ -9,38 +9,46 @@ type Props = { children: React.ReactNode; isDisabled?: 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) => { const router = useRouter(); const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false); - const ref = React.useRef(null); - useOutsideClick({ ref, handler: setIsTooltipShown.off }); + const innerRef = React.useRef(null); + useOutsideClick({ ref: innerRef, handler: setIsTooltipShown.off }); + + const label = React.useMemo(() => { + if (isWalletConnected) { + if (isAutoConnectDisabled) { + return Your wallet is not
connected to this app.
Connect your wallet
in the app directly
; + } + return Your wallet is connected
with Blockscout
; + } + return Connect your wallet
to Blockscout for
full-featured access
; + }, [ isWalletConnected, isAutoConnectDisabled ]); - const { defaultLabel, label, localStorageKey } = React.useMemo(() => { - const isAppPage = router.pathname === '/apps/[id]'; - const defaultLabel = Your wallet is used to interact with
apps and contracts in the explorer
; - const label = isAppPage ? - Connect once to use your wallet with
all apps in the DAppscout marketplace!
: - defaultLabel; - const localStorageKey = `${ isAppPage ? 'dapp-' : '' }wallet-connect-tooltip-shown`; - return { defaultLabel, label, localStorageKey }; - }, [ router.pathname ]); + const isAppPage = router.pathname === '/apps/[id]'; React.useEffect(() => { - const wasShown = window.localStorage.getItem(localStorageKey); - const isMarketplacePage = [ '/apps', '/apps/[id]' ].includes(router.pathname); + const wasShown = window.localStorage.getItem(LOCAL_STORAGE_KEY); + const isMarketplacePage = router.pathname === '/apps'; const isTooltipShowAction = router.query.action === 'tooltip'; const isConnectWalletAction = router.query.action === 'connect'; - const needToShow = (!wasShown && !isConnectWalletAction) || isTooltipShowAction; + const needToShow = (isAppPage && !isConnectWalletAction) || isTooltipShowAction || (!wasShown && isMarketplacePage); let timer1: ReturnType; let timer2: ReturnType; - if (!isDisabled && isMarketplacePage && needToShow) { + if (!isDisabled && needToShow) { timer1 = setTimeout(() => { setIsTooltipShown.on(); - window.localStorage.setItem(localStorageKey, 'true'); timer2 = setTimeout(() => setIsTooltipShown.off(), 5 * SECOND); + if (!wasShown && isMarketplacePage) { + window.localStorage.setItem(LOCAL_STORAGE_KEY, 'true'); + } if (isTooltipShowAction) { removeQueryParam(router, 'action'); } @@ -51,23 +59,25 @@ const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => { clearTimeout(timer1); clearTimeout(timer2); }; - }, [ setIsTooltipShown, localStorageKey, isDisabled, router ]); + }, [ setIsTooltipShown, isDisabled, router, isAppPage ]); return ( - - { children } - + + + { children } + + ); }; -export default WalletTooltip; +export default React.forwardRef(WalletTooltip);