mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
custom error screen for 403
This commit is contained in:
@@ -63,6 +63,9 @@ export const RESOURCES = {
|
||||
user_info: {
|
||||
path: '/api/account/v1/user/info',
|
||||
},
|
||||
email_resend: {
|
||||
path: '/api/account/v1/email/resend',
|
||||
},
|
||||
custom_abi: {
|
||||
path: '/api/account/v1/user/custom_abis/:id?',
|
||||
pathParams: [ 'id' as const ],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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);
|
||||
return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default function getErrorObj(error: unknown) {
|
||||
if (typeof error !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
}
|
||||
+3
-3
@@ -9,7 +9,7 @@ import type { ResourceError } from 'lib/api/resources';
|
||||
import { AppContextProvider } from 'lib/appContext';
|
||||
import { Chakra } from 'lib/Chakra';
|
||||
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 { SocketProvider } from 'lib/socket/context';
|
||||
import theme from 'theme';
|
||||
@@ -27,7 +27,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: (failureCount, _error) => {
|
||||
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) {
|
||||
// don't do retry for client error responses
|
||||
return false;
|
||||
@@ -40,7 +40,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||
}));
|
||||
|
||||
const renderErrorScreen = React.useCallback((error?: Error) => {
|
||||
const statusCode = getErrorStatusCode(error);
|
||||
const statusCode = getErrorCauseStatusCode(error);
|
||||
|
||||
return (
|
||||
<AppError
|
||||
|
||||
@@ -4,13 +4,16 @@ import React from 'react';
|
||||
|
||||
import getNetworkTitle from 'lib/networks/getNetworkTitle';
|
||||
import ApiKeys from 'ui/pages/ApiKeys';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const ApiKeysPage: NextPage = () => {
|
||||
const title = getNetworkTitle();
|
||||
return (
|
||||
<>
|
||||
<Head><title>{ title }</title></Head>
|
||||
<ApiKeys/>
|
||||
<Page>
|
||||
<ApiKeys/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,13 +4,16 @@ import React from 'react';
|
||||
|
||||
import getNetworkTitle from 'lib/networks/getNetworkTitle';
|
||||
import CustomAbi from 'ui/pages/CustomAbi';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const CustomAbiPage: NextPage = () => {
|
||||
const title = getNetworkTitle();
|
||||
return (
|
||||
<>
|
||||
<Head><title>{ title }</title></Head>
|
||||
<CustomAbi/>
|
||||
<Page>
|
||||
<CustomAbi/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,13 +4,16 @@ import React from 'react';
|
||||
|
||||
import getNetworkTitle from 'lib/networks/getNetworkTitle';
|
||||
import PublicTags from 'ui/pages/PublicTags';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const PublicTagsPage: NextPage = () => {
|
||||
const title = getNetworkTitle();
|
||||
return (
|
||||
<>
|
||||
<Head><title>{ title }</title></Head>
|
||||
<PublicTags/>
|
||||
<Page>
|
||||
<PublicTags/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,13 +4,16 @@ import React from 'react';
|
||||
|
||||
import getNetworkTitle from 'lib/networks/getNetworkTitle';
|
||||
import PrivateTags from 'ui/pages/PrivateTags';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const AddressTagsPage: NextPage = () => {
|
||||
const title = getNetworkTitle();
|
||||
return (
|
||||
<>
|
||||
<Head><title>{ title }</title></Head>
|
||||
<PrivateTags/>
|
||||
<Page>
|
||||
<PrivateTags/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
import getNetworkTitle from 'lib/networks/getNetworkTitle';
|
||||
import WatchList from 'ui/pages/Watchlist';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const WatchListPage: NextPage = () => {
|
||||
const title = getNetworkTitle();
|
||||
@@ -12,7 +13,9 @@ const WatchListPage: NextPage = () => {
|
||||
<Head>
|
||||
<title>{ title }</title>
|
||||
</Head>
|
||||
<WatchList/>
|
||||
<Page>
|
||||
<WatchList/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@ import Head from 'next/head';
|
||||
import React from 'react';
|
||||
|
||||
import MyProfile from 'ui/pages/MyProfile';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const MyProfilePage: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head><title>My profile</title></Head>
|
||||
<MyProfile/>
|
||||
<Page>
|
||||
<MyProfile/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,10 +7,12 @@ import type { UserInfo } from 'types/api/account';
|
||||
|
||||
import starFilledIcon from 'icons/star_filled.svg';
|
||||
import starOutlineIcon from 'icons/star_outline.svg';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import { resourceKey } from 'lib/api/resources';
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import useLoginUrl from 'lib/hooks/useLoginUrl';
|
||||
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
|
||||
import useToast from 'lib/hooks/useToast';
|
||||
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
|
||||
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
|
||||
|
||||
@@ -25,18 +27,35 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
|
||||
const deleteModalProps = useDisclosure();
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]);
|
||||
const profileState = queryClient.getQueryState<unknown, ResourceError<{ message: string }>>([ resourceKey('user_info') ]);
|
||||
const isAuth = Boolean(profileData);
|
||||
const loginUrl = useLoginUrl();
|
||||
|
||||
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) {
|
||||
window.location.assign(loginUrl);
|
||||
return;
|
||||
}
|
||||
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
|
||||
}, [ addModalProps, deleteModalProps, watchListId, isAuth, loginUrl ]);
|
||||
}, [ profileState?.error, isAuth, watchListId, deleteModalProps, addModalProps, loginUrl, toast ]);
|
||||
|
||||
const handleAddOrDeleteSuccess = React.useCallback(async() => {
|
||||
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
|
||||
|
||||
+6
-10
@@ -12,8 +12,6 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
|
||||
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
|
||||
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
|
||||
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 SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
@@ -29,7 +27,7 @@ const ApiKeysPage: React.FC = () => {
|
||||
const [ apiKeyModalData, setApiKeyModalData ] = 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) => {
|
||||
setApiKeyModalData(data);
|
||||
@@ -76,7 +74,7 @@ const ApiKeysPage: React.FC = () => {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
throw new Error('API keys fetch error', { cause: error });
|
||||
}
|
||||
|
||||
const list = isMobile ? (
|
||||
@@ -130,12 +128,10 @@ const ApiKeysPage: React.FC = () => {
|
||||
})();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Box h="100%">
|
||||
<PageTitle text="API keys"/>
|
||||
{ content }
|
||||
</Box>
|
||||
</Page>
|
||||
<>
|
||||
<PageTitle text="API keys"/>
|
||||
{ content }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+6
-10
@@ -11,8 +11,6 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
|
||||
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
|
||||
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
|
||||
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 SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
@@ -26,7 +24,7 @@ const CustomAbiPage: React.FC = () => {
|
||||
const [ customAbiModalData, setCustomAbiModalData ] = 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) => {
|
||||
setCustomAbiModalData(data);
|
||||
@@ -72,7 +70,7 @@ const CustomAbiPage: React.FC = () => {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
throw new Error('Custom ABI fetch error', { cause: error });
|
||||
}
|
||||
|
||||
const list = isMobile ? (
|
||||
@@ -113,12 +111,10 @@ const CustomAbiPage: React.FC = () => {
|
||||
})();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Box h="100%">
|
||||
<PageTitle text="Custom ABI"/>
|
||||
{ content }
|
||||
</Box>
|
||||
</Page>
|
||||
<>
|
||||
<PageTitle text="Custom ABI"/>
|
||||
{ content }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,13 +4,11 @@ import React from 'react';
|
||||
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
|
||||
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
|
||||
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 UserAvatar from 'ui/shared/UserAvatar';
|
||||
|
||||
const MyProfile = () => {
|
||||
const { data, isLoading, isError, isFetched } = useFetchProfileInfo();
|
||||
const { data, isLoading, isError, error, isFetched } = useFetchProfileInfo();
|
||||
useRedirectForInvalidAuthToken();
|
||||
|
||||
const content = (() => {
|
||||
@@ -19,7 +17,7 @@ const MyProfile = () => {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
throw new Error('My profile fetch error', { cause: error });
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -54,10 +52,10 @@ const MyProfile = () => {
|
||||
})();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<>
|
||||
<PageTitle text="My profile"/>
|
||||
{ content }
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
|
||||
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
|
||||
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
|
||||
@@ -18,10 +17,10 @@ const PrivateTags = () => {
|
||||
useRedirectForInvalidAuthToken();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<>
|
||||
<PageTitle text="Private tags"/>
|
||||
<RoutedTabs tabs={ TABS }/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthT
|
||||
import useToast from 'lib/hooks/useToast';
|
||||
import PublicTagsData from 'ui/publicTags/PublicTagsData';
|
||||
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
type TScreen = 'data' | 'form';
|
||||
@@ -78,7 +77,7 @@ const PublicTagsComponent: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<>
|
||||
{ screen === 'form' && (
|
||||
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
|
||||
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
|
||||
@@ -87,7 +86,7 @@ const PublicTagsComponent: React.FC = () => {
|
||||
) }
|
||||
<PageTitle text={ header } display={{ base: 'block', lg: 'inline-flex' }} ml={{ base: 0, lg: 3 }}/>
|
||||
{ content }
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+46
-51
@@ -10,8 +10,6 @@ import useApiFetch from 'lib/api/useApiFetch';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
|
||||
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 SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
@@ -22,31 +20,27 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
|
||||
|
||||
const WatchList: React.FC = () => {
|
||||
const apiFetch = useApiFetch();
|
||||
const { data, isLoading, isError } = useQuery<unknown, unknown, TWatchlist>([ resourceKey('watchlist') ], async() => {
|
||||
try {
|
||||
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
|
||||
const { data, isLoading, isError, error } = useQuery<unknown, unknown, TWatchlist>([ resourceKey('watchlist') ], async() => {
|
||||
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
|
||||
|
||||
if (!Array.isArray(watchlistAddresses)) {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => {
|
||||
if (!address?.hash) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
return apiFetch<'old_api', WatchlistTokensResponse>('old_api', { queryParams: { address: address.hash, module: 'account', action: 'tokenlist' } })
|
||||
.then((response) => {
|
||||
if ('result' in response && Array.isArray(response.result)) {
|
||||
return response.result.length;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}));
|
||||
|
||||
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] }));
|
||||
} catch (error) {
|
||||
return error;
|
||||
if (!Array.isArray(watchlistAddresses)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => {
|
||||
if (!address?.hash) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
return apiFetch<'old_api', WatchlistTokensResponse>('old_api', { queryParams: { address: address.hash, module: 'account', action: 'tokenlist' } })
|
||||
.then((response) => {
|
||||
if ('result' in response && Array.isArray(response.result)) {
|
||||
return response.result.length;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}));
|
||||
|
||||
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] }));
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -96,24 +90,27 @@ const WatchList: React.FC = () => {
|
||||
</AccountPageDescription>
|
||||
);
|
||||
|
||||
let content;
|
||||
if (isLoading && !data) {
|
||||
const loader = isMobile ? <SkeletonListAccount showFooterSlot/> : (
|
||||
<>
|
||||
<SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/>
|
||||
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
|
||||
</>
|
||||
);
|
||||
if (isError) {
|
||||
throw new Error('Watch list fetch error', { cause: error });
|
||||
}
|
||||
|
||||
const content = (() => {
|
||||
if (isLoading && !data) {
|
||||
const loader = isMobile ? <SkeletonListAccount showFooterSlot/> : (
|
||||
<>
|
||||
<SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/>
|
||||
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ description }
|
||||
{ loader }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
content = (
|
||||
<>
|
||||
{ description }
|
||||
{ loader }
|
||||
</>
|
||||
);
|
||||
} else if (isError) {
|
||||
content = <DataFetchAlert/>;
|
||||
} else {
|
||||
const list = isMobile ? (
|
||||
<Box>
|
||||
{ data.map((item) => (
|
||||
@@ -133,7 +130,7 @@ const WatchList: React.FC = () => {
|
||||
/>
|
||||
);
|
||||
|
||||
content = (
|
||||
return (
|
||||
<>
|
||||
{ description }
|
||||
{ Boolean(data?.length) && list }
|
||||
@@ -142,7 +139,7 @@ const WatchList: React.FC = () => {
|
||||
size="lg"
|
||||
onClick={ addressModalProps.onOpen }
|
||||
>
|
||||
Add address
|
||||
Add address
|
||||
</Button>
|
||||
</Box>
|
||||
<AddressModal
|
||||
@@ -162,15 +159,13 @@ const WatchList: React.FC = () => {
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Box h="100%">
|
||||
<PageTitle text="Watch list"/>
|
||||
{ content }
|
||||
</Box>
|
||||
</Page>
|
||||
<>
|
||||
<PageTitle text="Watch list"/>
|
||||
{ content }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import useApiQuery from 'lib/api/useApiQuery';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
|
||||
import AccountPageDescription from 'ui/shared/AccountPageDescription';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
|
||||
import AddressModal from './AddressModal/AddressModal';
|
||||
import AddressTagListItem from './AddressTagTable/AddressTagListItem';
|
||||
@@ -15,7 +14,7 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
|
||||
import DeletePrivateTagModal from './DeletePrivateTagModal';
|
||||
|
||||
const PrivateAddressTags = () => {
|
||||
const { data: addressTagsData, isError, isPlaceholderData } = useApiQuery('private_tags_address', {
|
||||
const { data: addressTagsData, isError, error, isPlaceholderData } = useApiQuery('private_tags_address', {
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
|
||||
@@ -50,7 +49,7 @@ const PrivateAddressTags = () => {
|
||||
}, [ deleteModalProps ]);
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
throw new Error('Private tags fetch error', { cause: error });
|
||||
}
|
||||
|
||||
const list = isMobile ? (
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { TransactionTag } from 'types/api/account';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import AccountPageDescription from 'ui/shared/AccountPageDescription';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
|
||||
@@ -16,7 +15,7 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
|
||||
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
|
||||
|
||||
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 deleteModalProps = useDisclosure();
|
||||
@@ -69,7 +68,7 @@ const PrivateTransactionTags = () => {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
throw new Error('Private tags fetch error', { cause: error });
|
||||
}
|
||||
|
||||
const list = isMobile ? (
|
||||
|
||||
@@ -7,7 +7,6 @@ import useApiQuery from 'lib/api/useApiQuery';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
|
||||
import AccountPageDescription from 'ui/shared/AccountPageDescription';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
|
||||
@@ -24,7 +23,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
|
||||
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { data, isLoading, isError } = useApiQuery('public_tags');
|
||||
const { data, isLoading, isError, error } = useApiQuery('public_tags');
|
||||
|
||||
const onDeleteModalClose = useCallback(() => {
|
||||
setDeleteModalData(undefined);
|
||||
@@ -70,7 +69,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
throw new Error('Public tags fetch error', { cause: error });
|
||||
}
|
||||
|
||||
const list = isMobile ? (
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
|
||||
import txIcon from 'icons/transactions.svg';
|
||||
|
||||
const ErrorInvalidTxHash = () => {
|
||||
const AppErrorInvalidTxHash = () => {
|
||||
const textColor = useColorModeValue('gray.500', 'gray.400');
|
||||
const snippet = {
|
||||
borderColor: useColorModeValue('blackAlpha.300', 'whiteAlpha.300'),
|
||||
@@ -54,4 +54,4 @@ const ErrorInvalidTxHash = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorInvalidTxHash;
|
||||
export default AppErrorInvalidTxHash;
|
||||
@@ -0,0 +1,68 @@
|
||||
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);
|
||||
+10
-4
@@ -1,14 +1,15 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import getErrorStatusCode from 'lib/errors/getErrorStatusCode';
|
||||
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
|
||||
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
|
||||
import useAdblockDetect from 'lib/hooks/useAdblockDetect';
|
||||
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
|
||||
import AppError from 'ui/shared/AppError/AppError';
|
||||
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 ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
|
||||
import PageContent from 'ui/shared/Page/PageContent';
|
||||
import Header from 'ui/snippets/header/Header';
|
||||
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
|
||||
@@ -32,7 +33,7 @@ const Page = ({
|
||||
useAdblockDetect();
|
||||
|
||||
const renderErrorScreen = React.useCallback((error?: Error) => {
|
||||
const statusCode = getErrorStatusCode(error) || 500;
|
||||
const statusCode = getErrorCauseStatusCode(error) || 500;
|
||||
const resourceErrorPayload = getResourceErrorPayload(error);
|
||||
const messageInPayload = resourceErrorPayload && 'message' in resourceErrorPayload && typeof resourceErrorPayload.message === 'string' ?
|
||||
resourceErrorPayload.message :
|
||||
@@ -40,9 +41,14 @@ const Page = ({
|
||||
|
||||
const isInvalidTxHash = error?.message.includes('Invalid tx hash');
|
||||
const isBlockConsensus = messageInPayload?.includes('Block lost consensus');
|
||||
const isUnverifiedEmail = statusCode === 403 && messageInPayload?.includes('Unverified email');
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user