Commit 18eb8120 authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge pull request #2153 from blockscout/ens-in-wallet-menu

Display ENS on the wallet button and in the menu
parents 0684b062 b870a432
......@@ -52,5 +52,6 @@ NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
......@@ -232,6 +232,12 @@ export const RESOURCES = {
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const, 'protocols' as const ],
},
address_domain: {
path: '/api/v1/:chainId/addresses/:address',
pathParams: [ 'chainId' as const, 'address' as const ],
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
},
domain_info: {
path: '/api/v1/:chainId/domains/:name',
pathParams: [ 'chainId' as const, 'name' as const ],
......@@ -1092,6 +1098,7 @@ Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch :
Q extends 'zksync_l2_txn_batch_txs' ? ZkSyncBatchTxs :
Q extends 'contract_security_audits' ? SmartContractSecurityAudits :
Q extends 'addresses_lookup' ? bens.LookupAddressResponse :
Q extends 'address_domain' ? bens.GetAddressResponse :
Q extends 'domain_info' ? bens.DetailedDomain :
Q extends 'domain_events' ? bens.ListDomainEventsResponse :
Q extends 'domains_lookup' ? bens.LookupDomainNameResponse :
......
......@@ -10,13 +10,14 @@ import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
address?: string;
ensDomainName?: string | null;
disconnect?: () => void;
isAutoConnectDisabled?: boolean;
openWeb3Modal: () => void;
closeWalletMenu: () => void;
};
const WalletMenuContent = ({ address, disconnect, isAutoConnectDisabled, openWeb3Modal, closeWalletMenu }: Props) => {
const WalletMenuContent = ({ address, ensDomainName, disconnect, isAutoConnectDisabled, openWeb3Modal, closeWalletMenu }: Props) => {
const { themedBackgroundOrange } = useMenuButtonColors();
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
......@@ -71,7 +72,7 @@ const WalletMenuContent = ({ address, disconnect, isAutoConnectDisabled, openWeb
</Text>
<Flex alignItems="center" mb={ 6 }>
<AddressEntity
address={{ hash: address }}
address={{ hash: address, ens_domain_name: ensDomainName }}
noTooltip
truncation="dynamic"
fontSize="sm"
......
import React from 'react';
import type * as bens from '@blockscout/bens-types';
import config from 'configs/app';
import * as addressMock from 'mocks/address/address';
import * as domainMock from 'mocks/ens/domain';
import { test, expect } from 'playwright/lib';
import { WalletMenuDesktop } from './WalletMenuDesktop';
const props = {
isWalletConnected: false,
address: '',
connect: () => {},
disconnect: () => {},
isModalOpening: false,
isModalOpen: false,
openModal: () => {},
};
test.use({ viewport: { width: 1440, height: 750 } }); // xl
test('wallet is not connected +@dark-mode', async({ page, render }) => {
await render(<WalletMenuDesktop { ...props }/>);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 50 } });
});
test('wallet is not connected (home page) +@dark-mode', async({ page, render }) => {
await render(<WalletMenuDesktop { ...props } isHomePage/>);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 50 } });
});
test('wallet is loading', async({ page, render }) => {
await render(<WalletMenuDesktop { ...props } isModalOpen/>);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 50 } });
});
test('wallet connected +@dark-mode', async({ page, render, mockApiResponse }) => {
await mockApiResponse(
'address_domain',
{ domain: undefined, resolved_domains_count: 0 } as bens.GetAddressResponse,
{ pathParams: { address: addressMock.hash, chainId: config.chain.id } },
);
const component = await render(<WalletMenuDesktop { ...props } isWalletConnected address={ addressMock.hash }/>);
await component.locator('button').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 300 } });
});
test('wallet connected (home page) +@dark-mode', async({ page, render }) => {
const component = await render(<WalletMenuDesktop { ...props } isHomePage isWalletConnected address={ addressMock.hash }/>);
await component.locator('button').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 300 } });
});
test('wallet with ENS connected', async({ page, render, mockApiResponse }) => {
await mockApiResponse(
'address_domain',
{ domain: domainMock.ensDomainB, resolved_domains_count: 1 } as bens.GetAddressResponse,
{ pathParams: { address: addressMock.hash, chainId: config.chain.id } },
);
const component = await render(<WalletMenuDesktop { ...props } isWalletConnected address={ addressMock.hash }/>);
await component.locator('button').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 300 } });
});
......@@ -2,6 +2,8 @@ import type { ButtonProps } from '@chakra-ui/react';
import { PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -21,12 +23,33 @@ type Props = {
size?: 'sm' | 'md';
};
const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen, openModal } = useWallet({ source: 'Header' });
type ComponentProps = Props & {
isWalletConnected: boolean;
address: string;
connect: () => void;
disconnect: () => void;
isModalOpening: boolean;
isModalOpen: boolean;
openModal: () => void;
};
export const WalletMenuDesktop = ({
isHomePage, className, size = 'md', isWalletConnected, address, connect,
disconnect, isModalOpening, isModalOpen, openModal,
}: ComponentProps) => {
const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
const addressDomainQuery = useApiQuery('address_domain', {
pathParams: {
chainId: config.chain.id,
address,
},
queryOptions: {
enabled: config.features.nameService.isEnabled,
},
});
const variant = React.useMemo(() => {
if (isWalletConnected) {
......@@ -83,7 +106,10 @@ const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
variant={ variant }
colorScheme="blue"
flexShrink={ 0 }
isLoading={ (isModalOpening || isModalOpen) && !isWalletConnected }
isLoading={
((isModalOpening || isModalOpen) && !isWalletConnected) ||
(addressDomainQuery.isLoading && isWalletConnected)
}
loadingText="Connect wallet"
onClick={ isWalletConnected ? openPopover : connect }
fontSize="sm"
......@@ -94,7 +120,11 @@ const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
{ isWalletConnected ? (
<>
<WalletIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled } mr={ 2 }/>
{ addressDomainQuery.data?.domain?.name ? (
<chakra.span>{ addressDomainQuery.data.domain?.name }</chakra.span>
) : (
<HashStringShorten hash={ address } isTooltipDisabled/>
) }
</>
) : (
<>
......@@ -111,6 +141,7 @@ const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
<PopoverBody padding="24px 16px 16px 16px">
<WalletMenuContent
address={ address }
ensDomainName={ addressDomainQuery.data?.domain?.name }
disconnect={ disconnect }
isAutoConnectDisabled={ isAutoConnectDisabled }
openWeb3Modal={ openModal }
......@@ -123,4 +154,27 @@ const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
);
};
export default chakra(WalletMenuDesktop);
// separated the useWallet hook from the main component because it's hard to mock it in tests
const WalletMenuDesktopWrapper = ({ isHomePage, className, size = 'md' }: Props) => {
const {
isWalletConnected, address, connect, disconnect,
isModalOpening, isModalOpen, openModal,
} = useWallet({ source: 'Header' });
return (
<WalletMenuDesktop
isHomePage={ isHomePage }
className={ className }
size={ size }
isWalletConnected={ isWalletConnected }
address={ address }
connect={ connect }
disconnect={ disconnect }
isModalOpening={ isModalOpening }
isModalOpen={ isModalOpen }
openModal={ openModal }
/>
);
};
export default chakra(WalletMenuDesktopWrapper);
import React from 'react';
import type * as bens from '@blockscout/bens-types';
import config from 'configs/app';
import * as addressMock from 'mocks/address/address';
import * as domainMock from 'mocks/ens/domain';
import { test, expect, devices } from 'playwright/lib';
import { WalletMenuMobile } from './WalletMenuMobile';
const props = {
isWalletConnected: false,
address: '',
connect: () => {},
disconnect: () => {},
isModalOpening: false,
isModalOpen: false,
openModal: () => {},
};
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('wallet is not connected +@dark-mode', async({ page, render }) => {
await render(<WalletMenuMobile { ...props }/>);
await expect(page).toHaveScreenshot();
});
test('wallet is loading', async({ page, render }) => {
await render(<WalletMenuMobile { ...props } isModalOpen/>);
await expect(page).toHaveScreenshot();
});
test('wallet connected +@dark-mode', async({ page, render, mockApiResponse }) => {
await mockApiResponse(
'address_domain',
{ domain: undefined, resolved_domains_count: 0 } as bens.GetAddressResponse,
{ pathParams: { address: addressMock.hash, chainId: config.chain.id } },
);
const component = await render(<WalletMenuMobile { ...props } isWalletConnected address={ addressMock.hash }/>);
await component.locator('button').click();
await expect(page).toHaveScreenshot();
});
test('wallet with ENS connected', async({ page, render, mockApiResponse }) => {
await mockApiResponse(
'address_domain',
{ domain: domainMock.ensDomainB, resolved_domains_count: 1 } as bens.GetAddressResponse,
{ pathParams: { address: addressMock.hash, chainId: config.chain.id } },
);
const component = await render(<WalletMenuMobile { ...props } isWalletConnected address={ addressMock.hash }/>);
await component.locator('button').click();
await expect(page).toHaveScreenshot();
});
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -12,12 +14,32 @@ import useMenuButtonColors from '../useMenuButtonColors';
import WalletIdenticon from './WalletIdenticon';
import WalletTooltip from './WalletTooltip';
const WalletMenuMobile = () => {
type ComponentProps = {
isWalletConnected: boolean;
address: string;
connect: () => void;
disconnect: () => void;
isModalOpening: boolean;
isModalOpen: boolean;
openModal: () => void;
};
export const WalletMenuMobile = (
{ isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen, openModal }: ComponentProps,
) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen, openModal } = useWallet({ source: 'Header' });
const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
const addressDomainQuery = useApiQuery('address_domain', {
pathParams: {
chainId: config.chain.id,
address,
},
queryOptions: {
enabled: config.features.nameService.isEnabled,
},
});
const openPopover = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' });
......@@ -48,7 +70,10 @@ const WalletMenuMobile = () => {
color={ themedColor }
borderColor={ !isWalletConnected ? themedBorderColor : undefined }
onClick={ isWalletConnected ? openPopover : connect }
isLoading={ (isModalOpening || isModalOpen) && !isWalletConnected }
isLoading={
((isModalOpening || isModalOpen) && !isWalletConnected) ||
(addressDomainQuery.isLoading && isWalletConnected)
}
/>
</WalletTooltip>
{ isWalletConnected && (
......@@ -63,6 +88,7 @@ const WalletMenuMobile = () => {
<DrawerBody p={ 6 }>
<WalletMenuContent
address={ address }
ensDomainName={ addressDomainQuery.data?.domain?.name }
disconnect={ disconnect }
isAutoConnectDisabled={ isAutoConnectDisabled }
openWeb3Modal={ openModal }
......@@ -76,4 +102,23 @@ const WalletMenuMobile = () => {
);
};
export default WalletMenuMobile;
const WalletMenuMobileWrapper = () => {
const {
isWalletConnected, address, connect, disconnect,
isModalOpening, isModalOpen, openModal,
} = useWallet({ source: 'Header' });
return (
<WalletMenuMobile
isWalletConnected={ isWalletConnected }
address={ address }
connect={ connect }
disconnect={ disconnect }
isModalOpening={ isModalOpening }
isModalOpen={ isModalOpen }
openModal={ openModal }
/>
);
};
export default WalletMenuMobileWrapper;
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