From abdd2a5119f084b1cd93e14ffe7bbb2dcc445cd9 Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 10 May 2023 16:07:40 -0300 Subject: [PATCH 1/5] custom error screen for 403 --- lib/api/resources.ts | 3 + .../{getErrorCause.tsx => getErrorCause.ts} | 0 ...tusCode.tsx => getErrorCauseStatusCode.ts} | 2 +- lib/errors/getErrorObj.ts | 15 +++ lib/errors/getErrorObjPayload.ts | 23 +++++ lib/errors/getErrorObjStatusCode.ts | 11 +++ pages/_app.tsx | 6 +- pages/account/api_key.tsx | 5 +- pages/account/custom_abi.tsx | 5 +- pages/account/public_tags_request.tsx | 5 +- pages/account/tag_address.tsx | 5 +- pages/account/watchlist.tsx | 5 +- pages/auth/profile.tsx | 5 +- ui/address/details/AddressFavoriteButton.tsx | 21 +++- ui/pages/ApiKeys.tsx | 16 ++- ui/pages/CustomAbi.tsx | 16 ++- ui/pages/MyProfile.tsx | 10 +- ui/pages/PrivateTags.tsx | 5 +- ui/pages/PublicTags.tsx | 5 +- ui/pages/Watchlist.tsx | 97 +++++++++---------- ui/privateTags/PrivateAddressTags.tsx | 5 +- ui/privateTags/PrivateTransactionTags.tsx | 5 +- ui/publicTags/PublicTagsData.tsx | 5 +- .../AppErrorInvalidTxHash.tsx} | 4 +- .../AppError/AppErrorUnverifiedEmail.tsx | 68 +++++++++++++ ui/shared/Page/Page.tsx | 14 ++- 26 files changed, 252 insertions(+), 109 deletions(-) rename lib/errors/{getErrorCause.tsx => getErrorCause.ts} (100%) rename lib/errors/{getErrorStatusCode.tsx => getErrorCauseStatusCode.ts} (65%) create mode 100644 lib/errors/getErrorObj.ts create mode 100644 lib/errors/getErrorObjPayload.ts create mode 100644 lib/errors/getErrorObjStatusCode.ts rename ui/shared/{ErrorInvalidTxHash.tsx => AppError/AppErrorInvalidTxHash.tsx} (97%) create mode 100644 ui/shared/AppError/AppErrorUnverifiedEmail.tsx diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 2d1564167f..1bfe23be0d 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -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 ], diff --git a/lib/errors/getErrorCause.tsx b/lib/errors/getErrorCause.ts similarity index 100% rename from lib/errors/getErrorCause.tsx rename to lib/errors/getErrorCause.ts diff --git a/lib/errors/getErrorStatusCode.tsx b/lib/errors/getErrorCauseStatusCode.ts similarity index 65% rename from lib/errors/getErrorStatusCode.tsx rename to lib/errors/getErrorCauseStatusCode.ts index d662ac6e65..f9884f03cd 100644 --- a/lib/errors/getErrorStatusCode.tsx +++ b/lib/errors/getErrorCauseStatusCode.ts @@ -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; } diff --git a/lib/errors/getErrorObj.ts b/lib/errors/getErrorObj.ts new file mode 100644 index 0000000000..7a06fdc6cd --- /dev/null +++ b/lib/errors/getErrorObj.ts @@ -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; +} diff --git a/lib/errors/getErrorObjPayload.ts b/lib/errors/getErrorObjPayload.ts new file mode 100644 index 0000000000..524979fe41 --- /dev/null +++ b/lib/errors/getErrorObjPayload.ts @@ -0,0 +1,23 @@ +import getErrorObj from './getErrorObj'; + +export default function getErrorObjPayload(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; +} diff --git a/lib/errors/getErrorObjStatusCode.ts b/lib/errors/getErrorObjStatusCode.ts new file mode 100644 index 0000000000..4d80779099 --- /dev/null +++ b/lib/errors/getErrorObjStatusCode.ts @@ -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; +} diff --git a/pages/_app.tsx b/pages/_app.tsx index bc211501a6..be91906482 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -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 ( { const title = getNetworkTitle(); return ( <> { title } - + + + ); }; diff --git a/pages/account/custom_abi.tsx b/pages/account/custom_abi.tsx index 1e3c259d8a..e65976866b 100644 --- a/pages/account/custom_abi.tsx +++ b/pages/account/custom_abi.tsx @@ -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 ( <> { title } - + + + ); }; diff --git a/pages/account/public_tags_request.tsx b/pages/account/public_tags_request.tsx index edc01b91f0..1c86c8a56c 100644 --- a/pages/account/public_tags_request.tsx +++ b/pages/account/public_tags_request.tsx @@ -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 ( <> { title } - + + + ); }; diff --git a/pages/account/tag_address.tsx b/pages/account/tag_address.tsx index 75d1f88ffe..aebc4fecdc 100644 --- a/pages/account/tag_address.tsx +++ b/pages/account/tag_address.tsx @@ -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 ( <> { title } - + + + ); }; diff --git a/pages/account/watchlist.tsx b/pages/account/watchlist.tsx index fb45d1bf2a..93a0402cfc 100644 --- a/pages/account/watchlist.tsx +++ b/pages/account/watchlist.tsx @@ -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 = () => { { title } - + + + ); }; diff --git a/pages/auth/profile.tsx b/pages/auth/profile.tsx index 1d0db56a4f..5232bbbf93 100644 --- a/pages/auth/profile.tsx +++ b/pages/auth/profile.tsx @@ -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 ( <> My profile - + + + ); }; diff --git a/ui/address/details/AddressFavoriteButton.tsx b/ui/address/details/AddressFavoriteButton.tsx index 24399b4d5a..75e37f449e 100644 --- a/ui/address/details/AddressFavoriteButton.tsx +++ b/ui/address/details/AddressFavoriteButton.tsx @@ -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([ resourceKey('user_info') ]); + const profileState = queryClient.getQueryState>([ 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() } }); diff --git a/ui/pages/ApiKeys.tsx b/ui/pages/ApiKeys.tsx index 6113913c8d..43d66c8aed 100644 --- a/ui/pages/ApiKeys.tsx +++ b/ui/pages/ApiKeys.tsx @@ -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(); const [ deleteModalData, setDeleteModalData ] = useState(); - 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 ; + throw new Error('API keys fetch error', { cause: error }); } const list = isMobile ? ( @@ -130,12 +128,10 @@ const ApiKeysPage: React.FC = () => { })(); return ( - - - - { content } - - + <> + + { content } + ); }; diff --git a/ui/pages/CustomAbi.tsx b/ui/pages/CustomAbi.tsx index 96b105d340..80dedbbeb2 100644 --- a/ui/pages/CustomAbi.tsx +++ b/ui/pages/CustomAbi.tsx @@ -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(); const [ deleteModalData, setDeleteModalData ] = useState(); - 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 ; + throw new Error('Custom ABI fetch error', { cause: error }); } const list = isMobile ? ( @@ -113,12 +111,10 @@ const CustomAbiPage: React.FC = () => { })(); return ( - - - - { content } - - + <> + + { content } + ); }; diff --git a/ui/pages/MyProfile.tsx b/ui/pages/MyProfile.tsx index c063b2f73a..056f8b86d2 100644 --- a/ui/pages/MyProfile.tsx +++ b/ui/pages/MyProfile.tsx @@ -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 ; + throw new Error('My profile fetch error', { cause: error }); } return ( @@ -54,10 +52,10 @@ const MyProfile = () => { })(); return ( - + <> { content } - + ); }; diff --git a/ui/pages/PrivateTags.tsx b/ui/pages/PrivateTags.tsx index 1f5a505039..6be8748360 100644 --- a/ui/pages/PrivateTags.tsx +++ b/ui/pages/PrivateTags.tsx @@ -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 ( - + <> - + ); }; diff --git a/ui/pages/PublicTags.tsx b/ui/pages/PublicTags.tsx index d0b6e1e074..9081039582 100644 --- a/ui/pages/PublicTags.tsx +++ b/ui/pages/PublicTags.tsx @@ -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 ( - + <> { screen === 'form' && ( @@ -87,7 +86,7 @@ const PublicTagsComponent: React.FC = () => { ) } { content } - + ); }; diff --git a/ui/pages/Watchlist.tsx b/ui/pages/Watchlist.tsx index 2433737f5c..8fac9e3aab 100644 --- a/ui/pages/Watchlist.tsx +++ b/ui/pages/Watchlist.tsx @@ -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([ resourceKey('watchlist') ], async() => { - try { - const watchlistAddresses = await apiFetch<'watchlist', Array>('watchlist'); + const { data, isLoading, isError, error } = useQuery([ resourceKey('watchlist') ], async() => { + const watchlistAddresses = await apiFetch<'watchlist', Array>('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 = () => { ); - let content; - if (isLoading && !data) { - const loader = isMobile ? : ( - <> - - - - ); + if (isError) { + throw new Error('Watch list fetch error', { cause: error }); + } + + const content = (() => { + if (isLoading && !data) { + const loader = isMobile ? : ( + <> + + + + ); + + return ( + <> + { description } + { loader } + + ); + } - content = ( - <> - { description } - { loader } - - ); - } else if (isError) { - content = ; - } else { const list = isMobile ? ( { 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 { ) } ); - } + })(); return ( - - - - { content } - - + <> + + { content } + ); }; diff --git a/ui/privateTags/PrivateAddressTags.tsx b/ui/privateTags/PrivateAddressTags.tsx index d2e0c528cd..d0a53be0ca 100644 --- a/ui/privateTags/PrivateAddressTags.tsx +++ b/ui/privateTags/PrivateAddressTags.tsx @@ -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 ; + throw new Error('Private tags fetch error', { cause: error }); } const list = isMobile ? ( diff --git a/ui/privateTags/PrivateTransactionTags.tsx b/ui/privateTags/PrivateTransactionTags.tsx index f8ff7bc065..85629c1bfd 100644 --- a/ui/privateTags/PrivateTransactionTags.tsx +++ b/ui/privateTags/PrivateTransactionTags.tsx @@ -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 ; + throw new Error('Private tags fetch error', { cause: error }); } const list = isMobile ? ( diff --git a/ui/publicTags/PublicTagsData.tsx b/ui/publicTags/PublicTagsData.tsx index 3a623469e4..7ace7b7ec3 100644 --- a/ui/publicTags/PublicTagsData.tsx +++ b/ui/publicTags/PublicTagsData.tsx @@ -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(); 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 ; + throw new Error('Public tags fetch error', { cause: error }); } const list = isMobile ? ( diff --git a/ui/shared/ErrorInvalidTxHash.tsx b/ui/shared/AppError/AppErrorInvalidTxHash.tsx similarity index 97% rename from ui/shared/ErrorInvalidTxHash.tsx rename to ui/shared/AppError/AppErrorInvalidTxHash.tsx index 9621c114e5..7c0240b02b 100644 --- a/ui/shared/ErrorInvalidTxHash.tsx +++ b/ui/shared/AppError/AppErrorInvalidTxHash.tsx @@ -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; diff --git a/ui/shared/AppError/AppErrorUnverifiedEmail.tsx b/ui/shared/AppError/AppErrorUnverifiedEmail.tsx new file mode 100644 index 0000000000..009b4ff9e9 --- /dev/null +++ b/ui/shared/AppError/AppErrorUnverifiedEmail.tsx @@ -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 ( + + + Email is not verified + + 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?` } + + + + ); +}; + +export default chakra(AppErrorUnverifiedEmail); diff --git a/ui/shared/Page/Page.tsx b/ui/shared/Page/Page.tsx index 4d82f380b3..1b07f149eb 100644 --- a/ui/shared/Page/Page.tsx +++ b/ui/shared/Page/Page.tsx @@ -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 ; + return ; + } + + if (isUnverifiedEmail) { + return ; } if (isBlockConsensus) { From d4cc95100cf90580434ac77551e4ff28885afe71 Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 10 May 2023 16:33:12 -0300 Subject: [PATCH 2/5] throw out only specific error --- ui/pages/ApiKeys.tsx | 6 +++++- ui/pages/CustomAbi.tsx | 6 +++++- ui/pages/MyProfile.tsx | 6 +++++- ui/pages/Watchlist.tsx | 9 +++++++-- ui/privateTags/PrivateAddressTags.tsx | 6 +++++- ui/privateTags/PrivateTransactionTags.tsx | 6 +++++- ui/publicTags/PublicTagsData.tsx | 6 +++++- 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/ui/pages/ApiKeys.tsx b/ui/pages/ApiKeys.tsx index 43d66c8aed..ea8e22832b 100644 --- a/ui/pages/ApiKeys.tsx +++ b/ui/pages/ApiKeys.tsx @@ -12,6 +12,7 @@ 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 PageTitle from 'ui/shared/Page/PageTitle'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; @@ -74,7 +75,10 @@ const ApiKeysPage: React.FC = () => { } if (isError) { - throw new Error('API keys fetch error', { cause: error }); + if (error.status === 403) { + throw new Error('Unverified email error', { cause: error }); + } + return ; } const list = isMobile ? ( diff --git a/ui/pages/CustomAbi.tsx b/ui/pages/CustomAbi.tsx index 80dedbbeb2..d209c2fa6c 100644 --- a/ui/pages/CustomAbi.tsx +++ b/ui/pages/CustomAbi.tsx @@ -11,6 +11,7 @@ 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 PageTitle from 'ui/shared/Page/PageTitle'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; @@ -70,7 +71,10 @@ const CustomAbiPage: React.FC = () => { } if (isError) { - throw new Error('Custom ABI fetch error', { cause: error }); + if (error.status === 403) { + throw new Error('Unverified email error', { cause: error }); + } + return ; } const list = isMobile ? ( diff --git a/ui/pages/MyProfile.tsx b/ui/pages/MyProfile.tsx index 056f8b86d2..8df6ceab13 100644 --- a/ui/pages/MyProfile.tsx +++ b/ui/pages/MyProfile.tsx @@ -4,6 +4,7 @@ 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 PageTitle from 'ui/shared/Page/PageTitle'; import UserAvatar from 'ui/shared/UserAvatar'; @@ -17,7 +18,10 @@ const MyProfile = () => { } if (isError) { - throw new Error('My profile fetch error', { cause: error }); + if (error.status === 403) { + throw new Error('Unverified email error', { cause: error }); + } + return ; } return ( diff --git a/ui/pages/Watchlist.tsx b/ui/pages/Watchlist.tsx index 8fac9e3aab..64ae471b70 100644 --- a/ui/pages/Watchlist.tsx +++ b/ui/pages/Watchlist.tsx @@ -5,11 +5,13 @@ import React, { useCallback, useState } from 'react'; import type { WatchlistAddress, WatchlistTokensResponse } from 'types/api/account'; import type { TWatchlist, TWatchlistItem } from 'types/client/account'; +import type { ResourceError } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources'; 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 PageTitle from 'ui/shared/Page/PageTitle'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; @@ -20,7 +22,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; const WatchList: React.FC = () => { const apiFetch = useApiFetch(); - const { data, isLoading, isError, error } = useQuery([ resourceKey('watchlist') ], async() => { + const { data, isLoading, isError, error } = useQuery([ resourceKey('watchlist') ], async() => { const watchlistAddresses = await apiFetch<'watchlist', Array>('watchlist'); if (!Array.isArray(watchlistAddresses)) { @@ -91,7 +93,10 @@ const WatchList: React.FC = () => { ); if (isError) { - throw new Error('Watch list fetch error', { cause: error }); + if (error.status === 403) { + throw new Error('Unverified email error', { cause: error }); + } + return ; } const content = (() => { diff --git a/ui/privateTags/PrivateAddressTags.tsx b/ui/privateTags/PrivateAddressTags.tsx index d0a53be0ca..8b8b3eb94c 100644 --- a/ui/privateTags/PrivateAddressTags.tsx +++ b/ui/privateTags/PrivateAddressTags.tsx @@ -7,6 +7,7 @@ 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'; @@ -49,7 +50,10 @@ const PrivateAddressTags = () => { }, [ deleteModalProps ]); if (isError) { - throw new Error('Private tags fetch error', { cause: error }); + if (error.status === 403) { + throw new Error('Unverified email error', { cause: error }); + } + return ; } const list = isMobile ? ( diff --git a/ui/privateTags/PrivateTransactionTags.tsx b/ui/privateTags/PrivateTransactionTags.tsx index 85629c1bfd..e6da5c6037 100644 --- a/ui/privateTags/PrivateTransactionTags.tsx +++ b/ui/privateTags/PrivateTransactionTags.tsx @@ -6,6 +6,7 @@ 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'; @@ -68,7 +69,10 @@ const PrivateTransactionTags = () => { } if (isError) { - throw new Error('Private tags fetch error', { cause: error }); + if (error.status === 403) { + throw new Error('Unverified email error', { cause: error }); + } + return ; } const list = isMobile ? ( diff --git a/ui/publicTags/PublicTagsData.tsx b/ui/publicTags/PublicTagsData.tsx index 7ace7b7ec3..416105a0fe 100644 --- a/ui/publicTags/PublicTagsData.tsx +++ b/ui/publicTags/PublicTagsData.tsx @@ -7,6 +7,7 @@ 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'; @@ -69,7 +70,10 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { } if (isError) { - throw new Error('Public tags fetch error', { cause: error }); + if (error.status === 403) { + throw new Error('Unverified email error', { cause: error }); + } + return ; } const list = isMobile ? ( From 08371b59fc6c48db06a771bb0eb3d400d20a9afc Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 10 May 2023 16:49:30 -0300 Subject: [PATCH 3/5] status code field name fix --- lib/errors/getErrorObjStatusCode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/errors/getErrorObjStatusCode.ts b/lib/errors/getErrorObjStatusCode.ts index 4d80779099..a672b9d7a4 100644 --- a/lib/errors/getErrorObjStatusCode.ts +++ b/lib/errors/getErrorObjStatusCode.ts @@ -3,9 +3,9 @@ import getErrorObj from './getErrorObj'; export default function getErrorObjStatusCode(error: unknown) { const errorObj = getErrorObj(error); - if (!errorObj || !('statusCode' in errorObj) || typeof errorObj.statusCode !== 'number') { + if (!errorObj || !('status' in errorObj) || typeof errorObj.status !== 'number') { return; } - return errorObj.statusCode; + return errorObj.status; } From 594430fd0b0ca9a8cc693b252cf516e2d42a09a4 Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 10 May 2023 17:35:13 -0300 Subject: [PATCH 4/5] proper message about resend time --- ui/shared/AppError/AppErrorUnverifiedEmail.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/ui/shared/AppError/AppErrorUnverifiedEmail.tsx b/ui/shared/AppError/AppErrorUnverifiedEmail.tsx index 009b4ff9e9..6b71c137f1 100644 --- a/ui/shared/AppError/AppErrorUnverifiedEmail.tsx +++ b/ui/shared/AppError/AppErrorUnverifiedEmail.tsx @@ -3,6 +3,7 @@ import React from 'react'; import icon404 from 'icons/error-pages/404.svg'; import useApiFetch from 'lib/api/useApiFetch'; +import dayjs from 'lib/date/dayjs'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import useToast from 'lib/hooks/useToast'; @@ -31,8 +32,20 @@ const AppErrorUnverifiedEmail = ({ className }: Props) => { }); } catch (error) { const statusCode = getErrorObjStatusCode(error); - const payload = getErrorObjPayload<{ message: string }>(error); - const message = statusCode === 429 ? payload?.message : undefined; + + const message = (() => { + if (statusCode !== 429) { + return; + } + + const payload = getErrorObjPayload<{ seconds_before_next_resend: number }>(error); + if (!payload) { + return; + } + + const timeUntilNextResend = dayjs().add(payload.seconds_before_next_resend, 'seconds').fromNow(); + return `Email resend is available ${ timeUntilNextResend }.`; + })(); !toast.isActive(toastId) && toast({ id: toastId, From 90780a0096b999f8737d29f17860530969e4988f Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 11 May 2023 12:32:39 -0300 Subject: [PATCH 5/5] change error code icon and add user email --- icons/error-pages/403.svg | 3 +++ ui/shared/AppError/AppErrorUnverifiedEmail.tsx | 11 +++++++---- ui/shared/Page/Page.tsx | 5 ++++- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 icons/error-pages/403.svg diff --git a/icons/error-pages/403.svg b/icons/error-pages/403.svg new file mode 100644 index 0000000000..ee8d9669f8 --- /dev/null +++ b/icons/error-pages/403.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/shared/AppError/AppErrorUnverifiedEmail.tsx b/ui/shared/AppError/AppErrorUnverifiedEmail.tsx index 6b71c137f1..02cc77e8f7 100644 --- a/ui/shared/AppError/AppErrorUnverifiedEmail.tsx +++ b/ui/shared/AppError/AppErrorUnverifiedEmail.tsx @@ -1,7 +1,7 @@ import { Box, Text, Button, Heading, Icon, chakra } from '@chakra-ui/react'; import React from 'react'; -import icon404 from 'icons/error-pages/404.svg'; +import icon403 from 'icons/error-pages/403.svg'; import useApiFetch from 'lib/api/useApiFetch'; import dayjs from 'lib/date/dayjs'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; @@ -10,9 +10,10 @@ import useToast from 'lib/hooks/useToast'; interface Props { className?: string; + email?: string; } -const AppErrorUnverifiedEmail = ({ className }: Props) => { +const AppErrorUnverifiedEmail = ({ className, email }: Props) => { const apiFetch = useApiFetch(); const toast = useToast(); @@ -61,10 +62,12 @@ const AppErrorUnverifiedEmail = ({ className }: Props) => { return ( - + Email is not verified - 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?` } + Please confirm your email address to use the My Account feature. A confirmation email was sent to + { email || 'your email address' } + on signup. { `Didn't receive?` }