Commit 68a10c8a authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'release/v2-0-0' of https://github.com/blockscout/frontend into release/v2-0-0

parents 1d42903d f8767a3e
......@@ -170,7 +170,6 @@ export interface ApiResource {
endpoint?: string;
basePath?: string;
pathParams?: Array<string>;
needAuth?: boolean; // for external APIs which require authentication
headers?: RequestInit['headers'];
}
......@@ -214,7 +213,6 @@ export const RESOURCES = {
pathParams: [ 'chainId' as const, 'type' as const ],
endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
needAuth: true,
},
verified_addresses: {
......@@ -222,7 +220,6 @@ export const RESOURCES = {
pathParams: [ 'chainId' as const ],
endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
needAuth: true,
},
token_info_applications_config: {
......@@ -230,7 +227,6 @@ export const RESOURCES = {
pathParams: [ 'chainId' as const ],
endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
needAuth: true,
},
token_info_applications: {
......@@ -238,7 +234,6 @@ export const RESOURCES = {
pathParams: [ 'chainId' as const, 'id' as const ],
endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
needAuth: true,
},
// AUTH
......
......@@ -8,7 +8,6 @@ import config from 'configs/app';
import isBodyAllowed from 'lib/api/isBodyAllowed';
import isNeedProxy from 'lib/api/isNeedProxy';
import { getResourceKey } from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
......@@ -32,14 +31,11 @@ export default function useApiFetch() {
resourceName: R,
{ pathParams, queryParams, fetchParams, logError }: Params<R> = {},
) => {
const apiToken = cookies.get(cookies.NAMES.API_TOKEN);
const resource: ApiResource = RESOURCES[resourceName];
const url = buildUrl(resourceName, pathParams, queryParams);
const withBody = isBodyAllowed(fetchParams?.method);
const headers = pickBy({
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...resource.headers,
...fetchParams?.headers,
......
......@@ -27,7 +27,7 @@ export type ScrollL2TxnBatch = {
confirmation_transaction: ScrollL2TxnBatchConfirmationTransaction;
start_block: number;
end_block: number;
transaction_count: number;
transaction_count: number | null;
data_availability: {
batch_data_container: 'in_blob4844' | 'in_calldata';
};
......
......@@ -12,6 +12,7 @@ import { Tooltip } from 'toolkit/chakra/tooltip';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import IconSvg from 'ui/shared/IconSvg';
import AuthGuard from 'ui/snippets/auth/AuthGuard';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
......@@ -27,6 +28,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const queryClient = useQueryClient();
const router = useRouter();
const onFocusCapture = usePreventFocusAfterModalClosing();
const profileQuery = useProfileQuery();
const handleAddToFavorite = React.useCallback(() => {
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
......@@ -78,6 +80,8 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
isAdd
onSuccess={ handleAddOrDeleteSuccess }
data={ formData }
hasEmail={ Boolean(profileQuery.data?.email) }
showEmailAlert
/>
{ formData.id && (
<DeleteAddressModal
......
......@@ -87,7 +87,6 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
<AddressEntity
mb={ 3 }
fontWeight={ 500 }
color="text"
address={{ hash }}
noLink
/>
......
......@@ -6,6 +6,7 @@ import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import { Button } from 'toolkit/chakra/button';
import { Heading } from 'toolkit/chakra/heading';
import { Link } from 'toolkit/chakra/link';
const easterEggBadgeFeature = config.features.easterEggBadge;
......@@ -36,7 +37,7 @@ const CapybaraRunner = () => {
return (
<>
<Box as="h2" mt={ 12 } mb={ 2 } fontWeight={ 600 } fontSize="xl">Score 1000 to win a special prize!</Box>
<Heading level="2" mt={ 12 } mb={ 2 }>Score 1000 to win a special prize!</Heading>
<Box mb={ 4 }>{ isMobile ? 'Tap below to start' : 'Press space to start' }</Box>
<Script strategy="lazyOnload" src="/static/capibara/index.js"/>
<Box width={{ base: '100%', lg: '600px' }} height="300px" p="50px 0">
......
......@@ -34,13 +34,10 @@ test.beforeEach(async({ mockApiResponse, mockTextAd }) => {
});
test('base view', async({ render, page, createSocket }) => {
test.slow();
const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await component.getByText('100 ARIA').waitFor({ state: 'visible', timeout: 10_000 });
await socketServer.joinChannel(socket, `tokens:${ hash }`);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
......@@ -20,6 +20,7 @@ import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
import WatchlistEmailAlert from 'ui/watchlist/WatchlistEmailAlert';
import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
......@@ -41,6 +42,8 @@ const WatchList: React.FC = () => {
const [ addressModalData, setAddressModalData ] = useState<WatchlistAddress>();
const [ deleteModalData, setDeleteModalData ] = useState<WatchlistAddress>();
const hasEmail = Boolean(profileQuery.data?.email);
const onEditClick = useCallback((data: WatchlistAddress) => {
setAddressModalData(data);
addressModalProps.onOpen();
......@@ -75,12 +78,6 @@ const WatchList: React.FC = () => {
);
}, [ deleteModalData?.id, queryClient ]);
const description = (
<AccountPageDescription>
An email notification can be sent to you when an address on your watch list sends or receives any transactions.
</AccountPageDescription>
);
const content = (() => {
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
......@@ -90,7 +87,10 @@ const WatchList: React.FC = () => {
return (
<>
{ description }
{ !hasEmail && <WatchlistEmailAlert/> }
<AccountPageDescription>
An email notification can be sent to you when an address on your watch list sends or receives any transactions.
</AccountPageDescription>
<DataListDisplay
isError={ isError }
itemsNum={ data?.items.length }
......@@ -105,7 +105,7 @@ const WatchList: React.FC = () => {
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
hasEmail={ Boolean(profileQuery.data?.email) }
hasEmail={ hasEmail }
/>
)) }
</Box>
......@@ -116,7 +116,7 @@ const WatchList: React.FC = () => {
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
hasEmail={ Boolean(profileQuery.data?.email) }
hasEmail={ hasEmail }
/>
</Box>
</DataListDisplay>
......@@ -133,6 +133,7 @@ const WatchList: React.FC = () => {
onSuccess={ onAddOrEditSuccess }
data={ addressModalData }
isAdd={ !addressModalData }
hasEmail={ hasEmail }
/>
{ deleteModalData && (
<DeleteAddressModal
......
......@@ -14,6 +14,7 @@ import getErrorObj from 'lib/errors/getErrorObj';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useIsMobile from 'lib/hooks/useIsMobile';
import { Button } from 'toolkit/chakra/button';
import { Heading } from 'toolkit/chakra/heading';
import { FormFieldEmail } from 'toolkit/components/forms/fields/FormFieldEmail';
import { FormFieldText } from 'toolkit/components/forms/fields/FormFieldText';
import { FormFieldUrl } from 'toolkit/components/forms/fields/FormFieldUrl';
......@@ -96,8 +97,10 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
rowGap={ 3 }
templateColumns={{ base: '1fr', lg: '1fr 1fr minmax(0, 200px)', xl: '1fr 1fr minmax(0, 250px)' }}
>
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4">
Company info
<GridItem colSpan={{ base: 1, lg: 3 }}>
<Heading level="2">
Company info
</Heading>
</GridItem>
<FormFieldText<FormFields> name="requesterName" required placeholder="Your name"/>
<FormFieldEmail<FormFields> name="requesterEmail" required/>
......@@ -107,9 +110,11 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
<FormFieldUrl<FormFields> name="companyWebsite" placeholder="Company website"/>
{ !isMobile && <div/> }
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}>
Public tags/labels
<Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link.primary"/>
<GridItem colSpan={{ base: 1, lg: 3 }} mt={{ base: 3, lg: 5 }}>
<Heading level="2" display="flex" alignItems="center" columnGap={ 1 }>
Public tags/labels
<Hint label="Submit a public tag proposal for our moderation team to review"/>
</Heading>
</GridItem>
<PublicTagsSubmitFieldAddresses/>
<PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/>
......
import { Box, Flex, Grid, GridItem } from '@chakra-ui/react';
import { Flex, Grid, GridItem } from '@chakra-ui/react';
import { pickBy } from 'es-toolkit';
import React from 'react';
......@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';
import { Alert } from 'toolkit/chakra/alert';
import { Button } from 'toolkit/chakra/button';
import { Heading } from 'toolkit/chakra/heading';
import { Link } from 'toolkit/chakra/link';
import { makePrettyLink } from 'toolkit/utils/url';
......@@ -43,7 +44,7 @@ const PublicTagsSubmitResult = ({ data }: Props) => {
</Alert>
) }
<Box as="h2" textStyle="h4">Company info</Box>
<Heading level="2">Company info</Heading>
<Grid rowGap={ 3 } columnGap={ 6 } gridTemplateColumns="170px 1fr" mt={ 6 }>
<GridItem>Your name</GridItem>
<GridItem>{ groupedData.requesterName }</GridItem>
......@@ -65,7 +66,7 @@ const PublicTagsSubmitResult = ({ data }: Props) => {
) }
</Grid>
<Box as="h2" textStyle="h4" mt={ 8 } mb={ 5 }>Public tags/labels</Box>
<Heading level="2" mt={ 8 } mb={ 5 }>Public tags/labels</Heading>
{ hasErrors ? <PublicTagsSubmitResultWithErrors data={ groupedData }/> : <PublicTagsSubmitResultSuccess data={ groupedData }/> }
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 6 } mt={ 8 } rowGap={ 3 }>
......
import type { DialogRootProps } from '@chakra-ui/react';
import { Box, Text } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
interface Props<TData> {
open: boolean;
onOpenChange: ({ open }: { open: boolean }) => void;
interface Props<TData> extends Omit<DialogRootProps, 'children'> {
data?: TData;
title: string;
text?: string;
......@@ -23,15 +22,16 @@ export default function FormModal<TData>({
renderForm,
isAlertVisible,
setAlertVisible,
...rest
}: Props<TData>) {
const handleOpenChange = useCallback(({ open }: { open: boolean }) => {
!open && setAlertVisible?.(false);
onOpenChange({ open });
onOpenChange?.({ open });
}, [ onOpenChange, setAlertVisible ]);
return (
<DialogRoot open={ open } onOpenChange={ handleOpenChange } size={{ lgDown: 'full', lg: 'md' }}>
<DialogRoot open={ open } onOpenChange={ handleOpenChange } size={{ lgDown: 'full', lg: 'md' }} { ...rest }>
<DialogContent>
<DialogHeader>{ title }</DialogHeader>
<DialogBody>
......
......@@ -115,17 +115,21 @@ const ScrollL2TxnBatchDetails = ({ query }: Props) => {
}
</DetailedInfo.ItemValue>
<DetailedInfo.ItemLabel
isLoading={ isPlaceholderData }
hint="Number of transactions in this batch"
>
Transactions
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<Link loading={ isPlaceholderData } href={ route({ pathname: '/batches/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
{ data.transaction_count.toLocaleString() } transaction{ data.transaction_count === 1 ? '' : 's' }
</Link>
</DetailedInfo.ItemValue>
{ typeof data.transaction_count === 'number' ? (
<>
<DetailedInfo.ItemLabel
isLoading={ isPlaceholderData }
hint="Number of transactions in this batch"
>
Transactions
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<Link loading={ isPlaceholderData } href={ route({ pathname: '/batches/[number]', query: { number: data.number.toString(), tab: 'txs' } }) }>
{ data.transaction_count.toLocaleString() } transaction{ data.transaction_count === 1 ? '' : 's' }
</Link>
</DetailedInfo.ItemValue>
</>
) : null }
<DetailedInfo.ItemLabel
isLoading={ isPlaceholderData }
......
......@@ -104,17 +104,21 @@ const ScrollL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
</Link>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
fontWeight={ 600 }
minW="40px"
>
{ item.transaction_count.toLocaleString() }
</Link>
</ListItemMobileGrid.Value>
{ typeof item.transaction_count === 'number' ? (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
fontWeight={ 600 }
minW="40px"
>
{ item.transaction_count.toLocaleString() }
</Link>
</ListItemMobileGrid.Value>
</>
) : null }
</ListItemMobileGrid.Container>
);
......
......@@ -87,12 +87,14 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
</Link>
</TableCell>
<TableCell verticalAlign="middle" isNumeric>
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
>
{ item.transaction_count.toLocaleString() }
</Link>
{ typeof item.transaction_count === 'number' ? (
<Link
href={ route({ pathname: '/batches/[number]', query: { number: item.number.toString(), tab: 'txs' } }) }
loading={ isLoading }
>
{ item.transaction_count.toLocaleString() }
</Link>
) : 'N/A' }
</TableCell>
</TableRow>
);
......
......@@ -14,9 +14,6 @@ import { Button } from 'toolkit/chakra/button';
import { FormFieldAddress } from 'toolkit/components/forms/fields/FormFieldAddress';
import { FormFieldCheckbox } from 'toolkit/components/forms/fields/FormFieldCheckbox';
import { FormFieldText } from 'toolkit/components/forms/fields/FormFieldText';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import AuthModal from 'ui/snippets/auth/AuthModal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import AddressFormNotifications from './AddressFormNotifications';
......@@ -30,6 +27,8 @@ type Props = {
onSuccess: () => Promise<void>;
setAlertVisible: (isAlertVisible: boolean) => void;
isAdd: boolean;
hasEmail: boolean;
showEmailAlert?: boolean;
};
export type Inputs = {
......@@ -56,16 +55,12 @@ export type Inputs = {
};
};
const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd }) => {
const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd, hasEmail, showEmailAlert }) => {
const [ pending, setPending ] = useState(false);
const profileQuery = useProfileQuery();
const userWithoutEmail = profileQuery.data && !profileQuery.data.email;
const authModal = useDisclosure();
let notificationsDefault = {} as Inputs['notification_settings'];
if (!data?.notification_settings) {
NOTIFICATIONS.forEach(n => notificationsDefault[n] = { incoming: !userWithoutEmail, outcoming: !userWithoutEmail });
NOTIFICATIONS.forEach(n => notificationsDefault[n] = { incoming: hasEmail, outcoming: hasEmail });
} else {
notificationsDefault = data.notification_settings;
}
......@@ -74,7 +69,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
defaultValues: {
address: data?.address_hash || '',
tag: data?.name || '',
notification: data?.notification_methods ? data.notification_methods.email : !userWithoutEmail,
notification: data?.notification_methods ? data.notification_methods.email : hasEmail,
notification_settings: notificationsDefault,
},
mode: 'onTouched',
......@@ -149,23 +144,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
bgColor="dialog.bg"
mb={ 8 }
/>
{ userWithoutEmail ? (
<>
<Alert
status="info"
display="flex"
flexDirection={{ base: 'column', md: 'row' }}
alignItems={{ base: 'flex-start', lg: 'center' }}
columnGap={ 2 }
rowGap={ 2 }
w="fit-content"
>
To receive notifications you need to add an email to your profile.
<Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button>
</Alert>
{ authModal.open && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> }
</>
) : (
{ hasEmail ? (
<>
<Text color="text.secondary" fontSize="sm" marginBottom={ 5 }>
Please select what types of notifications you will receive
......@@ -179,7 +158,17 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
label="Email notifications"
/>
</>
) }
) : null }
{ !hasEmail && showEmailAlert ? (
<Alert
status="info"
descriptionProps={{ alignItems: 'center', gap: 2 }}
w="fit-content"
mb={ 6 }
>
To receive notifications you need to add an email to your profile.
</Alert>
) : null }
<Button
type="submit"
loading={ pending }
......@@ -190,6 +179,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
</Button>
</form>
</FormProvider>
);
};
......
......@@ -12,17 +12,28 @@ type Props = {
onOpenChange: ({ open }: { open: boolean }) => void;
onSuccess: () => Promise<void>;
data?: Partial<WatchlistAddress>;
hasEmail: boolean;
showEmailAlert?: boolean;
};
const AddressModal: React.FC<Props> = ({ open, onOpenChange, onSuccess, data, isAdd }) => {
const AddressModal: React.FC<Props> = ({ open, onOpenChange, onSuccess, data, isAdd, hasEmail, showEmailAlert }) => {
const title = !isAdd ? 'Edit watch list address' : 'New address to watch list';
const text = isAdd ? 'An email notification can be sent to you when an address on your watch list sends or receives any transactions.' : '';
const [ isAlertVisible, setAlertVisible ] = useState(false);
const renderForm = useCallback(() => {
return <AddressForm data={ data } onSuccess={ onSuccess } setAlertVisible={ setAlertVisible } isAdd={ isAdd }/>;
}, [ data, isAdd, onSuccess ]);
return (
<AddressForm
data={ data }
onSuccess={ onSuccess }
setAlertVisible={ setAlertVisible }
isAdd={ isAdd }
hasEmail={ hasEmail }
showEmailAlert={ showEmailAlert }
/>
);
}, [ data, isAdd, onSuccess, hasEmail, showEmailAlert ]);
return (
<FormModal<WatchlistAddress>
......
import React from 'react';
import { Alert } from 'toolkit/chakra/alert';
import { Button } from 'toolkit/chakra/button';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import AuthModal from 'ui/snippets/auth/AuthModal';
const WatchlistEmailAlert = () => {
const authModal = useDisclosure();
return (
<>
<Alert
status="info"
descriptionProps={{ alignItems: 'center', gap: 2 }}
w="fit-content"
mb={ 6 }
>
To receive notifications you need to add an email to your profile.
<Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button>
</Alert>
{ authModal.open && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> }
</>
);
};
export default React.memo(WatchlistEmailAlert);
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