Commit 0cfe93cf authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #155 from blockscout/auth

user authentication
parents 671d8ad5 22c2b8fd
API_AUTHORIZATION_TOKEN=xxx
SENTRY_DSN=xxx SENTRY_DSN=xxx
NEXT_PUBLIC_SENTRY_DSN=xxx NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_ORG=block-scout SENTRY_ORG=block-scout
...@@ -11,4 +10,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout ...@@ -11,4 +10,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets"},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other"}] NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other"}]
\ No newline at end of file \ No newline at end of file
import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch'; import nodeFetch from 'node-fetch';
import * as cookies from 'lib/cookies';
// first arg can be only a string // first arg can be only a string
// FIXME migrate to RequestInfo later if needed // FIXME migrate to RequestInfo later if needed
export default function fetch(path: string, init?: RequestInit): Promise<Response> { export default function fetchFactory(_req: NextApiRequest) {
const headers = { return function fetch(path: string, init?: RequestInit): Promise<Response> {
accept: 'application/json', const headers = {
authorization: `Bearer ${ process.env.API_AUTHORIZATION_TOKEN }`, accept: 'application/json',
'content-type': 'application/json', 'content-type': 'application/json',
}; cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
const url = `https://blockscout.com${ path }`; };
const url = `https://blockscout.com${ path }`;
return nodeFetch(url, { return nodeFetch(url, {
headers, headers,
...init, ...init,
}); });
};
} }
import { withSentry } from '@sentry/nextjs'; import { withSentry } from '@sentry/nextjs';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import fetch from 'lib/api/fetch'; import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork'; import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE'; type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
...@@ -16,7 +16,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string, ...@@ -16,7 +16,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD'; const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = getUrlWithNetwork(_req, `/api${ getUrl(_req) }`); const url = getUrlWithNetwork(_req, `api${ getUrl(_req) }`);
const fetch = fetchFactory(_req);
const response = await fetch(url, { const response = await fetch(url, {
method: _req.method, method: _req.method,
body: isBodyDisallowed ? undefined : _req.body, body: isBodyDisallowed ? undefined : _req.body,
...@@ -29,12 +30,13 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string, ...@@ -29,12 +30,13 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
} }
let responseError; let responseError;
const defaultError = { statusText: response.statusText, status: response.status };
try { try {
const error = await response.json() as { errors: unknown }; const error = await response.json() as { errors: unknown };
responseError = error?.errors || {}; responseError = error?.errors || defaultError;
} catch (error) { } catch (error) {
responseError = { statusText: response.statusText, status: response.status }; responseError = defaultError;
} }
res.status(500).json(responseError); res.status(500).json(responseError);
......
import * as Sentry from '@sentry/nextjs';
export interface ErrorType<T> {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
export default function clientFetch<Success, Error>(path: string, init?: RequestInit): Promise<Success | ErrorType<Error>> {
return fetch(path, init).then(response => {
if (!response.ok) {
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
status: response.status,
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
} else {
return response.json() as Promise<Success>;
}
});
}
...@@ -7,6 +7,7 @@ export enum NAMES { ...@@ -7,6 +7,7 @@ export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
NETWORK_TYPE='network_type', NETWORK_TYPE='network_type',
NETWORK_SUB_TYPE='network_sub_type', NETWORK_SUB_TYPE='network_sub_type',
API_TOKEN='_explorer_key',
} }
export function get(name?: string | undefined | null) { export function get(name?: string | undefined | null) {
......
import * as Sentry from '@sentry/nextjs';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { CsrfData } from 'types/client/account';
export interface ErrorType<T> {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
interface Params {
method?: RequestInit['method'];
body?: Record<string, unknown>;
}
export default function useFetch() {
const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ErrorType<Error>> => {
const reqParams = {
...params,
body: params?.method && ![ 'GET', 'HEAD' ].includes(params.method) ?
JSON.stringify({ ...params?.body, _csrf_token: token }) :
undefined,
};
return fetch(path, reqParams).then(response => {
if (!response.ok) {
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
status: response.status,
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
} else {
return response.json() as Promise<Success>;
}
});
}, [ token ]);
}
...@@ -2,10 +2,21 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -2,10 +2,21 @@ import { useQuery } from '@tanstack/react-query';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
interface Error {
error?: {
status?: number;
statusText?: string;
};
}
export default function useFetchProfileInfo() { export default function useFetchProfileInfo() {
return useQuery<unknown, unknown, UserInfo>([ 'profile' ], async() => { const fetch = useFetch();
return useQuery<unknown, Error, UserInfo>([ 'profile' ], async() => {
return fetch('/api/account/profile'); return fetch('/api/account/profile');
}, { refetchOnMount: false }); }, {
refetchOnMount: false,
});
} }
...@@ -6,7 +6,7 @@ export default function useNetwork() { ...@@ -6,7 +6,7 @@ export default function useNetwork() {
const router = useRouter(); const router = useRouter();
const selectedNetwork = findNetwork({ const selectedNetwork = findNetwork({
network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '', network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '',
network_sub_type: typeof router.query.network_type === 'string' ? router.query.network_type : undefined, network_sub_type: typeof router.query.network_sub_type === 'string' ? router.query.network_sub_type : undefined,
}); });
return selectedNetwork; return selectedNetwork;
} }
...@@ -101,6 +101,12 @@ export const ROUTES = { ...@@ -101,6 +101,12 @@ export const ROUTES = {
other: { other: {
pattern: `${ BASE_PATH }/search-results`, pattern: `${ BASE_PATH }/search-results`,
}, },
// AUTH
auth: {
// no slash required, it is correct
pattern: `${ BASE_PATH }auth/auth0`,
},
}; };
// !!! for development purpose only !!! // !!! for development purpose only !!!
......
const { NextResponse } = require('next/server');
const { NAMES } = require('lib/cookies');
const { link } = require('lib/link/link');
const findNetwork = require('lib/networks/findNetwork').default;
export function middleware(req) {
const [ , networkType, networkSubtype ] = req.nextUrl.pathname.split('/');
const networkParams = {
network_type: networkType,
network_sub_type: networkSubtype,
};
const selectedNetwork = findNetwork(networkParams);
if (selectedNetwork) {
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if (!apiToken) {
const authUrl = link('auth', networkParams);
return NextResponse.redirect(authUrl);
}
}
}
export const config = {
matcher: '/:network_type/:network_sub_type/account/:path*',
};
...@@ -22,8 +22,8 @@ const moduleExports = { ...@@ -22,8 +22,8 @@ const moduleExports = {
return [ return [
{ {
source: '/', source: '/',
destination: '/xdai/testnet', destination: '/poa/core',
permanent: true, permanent: false,
}, },
]; ];
}, },
......
import { Center, VStack, Box } from '@chakra-ui/react'; import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import type { NextPage, GetStaticPaths } from 'next'; import type { NextPage, GetStaticPaths } from 'next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import getAvailablePaths from 'lib/networks/getAvailablePaths'; import getAvailablePaths from 'lib/networks/getAvailablePaths';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
const Home: NextPage = () => { const Home: NextPage = () => {
const router = useRouter(); const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
}, []);
const handleSetTokenClick = React.useCallback(() => {
cookies.set(cookies.NAMES.API_TOKEN, token);
setToken('');
toast({
position: 'top-right',
title: 'Success 🥳',
description: 'Successfully set cookie',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
setFormVisibility(false);
},
});
}, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString();
return ( return (
<Page> <Page>
<Center h="100%" fontSize={{ base: 'sm', lg: 'xl' }}> <VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<VStack gap={ 4 }> <PageHeader text={
<Box> `Home Page for ${ selectedNetwork?.name } network`
<p>home page for { router.query.network_type } { router.query.network_sub_type } network</p> }/>
</Box> { /* will be deleted when we move to new CI */ }
</VStack> { isFormVisible && (
</Center> <>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
</Alert>
<Textarea value={ token } onChange={ handleTokenChange } placeholder="API token"/>
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
</VStack>
</Page> </Page>
); );
}; };
......
...@@ -4,6 +4,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; ...@@ -4,6 +4,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import React, { useState } from 'react'; import React, { useState } from 'react';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme'; import theme from 'theme';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
...@@ -11,6 +12,16 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -11,6 +12,16 @@ function MyApp({ Component, pageProps }: AppProps) {
defaultOptions: { defaultOptions: {
queries: { queries: {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: (failureCount, _error) => {
const error = _error as ErrorType<{ status: number }>;
const status = error?.error?.status;
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
}
return failureCount < 2;
},
}, },
}, },
})); }));
......
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
const url = getUrlWithNetwork(_req, `api/account/v1/get_csrf`);
const fetch = fetchFactory(_req);
const response = await fetch(url);
if (response.status === 200) {
const token = response.headers.get('x-bs-account-csrf');
res.status(200).json({ token });
return;
}
res.status(500).json({ statusText: response.statusText, status: response.status });
}
...@@ -4,10 +4,11 @@ import type { WatchlistAddresses } from 'types/api/account'; ...@@ -4,10 +4,11 @@ import type { WatchlistAddresses } from 'types/api/account';
import type { Tokenlist } from 'types/api/tokenlist'; import type { Tokenlist } from 'types/api/tokenlist';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import fetch from 'lib/api/fetch'; import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork'; import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => { const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => {
const fetch = fetchFactory(_req);
const url = getUrlWithNetwork(_req, 'api/account/v1/user/watchlist'); const url = getUrlWithNetwork(_req, 'api/account/v1/user/watchlist');
const watchlistResponse = await fetch(url, { method: 'GET' }); const watchlistResponse = await fetch(url, { method: 'GET' });
......
...@@ -3,3 +3,7 @@ import type { WatchlistAddress } from '../api/account'; ...@@ -3,3 +3,7 @@ import type { WatchlistAddress } from '../api/account';
export type TWatchlistItem = WatchlistAddress & {tokens_count: number}; export type TWatchlistItem = WatchlistAddress & {tokens_count: number};
export type TWatchlist = Array<TWatchlistItem>; export type TWatchlist = Array<TWatchlistItem>;
export interface CsrfData {
token: string;
}
...@@ -13,10 +13,10 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -13,10 +13,10 @@ import { useForm, Controller } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account'; import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
type Props = { type Props = {
data?: ApiKey; data?: ApiKey;
...@@ -39,11 +39,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -39,11 +39,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
name: data?.name || '', name: data?.name || '',
}, },
}); });
const fetch = useFetch();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
const updateApiKey = (data: Inputs) => { const updateApiKey = (data: Inputs) => {
const body = JSON.stringify({ name: data.name }); const body = { name: data.name };
if (!data.token) { if (!data.token) {
return fetch('/api/account/api-keys', { method: 'POST', body }); return fetch('/api/account/api-keys', { method: 'POST', body });
......
...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey, ApiKeys } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -15,10 +15,11 @@ type Props = { ...@@ -15,10 +15,11 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account/api-keys/${ data.api_key }`, { method: 'DELETE' }); return fetch(`/api/account/api-keys/${ data.api_key }`, { method: 'DELETE' });
}, [ data ]); }, [ data.api_key, fetch ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => { queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => {
......
...@@ -39,7 +39,7 @@ const Burger = () => { ...@@ -39,7 +39,7 @@ const Burger = () => {
> >
<DrawerOverlay/> <DrawerOverlay/>
<DrawerContent maxWidth="260px"> <DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }> <DrawerBody p={ 6 } display="flex" flexDirection="column">
<Flex alignItems="center" justifyContent="space-between"> <Flex alignItems="center" justifyContent="space-between">
<NetworkLogo onClick={ handleNetworkLogoClick }/> <NetworkLogo onClick={ handleNetworkLogoClick }/>
<NetworkMenuButton <NetworkMenuButton
......
...@@ -20,9 +20,10 @@ const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT ...@@ -20,9 +20,10 @@ const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT
interface Props { interface Props {
isCollapsed?: boolean; isCollapsed?: boolean;
hasAccount?: boolean;
} }
const NavFooter = ({ isCollapsed }: Props) => { const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const width = (() => { const width = (() => {
...@@ -33,6 +34,14 @@ const NavFooter = ({ isCollapsed }: Props) => { ...@@ -33,6 +34,14 @@ const NavFooter = ({ isCollapsed }: Props) => {
return isCollapsed ? '20px' : '180px'; return isCollapsed ? '20px' : '180px';
})(); })();
const marginTop = (() => {
if (!hasAccount) {
return 'auto';
}
return isMobile ? 6 : 20;
})();
return ( return (
<VStack <VStack
as="footer" as="footer"
...@@ -41,7 +50,7 @@ const NavFooter = ({ isCollapsed }: Props) => { ...@@ -41,7 +50,7 @@ const NavFooter = ({ isCollapsed }: Props) => {
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
width={ width } width={ width }
paddingTop={ isMobile ? 6 : 8 } paddingTop={ isMobile ? 6 : 8 }
marginTop={ isMobile ? 6 : 20 } marginTop={ marginTop }
alignItems="flex-start" alignItems="flex-start"
alignSelf="center" alignSelf="center"
color="gray.500" color="gray.500"
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu'; import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu';
...@@ -13,16 +14,19 @@ import NavLink from './NavLink'; ...@@ -13,16 +14,19 @@ import NavLink from './NavLink';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isLargeScreen = useBreakpointValue({ base: false, xl: true }); const isLargeScreen = useBreakpointValue({ base: false, xl: true });
const cookieValue = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED); const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const [ isCollapsed, setCollapsedState ] = React.useState(cookieValue === 'true'); const [ isCollapsed, setCollapsedState ] = React.useState(navBarCollapsedCookie === 'true');
React.useEffect(() => { React.useEffect(() => {
if (!cookieValue) { if (!navBarCollapsedCookie) {
setCollapsedState(!isLargeScreen); setCollapsedState(!isLargeScreen);
} }
}, [ isLargeScreen, cookieValue ]); }, [ isLargeScreen, navBarCollapsedCookie ]);
const handleTogglerClick = React.useCallback(() => { const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag); setCollapsedState((flag) => !flag);
...@@ -66,12 +70,14 @@ const NavigationDesktop = () => { ...@@ -66,12 +70,14 @@ const NavigationDesktop = () => {
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
<Box as="nav" mt={ 12 }> { hasAccount && (
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <Box as="nav" mt={ 12 }>
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
</VStack> { accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</Box> </VStack>
<NavFooter isCollapsed={ isCollapsed }/> </Box>
) }
<NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/>
<ChevronLeftIcon <ChevronLeftIcon
width={ 6 } width={ 6 }
height={ 6 } height={ 6 }
......
import { Box, VStack } from '@chakra-ui/react'; import { Box, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/blocks/navigation/NavFooter'; import NavFooter from 'ui/blocks/navigation/NavFooter';
import NavLink from 'ui/blocks/navigation/NavLink'; import NavLink from 'ui/blocks/navigation/NavLink';
const NavigationMobile = () => { const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
return ( return (
<> <>
...@@ -15,12 +21,14 @@ const NavigationMobile = () => { ...@@ -15,12 +21,14 @@ const NavigationMobile = () => {
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
<Box as="nav" mt={ 6 }> { isAuth && (
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <Box as="nav" mt={ 6 }>
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
</VStack> { accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</Box> </VStack>
<NavFooter/> </Box>
) }
<NavFooter hasAccount={ hasAccount }/>
</> </>
); );
}; };
......
...@@ -14,10 +14,10 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -14,10 +14,10 @@ import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account'; import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
...@@ -46,9 +46,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -46,9 +46,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const customAbiKey = (data: Inputs & { id?: number }) => { const customAbiKey = (data: Inputs & { id?: number }) => {
const body = JSON.stringify({ name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi }); const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi };
if (!data.id) { if (!data.id) {
return fetch<CustomAbi, CustomAbiErrors>('/api/account/custom-abis', { method: 'POST', body }); return fetch<CustomAbi, CustomAbiErrors>('/api/account/custom-abis', { method: 'POST', body });
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey, ApiKeys } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal'; import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
...@@ -25,6 +25,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -25,6 +25,7 @@ const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure(); const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account'; import type { CustomAbi, CustomAbis } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal'; import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem'; import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
...@@ -22,6 +22,7 @@ const CustomAbiPage: React.FC = () => { ...@@ -22,6 +22,7 @@ const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure(); const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlist, TWatchlistItem } from 'types/client/account'; import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -24,6 +24,7 @@ const WatchList: React.FC = () => { ...@@ -24,6 +24,7 @@ const WatchList: React.FC = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>(); const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>(); const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>();
......
...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account'; import type { AddressTag, AddressTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
...@@ -31,6 +31,7 @@ type Inputs = { ...@@ -31,6 +31,7 @@ type Inputs = {
} }
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const fetch = useFetch();
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
mode: 'all', mode: 'all',
...@@ -45,10 +46,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -45,10 +46,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate } = useMutation((formData: Inputs) => { const { mutate } = useMutation((formData: Inputs) => {
const body = JSON.stringify({ const body = {
name: formData?.tag, name: formData?.tag,
address_hash: formData?.address, address_hash: formData?.address,
}); };
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { if (isEdit) {
......
...@@ -4,6 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account'; import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -18,18 +19,19 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type }) ...@@ -18,18 +19,19 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const id = data.id; const id = data.id;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' }); return fetch(`/api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' });
}, [ type, id ]); }, [ fetch, type, id ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
if (type === 'address') { if (type === 'address') {
queryClient.setQueryData([ type ], (prevData: AddressTags | undefined) => { queryClient.setQueryData([ 'address-tags' ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id); return prevData?.filter((item: AddressTag) => item.id !== id);
}); });
} else { } else {
queryClient.setQueryData([ type ], (prevData: TransactionTags | undefined) => { queryClient.setQueryData([ 'transaction-tags' ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id); return prevData?.filter((item: TransactionTag) => item.id !== id);
}); });
} }
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { AddressTags, AddressTag } from 'types/api/account'; import type { AddressTags, AddressTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -23,6 +23,7 @@ const PrivateAddressTags = () => { ...@@ -23,6 +23,7 @@ const PrivateAddressTags = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>(); const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>(); const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account'; import type { TransactionTags, TransactionTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -23,6 +23,7 @@ const PrivateTransactionTags = () => { ...@@ -23,6 +23,7 @@ const PrivateTransactionTags = () => {
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>(); const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>(); const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
......
...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account'; import type { TransactionTag, TransactionTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction'; import { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
import TransactionInput from 'ui/shared/TransactionInput'; import TransactionInput from 'ui/shared/TransactionInput';
...@@ -43,12 +43,13 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -43,12 +43,13 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const { mutate } = useMutation((formData: Inputs) => { const { mutate } = useMutation((formData: Inputs) => {
const body = JSON.stringify({ const body = {
name: formData?.tag, name: formData?.tag,
transaction_hash: formData?.transaction, transaction_hash: formData?.transaction,
}); };
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { if (isEdit) {
......
...@@ -5,7 +5,7 @@ import type { ChangeEvent } from 'react'; ...@@ -5,7 +5,7 @@ import type { ChangeEvent } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTags, PublicTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -22,12 +22,13 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -22,12 +22,13 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tags = data.tags.split(';'); const tags = data.tags.split(';');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
const deleteApiKey = useCallback(() => { const deleteApiKey = useCallback(() => {
const body = JSON.stringify({ remove_reason: reason }); const body = { remove_reason: reason };
return fetch(`/api/account/public-tags/${ data.id }`, { method: 'DELETE', body }); return fetch(`/api/account/public-tags/${ data.id }`, { method: 'DELETE', body });
}, [ data, reason ]); }, [ data.id, fetch, reason ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
onDeleteSuccess(); onDeleteSuccess();
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTags, PublicTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem'; import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
...@@ -24,6 +24,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -24,6 +24,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>(); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags')); const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags'));
......
...@@ -13,9 +13,9 @@ import { useForm, useFieldArray } from 'react-hook-form'; ...@@ -13,9 +13,9 @@ import { useForm, useFieldArray } from 'react-hook-form';
import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account'; import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { EMAIL_REGEXP } from 'lib/validations/email'; import { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert'; import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
...@@ -58,6 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100; ...@@ -58,6 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const inputSize = isMobile ? 'md' : 'lg'; const inputSize = isMobile ? 'md' : 'lg';
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
...@@ -87,7 +88,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -87,7 +88,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]); const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]);
const updatePublicTag = (formData: Inputs) => { const updatePublicTag = (formData: Inputs) => {
const payload: PublicTagNew = { const body: PublicTagNew = {
full_name: formData.fullName || '', full_name: formData.fullName || '',
email: formData.email || '', email: formData.email || '',
company: formData.companyName || '', company: formData.companyName || '',
...@@ -97,7 +98,6 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -97,7 +98,6 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
tags: formData.tags?.split(';').map((s) => s.trim()).join(';') || '', tags: formData.tags?.split(';').map((s) => s.trim()).join(';') || '',
additional_comment: formData.comment || '', additional_comment: formData.comment || '',
}; };
const body = JSON.stringify(payload);
if (!data?.id) { if (!data?.id) {
return fetch<PublicTag, PublicTagErrors>('/api/account/public-tags', { method: 'POST', body }); return fetch<PublicTag, PublicTagErrors>('/api/account/public-tags', { method: 'POST', body });
......
import { Box, HStack, VStack } from '@chakra-ui/react'; import { Box, HStack, VStack } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Header from 'ui/blocks/header/Header'; import Header from 'ui/blocks/header/Header';
import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop';
...@@ -14,9 +16,13 @@ interface Props { ...@@ -14,9 +16,13 @@ interface Props {
const Page = ({ children }: Props) => { const Page = ({ children }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const networkType = router.query.network_type; const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type; const networkSubType = router.query.network_sub_type;
useQuery<unknown, unknown, unknown>([ 'csrf' ], async() => await fetch('/api/account/csrf'));
React.useEffect(() => { React.useEffect(() => {
if (typeof networkType === 'string') { if (typeof networkType === 'string') {
cookies.set(cookies.NAMES.NETWORK_TYPE, networkType); cookies.set(cookies.NAMES.NETWORK_TYPE, networkType);
......
...@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { WatchlistErrors } from 'types/api/account'; import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
...@@ -83,9 +83,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -83,9 +83,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
function updateWatchlist(formData: Inputs) { function updateWatchlist(formData: Inputs) {
const requestParams = { const body = {
name: formData?.tag, name: formData?.tag,
address_hash: formData?.address, address_hash: formData?.address,
notification_settings: formData.notification_settings, notification_settings: formData.notification_settings,
...@@ -95,11 +96,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -95,11 +96,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}; };
if (data) { if (data) {
// edit address // edit address
return fetch<TWatchlistItem, WatchlistErrors>(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body: JSON.stringify(requestParams) }); return fetch<TWatchlistItem, WatchlistErrors>(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body });
} else { } else {
// add address // add address
return fetch<TWatchlistItem, WatchlistErrors>('/api/account/watchlist', { method: 'POST', body: JSON.stringify(requestParams) }); return fetch<TWatchlistItem, WatchlistErrors>('/api/account/watchlist', { method: 'POST', body });
} }
} }
......
...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { TWatchlistItem, TWatchlist } from 'types/client/account'; import type { TWatchlistItem, TWatchlist } from 'types/client/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
...@@ -17,10 +17,11 @@ type Props = { ...@@ -17,10 +17,11 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' }); return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' });
}, [ data ]); }, [ data?.id, fetch ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'watchlist' ], (prevData: TWatchlist | undefined) => { queryClient.setQueryData([ 'watchlist' ], (prevData: TWatchlist | undefined) => {
......
...@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
...@@ -34,6 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -34,6 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
const toast = useToast(); const toast = useToast();
const fetch = useFetch();
const showToast = useCallback(() => { const showToast = useCallback(() => {
toast({ toast({
...@@ -49,9 +50,9 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -49,9 +50,9 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const { mutate } = useMutation(() => { const { mutate } = useMutation(() => {
setSwitchDisabled(true); setSwitchDisabled(true);
const data = { ...item, notification_methods: { email: !notificationEnabled } }; const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState); setNotificationEnabled(prevState => !prevState);
return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) }); return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body });
}, { }, {
onError: () => { onError: () => {
showToast(); showToast();
......
...@@ -1380,10 +1380,10 @@ ...@@ -1380,10 +1380,10 @@
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3" resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3"
integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw== integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw==
"@next/eslint-plugin-next@12.1.6": "@next/eslint-plugin-next@12.3.0":
version "12.1.6" version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.0.tgz#302c1f03618d5001ce92ea6826c329268759128e"
integrity sha512-yNUtJ90NEiYFT6TJnNyofKMPYqirKDwpahcbxBgSIuABwYOdkGwzos1ZkYD51Qf0diYwpQZBeVqElTk7Q2WNqw== integrity sha512-jVdq1qYTNDjUtulnE8/hkPv0pHILV4jMg5La99iaY/FFm20WxVnsAZtbNnMvlPbf8dc010oO304SX9yXbg5PAw==
dependencies: dependencies:
glob "7.1.7" glob "7.1.7"
...@@ -2709,12 +2709,12 @@ escape-string-regexp@^4.0.0: ...@@ -2709,12 +2709,12 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-next@12.1.6: eslint-config-next@^12.3.0:
version "12.1.6" version "12.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.1.6.tgz#55097028982dce49159d8753000be3916ac55254" resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.3.0.tgz#d887ab2d143fe1a2b308e9321e932a613e610800"
integrity sha512-qoiS3g/EPzfCTkGkaPBSX9W0NGE/B1wNO3oWrd76QszVGrdpLggNqcO8+LR6MB0CNqtp9Q8NoeVrxNVbzM9hqA== integrity sha512-guHSkNyKnTBB8HU35COgAMeMV0E026BiYRYvyEVVaTOeFcnU3i1EI8/Da0Rl7H3Sgua5FEvoA0vYd2s8kdIUXg==
dependencies: dependencies:
"@next/eslint-plugin-next" "12.1.6" "@next/eslint-plugin-next" "12.3.0"
"@rushstack/eslint-patch" "^1.1.3" "@rushstack/eslint-patch" "^1.1.3"
"@typescript-eslint/parser" "^5.21.0" "@typescript-eslint/parser" "^5.21.0"
eslint-import-resolver-node "^0.3.6" eslint-import-resolver-node "^0.3.6"
......
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