diff --git a/icons/docs.svg b/icons/docs.svg
index 13b4878076..71a628d93c 100644
--- a/icons/docs.svg
+++ b/icons/docs.svg
@@ -1,4 +1,4 @@
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/icons/rocket.svg b/icons/rocket.svg
index b0001568d7..46523e1b05 100644
--- a/icons/rocket.svg
+++ b/icons/rocket.svg
@@ -1,4 +1,4 @@
diff --git a/icons/social/coingecko.svg b/icons/social/coingecko.svg
index cb4ccc128d..baf56eef3a 100644
--- a/icons/social/coingecko.svg
+++ b/icons/social/coingecko.svg
@@ -1,8 +1,8 @@
diff --git a/icons/social/defi_llama.svg b/icons/social/defi_llama.svg
index 4d9aeb54b7..f7d2cc51a2 100644
--- a/icons/social/defi_llama.svg
+++ b/icons/social/defi_llama.svg
@@ -1,6 +1,6 @@
diff --git a/icons/social/twitter_filled.svg b/icons/social/twitter_filled.svg
index 02951fde8a..5fc356a969 100644
--- a/icons/social/twitter_filled.svg
+++ b/icons/social/twitter_filled.svg
@@ -1,3 +1,3 @@
diff --git a/lib/api/resources.ts b/lib/api/resources.ts
index a9615103a0..3134238ed1 100644
--- a/lib/api/resources.ts
+++ b/lib/api/resources.ts
@@ -75,6 +75,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..a672b9d7a4
--- /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 || !('status' in errorObj) || typeof errorObj.status !== 'number') {
+ return;
+ }
+
+ return errorObj.status;
+}
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 0721d68511..97b180267d 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 cb71893490..12877caf11 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 6d607280de..7146724e19 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 6b76934557..7344c339c3 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 4ee5994360..54e0ad4c68 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 8642112023..393e73753b 100644
--- a/ui/address/details/AddressFavoriteButton.tsx
+++ b/ui/address/details/AddressFavoriteButton.tsx
@@ -5,9 +5,12 @@ import React from 'react';
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 usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import useRedirectIfNotAuth from 'lib/hooks/useRedirectIfNotAuth';
+import useToast from 'lib/hooks/useToast';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
@@ -22,14 +25,33 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const deleteModalProps = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
+ const toast = useToast();
+
const redirectIfNotAuth = useRedirectIfNotAuth();
+ const profileState = queryClient.getQueryState>([ resourceKey('user_info') ]);
+
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 (redirectIfNotAuth()) {
return;
}
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
- }, [ addModalProps, deleteModalProps, watchListId, redirectIfNotAuth ]);
+ }, [ profileState, redirectIfNotAuth, watchListId, deleteModalProps, addModalProps, 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 c37957f146..5914fd6c74 100644
--- a/ui/pages/ApiKeys.tsx
+++ b/ui/pages/ApiKeys.tsx
@@ -13,7 +13,6 @@ 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 +28,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,6 +75,9 @@ const ApiKeysPage: React.FC = () => {
}
if (isError) {
+ if (error.status === 403) {
+ throw new Error('Unverified email error', { cause: error });
+ }
return ;
}
@@ -130,12 +132,10 @@ const ApiKeysPage: React.FC = () => {
})();
return (
-
-
-
- { content }
-
-
+ <>
+
+ { content }
+ >
);
};
diff --git a/ui/pages/CustomAbi.tsx b/ui/pages/CustomAbi.tsx
index 2720de08dc..858279e16c 100644
--- a/ui/pages/CustomAbi.tsx
+++ b/ui/pages/CustomAbi.tsx
@@ -12,7 +12,6 @@ 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 +25,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,6 +71,9 @@ const CustomAbiPage: React.FC = () => {
}
if (isError) {
+ if (error.status === 403) {
+ throw new Error('Unverified email error', { cause: error });
+ }
return ;
}
@@ -113,12 +115,10 @@ const CustomAbiPage: React.FC = () => {
})();
return (
-
-
-
- { content }
-
-
+ <>
+
+ { content }
+ >
);
};
diff --git a/ui/pages/MyProfile.tsx b/ui/pages/MyProfile.tsx
index b59acafbe6..905ec63391 100644
--- a/ui/pages/MyProfile.tsx
+++ b/ui/pages/MyProfile.tsx
@@ -5,12 +5,11 @@ 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,6 +18,9 @@ const MyProfile = () => {
}
if (isError) {
+ if (error.status === 403) {
+ throw new Error('Unverified email error', { cause: error });
+ }
return ;
}
@@ -54,10 +56,10 @@ const MyProfile = () => {
})();
return (
-
+ <>
{ content }
-
+ >
);
};
diff --git a/ui/pages/PrivateTags.tsx b/ui/pages/PrivateTags.tsx
index 49f24feb1f..392ac419ea 100644
--- a/ui/pages/PrivateTags.tsx
+++ b/ui/pages/PrivateTags.tsx
@@ -5,7 +5,6 @@ import type { RoutedTab } from 'ui/shared/Tabs/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/Tabs/RoutedTabs';
@@ -18,10 +17,10 @@ const PrivateTags = () => {
useRedirectForInvalidAuthToken();
return (
-
+ <>
-
+ >
);
};
diff --git a/ui/pages/PublicTags.tsx b/ui/pages/PublicTags.tsx
index 3ea7d9aa11..10e4001517 100644
--- a/ui/pages/PublicTags.tsx
+++ b/ui/pages/PublicTags.tsx
@@ -9,7 +9,6 @@ import useToast from 'lib/hooks/useToast';
import getQueryParamString from 'lib/router/getQueryParamString';
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';
@@ -90,14 +89,14 @@ const PublicTagsComponent: React.FC = () => {
};
return (
-
+ <>
{ content }
-
+ >
);
};
diff --git a/ui/pages/Watchlist.tsx b/ui/pages/Watchlist.tsx
index 8685f4896f..8eed842137 100644
--- a/ui/pages/Watchlist.tsx
+++ b/ui/pages/Watchlist.tsx
@@ -5,13 +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 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 +22,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 +92,30 @@ const WatchList: React.FC = () => {
);
- let content;
- if (isLoading && !data) {
- const loader = isMobile ? : (
- <>
-
-
- >
- );
+ if (isError) {
+ if (error.status === 403) {
+ throw new Error('Unverified email error', { cause: error });
+ }
+ return ;
+ }
+
+ 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 +135,7 @@ const WatchList: React.FC = () => {
/>
);
- content = (
+ return (
<>
{ description }
{ Boolean(data?.length) && list }
@@ -142,7 +144,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 7ea9b17a6e..5eb8af8e82 100644
--- a/ui/privateTags/PrivateAddressTags.tsx
+++ b/ui/privateTags/PrivateAddressTags.tsx
@@ -15,7 +15,7 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => {
- const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
+ const { data: addressTagsData, isError, error, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
queryOptions: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
@@ -54,6 +54,9 @@ const PrivateAddressTags = () => {
}, [ deleteModalProps ]);
if (isError) {
+ if (error.status === 403) {
+ throw new Error('Unverified email error', { cause: error });
+ }
return ;
}
diff --git a/ui/privateTags/PrivateTransactionTags.tsx b/ui/privateTags/PrivateTransactionTags.tsx
index f8ff7bc065..e6da5c6037 100644
--- a/ui/privateTags/PrivateTransactionTags.tsx
+++ b/ui/privateTags/PrivateTransactionTags.tsx
@@ -16,7 +16,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,6 +69,9 @@ const PrivateTransactionTags = () => {
}
if (isError) {
+ if (error.status === 403) {
+ throw new Error('Unverified email error', { cause: error });
+ }
return ;
}
diff --git a/ui/publicTags/PublicTagsData.tsx b/ui/publicTags/PublicTagsData.tsx
index 3a623469e4..416105a0fe 100644
--- a/ui/publicTags/PublicTagsData.tsx
+++ b/ui/publicTags/PublicTagsData.tsx
@@ -24,7 +24,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,6 +70,9 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
}
if (isError) {
+ if (error.status === 403) {
+ throw new Error('Unverified email error', { cause: error });
+ }
return ;
}
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..02cc77e8f7
--- /dev/null
+++ b/ui/shared/AppError/AppErrorUnverifiedEmail.tsx
@@ -0,0 +1,84 @@
+import { Box, Text, Button, Heading, Icon, chakra } from '@chakra-ui/react';
+import React from 'react';
+
+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';
+import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
+import useToast from 'lib/hooks/useToast';
+
+interface Props {
+ className?: string;
+ email?: string;
+}
+
+const AppErrorUnverifiedEmail = ({ className, email }: 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 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,
+ 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
+ { email || 'your email address' }
+ 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..347b017e13 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,17 @@ 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) {
+ const email = resourceErrorPayload && 'email' in resourceErrorPayload && typeof resourceErrorPayload.email === 'string' ?
+ resourceErrorPayload.email :
+ undefined;
+ return ;
}
if (isBlockConsensus) {