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 = {
name: 'tom goriunov',
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 * 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 buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -9,6 +11,14 @@ import VerifiedAddresses from './VerifiedAddresses';
const VERIFIED_ADDRESS_URL = buildApiUrl('verified_addresses', { chainId: '1' });
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 }) => {
await context.route(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, (route) => {
......@@ -28,6 +38,32 @@ test('base view +@mobile', async({ mount, page }) => {
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(
<TestApp>
<VerifiedAddresses/>
......@@ -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(
<TestApp>
<VerifiedAddresses/>
......@@ -100,6 +140,10 @@ test('application update flow', async({ mount, page }) => {
body: JSON.stringify(mocks.TOKEN_INFO_FORM_CONFIG),
}));
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
// PUT request
await page.route(TOKEN_INFO_APPLICATION_URL, (route) => route.fulfill({
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 { useRouter } from 'next/router';
import React from 'react';
......@@ -7,6 +7,7 @@ import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, Veri
import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -37,16 +38,20 @@ const VerifiedAddresses = () => {
const modalProps = useDisclosure();
const queryClient = useQueryClient();
const userInfoQuery = useFetchProfileInfo();
const addressesQuery = useApiQuery('verified_addresses', {
pathParams: { chainId: config.chain.id },
queryOptions: {
placeholderData: { verifiedAddresses: Array(3).fill(VERIFIED_ADDRESS) },
enabled: Boolean(userInfoQuery.data?.email),
},
});
const applicationsQuery = useApiQuery('token_info_applications', {
pathParams: { chainId: config.chain.id, id: undefined },
queryOptions: {
placeholderData: { submissions: Array(3).fill(TOKEN_INFO_APPLICATION) },
enabled: Boolean(userInfoQuery.data?.email),
select: (data) => {
return {
...data,
......@@ -57,6 +62,7 @@ const VerifiedAddresses = () => {
});
const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData;
const userWithoutEmail = userInfoQuery.data && !userInfoQuery.data.email;
const handleGoBack = React.useCallback(() => {
setSelectedAddress(undefined);
......@@ -100,13 +106,23 @@ const VerifiedAddresses = () => {
});
}, [ queryClient ]);
const addButton = (
const addButton = (() => {
if (userWithoutEmail) {
return (
<Button size="lg" isDisabled mt={ 8 }>
Add address
</Button>
);
}
return (
<Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block">
<Button size="lg" onClick={ modalProps.onOpen }>
Add address
</Button>
</Skeleton>
);
})();
const backLink = React.useMemo(() => {
if (!selectedAddress) {
......@@ -135,14 +151,23 @@ const VerifiedAddresses = () => {
);
}
const content = addressesQuery.data?.verifiedAddresses ? (
const content = (() => {
if (userWithoutEmail) {
return null;
}
if (addressesQuery.data?.verifiedAddresses) {
return (
<>
<Show below="lg" key="content-mobile" ssr={ false }>
{ addressesQuery.data.verifiedAddresses.map((item, index) => (
<VerifiedAddressesListItem
key={ item.contractAddress + (isLoading ? index : '') }
item={ item }
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) }
application={
applicationsQuery.data?.submissions
?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase())
}
onAdd={ handleItemAdd }
onEdit={ handleItemEdit }
isLoading={ isLoading }
......@@ -159,11 +184,20 @@ const VerifiedAddresses = () => {
/>
</Hide>
</>
) : null;
);
}
return null;
})();
return (
<>
<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 }>
<span>
Verify ownership of a smart contract address to easily update information in Blockscout.
......@@ -188,7 +222,7 @@ const VerifiedAddresses = () => {
<AdminSupportText mt={ 5 }/>
</AccountPageDescription>
<DataListDisplay
isError={ addressesQuery.isError || applicationsQuery.isError }
isError={ userInfoQuery.isError || addressesQuery.isError || applicationsQuery.isError }
items={ addressesQuery.data?.verifiedAddresses }
content={ content }
emptyText=""
......
import { test, expect } from '@playwright/experimental-ct-react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
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.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 }) => {
const component = await mount(
<TestApp>
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { ItemProps } from './types';
import config from 'configs/app';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -27,6 +28,8 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
const isTxPage = router.pathname === '/tx/[hash]';
const isAccountActionAllowed = useIsAccountActionAllowed();
const userInfoQuery = useFetchProfileInfo();
const handleButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' });
}, []);
......@@ -35,10 +38,12 @@ const AccountActionsMenu = ({ isLoading, className }: Props) => {
return null;
}
const userWithoutEmail = userInfoQuery.data && !userInfoQuery.data.email;
const items = [
{
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' }/>,
......
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