Commit 75a3b2f1 authored by tom goriunov's avatar tom goriunov Committed by GitHub

unverified user journey improvements (#1029)

* unverified email page

* add cookies and redirects

* remove unnecessary code

* page cookie reset
parent 420246dc
...@@ -257,6 +257,7 @@ frontend: ...@@ -257,6 +257,7 @@ frontend:
- "/apps" - "/apps"
- "/static" - "/static"
- "/auth/profile" - "/auth/profile"
- "/auth/unverified-email"
- "/txs" - "/txs"
- "/tx" - "/tx"
- "/blocks" - "/blocks"
......
...@@ -212,6 +212,7 @@ frontend: ...@@ -212,6 +212,7 @@ frontend:
- "/apps" - "/apps"
- "/static" - "/static"
- "/auth/profile" - "/auth/profile"
- "/auth/unverified-email"
- "/txs" - "/txs"
- "/tx" - "/tx"
- "/blocks" - "/blocks"
......
...@@ -25,6 +25,7 @@ frontend: ...@@ -25,6 +25,7 @@ frontend:
- "/apps" - "/apps"
- "/static" - "/static"
- "/auth/profile" - "/auth/profile"
- "/auth/unverified-email"
- "/txs" - "/txs"
- "/tx" - "/tx"
- "/blocks" - "/blocks"
......
...@@ -24,6 +24,7 @@ frontend: ...@@ -24,6 +24,7 @@ frontend:
- "/apps" - "/apps"
- "/static" - "/static"
- "/auth/profile" - "/auth/profile"
- "/auth/unverified-email"
- "/txs" - "/txs"
- "/tx" - "/tx"
- "/blocks" - "/blocks"
......
<svg viewBox="0 0 177 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.961 7.644a2 2 0 0 0-2.251-2.812l-76.313 17.5a2 2 0 0 0-.727 3.569l17.58 12.744v17.636a2.001 2.001 0 0 0 3.321 1.502l10.716-9.426 21.384 9.744a2 2 0 0 0 2.634-.957l23.656-49.5ZM108.308 35.92 93.583 25.247l62.923-14.43-48.198 25.104Zm.942 1.764v14.173l8.367-7.36a.385.385 0 0 1 .022-.018l.016-.014a.998.998 0 0 1 .214-.242l37.608-30.616-46.227 24.077Zm31.293 15.962-19.927-9.08 39.719-32.335-19.792 41.415ZM93.278 65.729a1.5 1.5 0 0 0 1.93 2.296 93.435 93.435 0 0 0 2.449-2.13 57.65 57.65 0 0 0 .819-.753l.044-.042.012-.011.004-.004.001-.001L97.5 64l1.038 1.083a1.5 1.5 0 0 0-2.075-2.167l-.002.002-.008.008-.037.035a19.011 19.011 0 0 1-.154.145 90.663 90.663 0 0 1-2.984 2.623Zm-5.037 7.714a1.5 1.5 0 0 0-1.751-2.436 105.47 105.47 0 0 1-7.163 4.73 1.5 1.5 0 1 0 1.547 2.57c2.69-1.618 5.17-3.284 7.367-4.864Zm-15.172 9.06a1.5 1.5 0 0 0-1.28-2.714c-2.556 1.205-5.207 2.277-7.906 3.13a1.5 1.5 0 0 0 .905 2.86c2.847-.9 5.624-2.024 8.28-3.276Zm-17.046 5.22a1.5 1.5 0 0 0-.358-2.98A34.938 34.938 0 0 1 51.5 85c-1.392 0-2.728-.09-4.012-.26a1.5 1.5 0 1 0-.394 2.973c1.416.188 2.884.287 4.406.287 1.51 0 3.02-.097 4.523-.278Zm-17.422-2.388a1.5 1.5 0 1 0 1.212-2.745c-2.517-1.11-4.783-2.55-6.816-4.194a1.5 1.5 0 1 0-1.887 2.332c2.216 1.792 4.705 3.377 7.491 4.607ZM24.974 74.523a1.5 1.5 0 1 0 2.338-1.881C25.51 70.403 24 68.076 22.756 65.86a1.5 1.5 0 0 0-2.616 1.47c1.311 2.333 2.911 4.803 4.834 7.193Zm-8.515-15a1.5 1.5 0 0 0 2.796-1.086 49.986 49.986 0 0 1-1-2.813 32.043 32.043 0 0 1-.29-.956l-.013-.045-.002-.01a1.5 1.5 0 0 0-2.899.775l1.45-.388-1.45.388.001.003.002.005.004.018.018.061.064.227c.057.196.143.478.258.837.23.718.579 1.741 1.06 2.984Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 201 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M127.992 76.305c0 3.301-.633 6.4-1.9 9.294-1.221 2.895-2.917 5.428-5.088 7.599-2.171 2.17-4.704 3.89-7.598 5.156-2.895 1.22-5.993 1.831-9.295 1.831h-4.138c-3.302 0-6.423-.61-9.362-1.831-2.895-1.267-5.428-2.986-7.599-5.156-2.17-2.171-3.89-4.704-5.156-7.599-1.221-2.895-1.832-5.993-1.832-9.294v-52.24c0-3.3.611-6.399 1.832-9.294 1.267-2.894 2.985-5.427 5.156-7.598 2.171-2.17 4.704-3.867 7.599-5.088 2.94-1.266 6.06-1.9 9.362-1.9h4.138c3.302 0 6.4.633 9.295 1.9 2.894 1.221 5.427 2.917 7.598 5.088 2.171 2.171 3.867 4.704 5.088 7.598 1.267 2.895 1.9 5.993 1.9 9.295v52.239Zm-8.752-52.24c0-2.17-.407-4.183-1.221-6.037-.814-1.9-1.922-3.55-3.324-4.953-1.402-1.402-3.053-2.51-4.953-3.324-1.854-.814-3.867-1.221-6.038-1.221h-3.324c-2.171 0-4.206.407-6.106 1.221a16.718 16.718 0 0 0-4.952 3.324c-1.403 1.402-2.51 3.053-3.325 4.953-.814 1.854-1.22 3.867-1.22 6.038v52.239c0 2.17.406 4.206 1.22 6.105a16.72 16.72 0 0 0 3.325 4.953 16.72 16.72 0 0 0 4.952 3.324c1.9.814 3.935 1.222 6.106 1.222h3.324c2.171 0 4.184-.408 6.038-1.222 1.9-.814 3.551-1.922 4.953-3.324a16.72 16.72 0 0 0 3.324-4.953c.814-1.9 1.221-3.934 1.221-6.105v-52.24Zm-71.411 57.94v16.824a8.752 8.752 0 0 1-8.752-8.751v-8.073H3.275a3.275 3.275 0 0 1-2.85-4.89L42.38 3.021a2.914 2.914 0 0 1 5.45 1.436v69.202h8.345a8.345 8.345 0 0 1-8.345 8.344ZM39.077 25.49 12.28 73.659h26.797V25.49ZM198.22 86.25c1.276-2.917 1.914-6.039 1.914-9.365v-6.7c0-2.324-.319-4.534-.957-6.63a22.47 22.47 0 0 0-2.598-5.948 22.916 22.916 0 0 0-3.965-4.922 22.222 22.222 0 0 0-5.195-3.76 25.193 25.193 0 0 0 5.195-3.76 24.48 24.48 0 0 0 3.965-4.99 22.69 22.69 0 0 0 2.598-5.878c.638-2.142.957-4.375.957-6.7v-3.35c0-3.326-.638-6.448-1.914-9.364-1.231-2.917-2.94-5.47-5.127-7.657-2.188-2.187-4.74-3.896-7.657-5.127C182.519.823 179.397.185 176.07.185h-4.17c-3.327 0-6.471.638-9.433 1.914-2.917 1.23-5.469 2.94-7.657 5.127-2.187 2.188-3.919 4.74-5.195 7.657-1.23 2.916-1.846 6.038-1.846 9.365a2.974 2.974 0 0 0 2.497 2.935l2.408.393a3.372 3.372 0 0 0 3.914-3.328c0-2.188.41-4.216 1.23-6.084.82-1.914 1.937-3.578 3.35-4.99a16.835 16.835 0 0 1 4.99-3.35c1.914-.82 3.965-1.23 6.152-1.23h3.35c2.187 0 4.215.41 6.084 1.23 1.914.82 3.577 1.937 4.99 3.35 1.413 1.412 2.529 3.076 3.35 4.99.82 1.868 1.23 3.896 1.23 6.084v4.785c0 2.188-.41 4.238-1.23 6.152a15.982 15.982 0 0 1-3.35 4.922c-1.413 1.413-3.076 2.53-4.99 3.35-1.869.82-3.897 1.23-6.084 1.23h-2.358a4.205 4.205 0 1 0 0 8.409h2.358c2.187 0 4.215.41 6.084 1.23 1.914.82 3.577 1.937 4.99 3.35 1.413 1.412 2.529 3.076 3.35 4.99.82 1.868 1.23 3.896 1.23 6.084v8.135c0 2.187-.41 4.238-1.23 6.152a16.856 16.856 0 0 1-3.35 4.99c-1.413 1.413-3.076 2.53-4.99 3.35-1.869.82-3.897 1.23-6.084 1.23h-3.35c-2.187 0-4.238-.41-6.152-1.23a16.834 16.834 0 0 1-4.99-3.35 16.837 16.837 0 0 1-3.35-4.99c-.82-1.914-1.23-3.965-1.23-6.153a3.34 3.34 0 0 0-3.851-3.299l-2.468.383a2.952 2.952 0 0 0-2.5 2.916c0 3.327.616 6.45 1.846 9.366 1.276 2.916 3.008 5.469 5.195 7.656 2.188 2.188 4.74 3.92 7.657 5.195 2.962 1.231 6.106 1.846 9.433 1.846h4.17c3.327 0 6.449-.615 9.365-1.846 2.917-1.276 5.469-3.007 7.657-5.195 2.187-2.188 3.896-4.74 5.127-7.656Z" fill="currentColor"/>
</svg>
...@@ -5,6 +5,8 @@ import isBrowser from './isBrowser'; ...@@ -5,6 +5,8 @@ import isBrowser from './isBrowser';
export enum NAMES { export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
INVALID_SESSION='invalid_session',
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
INDEXING_ALERT='indexing_alert', INDEXING_ALERT='indexing_alert',
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import type { Route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import type { ResourceError } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import useLoginUrl from 'lib/hooks/useLoginUrl'; import useLoginUrl from 'lib/hooks/useLoginUrl';
...@@ -13,22 +10,15 @@ export default function useIsAccountActionAllowed() { ...@@ -13,22 +10,15 @@ export default function useIsAccountActionAllowed() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]); const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]);
const profileState = queryClient.getQueryState<unknown, ResourceError<{ message: string }>>([ resourceKey('user_info') ]);
const isAuth = Boolean(profileData); const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
const router = useRouter();
return React.useCallback((accountRoute: Route) => {
if (profileState?.error?.status === 403) {
router.push(accountRoute);
return false;
}
return React.useCallback(() => {
if (!isAuth) { if (!isAuth) {
window.location.assign(loginUrl); window.location.assign(loginUrl);
return false; return false;
} }
return true; return true;
}, [ isAuth, loginUrl, profileState?.error?.status, router ]); }, [ isAuth, loginUrl ]);
} }
...@@ -41,6 +41,7 @@ const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -41,6 +41,7 @@ const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/api/csrf': 'Node API: CSRF token', '/api/csrf': 'Node API: CSRF token',
'/api/healthz': 'Node API: Health check', '/api/healthz': 'Node API: Health check',
'/auth/auth0': 'Auth', '/auth/auth0': 'Auth',
'/auth/unverified-email': 'Auth',
}; };
export default function getPageType(pathname: Route['pathname']) { export default function getPageType(pathname: Route['pathname']) {
......
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import appConfig from 'configs/app/config';
import { httpLogger } from 'lib/api/logger';
import { DAY } from 'lib/consts';
import * as cookies from 'lib/cookies';
export function account(req: NextRequest) {
if (!appConfig.isAccountSupported) {
return;
}
const apiTokenCookie = req.cookies.get(cookies.NAMES.API_TOKEN);
// if user doesn't have api token cookie and he is trying to access account page
// do redirect to auth page
if (!apiTokenCookie) {
// we don't have any info from router here, so just do straight forward sub-string search (sorry)
const isAccountRoute =
req.nextUrl.pathname.includes('/account/') ||
(req.nextUrl.pathname === '/txs' && req.nextUrl.searchParams.get('tab') === 'watchlist');
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
if ((isAccountRoute || isProfileRoute)) {
const authUrl = appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } });
return NextResponse.redirect(authUrl);
}
}
// if user hasn't confirmed email yet
if (req.cookies.get(cookies.NAMES.INVALID_SESSION)) {
// if user has both cookies, make redirect to logout
if (apiTokenCookie) {
// temporary solution
// TODO check app for integrity https://github.com/blockscout/frontend/issues/1028 and make typescript happy here
if (!appConfig.logoutUrl) {
httpLogger.logger.error({
message: 'Logout URL is not configured',
});
return;
}
// yes, we could have checked that the current URL is not the logout URL, but we hadn't
// logout URL is always external URL in auth0.com sub-domain
// at least we hope so
const res = NextResponse.redirect(appConfig.logoutUrl);
res.cookies.delete(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED); // reset cookie to show email verification page again
return res;
}
// if user hasn't seen email verification page, make redirect to it
if (!req.cookies.get(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED)) {
if (!req.nextUrl.pathname.includes('/auth/unverified-email')) {
const url = appConfig.baseUrl + route({ pathname: '/auth/unverified-email' });
const res = NextResponse.redirect(url);
res.cookies.set({
name: cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED,
value: 'true',
expires: Date.now() + 7 * DAY,
});
return res;
}
}
}
}
export { account } from './account';
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import appConfig from 'configs/app/config';
import { NAMES } from 'lib/cookies';
import generateCspPolicy from 'lib/csp/generateCspPolicy'; import generateCspPolicy from 'lib/csp/generateCspPolicy';
import * as middlewares from 'lib/next/middlewares/index';
const cspPolicy = generateCspPolicy(); const cspPolicy = generateCspPolicy();
...@@ -16,16 +14,9 @@ export function middleware(req: NextRequest) { ...@@ -16,16 +14,9 @@ export function middleware(req: NextRequest) {
return; return;
} }
// we don't have any info from router here, so just do straight forward sub-string search (sorry) const accountResponse = middlewares.account(req);
const isAccountRoute = if (accountResponse) {
req.nextUrl.pathname.includes('/account/') || return accountResponse;
(req.nextUrl.pathname === '/txs' && req.nextUrl.searchParams.get('tab') === 'watchlist');
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) {
const authUrl = appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } });
return NextResponse.redirect(authUrl);
} }
const end = Date.now(); const end = Date.now();
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import UnverifiedEmail from 'ui/pages/UnverifiedEmail';
import Page from 'ui/shared/Page/Page';
const UnverifiedEmailPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<Page>
<UnverifiedEmail/>
</Page>
</>
);
};
export default UnverifiedEmailPage;
export { getServerSideProps } from 'lib/next/account/getServerSideProps';
...@@ -24,6 +24,7 @@ declare module "nextjs-routes" { ...@@ -24,6 +24,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/apps"> | StaticRoute<"/apps">
| StaticRoute<"/auth/auth0"> | StaticRoute<"/auth/auth0">
| StaticRoute<"/auth/profile"> | StaticRoute<"/auth/profile">
| StaticRoute<"/auth/unverified-email">
| DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }> | DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }>
| StaticRoute<"/blocks"> | StaticRoute<"/blocks">
| StaticRoute<"/csv-export"> | StaticRoute<"/csv-export">
......
...@@ -25,7 +25,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { ...@@ -25,7 +25,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const isAccountActionAllowed = useIsAccountActionAllowed(); const isAccountActionAllowed = useIsAccountActionAllowed();
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (!isAccountActionAllowed({ pathname: '/account/watchlist' })) { if (!isAccountActionAllowed()) {
return; return;
} }
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen(); watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
......
...@@ -25,7 +25,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -25,7 +25,7 @@ const ApiKeysPage: React.FC = () => {
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
const { data, isPlaceholderData, isError, error } = useApiQuery('api_keys', { const { data, isPlaceholderData, isError } = useApiQuery('api_keys', {
queryOptions: { queryOptions: {
placeholderData: Array(3).fill(API_KEY), placeholderData: Array(3).fill(API_KEY),
}, },
...@@ -60,9 +60,6 @@ const ApiKeysPage: React.FC = () => { ...@@ -60,9 +60,6 @@ const ApiKeysPage: React.FC = () => {
const content = (() => { const content = (() => {
if (isError) { if (isError) {
if (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
}
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -22,7 +22,7 @@ const CustomAbiPage: React.FC = () => { ...@@ -22,7 +22,7 @@ const CustomAbiPage: React.FC = () => {
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
const { data, isPlaceholderData, isError, error } = useApiQuery('custom_abi', { const { data, isPlaceholderData, isError } = useApiQuery('custom_abi', {
queryOptions: { queryOptions: {
placeholderData: Array(3).fill(CUSTOM_ABI), placeholderData: Array(3).fill(CUSTOM_ABI),
}, },
...@@ -56,9 +56,6 @@ const CustomAbiPage: React.FC = () => { ...@@ -56,9 +56,6 @@ const CustomAbiPage: React.FC = () => {
const content = (() => { const content = (() => {
if (isError) { if (isError) {
if (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
}
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => { const MyProfile = () => {
const { data, isLoading, isError, error } = useFetchProfileInfo(); const { data, isLoading, isError } = useFetchProfileInfo();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const content = (() => { const content = (() => {
...@@ -18,9 +18,6 @@ const MyProfile = () => { ...@@ -18,9 +18,6 @@ const MyProfile = () => {
} }
if (isError) { if (isError) {
if (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
}
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
import { Box, Text, Button, Heading, Icon, chakra } from '@chakra-ui/react'; import { Box, Text, Button, Heading, Icon, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import icon403 from 'icons/error-pages/403.svg'; import iconEmailSent from 'icons/email-sent.svg';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
...@@ -9,17 +9,19 @@ import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; ...@@ -9,17 +9,19 @@ import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
interface Props { interface Props {
className?: string; email?: string; // TODO: obtain email from API
email?: string;
} }
const AppErrorUnverifiedEmail = ({ className, email }: Props) => { const UnverifiedEmail = ({ email }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const [ isLoading, setIsLoading ] = React.useState(false);
const toast = useToast(); const toast = useToast();
const handleButtonClick = React.useCallback(async() => { const handleButtonClick = React.useCallback(async() => {
const toastId = 'resend-email-error'; const toastId = 'resend-email-error';
setIsLoading(true);
try { try {
await apiFetch('email_resend'); await apiFetch('email_resend');
toast({ toast({
...@@ -44,6 +46,10 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => { ...@@ -44,6 +46,10 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
return; return;
} }
if (!payload.seconds_before_next_resend) {
return;
}
const timeUntilNextResend = dayjs().add(payload.seconds_before_next_resend, 'seconds').fromNow(); const timeUntilNextResend = dayjs().add(payload.seconds_before_next_resend, 'seconds').fromNow();
return `Email resend is available ${ timeUntilNextResend }.`; return `Email resend is available ${ timeUntilNextResend }.`;
})(); })();
...@@ -58,12 +64,14 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => { ...@@ -58,12 +64,14 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
isClosable: true, isClosable: true,
}); });
} }
setIsLoading(false);
}, [ apiFetch, toast ]); }, [ apiFetch, toast ]);
return ( return (
<Box className={ className }> <Box>
<Icon as={ icon403 } width="200px" height="auto"/> <Icon as={ iconEmailSent } width="180px" height="auto" mt="52px"/>
<Heading mt={ 8 } size="2xl" fontFamily="body">Email is not verified</Heading> <Heading mt={ 6 } size="2xl">Verify your email address</Heading>
<Text variant="secondary" mt={ 3 }> <Text variant="secondary" mt={ 3 }>
<span>Please confirm your email address to use the My Account feature. A confirmation email was sent to </span> <span>Please confirm your email address to use the My Account feature. A confirmation email was sent to </span>
<span>{ email || 'your email address' }</span> <span>{ email || 'your email address' }</span>
...@@ -73,6 +81,8 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => { ...@@ -73,6 +81,8 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
mt={ 8 } mt={ 8 }
size="lg" size="lg"
variant="outline" variant="outline"
isLoading={ isLoading }
loadingText="Resending..."
onClick={ handleButtonClick } onClick={ handleButtonClick }
> >
Resend verification email Resend verification email
...@@ -81,4 +91,4 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => { ...@@ -81,4 +91,4 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
); );
}; };
export default chakra(AppErrorUnverifiedEmail); export default chakra(UnverifiedEmail);
...@@ -7,7 +7,6 @@ import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, Veri ...@@ -7,7 +7,6 @@ import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, Veri
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account'; import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account';
...@@ -55,7 +54,6 @@ const VerifiedAddresses = () => { ...@@ -55,7 +54,6 @@ const VerifiedAddresses = () => {
}, },
}, },
}); });
const profileQuery = useFetchProfileInfo();
const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData; const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData;
...@@ -101,10 +99,6 @@ const VerifiedAddresses = () => { ...@@ -101,10 +99,6 @@ const VerifiedAddresses = () => {
}); });
}, [ queryClient ]); }, [ queryClient ]);
if (profileQuery.isError && profileQuery.error.status === 403) {
throw new Error('Unverified email error', { cause: profileQuery.error });
}
const addButton = ( const addButton = (
<Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block"> <Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block">
<Button size="lg" onClick={ modalProps.onOpen }> <Button size="lg" onClick={ modalProps.onOpen }>
......
...@@ -20,7 +20,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; ...@@ -20,7 +20,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { data, isPlaceholderData, isError, error } = useQuery<unknown, ResourceError, TWatchlist>( const { data, isPlaceholderData, isError } = useQuery<unknown, ResourceError, TWatchlist>(
[ resourceKey('watchlist') ], [ resourceKey('watchlist') ],
async() => { async() => {
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist'); const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
...@@ -96,9 +96,6 @@ const WatchList: React.FC = () => { ...@@ -96,9 +96,6 @@ const WatchList: React.FC = () => {
); );
if (isError) { if (isError) {
if (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
}
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -14,7 +14,7 @@ import AddressTagTable from './AddressTagTable/AddressTagTable'; ...@@ -14,7 +14,7 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => { const PrivateAddressTags = () => {
const { data: addressTagsData, isError, error, isPlaceholderData, refetch } = useApiQuery('private_tags_address', { const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS), placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
...@@ -52,9 +52,6 @@ const PrivateAddressTags = () => { ...@@ -52,9 +52,6 @@ const PrivateAddressTags = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
if (isError) { if (isError) {
if (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
}
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -14,7 +14,7 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem ...@@ -14,7 +14,7 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable'; import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => { const PrivateTransactionTags = () => {
const { data: transactionTagsData, isPlaceholderData, isError, error } = useApiQuery('private_tags_tx', { const { data: transactionTagsData, isPlaceholderData, isError } = useApiQuery('private_tags_tx', {
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_TX), placeholderData: Array(3).fill(PRIVATE_TAG_TX),
...@@ -55,9 +55,6 @@ const PrivateTransactionTags = () => { ...@@ -55,9 +55,6 @@ const PrivateTransactionTags = () => {
); );
if (isError) { if (isError) {
if (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
}
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
...@@ -21,7 +21,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -21,7 +21,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>(); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const { data, isPlaceholderData, isError, error } = useApiQuery('public_tags', { const { data, isPlaceholderData, isError } = useApiQuery('public_tags', {
queryOptions: { queryOptions: {
placeholderData: Array(3).fill(PUBLIC_TAG), placeholderData: Array(3).fill(PUBLIC_TAG),
}, },
...@@ -55,9 +55,6 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -55,9 +55,6 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
); );
if (isError) { if (isError) {
if (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
}
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
......
import { MenuItem, Icon, chakra, useDisclosure } from '@chakra-ui/react'; import { MenuItem, Icon, chakra, useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { Route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
...@@ -12,7 +11,7 @@ import PrivateTagModal from 'ui/privateTags/AddressModal/AddressModal'; ...@@ -12,7 +11,7 @@ import PrivateTagModal from 'ui/privateTags/AddressModal/AddressModal';
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
onBeforeClick: (route: Route) => boolean; onBeforeClick: () => boolean;
} }
const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
...@@ -23,7 +22,7 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { ...@@ -23,7 +22,7 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const addressData = queryClient.getQueryData<Address>(queryKey); const addressData = queryClient.getQueryData<Address>(queryKey);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (!onBeforeClick({ pathname: '/account/tag-address' })) { if (!onBeforeClick()) {
return; return;
} }
......
import { MenuItem, Icon, chakra } from '@chakra-ui/react'; import { MenuItem, Icon, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { Route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import iconPublicTags from 'icons/publictags.svg'; import iconPublicTags from 'icons/publictags.svg';
...@@ -8,14 +7,14 @@ import iconPublicTags from 'icons/publictags.svg'; ...@@ -8,14 +7,14 @@ import iconPublicTags from 'icons/publictags.svg';
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
onBeforeClick: (route: Route) => boolean; onBeforeClick: () => boolean;
} }
const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const router = useRouter(); const router = useRouter();
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (!onBeforeClick({ pathname: '/account/public-tags-request' })) { if (!onBeforeClick()) {
return; return;
} }
......
...@@ -9,7 +9,6 @@ import * as mixpanel from 'lib/mixpanel'; ...@@ -9,7 +9,6 @@ import * as mixpanel from 'lib/mixpanel';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus'; import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus';
import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash'; import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash';
import AppErrorUnverifiedEmail from 'ui/shared/AppError/AppErrorUnverifiedEmail';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Footer from 'ui/snippets/footer/Footer'; import Footer from 'ui/snippets/footer/Footer';
...@@ -46,19 +45,11 @@ const Page = ({ ...@@ -46,19 +45,11 @@ const Page = ({
const isInvalidTxHash = error?.message.includes('Invalid tx hash'); const isInvalidTxHash = error?.message.includes('Invalid tx hash');
const isBlockConsensus = messageInPayload?.includes('Block lost consensus'); const isBlockConsensus = messageInPayload?.includes('Block lost consensus');
const isUnverifiedEmail = statusCode === 403 && messageInPayload?.includes('Unverified email');
if (isInvalidTxHash) { if (isInvalidTxHash) {
return <PageContent isHomePage={ isHomePage }><AppErrorInvalidTxHash/></PageContent>; return <PageContent isHomePage={ isHomePage }><AppErrorInvalidTxHash/></PageContent>;
} }
if (isUnverifiedEmail) {
const email = resourceErrorPayload && 'email' in resourceErrorPayload && typeof resourceErrorPayload.email === 'string' ?
resourceErrorPayload.email :
undefined;
return <PageContent isHomePage={ isHomePage }><AppErrorUnverifiedEmail mt="50px" email={ email }/></PageContent>;
}
if (isBlockConsensus) { if (isBlockConsensus) {
const hash = resourceErrorPayload && 'hash' in resourceErrorPayload && typeof resourceErrorPayload.hash === 'string' ? const hash = resourceErrorPayload && 'hash' in resourceErrorPayload && typeof resourceErrorPayload.hash === 'string' ?
resourceErrorPayload.hash : resourceErrorPayload.hash :
......
...@@ -14,7 +14,7 @@ const ProfileMenuDesktop = () => { ...@@ -14,7 +14,7 @@ const ProfileMenuDesktop = () => {
React.useEffect(() => { React.useEffect(() => {
if (!isLoading) { if (!isLoading) {
setHasMenu(Boolean(data) || error?.status === 403); setHasMenu(Boolean(data));
} }
}, [ data, error?.status, isLoading ]); }, [ data, error?.status, isLoading ]);
......
...@@ -16,7 +16,7 @@ const ProfileMenuMobile = () => { ...@@ -16,7 +16,7 @@ const ProfileMenuMobile = () => {
React.useEffect(() => { React.useEffect(() => {
if (!isLoading) { if (!isLoading) {
setHasMenu(Boolean(data) || error?.status === 403); setHasMenu(Boolean(data));
} }
}, [ data, error?.status, isLoading ]); }, [ data, error?.status, isLoading ]);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment