Commit 2b4ea97d authored by tom's avatar tom

manage success and failed submit

parent bb9d2256
...@@ -112,14 +112,14 @@ export const RESOURCES = { ...@@ -112,14 +112,14 @@ export const RESOURCES = {
basePath: appConfig.contractInfoApi.basePath, basePath: appConfig.contractInfoApi.basePath,
}, },
token_info_application_config: { token_info_applications_config: {
path: '/api/v1/chains/:chainId/token-info-submissions/selectors', path: '/api/v1/chains/:chainId/token-info-submissions/selectors',
pathParams: [ 'chainId' as const ], pathParams: [ 'chainId' as const ],
endpoint: appConfig.adminServiceApi.endpoint, endpoint: appConfig.adminServiceApi.endpoint,
basePath: appConfig.adminServiceApi.basePath, basePath: appConfig.adminServiceApi.basePath,
}, },
token_info_application: { token_info_applications: {
path: '/api/v1/chains/:chainId/token-info-submissions', path: '/api/v1/chains/:chainId/token-info-submissions',
pathParams: [ 'chainId' as const ], pathParams: [ 'chainId' as const ],
endpoint: appConfig.adminServiceApi.endpoint, endpoint: appConfig.adminServiceApi.endpoint,
...@@ -525,8 +525,8 @@ Q extends 'private_tags_tx' ? TransactionTags : ...@@ -525,8 +525,8 @@ Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'api_keys' ? ApiKeys : Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> : Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'verified_addresses' ? VerifiedAddressResponse : Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_application_config' ? TokenInfoApplicationConfig : Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
Q extends 'token_info_application' ? TokenInfoApplications : Q extends 'token_info_applications' ? TokenInfoApplications :
Q extends 'homepage_stats' ? HomeStats : Q extends 'homepage_stats' ? HomeStats :
Q extends 'homepage_chart_txs' ? ChartTransactionResponse : Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse : Q extends 'homepage_chart_market' ? ChartMarketResponse :
......
import { Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Link } from '@chakra-ui/react'; import { Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess } from './types'; import type { AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess } from './types';
import type { VerifiedAddress, VerifiedAddressResponse } from 'types/api/account'; import type { VerifiedAddress } from 'types/api/account';
import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress'; import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress';
...@@ -17,33 +14,22 @@ import AddressVerificationStepSuccess from './steps/AddressVerificationStepSucce ...@@ -17,33 +14,22 @@ import AddressVerificationStepSuccess from './steps/AddressVerificationStepSucce
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (address: VerifiedAddress) => void;
} }
const AddressVerificationModal = ({ isOpen, onClose }: Props) => { const AddressVerificationModal = ({ isOpen, onClose, onSubmit }: Props) => {
const [ stepIndex, setStepIndex ] = React.useState(0); const [ stepIndex, setStepIndex ] = React.useState(0);
const [ data, setData ] = React.useState<AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess>({ address: '', signingMessage: '' }); const [ data, setData ] = React.useState<AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess>({ address: '', signingMessage: '' });
const queryClient = useQueryClient();
const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => { const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => {
setData(firstStepResult); setData(firstStepResult);
setStepIndex((prev) => prev + 1); setStepIndex((prev) => prev + 1);
}, []); }, []);
const handleGoToThirdStep = React.useCallback((newItem: VerifiedAddress) => { const handleGoToThirdStep = React.useCallback((address: VerifiedAddress) => {
queryClient.setQueryData( onSubmit(address);
getResourceKey('verified_addresses', { pathParams: { chainId: appConfig.network.id } }),
(prevData: VerifiedAddressResponse | undefined) => {
if (!prevData) {
return { verifiedAddresses: [ newItem ] };
}
return {
verifiedAddresses: [ newItem, ...prevData.verifiedAddresses ],
};
});
setStepIndex((prev) => prev + 1); setStepIndex((prev) => prev + 1);
}, [ queryClient ]); }, [ onSubmit ]);
const handleGoToPrevStep = React.useCallback(() => { const handleGoToPrevStep = React.useCallback(() => {
setStepIndex((prev) => prev - 1); setStepIndex((prev) => prev - 1);
......
import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Box } from '@chakra-ui/react'; import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Box } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, VerifiedAddressResponse } from 'types/api/account';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal'; import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
...@@ -23,9 +26,10 @@ const VerifiedAddresses = () => { ...@@ -23,9 +26,10 @@ const VerifiedAddresses = () => {
const addressesQuery = useApiQuery('verified_addresses', { const addressesQuery = useApiQuery('verified_addresses', {
pathParams: { chainId: appConfig.network.id }, pathParams: { chainId: appConfig.network.id },
}); });
const applicationsQuery = useApiQuery('token_info_application', { const applicationsQuery = useApiQuery('token_info_applications', {
pathParams: { chainId: appConfig.network.id }, pathParams: { chainId: appConfig.network.id },
}); });
const queryClient = useQueryClient();
const handleGoBack = React.useCallback(() => { const handleGoBack = React.useCallback(() => {
setSelectedAddress(undefined); setSelectedAddress(undefined);
...@@ -38,6 +42,33 @@ const VerifiedAddresses = () => { ...@@ -38,6 +42,33 @@ const VerifiedAddresses = () => {
setSelectedAddress(address); setSelectedAddress(address);
}, []); }, []);
const handleAddressSubmit = React.useCallback((newItem: VerifiedAddress) => {
queryClient.setQueryData(
getResourceKey('verified_addresses', { pathParams: { chainId: appConfig.network.id } }),
(prevData: VerifiedAddressResponse | undefined) => {
if (!prevData) {
return { verifiedAddresses: [ newItem ] };
}
return {
verifiedAddresses: [ newItem, ...prevData.verifiedAddresses ],
};
});
}, [ queryClient ]);
const handleApplicationSubmit = React.useCallback((newItem: TokenInfoApplication) => {
queryClient.setQueryData(
getResourceKey('token_info_applications', { pathParams: { chainId: appConfig.network.id } }),
(prevData: TokenInfoApplications | undefined) => {
if (!prevData) {
return { submissions: [ newItem ] };
}
const submissions = prevData.submissions.map((item) => item.id === newItem.id ? newItem : item);
return { submissions };
});
}, [ queryClient ]);
const addButton = ( const addButton = (
<Button size="lg" onClick={ modalProps.onOpen } marginTop={ 8 }> <Button size="lg" onClick={ modalProps.onOpen } marginTop={ 8 }>
Add address Add address
...@@ -75,6 +106,7 @@ const VerifiedAddresses = () => { ...@@ -75,6 +106,7 @@ const VerifiedAddresses = () => {
<TokenInfoForm <TokenInfoForm
address={ selectedAddress } address={ selectedAddress }
application={ applicationsQuery.data?.submissions.find(({ tokenAddress }) => tokenAddress === selectedAddress) } application={ applicationsQuery.data?.submissions.find(({ tokenAddress }) => tokenAddress === selectedAddress) }
onSubmit={ handleApplicationSubmit }
/> />
</Page> </Page>
); );
...@@ -127,7 +159,7 @@ const VerifiedAddresses = () => { ...@@ -127,7 +159,7 @@ const VerifiedAddresses = () => {
emptyText="" emptyText=""
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: skeleton }}
/> />
<AddressVerificationModal isOpen={ modalProps.isOpen } onClose={ modalProps.onClose }/> <AddressVerificationModal isOpen={ modalProps.isOpen } onClose={ modalProps.onClose } onSubmit={ handleAddressSubmit }/>
</Page> </Page>
); );
}; };
......
import { Button, Grid, GridItem } from '@chakra-ui/react'; import { Alert, Button, Grid, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
...@@ -9,6 +9,7 @@ import type { TokenInfoApplication } from 'types/api/account'; ...@@ -9,6 +9,7 @@ import type { TokenInfoApplication } from 'types/api/account';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useToast from 'lib/hooks/useToast';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -31,13 +32,15 @@ import { getFormDefaultValues, prepareRequestBody } from './utils'; ...@@ -31,13 +32,15 @@ import { getFormDefaultValues, prepareRequestBody } from './utils';
interface Props { interface Props {
address: string; address: string;
application?: TokenInfoApplication; application?: TokenInfoApplication;
onSubmit: (application: TokenInfoApplication) => void;
} }
const TokenInfoForm = ({ address, application }: Props) => { const TokenInfoForm = ({ address, application, onSubmit }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast();
const configQuery = useApiQuery('token_info_application_config', { const configQuery = useApiQuery('token_info_applications_config', {
pathParams: { chainId: appConfig.network.id }, pathParams: { chainId: appConfig.network.id },
}); });
...@@ -50,17 +53,30 @@ const TokenInfoForm = ({ address, application }: Props) => { ...@@ -50,17 +53,30 @@ const TokenInfoForm = ({ address, application }: Props) => {
const onFormSubmit: SubmitHandler<Fields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<Fields> = React.useCallback(async(data) => {
try { try {
const submission = prepareRequestBody(data); const submission = prepareRequestBody(data);
await apiFetch('token_info_application', { const result = await apiFetch<'token_info_applications', TokenInfoApplication, { message: string }>('token_info_applications', {
pathParams: { chainId: appConfig.network.id }, pathParams: { chainId: appConfig.network.id },
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { submission }, body: { submission },
}, },
}); });
} catch (error) {}
}, [ apiFetch ]);
const onSubmit = handleSubmit(onFormSubmit); if ('id' in result) {
onSubmit(result);
} else {
throw result;
}
} catch (error) {
toast({
position: 'top-right',
title: 'Error',
description: (error as Error)?.message || 'Something went wrong. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}
}, [ apiFetch, onSubmit, toast ]);
if (configQuery.isError) { if (configQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
...@@ -73,8 +89,10 @@ const TokenInfoForm = ({ address, application }: Props) => { ...@@ -73,8 +89,10 @@ const TokenInfoForm = ({ address, application }: Props) => {
const fieldProps = { control, isReadOnly: application?.status === 'IN_PROCESS' }; const fieldProps = { control, isReadOnly: application?.status === 'IN_PROCESS' };
return ( return (
<form noValidate onSubmit={ onSubmit } autoComplete="off"> <form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off">
<div>Requests are sent to a moderator for review and approval. This process can take several days.</div> <div>Requests are sent to a moderator for review and approval. This process can take several days.</div>
{ application?.status === 'IN_PROCESS' &&
<Alert status="warning" mt={ 6 }>Request in progress. Once an admin approves your request you can edit token info.</Alert> }
<Grid mt={ 8 } gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 5 } rowGap={ 5 }> <Grid mt={ 8 } gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 5 } rowGap={ 5 }>
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
......
...@@ -19,9 +19,6 @@ interface Props { ...@@ -19,9 +19,6 @@ interface Props {
const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => { const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => {
const [ valueForPreview, setValueForPreview ] = React.useState<string>();
const imageLoadError = React.useRef(false);
const validatePreview = React.useCallback(() => { const validatePreview = React.useCallback(() => {
return imageLoadError.current ? 'Unable to load image' : true; return imageLoadError.current ? 'Unable to load image' : true;
}, [ ]); }, [ ]);
...@@ -35,6 +32,9 @@ const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => { ...@@ -35,6 +32,9 @@ const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => {
}, },
}); });
const [ valueForPreview, setValueForPreview ] = React.useState<string>(field.value);
const imageLoadError = React.useRef(false);
const handleImageLoadSuccess = React.useCallback(() => { const handleImageLoadSuccess = React.useCallback(() => {
imageLoadError.current = false; imageLoadError.current = false;
trigger('icon_url'); trigger('icon_url');
...@@ -46,7 +46,6 @@ const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => { ...@@ -46,7 +46,6 @@ const TokenInfoFieldIconUrl = ({ control, isReadOnly, trigger }: Props) => {
}, [ trigger ]); }, [ trigger ]);
const handleBlur = React.useCallback(() => { const handleBlur = React.useCallback(() => {
// make trigger()
field.onBlur(); field.onBlur();
const isValidUrl = validateUrl(field.value); const isValidUrl = validateUrl(field.value);
isValidUrl === true && setValueForPreview(field.value); isValidUrl === true && setValueForPreview(field.value);
......
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