Commit abdd2a51 authored by tom's avatar tom

custom error screen for 403

parent 90a73c95
...@@ -63,6 +63,9 @@ export const RESOURCES = { ...@@ -63,6 +63,9 @@ export const RESOURCES = {
user_info: { user_info: {
path: '/api/account/v1/user/info', path: '/api/account/v1/user/info',
}, },
email_resend: {
path: '/api/account/v1/email/resend',
},
custom_abi: { custom_abi: {
path: '/api/account/v1/user/custom_abis/:id?', path: '/api/account/v1/user/custom_abis/:id?',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
......
import getErrorCause from './getErrorCause'; import getErrorCause from './getErrorCause';
export default function getErrorStatusCode(error: Error | undefined): number | undefined { export default function getErrorCauseStatusCode(error: Error | undefined): number | undefined {
const cause = getErrorCause(error); const cause = getErrorCause(error);
return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined; return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined;
} }
export default function getErrorObj(error: unknown) {
if (typeof error !== 'object') {
return;
}
if (Array.isArray(error)) {
return;
}
if (error === null) {
return;
}
return error;
}
import getErrorObj from './getErrorObj';
export default function getErrorObjPayload<Payload extends object>(error: unknown): Payload | undefined {
const errorObj = getErrorObj(error);
if (!errorObj || !('payload' in errorObj)) {
return;
}
if (typeof errorObj.payload !== 'object') {
return;
}
if (errorObj === null) {
return;
}
if (Array.isArray(errorObj)) {
return;
}
return errorObj.payload as Payload;
}
import getErrorObj from './getErrorObj';
export default function getErrorObjStatusCode(error: unknown) {
const errorObj = getErrorObj(error);
if (!errorObj || !('statusCode' in errorObj) || typeof errorObj.statusCode !== 'number') {
return;
}
return errorObj.statusCode;
}
...@@ -9,7 +9,7 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -9,7 +9,7 @@ import type { ResourceError } from 'lib/api/resources';
import { AppContextProvider } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra'; import { Chakra } from 'lib/Chakra';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import getErrorStatusCode from 'lib/errors/getErrorStatusCode'; import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
import useConfigSentry from 'lib/hooks/useConfigSentry'; import useConfigSentry from 'lib/hooks/useConfigSentry';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import theme from 'theme'; import theme from 'theme';
...@@ -27,7 +27,7 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -27,7 +27,7 @@ function MyApp({ Component, pageProps }: AppProps) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: (failureCount, _error) => { retry: (failureCount, _error) => {
const error = _error as ResourceError<{ status: number }>; const error = _error as ResourceError<{ status: number }>;
const status = error?.status || error?.payload?.status; const status = error?.payload?.status || error?.status;
if (status && status >= 400 && status < 500) { if (status && status >= 400 && status < 500) {
// don't do retry for client error responses // don't do retry for client error responses
return false; return false;
...@@ -40,7 +40,7 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -40,7 +40,7 @@ function MyApp({ Component, pageProps }: AppProps) {
})); }));
const renderErrorScreen = React.useCallback((error?: Error) => { const renderErrorScreen = React.useCallback((error?: Error) => {
const statusCode = getErrorStatusCode(error); const statusCode = getErrorCauseStatusCode(error);
return ( return (
<AppError <AppError
......
...@@ -4,13 +4,16 @@ import React from 'react'; ...@@ -4,13 +4,16 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import ApiKeys from 'ui/pages/ApiKeys'; import ApiKeys from 'ui/pages/ApiKeys';
import Page from 'ui/shared/Page/Page';
const ApiKeysPage: NextPage = () => { const ApiKeysPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<Page>
<ApiKeys/> <ApiKeys/>
</Page>
</> </>
); );
}; };
......
...@@ -4,13 +4,16 @@ import React from 'react'; ...@@ -4,13 +4,16 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import CustomAbi from 'ui/pages/CustomAbi'; import CustomAbi from 'ui/pages/CustomAbi';
import Page from 'ui/shared/Page/Page';
const CustomAbiPage: NextPage = () => { const CustomAbiPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<Page>
<CustomAbi/> <CustomAbi/>
</Page>
</> </>
); );
}; };
......
...@@ -4,13 +4,16 @@ import React from 'react'; ...@@ -4,13 +4,16 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PublicTags from 'ui/pages/PublicTags'; import PublicTags from 'ui/pages/PublicTags';
import Page from 'ui/shared/Page/Page';
const PublicTagsPage: NextPage = () => { const PublicTagsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<Page>
<PublicTags/> <PublicTags/>
</Page>
</> </>
); );
}; };
......
...@@ -4,13 +4,16 @@ import React from 'react'; ...@@ -4,13 +4,16 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PrivateTags from 'ui/pages/PrivateTags'; import PrivateTags from 'ui/pages/PrivateTags';
import Page from 'ui/shared/Page/Page';
const AddressTagsPage: NextPage = () => { const AddressTagsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<Page>
<PrivateTags/> <PrivateTags/>
</Page>
</> </>
); );
}; };
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import WatchList from 'ui/pages/Watchlist'; import WatchList from 'ui/pages/Watchlist';
import Page from 'ui/shared/Page/Page';
const WatchListPage: NextPage = () => { const WatchListPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +13,9 @@ const WatchListPage: NextPage = () => { ...@@ -12,7 +13,9 @@ const WatchListPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<Page>
<WatchList/> <WatchList/>
</Page>
</> </>
); );
}; };
......
...@@ -3,12 +3,15 @@ import Head from 'next/head'; ...@@ -3,12 +3,15 @@ import Head from 'next/head';
import React from 'react'; import React from 'react';
import MyProfile from 'ui/pages/MyProfile'; import MyProfile from 'ui/pages/MyProfile';
import Page from 'ui/shared/Page/Page';
const MyProfilePage: NextPage = () => { const MyProfilePage: NextPage = () => {
return ( return (
<> <>
<Head><title>My profile</title></Head> <Head><title>My profile</title></Head>
<Page>
<MyProfile/> <MyProfile/>
</Page>
</> </>
); );
}; };
......
...@@ -7,10 +7,12 @@ import type { UserInfo } from 'types/api/account'; ...@@ -7,10 +7,12 @@ import type { UserInfo } from 'types/api/account';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import type { ResourceError } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useLoginUrl from 'lib/hooks/useLoginUrl'; import useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import useToast from 'lib/hooks/useToast';
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';
...@@ -25,18 +27,35 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -25,18 +27,35 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const toast = useToast();
const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]); const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]);
const profileState = queryClient.getQueryState<unknown, ResourceError<{ message: string }>>([ resourceKey('user_info') ]);
const isAuth = Boolean(profileData); const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (profileState?.error?.status === 403) {
const isUnverifiedEmail = profileState.error.payload?.message.includes('Unverified email');
if (isUnverifiedEmail) {
toast({
position: 'top-right',
title: 'Error',
description: 'Unable to add address to watch list. Please go to the watch list page instead.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
return;
}
}
if (!isAuth) { if (!isAuth) {
window.location.assign(loginUrl); window.location.assign(loginUrl);
return; return;
} }
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen(); watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
}, [ addModalProps, deleteModalProps, watchListId, isAuth, loginUrl ]); }, [ profileState?.error, isAuth, watchListId, deleteModalProps, addModalProps, loginUrl, toast ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => { const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } }); const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
......
...@@ -12,8 +12,6 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem'; ...@@ -12,8 +12,6 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable'; import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal'; import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
...@@ -29,7 +27,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -29,7 +27,7 @@ const ApiKeysPage: React.FC = () => {
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
const { data, isLoading, isError } = useApiQuery('api_keys'); const { data, isLoading, isError, error } = useApiQuery('api_keys');
const onEditClick = useCallback((data: ApiKey) => { const onEditClick = useCallback((data: ApiKey) => {
setApiKeyModalData(data); setApiKeyModalData(data);
...@@ -76,7 +74,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -76,7 +74,7 @@ const ApiKeysPage: React.FC = () => {
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; throw new Error('API keys fetch error', { cause: error });
} }
const list = isMobile ? ( const list = isMobile ? (
...@@ -130,12 +128,10 @@ const ApiKeysPage: React.FC = () => { ...@@ -130,12 +128,10 @@ const ApiKeysPage: React.FC = () => {
})(); })();
return ( return (
<Page> <>
<Box h="100%">
<PageTitle text="API keys"/> <PageTitle text="API keys"/>
{ content } { content }
</Box> </>
</Page>
); );
}; };
......
...@@ -11,8 +11,6 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem'; ...@@ -11,8 +11,6 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable'; import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal'; import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
...@@ -26,7 +24,7 @@ const CustomAbiPage: React.FC = () => { ...@@ -26,7 +24,7 @@ const CustomAbiPage: React.FC = () => {
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
const { data, isLoading, isError } = useApiQuery('custom_abi'); const { data, isLoading, isError, error } = useApiQuery('custom_abi');
const onEditClick = useCallback((data: CustomAbi) => { const onEditClick = useCallback((data: CustomAbi) => {
setCustomAbiModalData(data); setCustomAbiModalData(data);
...@@ -72,7 +70,7 @@ const CustomAbiPage: React.FC = () => { ...@@ -72,7 +70,7 @@ const CustomAbiPage: React.FC = () => {
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; throw new Error('Custom ABI fetch error', { cause: error });
} }
const list = isMobile ? ( const list = isMobile ? (
...@@ -113,12 +111,10 @@ const CustomAbiPage: React.FC = () => { ...@@ -113,12 +111,10 @@ const CustomAbiPage: React.FC = () => {
})(); })();
return ( return (
<Page> <>
<Box h="100%">
<PageTitle text="Custom ABI"/> <PageTitle text="Custom ABI"/>
{ content } { content }
</Box> </>
</Page>
); );
}; };
......
...@@ -4,13 +4,11 @@ import React from 'react'; ...@@ -4,13 +4,11 @@ import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => { const MyProfile = () => {
const { data, isLoading, isError, isFetched } = useFetchProfileInfo(); const { data, isLoading, isError, error, isFetched } = useFetchProfileInfo();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const content = (() => { const content = (() => {
...@@ -19,7 +17,7 @@ const MyProfile = () => { ...@@ -19,7 +17,7 @@ const MyProfile = () => {
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; throw new Error('My profile fetch error', { cause: error });
} }
return ( return (
...@@ -54,10 +52,10 @@ const MyProfile = () => { ...@@ -54,10 +52,10 @@ const MyProfile = () => {
})(); })();
return ( return (
<Page> <>
<PageTitle text="My profile"/> <PageTitle text="My profile"/>
{ content } { content }
</Page> </>
); );
}; };
......
...@@ -5,7 +5,6 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; ...@@ -5,7 +5,6 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags'; import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags'; import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
...@@ -18,10 +17,10 @@ const PrivateTags = () => { ...@@ -18,10 +17,10 @@ const PrivateTags = () => {
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
return ( return (
<Page> <>
<PageTitle text="Private tags"/> <PageTitle text="Private tags"/>
<RoutedTabs tabs={ TABS }/> <RoutedTabs tabs={ TABS }/>
</Page> </>
); );
}; };
......
...@@ -10,7 +10,6 @@ import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthT ...@@ -10,7 +10,6 @@ import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthT
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import PublicTagsData from 'ui/publicTags/PublicTagsData'; import PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm'; import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
type TScreen = 'data' | 'form'; type TScreen = 'data' | 'form';
...@@ -78,7 +77,7 @@ const PublicTagsComponent: React.FC = () => { ...@@ -78,7 +77,7 @@ const PublicTagsComponent: React.FC = () => {
} }
return ( return (
<Page> <>
{ screen === 'form' && ( { screen === 'form' && (
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }> <Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/> <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
...@@ -87,7 +86,7 @@ const PublicTagsComponent: React.FC = () => { ...@@ -87,7 +86,7 @@ const PublicTagsComponent: React.FC = () => {
) } ) }
<PageTitle text={ header } display={{ base: 'block', lg: 'inline-flex' }} ml={{ base: 0, lg: 3 }}/> <PageTitle text={ header } display={{ base: 'block', lg: 'inline-flex' }} ml={{ base: 0, lg: 3 }}/>
{ content } { content }
</Page> </>
); );
}; };
......
...@@ -10,8 +10,6 @@ import useApiFetch from 'lib/api/useApiFetch'; ...@@ -10,8 +10,6 @@ import useApiFetch from 'lib/api/useApiFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
...@@ -22,12 +20,11 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; ...@@ -22,12 +20,11 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, TWatchlist>([ resourceKey('watchlist') ], async() => { const { data, isLoading, isError, error } = useQuery<unknown, unknown, TWatchlist>([ resourceKey('watchlist') ], async() => {
try {
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist'); const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
if (!Array.isArray(watchlistAddresses)) { if (!Array.isArray(watchlistAddresses)) {
throw Error(); return;
} }
const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => { const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => {
...@@ -44,9 +41,6 @@ const WatchList: React.FC = () => { ...@@ -44,9 +41,6 @@ const WatchList: React.FC = () => {
})); }));
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] })); return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] }));
} catch (error) {
return error;
}
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -96,7 +90,11 @@ const WatchList: React.FC = () => { ...@@ -96,7 +90,11 @@ const WatchList: React.FC = () => {
</AccountPageDescription> </AccountPageDescription>
); );
let content; if (isError) {
throw new Error('Watch list fetch error', { cause: error });
}
const content = (() => {
if (isLoading && !data) { if (isLoading && !data) {
const loader = isMobile ? <SkeletonListAccount showFooterSlot/> : ( const loader = isMobile ? <SkeletonListAccount showFooterSlot/> : (
<> <>
...@@ -105,15 +103,14 @@ const WatchList: React.FC = () => { ...@@ -105,15 +103,14 @@ const WatchList: React.FC = () => {
</> </>
); );
content = ( return (
<> <>
{ description } { description }
{ loader } { loader }
</> </>
); );
} else if (isError) { }
content = <DataFetchAlert/>;
} else {
const list = isMobile ? ( const list = isMobile ? (
<Box> <Box>
{ data.map((item) => ( { data.map((item) => (
...@@ -133,7 +130,7 @@ const WatchList: React.FC = () => { ...@@ -133,7 +130,7 @@ const WatchList: React.FC = () => {
/> />
); );
content = ( return (
<> <>
{ description } { description }
{ Boolean(data?.length) && list } { Boolean(data?.length) && list }
...@@ -162,15 +159,13 @@ const WatchList: React.FC = () => { ...@@ -162,15 +159,13 @@ const WatchList: React.FC = () => {
) } ) }
</> </>
); );
} })();
return ( return (
<Page> <>
<Box h="100%">
<PageTitle text="Watch list"/> <PageTitle text="Watch list"/>
{ content } { content }
</Box> </>
</Page>
); );
}; };
......
...@@ -7,7 +7,6 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -7,7 +7,6 @@ import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account'; import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressModal from './AddressModal/AddressModal'; import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem'; import AddressTagListItem from './AddressTagTable/AddressTagListItem';
...@@ -15,7 +14,7 @@ import AddressTagTable from './AddressTagTable/AddressTagTable'; ...@@ -15,7 +14,7 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => { const PrivateAddressTags = () => {
const { data: addressTagsData, isError, isPlaceholderData } = useApiQuery('private_tags_address', { const { data: addressTagsData, isError, error, isPlaceholderData } = useApiQuery('private_tags_address', {
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS), placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
...@@ -50,7 +49,7 @@ const PrivateAddressTags = () => { ...@@ -50,7 +49,7 @@ const PrivateAddressTags = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
if (isError) { if (isError) {
return <DataFetchAlert/>; throw new Error('Private tags fetch error', { cause: error });
} }
const list = isMobile ? ( const list = isMobile ? (
......
...@@ -6,7 +6,6 @@ import type { TransactionTag } from 'types/api/account'; ...@@ -6,7 +6,6 @@ import type { TransactionTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
...@@ -16,7 +15,7 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem ...@@ -16,7 +15,7 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable'; import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => { const PrivateTransactionTags = () => {
const { data: transactionTagsData, isLoading, isError } = useApiQuery('private_tags_tx', { queryOptions: { refetchOnMount: false } }); const { data: transactionTagsData, isLoading, isError, error } = useApiQuery('private_tags_tx', { queryOptions: { refetchOnMount: false } });
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
...@@ -69,7 +68,7 @@ const PrivateTransactionTags = () => { ...@@ -69,7 +68,7 @@ const PrivateTransactionTags = () => {
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; throw new Error('Private tags fetch error', { cause: error });
} }
const list = isMobile ? ( const list = isMobile ? (
......
...@@ -7,7 +7,6 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -7,7 +7,6 @@ import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem'; import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
...@@ -24,7 +23,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -24,7 +23,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>(); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { data, isLoading, isError } = useApiQuery('public_tags'); const { data, isLoading, isError, error } = useApiQuery('public_tags');
const onDeleteModalClose = useCallback(() => { const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined); setDeleteModalData(undefined);
...@@ -70,7 +69,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -70,7 +69,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; throw new Error('Public tags fetch error', { cause: error });
} }
const list = isMobile ? ( const list = isMobile ? (
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
const ErrorInvalidTxHash = () => { const AppErrorInvalidTxHash = () => {
const textColor = useColorModeValue('gray.500', 'gray.400'); const textColor = useColorModeValue('gray.500', 'gray.400');
const snippet = { const snippet = {
borderColor: useColorModeValue('blackAlpha.300', 'whiteAlpha.300'), borderColor: useColorModeValue('blackAlpha.300', 'whiteAlpha.300'),
...@@ -54,4 +54,4 @@ const ErrorInvalidTxHash = () => { ...@@ -54,4 +54,4 @@ const ErrorInvalidTxHash = () => {
); );
}; };
export default ErrorInvalidTxHash; export default AppErrorInvalidTxHash;
import { Box, Text, Button, Heading, Icon, chakra } from '@chakra-ui/react';
import React from 'react';
import icon404 from 'icons/error-pages/404.svg';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useToast from 'lib/hooks/useToast';
interface Props {
className?: string;
}
const AppErrorUnverifiedEmail = ({ className }: Props) => {
const apiFetch = useApiFetch();
const toast = useToast();
const handleButtonClick = React.useCallback(async() => {
const toastId = 'resend-email-error';
try {
await apiFetch('email_resend');
toast({
id: toastId,
position: 'top-right',
title: 'Success',
description: 'Email successfully resent.',
status: 'success',
variant: 'subtle',
isClosable: true,
});
} catch (error) {
const statusCode = getErrorObjStatusCode(error);
const payload = getErrorObjPayload<{ message: string }>(error);
const message = statusCode === 429 ? payload?.message : undefined;
!toast.isActive(toastId) && toast({
id: toastId,
position: 'top-right',
title: 'Error',
description: message || 'Something went wrong. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}
}, [ apiFetch, toast ]);
return (
<Box className={ className }>
<Icon as={ icon404 } width="200px" height="auto"/>
<Heading mt={ 8 } size="2xl" fontFamily="body">Email is not verified</Heading>
<Text variant="secondary" mt={ 3 }>
Please confirm your email address to use the My Account feature. A confirmation email was sent to test@gmail.com on signup. { `Didn't receive?` }
</Text>
<Button
mt={ 8 }
size="lg"
variant="outline"
onClick={ handleButtonClick }
>
Resend verification email
</Button>
</Box>
);
};
export default chakra(AppErrorUnverifiedEmail);
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import getErrorStatusCode from 'lib/errors/getErrorStatusCode'; import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload'; import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
import useAdblockDetect from 'lib/hooks/useAdblockDetect'; import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus'; import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus';
import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash';
import AppErrorUnverifiedEmail from 'ui/shared/AppError/AppErrorUnverifiedEmail';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
...@@ -32,7 +33,7 @@ const Page = ({ ...@@ -32,7 +33,7 @@ const Page = ({
useAdblockDetect(); useAdblockDetect();
const renderErrorScreen = React.useCallback((error?: Error) => { const renderErrorScreen = React.useCallback((error?: Error) => {
const statusCode = getErrorStatusCode(error) || 500; const statusCode = getErrorCauseStatusCode(error) || 500;
const resourceErrorPayload = getResourceErrorPayload(error); const resourceErrorPayload = getResourceErrorPayload(error);
const messageInPayload = resourceErrorPayload && 'message' in resourceErrorPayload && typeof resourceErrorPayload.message === 'string' ? const messageInPayload = resourceErrorPayload && 'message' in resourceErrorPayload && typeof resourceErrorPayload.message === 'string' ?
resourceErrorPayload.message : resourceErrorPayload.message :
...@@ -40,9 +41,14 @@ const Page = ({ ...@@ -40,9 +41,14 @@ const Page = ({
const isInvalidTxHash = error?.message.includes('Invalid tx hash'); const isInvalidTxHash = error?.message.includes('Invalid tx hash');
const isBlockConsensus = messageInPayload?.includes('Block lost consensus'); const isBlockConsensus = messageInPayload?.includes('Block lost consensus');
const isUnverifiedEmail = statusCode === 403 && messageInPayload?.includes('Unverified email');
if (isInvalidTxHash) { if (isInvalidTxHash) {
return <PageContent isHomePage={ isHomePage }><ErrorInvalidTxHash/></PageContent>; return <PageContent isHomePage={ isHomePage }><AppErrorInvalidTxHash/></PageContent>;
}
if (isUnverifiedEmail) {
return <PageContent isHomePage={ isHomePage }><AppErrorUnverifiedEmail mt="50px"/></PageContent>;
} }
if (isBlockConsensus) { if (isBlockConsensus) {
......
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