mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
unverified user journey improvements (#1029)
* unverified email page * add cookies and redirects * remove unnecessary code * page cookie reset
This commit is contained in:
@@ -257,6 +257,7 @@ frontend:
|
||||
- "/apps"
|
||||
- "/static"
|
||||
- "/auth/profile"
|
||||
- "/auth/unverified-email"
|
||||
- "/txs"
|
||||
- "/tx"
|
||||
- "/blocks"
|
||||
|
||||
@@ -212,6 +212,7 @@ frontend:
|
||||
- "/apps"
|
||||
- "/static"
|
||||
- "/auth/profile"
|
||||
- "/auth/unverified-email"
|
||||
- "/txs"
|
||||
- "/tx"
|
||||
- "/blocks"
|
||||
|
||||
@@ -25,6 +25,7 @@ frontend:
|
||||
- "/apps"
|
||||
- "/static"
|
||||
- "/auth/profile"
|
||||
- "/auth/unverified-email"
|
||||
- "/txs"
|
||||
- "/tx"
|
||||
- "/blocks"
|
||||
|
||||
@@ -24,6 +24,7 @@ frontend:
|
||||
- "/apps"
|
||||
- "/static"
|
||||
- "/auth/profile"
|
||||
- "/auth/unverified-email"
|
||||
- "/txs"
|
||||
- "/tx"
|
||||
- "/blocks"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -5,6 +5,8 @@ import isBrowser from './isBrowser';
|
||||
export enum NAMES {
|
||||
NAV_BAR_COLLAPSED='nav_bar_collapsed',
|
||||
API_TOKEN='_explorer_key',
|
||||
INVALID_SESSION='invalid_session',
|
||||
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
|
||||
TXS_SORT='txs_sort',
|
||||
COLOR_MODE='chakra-ui-color-mode',
|
||||
INDEXING_ALERT='indexing_alert',
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { Route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import type { UserInfo } from 'types/api/account';
|
||||
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import { resourceKey } from 'lib/api/resources';
|
||||
import useLoginUrl from 'lib/hooks/useLoginUrl';
|
||||
|
||||
@@ -13,22 +10,15 @@ export default function useIsAccountActionAllowed() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const profileData = queryClient.getQueryData<UserInfo>([ resourceKey('user_info') ]);
|
||||
const profileState = queryClient.getQueryState<unknown, ResourceError<{ message: string }>>([ resourceKey('user_info') ]);
|
||||
const isAuth = Boolean(profileData);
|
||||
const loginUrl = useLoginUrl();
|
||||
const router = useRouter();
|
||||
|
||||
return React.useCallback((accountRoute: Route) => {
|
||||
if (profileState?.error?.status === 403) {
|
||||
router.push(accountRoute);
|
||||
return false;
|
||||
}
|
||||
|
||||
return React.useCallback(() => {
|
||||
if (!isAuth) {
|
||||
window.location.assign(loginUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [ isAuth, loginUrl, profileState?.error?.status, router ]);
|
||||
}, [ isAuth, loginUrl ]);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/api/csrf': 'Node API: CSRF token',
|
||||
'/api/healthz': 'Node API: Health check',
|
||||
'/auth/auth0': 'Auth',
|
||||
'/auth/unverified-email': 'Auth',
|
||||
};
|
||||
|
||||
export default function getPageType(pathname: Route['pathname']) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { account } from './account';
|
||||
+4
-13
@@ -1,10 +1,8 @@
|
||||
import type { NextRequest } 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 * as middlewares from 'lib/next/middlewares/index';
|
||||
|
||||
const cspPolicy = generateCspPolicy();
|
||||
|
||||
@@ -16,16 +14,9 @@ export function middleware(req: NextRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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');
|
||||
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 accountResponse = middlewares.account(req);
|
||||
if (accountResponse) {
|
||||
return accountResponse;
|
||||
}
|
||||
|
||||
const end = Date.now();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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';
|
||||
Vendored
+1
@@ -24,6 +24,7 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/apps">
|
||||
| StaticRoute<"/auth/auth0">
|
||||
| StaticRoute<"/auth/profile">
|
||||
| StaticRoute<"/auth/unverified-email">
|
||||
| DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }>
|
||||
| StaticRoute<"/blocks">
|
||||
| StaticRoute<"/csv-export">
|
||||
|
||||
@@ -25,7 +25,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
|
||||
const isAccountActionAllowed = useIsAccountActionAllowed();
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
if (!isAccountActionAllowed({ pathname: '/account/watchlist' })) {
|
||||
if (!isAccountActionAllowed()) {
|
||||
return;
|
||||
}
|
||||
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
|
||||
|
||||
@@ -25,7 +25,7 @@ const ApiKeysPage: React.FC = () => {
|
||||
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
|
||||
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
|
||||
|
||||
const { data, isPlaceholderData, isError, error } = useApiQuery('api_keys', {
|
||||
const { data, isPlaceholderData, isError } = useApiQuery('api_keys', {
|
||||
queryOptions: {
|
||||
placeholderData: Array(3).fill(API_KEY),
|
||||
},
|
||||
@@ -60,9 +60,6 @@ const ApiKeysPage: React.FC = () => {
|
||||
|
||||
const content = (() => {
|
||||
if (isError) {
|
||||
if (error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: error });
|
||||
}
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const CustomAbiPage: React.FC = () => {
|
||||
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
|
||||
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
|
||||
|
||||
const { data, isPlaceholderData, isError, error } = useApiQuery('custom_abi', {
|
||||
const { data, isPlaceholderData, isError } = useApiQuery('custom_abi', {
|
||||
queryOptions: {
|
||||
placeholderData: Array(3).fill(CUSTOM_ABI),
|
||||
},
|
||||
@@ -56,9 +56,6 @@ const CustomAbiPage: React.FC = () => {
|
||||
|
||||
const content = (() => {
|
||||
if (isError) {
|
||||
if (error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: error });
|
||||
}
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import UserAvatar from 'ui/shared/UserAvatar';
|
||||
|
||||
const MyProfile = () => {
|
||||
const { data, isLoading, isError, error } = useFetchProfileInfo();
|
||||
const { data, isLoading, isError } = useFetchProfileInfo();
|
||||
useRedirectForInvalidAuthToken();
|
||||
|
||||
const content = (() => {
|
||||
@@ -18,9 +18,6 @@ const MyProfile = () => {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: error });
|
||||
}
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Text, Button, Heading, Icon, chakra } from '@chakra-ui/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 dayjs from 'lib/date/dayjs';
|
||||
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
|
||||
@@ -9,17 +9,19 @@ import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
|
||||
import useToast from 'lib/hooks/useToast';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
email?: string;
|
||||
email?: string; // TODO: obtain email from API
|
||||
}
|
||||
|
||||
const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
|
||||
const UnverifiedEmail = ({ email }: Props) => {
|
||||
const apiFetch = useApiFetch();
|
||||
const [ isLoading, setIsLoading ] = React.useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
const handleButtonClick = React.useCallback(async() => {
|
||||
const toastId = 'resend-email-error';
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await apiFetch('email_resend');
|
||||
toast({
|
||||
@@ -44,6 +46,10 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload.seconds_before_next_resend) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeUntilNextResend = dayjs().add(payload.seconds_before_next_resend, 'seconds').fromNow();
|
||||
return `Email resend is available ${ timeUntilNextResend }.`;
|
||||
})();
|
||||
@@ -58,12 +64,14 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [ apiFetch, toast ]);
|
||||
|
||||
return (
|
||||
<Box className={ className }>
|
||||
<Icon as={ icon403 } width="200px" height="auto"/>
|
||||
<Heading mt={ 8 } size="2xl" fontFamily="body">Email is not verified</Heading>
|
||||
<Box>
|
||||
<Icon as={ iconEmailSent } width="180px" height="auto" mt="52px"/>
|
||||
<Heading mt={ 6 } size="2xl">Verify your email address</Heading>
|
||||
<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>{ email || 'your email address' }</span>
|
||||
@@ -73,6 +81,8 @@ const AppErrorUnverifiedEmail = ({ className, email }: Props) => {
|
||||
mt={ 8 }
|
||||
size="lg"
|
||||
variant="outline"
|
||||
isLoading={ isLoading }
|
||||
loadingText="Resending..."
|
||||
onClick={ handleButtonClick }
|
||||
>
|
||||
Resend verification email
|
||||
@@ -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
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
|
||||
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account';
|
||||
@@ -55,7 +54,6 @@ const VerifiedAddresses = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const profileQuery = useFetchProfileInfo();
|
||||
|
||||
const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData;
|
||||
|
||||
@@ -101,10 +99,6 @@ const VerifiedAddresses = () => {
|
||||
});
|
||||
}, [ queryClient ]);
|
||||
|
||||
if (profileQuery.isError && profileQuery.error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: profileQuery.error });
|
||||
}
|
||||
|
||||
const addButton = (
|
||||
<Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block">
|
||||
<Button size="lg" onClick={ modalProps.onOpen }>
|
||||
|
||||
@@ -20,7 +20,7 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
|
||||
|
||||
const WatchList: React.FC = () => {
|
||||
const apiFetch = useApiFetch();
|
||||
const { data, isPlaceholderData, isError, error } = useQuery<unknown, ResourceError, TWatchlist>(
|
||||
const { data, isPlaceholderData, isError } = useQuery<unknown, ResourceError, TWatchlist>(
|
||||
[ resourceKey('watchlist') ],
|
||||
async() => {
|
||||
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
|
||||
@@ -96,9 +96,6 @@ const WatchList: React.FC = () => {
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
if (error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: error });
|
||||
}
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
|
||||
import DeletePrivateTagModal from './DeletePrivateTagModal';
|
||||
|
||||
const PrivateAddressTags = () => {
|
||||
const { data: addressTagsData, isError, error, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
|
||||
const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
|
||||
@@ -52,9 +52,6 @@ const PrivateAddressTags = () => {
|
||||
}, [ deleteModalProps ]);
|
||||
|
||||
if (isError) {
|
||||
if (error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: error });
|
||||
}
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
|
||||
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
|
||||
|
||||
const PrivateTransactionTags = () => {
|
||||
const { data: transactionTagsData, isPlaceholderData, isError, error } = useApiQuery('private_tags_tx', {
|
||||
const { data: transactionTagsData, isPlaceholderData, isError } = useApiQuery('private_tags_tx', {
|
||||
queryOptions: {
|
||||
refetchOnMount: false,
|
||||
placeholderData: Array(3).fill(PRIVATE_TAG_TX),
|
||||
@@ -55,9 +55,6 @@ const PrivateTransactionTags = () => {
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
if (error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: error });
|
||||
}
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
|
||||
const deleteModalProps = useDisclosure();
|
||||
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
|
||||
|
||||
const { data, isPlaceholderData, isError, error } = useApiQuery('public_tags', {
|
||||
const { data, isPlaceholderData, isError } = useApiQuery('public_tags', {
|
||||
queryOptions: {
|
||||
placeholderData: Array(3).fill(PUBLIC_TAG),
|
||||
},
|
||||
@@ -55,9 +55,6 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
if (error.status === 403) {
|
||||
throw new Error('Unverified email error', { cause: error });
|
||||
}
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MenuItem, Icon, chakra, useDisclosure } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { Route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import type { Address } from 'types/api/address';
|
||||
@@ -12,7 +11,7 @@ import PrivateTagModal from 'ui/privateTags/AddressModal/AddressModal';
|
||||
interface Props {
|
||||
className?: string;
|
||||
hash: string;
|
||||
onBeforeClick: (route: Route) => boolean;
|
||||
onBeforeClick: () => boolean;
|
||||
}
|
||||
|
||||
const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
|
||||
@@ -23,7 +22,7 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
|
||||
const addressData = queryClient.getQueryData<Address>(queryKey);
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
if (!onBeforeClick({ pathname: '/account/tag-address' })) {
|
||||
if (!onBeforeClick()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MenuItem, Icon, chakra } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { Route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import iconPublicTags from 'icons/publictags.svg';
|
||||
@@ -8,14 +7,14 @@ import iconPublicTags from 'icons/publictags.svg';
|
||||
interface Props {
|
||||
className?: string;
|
||||
hash: string;
|
||||
onBeforeClick: (route: Route) => boolean;
|
||||
onBeforeClick: () => boolean;
|
||||
}
|
||||
|
||||
const PublicTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
if (!onBeforeClick({ pathname: '/account/public-tags-request' })) {
|
||||
if (!onBeforeClick()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as mixpanel from 'lib/mixpanel';
|
||||
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 PageContent from 'ui/shared/Page/PageContent';
|
||||
import Footer from 'ui/snippets/footer/Footer';
|
||||
@@ -46,19 +45,11 @@ const Page = ({
|
||||
|
||||
const isInvalidTxHash = error?.message.includes('Invalid tx hash');
|
||||
const isBlockConsensus = messageInPayload?.includes('Block lost consensus');
|
||||
const isUnverifiedEmail = statusCode === 403 && messageInPayload?.includes('Unverified email');
|
||||
|
||||
if (isInvalidTxHash) {
|
||||
return <PageContent isHomePage={ isHomePage }><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) {
|
||||
const hash = resourceErrorPayload && 'hash' in resourceErrorPayload && typeof resourceErrorPayload.hash === 'string' ?
|
||||
resourceErrorPayload.hash :
|
||||
|
||||
@@ -14,7 +14,7 @@ const ProfileMenuDesktop = () => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setHasMenu(Boolean(data) || error?.status === 403);
|
||||
setHasMenu(Boolean(data));
|
||||
}
|
||||
}, [ data, error?.status, isLoading ]);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const ProfileMenuMobile = () => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setHasMenu(Boolean(data) || error?.status === 403);
|
||||
setHasMenu(Boolean(data));
|
||||
}
|
||||
}, [ data, error?.status, isLoading ]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user