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