Commit 09e8ab96 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Add a banner to the My Verified Addresses page (#1695)

* Add a banner to the My Verified Addresses page

Fixes #1688

* [skip ci] change text copy
parent 754a976b
...@@ -4,3 +4,10 @@ export const base = { ...@@ -4,3 +4,10 @@ export const base = {
name: 'tom goriunov', name: 'tom goriunov',
nickname: 'tom2drum', nickname: 'tom2drum',
}; };
export const withoutEmail = {
avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: null,
name: 'tom goriunov',
nickname: 'tom2drum',
};
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as mocks from 'mocks/account/verifiedAddresses'; import * as mocks from 'mocks/account/verifiedAddresses';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -9,6 +11,14 @@ import VerifiedAddresses from './VerifiedAddresses'; ...@@ -9,6 +11,14 @@ import VerifiedAddresses from './VerifiedAddresses';
const VERIFIED_ADDRESS_URL = buildApiUrl('verified_addresses', { chainId: '1' }); const VERIFIED_ADDRESS_URL = buildApiUrl('verified_addresses', { chainId: '1' });
const TOKEN_INFO_APPLICATIONS_URL = buildApiUrl('token_info_applications', { chainId: '1', id: undefined }); const TOKEN_INFO_APPLICATIONS_URL = buildApiUrl('token_info_applications', { chainId: '1', id: undefined });
const USER_INFO_URL = buildApiUrl('user_info');
const test = base.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
test.beforeEach(async({ context }) => { test.beforeEach(async({ context }) => {
await context.route(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, (route) => { await context.route(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, (route) => {
...@@ -28,6 +38,32 @@ test('base view +@mobile', async({ mount, page }) => { ...@@ -28,6 +38,32 @@ test('base view +@mobile', async({ mount, page }) => {
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT), body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT),
})); }));
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
const component = await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('user without email', async({ mount, page }) => {
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT),
}));
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT),
}));
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.withoutEmail),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<VerifiedAddresses/> <VerifiedAddresses/>
...@@ -59,6 +95,10 @@ test('address verification flow', async({ mount, page }) => { ...@@ -59,6 +95,10 @@ test('address verification flow', async({ mount, page }) => {
}); });
}); });
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
await mount( await mount(
<TestApp> <TestApp>
<VerifiedAddresses/> <VerifiedAddresses/>
...@@ -100,6 +140,10 @@ test('application update flow', async({ mount, page }) => { ...@@ -100,6 +140,10 @@ test('application update flow', async({ mount, page }) => {
body: JSON.stringify(mocks.TOKEN_INFO_FORM_CONFIG), body: JSON.stringify(mocks.TOKEN_INFO_FORM_CONFIG),
})); }));
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
// PUT request // PUT request
await page.route(TOKEN_INFO_APPLICATION_URL, (route) => route.fulfill({ await page.route(TOKEN_INFO_APPLICATION_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM), body: JSON.stringify(mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM),
......
import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Link } from '@chakra-ui/react'; import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Link, Alert } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -7,6 +7,7 @@ import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, Veri ...@@ -7,6 +7,7 @@ import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, Veri
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType'; import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -37,16 +38,20 @@ const VerifiedAddresses = () => { ...@@ -37,16 +38,20 @@ const VerifiedAddresses = () => {
const modalProps = useDisclosure(); const modalProps = useDisclosure();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const userInfoQuery = useFetchProfileInfo();
const addressesQuery = useApiQuery('verified_addresses', { const addressesQuery = useApiQuery('verified_addresses', {
pathParams: { chainId: config.chain.id }, pathParams: { chainId: config.chain.id },
queryOptions: { queryOptions: {
placeholderData: { verifiedAddresses: Array(3).fill(VERIFIED_ADDRESS) }, placeholderData: { verifiedAddresses: Array(3).fill(VERIFIED_ADDRESS) },
enabled: Boolean(userInfoQuery.data?.email),
}, },
}); });
const applicationsQuery = useApiQuery('token_info_applications', { const applicationsQuery = useApiQuery('token_info_applications', {
pathParams: { chainId: config.chain.id, id: undefined }, pathParams: { chainId: config.chain.id, id: undefined },
queryOptions: { queryOptions: {
placeholderData: { submissions: Array(3).fill(TOKEN_INFO_APPLICATION) }, placeholderData: { submissions: Array(3).fill(TOKEN_INFO_APPLICATION) },
enabled: Boolean(userInfoQuery.data?.email),
select: (data) => { select: (data) => {
return { return {
...data, ...data,
...@@ -57,6 +62,7 @@ const VerifiedAddresses = () => { ...@@ -57,6 +62,7 @@ const VerifiedAddresses = () => {
}); });
const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData; const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData;
const userWithoutEmail = userInfoQuery.data && !userInfoQuery.data.email;
const handleGoBack = React.useCallback(() => { const handleGoBack = React.useCallback(() => {
setSelectedAddress(undefined); setSelectedAddress(undefined);
...@@ -100,13 +106,23 @@ const VerifiedAddresses = () => { ...@@ -100,13 +106,23 @@ const VerifiedAddresses = () => {
}); });
}, [ queryClient ]); }, [ queryClient ]);
const addButton = ( const addButton = (() => {
<Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block"> if (userWithoutEmail) {
<Button size="lg" onClick={ modalProps.onOpen }> return (
Add address <Button size="lg" isDisabled mt={ 8 }>
</Button> Add address
</Skeleton> </Button>
); );
}
return (
<Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block">
<Button size="lg" onClick={ modalProps.onOpen }>
Add address
</Button>
</Skeleton>
);
})();
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
if (!selectedAddress) { if (!selectedAddress) {
...@@ -135,35 +151,53 @@ const VerifiedAddresses = () => { ...@@ -135,35 +151,53 @@ const VerifiedAddresses = () => {
); );
} }
const content = addressesQuery.data?.verifiedAddresses ? ( const content = (() => {
<> if (userWithoutEmail) {
<Show below="lg" key="content-mobile" ssr={ false }> return null;
{ addressesQuery.data.verifiedAddresses.map((item, index) => ( }
<VerifiedAddressesListItem
key={ item.contractAddress + (isLoading ? index : '') } if (addressesQuery.data?.verifiedAddresses) {
item={ item } return (
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) } <>
onAdd={ handleItemAdd } <Show below="lg" key="content-mobile" ssr={ false }>
onEdit={ handleItemEdit } { addressesQuery.data.verifiedAddresses.map((item, index) => (
isLoading={ isLoading } <VerifiedAddressesListItem
/> key={ item.contractAddress + (isLoading ? index : '') }
)) } item={ item }
</Show> application={
<Hide below="lg" key="content-desktop" ssr={ false }> applicationsQuery.data?.submissions
<VerifiedAddressesTable ?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase())
data={ addressesQuery.data.verifiedAddresses } }
applications={ applicationsQuery.data?.submissions } onAdd={ handleItemAdd }
onItemEdit={ handleItemEdit } onEdit={ handleItemEdit }
onItemAdd={ handleItemAdd } isLoading={ isLoading }
isLoading={ isLoading } />
/> )) }
</Hide> </Show>
</> <Hide below="lg" key="content-desktop" ssr={ false }>
) : null; <VerifiedAddressesTable
data={ addressesQuery.data.verifiedAddresses }
applications={ applicationsQuery.data?.submissions }
onItemEdit={ handleItemEdit }
onItemAdd={ handleItemAdd }
isLoading={ isLoading }
/>
</Hide>
</>
);
}
return null;
})();
return ( return (
<> <>
<PageTitle title="My verified addresses"/> <PageTitle title="My verified addresses"/>
{ userWithoutEmail && (
<Alert status="warning" mb={ 6 }>
You need a valid email address to verify addresses. Please logout of MyAccount then login using your email to proceed.
</Alert>
) }
<AccountPageDescription allowCut={ false }> <AccountPageDescription allowCut={ false }>
<span> <span>
Verify ownership of a smart contract address to easily update information in Blockscout. Verify ownership of a smart contract address to easily update information in Blockscout.
...@@ -188,7 +222,7 @@ const VerifiedAddresses = () => { ...@@ -188,7 +222,7 @@ const VerifiedAddresses = () => {
<AdminSupportText mt={ 5 }/> <AdminSupportText mt={ 5 }/>
</AccountPageDescription> </AccountPageDescription>
<DataListDisplay <DataListDisplay
isError={ addressesQuery.isError || applicationsQuery.isError } isError={ userInfoQuery.isError || addressesQuery.isError || applicationsQuery.isError }
items={ addressesQuery.data?.verifiedAddresses } items={ addressesQuery.data?.verifiedAddresses }
content={ content } content={ content }
emptyText="" emptyText=""
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AccountActionsMenu from './AccountActionsMenu'; import AccountActionsMenu from './AccountActionsMenu';
const USER_INFO_URL = buildApiUrl('user_info');
const test = base.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
test.use({ viewport: { width: 200, height: 200 } }); test.use({ viewport: { width: 200, height: 200 } });
test.describe('with multiple items', async() => { test.describe('with multiple items', async() => {
...@@ -16,6 +28,12 @@ test.describe('with multiple items', async() => { ...@@ -16,6 +28,12 @@ test.describe('with multiple items', async() => {
}, },
}; };
test.beforeEach(async({ page }) => {
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
});
test('base view', async({ mount, page }) => { test('base view', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { ItemProps } from './types'; import type { ItemProps } from './types';
import config from 'configs/app'; import config from 'configs/app';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed'; import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -27,6 +28,8 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => { ...@@ -27,6 +28,8 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
const isTxPage = router.pathname === '/tx/[hash]'; const isTxPage = router.pathname === '/tx/[hash]';
const isAccountActionAllowed = useIsAccountActionAllowed(); const isAccountActionAllowed = useIsAccountActionAllowed();
const userInfoQuery = useFetchProfileInfo();
const handleButtonClick = React.useCallback(() => { const handleButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' });
}, []); }, []);
...@@ -35,10 +38,12 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => { ...@@ -35,10 +38,12 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
return null; return null;
} }
const userWithoutEmail = userInfoQuery.data && !userInfoQuery.data.email;
const items = [ const items = [
{ {
render: (props: ItemProps) => <TokenInfoMenuItem { ...props }/>, render: (props: ItemProps) => <TokenInfoMenuItem { ...props }/>,
enabled: isTokenPage && config.features.addressVerification.isEnabled, enabled: isTokenPage && config.features.addressVerification.isEnabled && !userWithoutEmail,
}, },
{ {
render: (props: ItemProps) => <PrivateTagMenuItem { ...props } entityType={ isTxPage ? 'tx' : 'address' }/>, render: (props: ItemProps) => <PrivateTagMenuItem { ...props } entityType={ isTxPage ? 'tx' : 'address' }/>,
......
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