Commit 7f1c505d authored by tom's avatar tom

Merge branch 'account/token-info-application' into token/verified-info

parents 3ac5cf01 48595715
...@@ -26,7 +26,8 @@ const baseUrl = [ ...@@ -26,7 +26,8 @@ const baseUrl = [
appPort && ':' + appPort, appPort && ':' + appPort,
].filter(Boolean).join(''); ].filter(Boolean).join('');
const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl; const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl;
const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST); // const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST);
const apiHost = 'eth-goerli.blockscout.com';
const apiSchema = getEnvValue(process.env.NEXT_PUBLIC_API_PROTOCOL) || 'https'; const apiSchema = getEnvValue(process.env.NEXT_PUBLIC_API_PROTOCOL) || 'https';
const apiPort = getEnvValue(process.env.NEXT_PUBLIC_API_PORT); const apiPort = getEnvValue(process.env.NEXT_PUBLIC_API_PORT);
const apiEndpoint = apiHost ? [ const apiEndpoint = apiHost ? [
......
...@@ -19,3 +19,5 @@ NEXT_PUBLIC_IS_TESTNET=true ...@@ -19,3 +19,5 @@ NEXT_PUBLIC_IS_TESTNET=true
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.aws-k8s.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs-test.aws-k8s.blockscout.com
import appConfig from 'configs/app/config'; // import appConfig from 'configs/app/config';
// FIXME // FIXME
// I was not able to figure out how to send CORS with credentials from localhost // I was not able to figure out how to send CORS with credentials from localhost
// unsuccessfully tried different ways, even custom local dev domain // unsuccessfully tried different ways, even custom local dev domain
// so for local development we have to use next.js api as proxy server // so for local development we have to use next.js api as proxy server
export default function isNeedProxy() { export default function isNeedProxy() {
return appConfig.host === 'localhost' && appConfig.host !== appConfig.api.host; return true;
// return appConfig.host === 'localhost' && appConfig.host !== appConfig.api.host;
} }
...@@ -224,7 +224,7 @@ export default function useNavItems(): ReturnType { ...@@ -224,7 +224,7 @@ export default function useNavItems(): ReturnType {
isNewUi: true, isNewUi: true,
}, },
appConfig.contractInfoApi.endpoint && appConfig.adminServiceApi.endpoint && { appConfig.contractInfoApi.endpoint && appConfig.adminServiceApi.endpoint && {
text: 'Verified addresses', text: 'Verified addrs',
nextRoute: { pathname: '/account/verified_addresses' as const }, nextRoute: { pathname: '/account/verified_addresses' as const },
icon: verifiedIcon, icon: verifiedIcon,
isActive: pathname === '/account/verified_addresses', isActive: pathname === '/account/verified_addresses',
......
import type { test } from '@playwright/experimental-ct-react'; import type { test } from '@playwright/experimental-ct-react';
import type { PlaywrightWorkerArgs } from '@playwright/test';
interface Env { interface Env {
name: string; name: string;
...@@ -6,18 +7,22 @@ interface Env { ...@@ -6,18 +7,22 @@ interface Env {
} }
// keep in mind that all passed variables here should be present in env config files (.env.pw or .env.poa) // keep in mind that all passed variables here should be present in env config files (.env.pw or .env.poa)
export default function createContextWithEnvs(envs: Array<Env>): Parameters<typeof test.extend>[0]['context'] { export default function contextWithEnvsFixture(envs: Array<Env>): Parameters<typeof test.extend>[0]['context'] {
return async({ browser }, use) => { return async({ browser }, use) => {
const context = await browser.newContext({ const context = await createContextWithEnvs(browser, envs);
storageState: {
origins: [
{ origin: 'http://localhost:3100', localStorage: envs },
],
cookies: [],
},
});
await use(context); await use(context);
await context.close(); await context.close();
}; };
} }
export function createContextWithEnvs(browser: PlaywrightWorkerArgs['browser'], envs: Array<Env>) {
return browser.newContext({
storageState: {
origins: [
{ origin: 'http://localhost:3100', localStorage: envs },
],
cookies: [],
},
});
}
...@@ -203,6 +203,7 @@ export interface TokenInfoApplication { ...@@ -203,6 +203,7 @@ export interface TokenInfoApplication {
telegram?: string; telegram?: string;
tokenAddress: string; tokenAddress: string;
twitter?: string; twitter?: string;
updatedAt: string;
} }
export interface TokenInfoApplications { export interface TokenInfoApplications {
......
...@@ -15,9 +15,10 @@ interface Props { ...@@ -15,9 +15,10 @@ interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (address: VerifiedAddress) => void; onSubmit: (address: VerifiedAddress) => void;
onAddTokenInfoClick: (address: string) => void;
} }
const AddressVerificationModal = ({ isOpen, onClose, onSubmit }: Props) => { const AddressVerificationModal = ({ isOpen, onClose, onSubmit, onAddTokenInfoClick }: 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: '' });
...@@ -38,12 +39,27 @@ const AddressVerificationModal = ({ isOpen, onClose, onSubmit }: Props) => { ...@@ -38,12 +39,27 @@ const AddressVerificationModal = ({ isOpen, onClose, onSubmit }: Props) => {
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
onClose(); onClose();
setStepIndex(0); setStepIndex(0);
setData({ address: '', signingMessage: '' });
}, [ onClose ]); }, [ onClose ]);
const handleAddTokenInfoClick = React.useCallback(() => {
onAddTokenInfoClick(data.address);
handleClose();
}, [ handleClose, data.address, onAddTokenInfoClick ]);
const steps = [ const steps = [
{ title: 'Verify new address ownership', content: <AddressVerificationStepAddress onContinue={ handleGoToSecondStep }/> }, {
{ title: 'Copy and sign message', content: <AddressVerificationStepSignature { ...data } onContinue={ handleGoToThirdStep }/> }, title: 'Verify new address ownership',
{ title: 'Congrats! Address is verified.', content: <AddressVerificationStepSuccess onShowListClick={ handleClose } onAddTokenClick={ handleClose }/> }, content: <AddressVerificationStepAddress onContinue={ handleGoToSecondStep }/>,
},
{
title: 'Copy and sign message',
content: <AddressVerificationStepSignature { ...data } onContinue={ handleGoToThirdStep }/>,
},
{
title: 'Congrats! Address is verified.',
content: <AddressVerificationStepSuccess onShowListClick={ handleClose } onAddTokenInfoClick={ handleAddTokenInfoClick }/>,
},
]; ];
const step = steps[stepIndex]; const step = steps[stepIndex];
......
...@@ -21,7 +21,7 @@ const AddressVerificationFieldAddress = ({ formState, control }: Props) => { ...@@ -21,7 +21,7 @@ const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined; const error = 'address' in formState.errors ? formState.errors.address : undefined;
return ( return (
<FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor }> <FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor } mt={ 8 }>
<Input <Input
{ ...field } { ...field }
required required
......
...@@ -28,7 +28,7 @@ const AddressVerificationFieldMessage = ({ formState, control }: Props) => { ...@@ -28,7 +28,7 @@ const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
isDisabled isDisabled
autoComplete="off" autoComplete="off"
maxH="105px" maxH={{ base: '140px', lg: '80px' }}
/> />
<InputPlaceholder text="Message to sign" error={ error } isInModal/> <InputPlaceholder text="Message to sign" error={ error } isInModal/>
</FormControl> </FormControl>
......
import { Alert, Box, Button, Flex, Link } from '@chakra-ui/react'; import { Alert, Box, Button, Flex, Link } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
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';
...@@ -14,6 +15,7 @@ import type { ...@@ -14,6 +15,7 @@ import type {
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import LinkInternal from 'ui/shared/LinkInternal';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress'; import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
type Fields = RootFields & AddressVerificationFormFirstStepFields; type Fields = RootFields & AddressVerificationFormFirstStepFields;
...@@ -46,24 +48,9 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => { ...@@ -46,24 +48,9 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => {
}); });
if (response.status !== 'SUCCESS') { if (response.status !== 'SUCCESS') {
switch (response.status) { const type = typeof response.status === 'number' ? 'UNKNOWN_ERROR' : response.status;
case 'INVALID_ADDRESS_ERROR': { const message = ('payload' in response ? response.payload?.message : undefined) || 'Oops! Something went wrong';
return setError('root', { type: 'manual', message: 'Specified address either does not exist or is EOA' }); return setError('root', { type, message });
}
case 'IS_OWNER_ERROR': {
return setError('root', { type: 'manual', message: 'User is already an owner of the address' });
}
case 'OWNERSHIP_VERIFIED_ERROR': {
return setError('root', { type: 'manual', message: 'Address ownership has been verified by another account' });
}
case 'SOURCE_CODE_NOT_VERIFIED_ERROR': {
return setError('root', { type: 'manual', message: 'Contract source code has not been verified' });
}
default: {
return setError('root', { type: 'manual', message: response.payload?.message || 'Oops! Something went wrong' });
}
}
} }
onContinue({ ...response.result, address: data.address }); onContinue({ ...response.result, address: data.address });
...@@ -76,10 +63,40 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => { ...@@ -76,10 +63,40 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => {
const onSubmit = handleSubmit(onFormSubmit); const onSubmit = handleSubmit(onFormSubmit);
const rootError = (() => {
switch (formState.errors.root?.type) {
case 'INVALID_ADDRESS_ERROR': {
return <span>Specified address either does not exist or is EOA.</span>;
}
case 'IS_OWNER_ERROR': {
return <span>Ownership of this contract address ownership is already verified by this account.</span>;
}
case 'OWNERSHIP_VERIFIED_ERROR': {
return <span>Ownership of this contract address is already verified by another account.</span>;
}
case 'SOURCE_CODE_NOT_VERIFIED_ERROR': {
const href = route({ pathname: '/address/[hash]/contract_verification', query: { hash: address } });
return (
<Box>
<span>The contract source code you entered is not yet verified. Please follow these steps to </span>
<LinkInternal href={ href }>verify the contract</LinkInternal>
<span>.</span>
</Box>
);
}
case undefined: {
return null;
}
default: {
return formState.errors.root?.message;
}
}
})();
return ( return (
<form noValidate onSubmit={ onSubmit }> <form noValidate onSubmit={ onSubmit }>
{ formState.errors.root?.type === 'manual' && <Alert status="warning" mb={ 6 }>{ formState.errors.root?.message }</Alert> } <Box>Let’s check your address...</Box>
<Box mb={ 8 }>Let’s check your address...</Box> { rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/> <AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }> <Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button size="lg" type="submit" isDisabled={ formState.isSubmitting }> <Button size="lg" type="submit" isDisabled={ formState.isSubmitting }>
......
...@@ -16,7 +16,6 @@ import type { ...@@ -16,7 +16,6 @@ import type {
import type { VerifiedAddress } from 'types/api/account'; import type { VerifiedAddress } from 'types/api/account';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import shortenString from 'lib/shortenString'; import shortenString from 'lib/shortenString';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -65,35 +64,15 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -65,35 +64,15 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
}); });
if (response.status !== 'SUCCESS') { if (response.status !== 'SUCCESS') {
switch (response.status) { const type = typeof response.status === 'number' ? 'UNKNOWN_STATUS' : response.status;
case 'INVALID_SIGNATURE_ERROR': { return setError('root', { type, message: response.status === 'INVALID_SIGNER_ERROR' ? response.invalidSigner.signer : undefined });
return setError('root', { type: 'manual', message: 'Invalid signature' });
}
case 'VALIDITY_EXPIRED_ERROR': {
return setError('root', { type: 'manual', message: 'Message validity expired' });
}
case 'INVALID_SIGNER_ERROR': {
const signer = shortenString(response.invalidSigner.signer);
const expectedSigners = [ contractCreator, contractOwner ].filter(Boolean).map(shortenString).join(', ');
const message = `Invalid signer ${ signer }. Expected: ${ expectedSigners }.`;
return setError('root', { type: 'manual', message });
}
case 'UNKNOWN_STATUS': {
return setError('root', { type: 'manual', message: 'Oops! Something went wrong' });
}
default: {
return setError('root', { type: 'manual', message: response.payload?.message || 'Oops! Something went wrong' });
}
}
} }
onContinue(response.result.verifiedAddress); onContinue(response.result.verifiedAddress);
} catch (_error: unknown) { } catch (error) {
const error = _error as ResourceError<AddressVerificationResponseError>; setError('root', { type: 'UNKNOWN_STATUS' });
setError('root', { type: 'manual', message: error.payload?.message || 'Oops! Something went wrong' });
} }
}, [ address, apiFetch, contractCreator, contractOwner, onContinue, setError ]); }, [ address, apiFetch, onContinue, setError ]);
const onSubmit = handleSubmit(onFormSubmit); const onSubmit = handleSubmit(onFormSubmit);
...@@ -103,7 +82,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -103,7 +82,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
onSubmit(); onSubmit();
}, },
onError: (error) => { onError: (error) => {
return setError('root', { type: 'manual', message: (error as Error)?.message || 'Oops! Something went wrong' }); return setError('root', { type: 'SIGNING_FAIL', message: (error as Error)?.message || 'Oops! Something went wrong' });
}, },
}); });
...@@ -154,15 +133,61 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -154,15 +133,61 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
); );
})(); })();
const contactUsLink = <Link>contact us</Link>;
const rootError = (() => {
switch (formState.errors.root?.type) {
case 'INVALID_SIGNATURE_ERROR': {
return <span>The signature could not be processed.</span>;
}
case 'VALIDITY_EXPIRED_ERROR': {
return <span>This verification message has expired. Add the contract address to restart the process.</span>;
}
case 'SIGNING_FAIL': {
return <span>{ formState.errors.root.message }</span>;
}
case 'INVALID_SIGNER_ERROR': {
const signer = shortenString(formState.errors.root.message || '');
const expectedSigners = [ contractCreator, contractOwner ].filter(Boolean).map(shortenString).join(', ');
return (
<Box>
<span>This address </span>
<span>{ signer }</span>
<span> is not a creator/owner of the requested contract and cannot claim ownership. Only </span>
<span>{ expectedSigners }2</span>
<span> can verify ownership of this contract.</span>
</Box>
);
}
case 'UNKNOWN_STATUS': {
return (
<Box>
<span>We are not able to process the verify account ownership for this contract address. Kindly </span>
{ contactUsLink }
<span> for further assistance.</span>
</Box>
);
}
case undefined: {
return null;
}
}
})();
return ( return (
<form noValidate onSubmit={ onSubmit }> <form noValidate onSubmit={ onSubmit }>
{ formState.errors.root?.type === 'manual' && <Alert status="warning" mb={ 6 }>{ formState.errors.root.message }</Alert> } { rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> }
<Box mb={ 8 }> <Box mb={ 8 }>
<span>Please select the address below you will use to sign, copy the message, and sign it using your preferred method. </span> <span>Please select the address to sign and copy the message and sign it using the Blockscout message provider of your choice. </span>
<Link>Additional instructions</Link> <Link href="https://docs.blockscout.com/for-users/my-account/verified-addresses/copy-and-sign-message" target="_blank">
Additional instructions
</Link>
<span>. If you do not see your address here but are sure that you are the owner of the contract, kindly </span>
{ contactUsLink }
<span> for further assistance.</span>
</Box> </Box>
{ (contractOwner || contractCreator) && ( { (contractOwner || contractCreator) && (
<Flex flexDir="column" rowGap={ 4 } mb={ 8 }> <Flex flexDir="column" rowGap={ 4 } mb={ 4 }>
{ contractCreator && ( { contractCreator && (
<Box> <Box>
<chakra.span fontWeight={ 600 }>Contract creator: </chakra.span> <chakra.span fontWeight={ 600 }>Contract creator: </chakra.span>
......
...@@ -3,10 +3,10 @@ import React from 'react'; ...@@ -3,10 +3,10 @@ import React from 'react';
interface Props { interface Props {
onShowListClick: () => void; onShowListClick: () => void;
onAddTokenClick: () => void; onAddTokenInfoClick: () => void;
} }
const AddressVerificationStepSuccess = ({ onAddTokenClick, onShowListClick }: Props) => { const AddressVerificationStepSuccess = ({ onAddTokenInfoClick, onShowListClick }: Props) => {
return ( return (
<Box> <Box>
<Alert status="success" flexWrap="wrap" whiteSpace="pre-wrap" wordBreak="break-word" mb={ 3 } display="inline-block"> <Alert status="success" flexWrap="wrap" whiteSpace="pre-wrap" wordBreak="break-word" mb={ 3 } display="inline-block">
...@@ -19,7 +19,7 @@ const AddressVerificationStepSuccess = ({ onAddTokenClick, onShowListClick }: Pr ...@@ -19,7 +19,7 @@ const AddressVerificationStepSuccess = ({ onAddTokenClick, onShowListClick }: Pr
<Button size="lg" variant="outline" onClick={ onShowListClick }> <Button size="lg" variant="outline" onClick={ onShowListClick }>
View my verified addresses View my verified addresses
</Button> </Button>
<Button size="lg" onClick={ onAddTokenClick }> <Button size="lg" onClick={ onAddTokenInfoClick }>
Add token information Add token information
</Button> </Button>
</Flex> </Flex>
......
...@@ -15,6 +15,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -15,6 +15,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm'; import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm';
import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem';
import VerifiedAddressesTable from 'ui/verifiedAddresses/VerifiedAddressesTable'; import VerifiedAddressesTable from 'ui/verifiedAddresses/VerifiedAddressesTable';
const VerifiedAddresses = () => { const VerifiedAddresses = () => {
...@@ -28,6 +29,14 @@ const VerifiedAddresses = () => { ...@@ -28,6 +29,14 @@ const VerifiedAddresses = () => {
}); });
const applicationsQuery = useApiQuery('token_info_applications', { const applicationsQuery = useApiQuery('token_info_applications', {
pathParams: { chainId: appConfig.network.id, id: undefined }, pathParams: { chainId: appConfig.network.id, id: undefined },
queryOptions: {
select: (data) => {
return {
...data,
submissions: data.submissions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)),
};
},
},
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -74,9 +83,11 @@ const VerifiedAddresses = () => { ...@@ -74,9 +83,11 @@ const VerifiedAddresses = () => {
}, [ queryClient ]); }, [ queryClient ]);
const addButton = ( const addButton = (
<Button size="lg" onClick={ modalProps.onOpen } marginTop={ 8 }> <Box marginTop={ 8 }>
Add address <Button size="lg" onClick={ modalProps.onOpen }>
</Button> Add address
</Button>
</Box>
); );
const skeleton = ( const skeleton = (
...@@ -119,8 +130,15 @@ const VerifiedAddresses = () => { ...@@ -119,8 +130,15 @@ const VerifiedAddresses = () => {
const content = addressesQuery.data?.verifiedAddresses ? ( const content = addressesQuery.data?.verifiedAddresses ? (
<> <>
<Show below="lg" key="content-mobile" ssr={ false }> <Show below="lg" key="content-mobile" ssr={ false }>
<div>mobile view</div> { addressesQuery.data.verifiedAddresses.map((item) => (
{ addButton } <VerifiedAddressesListItem
key={ item.contractAddress }
item={ item }
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress === item.contractAddress) }
onAdd={ handleItemAdd }
onEdit={ handleItemEdit }
/>
)) }
</Show> </Show>
<Hide below="lg" key="content-desktop" ssr={ false }> <Hide below="lg" key="content-desktop" ssr={ false }>
<VerifiedAddressesTable <VerifiedAddressesTable
...@@ -129,7 +147,6 @@ const VerifiedAddresses = () => { ...@@ -129,7 +147,6 @@ const VerifiedAddresses = () => {
onItemEdit={ handleItemEdit } onItemEdit={ handleItemEdit }
onItemAdd={ handleItemAdd } onItemAdd={ handleItemAdd }
/> />
{ addButton }
</Hide> </Hide>
</> </>
) : null; ) : null;
...@@ -147,8 +164,8 @@ const VerifiedAddresses = () => { ...@@ -147,8 +164,8 @@ const VerifiedAddresses = () => {
<chakra.p fontWeight={ 600 } mt={ 5 }> <chakra.p fontWeight={ 600 } mt={ 5 }>
Before starting, make sure that: Before starting, make sure that:
</chakra.p> </chakra.p>
<OrderedList> <OrderedList ml={ 6 }>
<ListItem>The source code for the smart contract is deployed on “Network Name”.</ListItem> <ListItem>The source code for the smart contract is deployed on “{ appConfig.network.name }”.</ListItem>
<ListItem>The source code is verified (if not yet verified, you can use this tool).</ListItem> <ListItem>The source code is verified (if not yet verified, you can use this tool).</ListItem>
</OrderedList> </OrderedList>
<chakra.div mt={ 5 }> <chakra.div mt={ 5 }>
...@@ -163,9 +180,15 @@ const VerifiedAddresses = () => { ...@@ -163,9 +180,15 @@ const VerifiedAddresses = () => {
emptyText="" emptyText=""
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: skeleton }}
/> />
<AddressVerificationModal isOpen={ modalProps.isOpen } onClose={ modalProps.onClose } onSubmit={ handleAddressSubmit }/> { addButton }
<AddressVerificationModal
isOpen={ modalProps.isOpen }
onClose={ modalProps.onClose }
onSubmit={ handleAddressSubmit }
onAddTokenInfoClick={ handleItemAdd }
/>
</Page> </Page>
); );
}; };
export default VerifiedAddresses; export default React.memo(VerifiedAddresses);
...@@ -68,7 +68,7 @@ const DataListDisplay = (props: Props) => { ...@@ -68,7 +68,7 @@ const DataListDisplay = (props: Props) => {
} }
if (!props.items?.length) { if (!props.items?.length) {
return <Text as="span">{ props.emptyText }</Text>; return props.emptyText ? <Text as="span">{ props.emptyText }</Text> : null;
} }
return ( return (
......
...@@ -32,7 +32,7 @@ test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => { ...@@ -32,7 +32,7 @@ test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => {
test.describe('auth', () => { test.describe('auth', () => {
const extendedTest = test.extend({ const extendedTest = test.extend({
context: ({ context }, use) => { context: async({ context }, use) => {
authFixture(context); authFixture(context);
use(context); use(context);
}, },
......
...@@ -34,7 +34,7 @@ export function getFormDefaultValues(address: string, application: TokenInfoAppl ...@@ -34,7 +34,7 @@ export function getFormDefaultValues(address: string, application: TokenInfoAppl
}; };
} }
export function prepareRequestBody(data: Fields): Omit<TokenInfoApplication, 'id' | 'status'> { export function prepareRequestBody(data: Fields): Omit<TokenInfoApplication, 'id' | 'status' | 'updatedAt'> {
return { return {
coinGeckoTicker: data.ticker_coin_gecko, coinGeckoTicker: data.ticker_coin_gecko,
coinMarketCapTicker: data.ticker_coin_market_cap, coinMarketCapTicker: data.ticker_coin_market_cap,
......
import { Icon, IconButton, Link, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
import editIcon from 'icons/edit.svg';
import dayjs from 'lib/date/dayjs';
import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import VerifiedAddressesStatus from './VerifiedAddressesStatus';
import VerifiedAddressesTokenSnippet from './VerifiedAddressesTokenSnippet';
interface Props {
item: VerifiedAddress;
application: TokenInfoApplication | undefined;
onAdd: (address: string) => void;
onEdit: (address: string) => void;
}
const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props) => {
const handleAddClick = React.useCallback(() => {
onAdd(item.contractAddress);
}, [ item, onAdd ]);
const handleEditClick = React.useCallback(() => {
onEdit(item.contractAddress);
}, [ item, onEdit ]);
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center">
{ application ? (
<>
<VerifiedAddressesTokenSnippet application={ application }/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
</ListItemMobileGrid.Value>
{ application && (
<>
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<VerifiedAddressesStatus status={ application.status }/>
</ListItemMobileGrid.Value>
</>
) }
{ application && (
<>
<ListItemMobileGrid.Label>Date</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ dayjs(application.updatedAt).format('MMM DD, YYYY') }
</ListItemMobileGrid.Value>
</>
) }
</ListItemMobileGrid.Container>
);
};
export default React.memo(VerifiedAddressesListItem);
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfoApplication } from 'types/api/account';
interface Props {
status?: TokenInfoApplication['status'];
}
const VerifiedAddressesStatus = ({ status }: Props) => {
switch (status) {
case 'IN_PROCESS': {
return <chakra.span fontWeight={ 500 }>In progress</chakra.span>;
}
case 'APPROVED': {
return <chakra.span fontWeight={ 500 } color="green.500">Approved</chakra.span>;
}
case 'UPDATE_REQUIRED': {
return <chakra.span fontWeight={ 500 } color="orange.500">Waiting for update</chakra.span>;
}
case 'REJECTED': {
return <chakra.span fontWeight={ 500 } color="red.500">Rejected</chakra.span>;
}
default:
return null;
}
};
export default VerifiedAddressesStatus;
...@@ -19,6 +19,7 @@ const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: P ...@@ -19,6 +19,7 @@ const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: P
<Tr> <Tr>
<Th>Address</Th> <Th>Address</Th>
<Th w="232px">Token info</Th> <Th w="232px">Token info</Th>
<Th w="94px"></Th>
<Th w="160px">Request status</Th> <Th w="160px">Request status</Th>
<Th w="150px">Date</Th> <Th w="150px">Date</Th>
</Tr> </Tr>
......
import { Td, Tr, Link, Flex, Image, Tooltip, IconButton, Icon, chakra } from '@chakra-ui/react'; import { Td, Tr, Link, Tooltip, IconButton, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
import editIcon from 'icons/edit.svg'; import editIcon from 'icons/edit.svg';
import AddressLink from 'ui/shared/address/AddressLink'; import dayjs from 'lib/date/dayjs';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
import VerifiedAddressesStatus from './VerifiedAddressesStatus';
import VerifiedAddressesTokenSnippet from './VerifiedAddressesTokenSnippet';
interface Props { interface Props {
item: VerifiedAddress; item: VerifiedAddress;
...@@ -25,67 +27,33 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -25,67 +27,33 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
onEdit(item.contractAddress); onEdit(item.contractAddress);
}, [ item, onEdit ]); }, [ item, onEdit ]);
const status = (() => {
switch (application?.status) {
case 'IN_PROCESS': {
return <chakra.span fontWeight={ 500 }>In progress</chakra.span>;
}
case 'APPROVED': {
return <chakra.span fontWeight={ 500 } color="green.500">Approved</chakra.span>;
}
case 'UPDATE_REQUIRED': {
return <chakra.span fontWeight={ 500 } color="orange.500">Waiting for update</chakra.span>;
}
case 'REJECTED': {
return <chakra.span fontWeight={ 500 } color="red.500">Rejected</chakra.span>;
}
default:
return null;
}
})();
return ( return (
<Tr> <Tr>
<Td> <Td>
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/> <AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</Td> </Td>
<Td fontSize="sm"> <Td fontSize="sm" verticalAlign="middle">
{ application ? ( { application ? (
<Flex alignItems="center" columnGap={ 2 } w="100%"> <VerifiedAddressesTokenSnippet application={ application }/>
<Image
borderRadius="base"
boxSize={ 6 }
objectFit="cover"
src={ application.iconUrl }
alt="Token logo"
fallback={ <TokenLogoPlaceholder boxSize={ 6 }/> }
/>
<AddressLink
hash={ application.tokenAddress }
alias={ application.projectName }
type="token"
isDisabled={ application.status === 'IN_PROCESS' }
fontWeight={ 500 }
/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</Flex>
) : ( ) : (
<Link onClick={ handleAddClick }>Add details</Link> <Link onClick={ handleAddClick }>Add details</Link>
) } ) }
</Td> </Td>
<Td fontSize="sm">{ status }</Td> <Td>{ application ? (
<Td fontSize="sm"></Td> <Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
) : null }</Td>
<Td fontSize="sm"><VerifiedAddressesStatus status={ application?.status }/></Td>
<Td fontSize="sm" color="text_secondary">{ dayjs(application?.updatedAt).format('MMM DD, YYYY') }</Td>
</Tr> </Tr>
); );
}; };
......
import { Image, Flex } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfoApplication } from 'types/api/account';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
interface Props {
application: TokenInfoApplication;
}
const VerifiedAddressesTokenSnippet = ({ application }: Props) => {
return (
<Flex alignItems="center" columnGap={ 2 } w="100%">
<Image
borderRadius="base"
boxSize={ 6 }
objectFit="cover"
src={ application.iconUrl }
alt="Token logo"
fallback={ <TokenLogoPlaceholder boxSize={ 6 }/> }
/>
<AddressLink
hash={ application.tokenAddress }
alias={ application.projectName }
type="token"
isDisabled={ application.status === 'IN_PROCESS' }
fontWeight={ 500 }
/>
</Flex>
);
};
export default React.memo(VerifiedAddressesTokenSnippet);
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