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 = [
appPort && ':' + appPort,
].filter(Boolean).join('');
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 apiPort = getEnvValue(process.env.NEXT_PUBLIC_API_PORT);
const apiEndpoint = apiHost ? [
......
......@@ -19,3 +19,5 @@ NEXT_PUBLIC_IS_TESTNET=true
# api config
NEXT_PUBLIC_API_HOST=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
// I was not able to figure out how to send CORS with credentials from localhost
// unsuccessfully tried different ways, even custom local dev domain
// so for local development we have to use next.js api as proxy server
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 {
isNewUi: true,
},
appConfig.contractInfoApi.endpoint && appConfig.adminServiceApi.endpoint && {
text: 'Verified addresses',
text: 'Verified addrs',
nextRoute: { pathname: '/account/verified_addresses' as const },
icon: verifiedIcon,
isActive: pathname === '/account/verified_addresses',
......
import type { test } from '@playwright/experimental-ct-react';
import type { PlaywrightWorkerArgs } from '@playwright/test';
interface Env {
name: string;
......@@ -6,9 +7,17 @@ interface Env {
}
// 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) => {
const context = await browser.newContext({
const context = await createContextWithEnvs(browser, envs);
await use(context);
await context.close();
};
}
export function createContextWithEnvs(browser: PlaywrightWorkerArgs['browser'], envs: Array<Env>) {
return browser.newContext({
storageState: {
origins: [
{ origin: 'http://localhost:3100', localStorage: envs },
......@@ -16,8 +25,4 @@ export default function createContextWithEnvs(envs: Array<Env>): Parameters<type
cookies: [],
},
});
await use(context);
await context.close();
};
}
......@@ -203,6 +203,7 @@ export interface TokenInfoApplication {
telegram?: string;
tokenAddress: string;
twitter?: string;
updatedAt: string;
}
export interface TokenInfoApplications {
......
......@@ -15,9 +15,10 @@ interface Props {
isOpen: boolean;
onClose: () => 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 [ data, setData ] = React.useState<AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess>({ address: '', signingMessage: '' });
......@@ -38,12 +39,27 @@ const AddressVerificationModal = ({ isOpen, onClose, onSubmit }: Props) => {
const handleClose = React.useCallback(() => {
onClose();
setStepIndex(0);
setData({ address: '', signingMessage: '' });
}, [ onClose ]);
const handleAddTokenInfoClick = React.useCallback(() => {
onAddTokenInfoClick(data.address);
handleClose();
}, [ handleClose, data.address, onAddTokenInfoClick ]);
const steps = [
{ title: 'Verify new address ownership', content: <AddressVerificationStepAddress onContinue={ handleGoToSecondStep }/> },
{ title: 'Copy and sign message', content: <AddressVerificationStepSignature { ...data } onContinue={ handleGoToThirdStep }/> },
{ title: 'Congrats! Address is verified.', content: <AddressVerificationStepSuccess onShowListClick={ handleClose } onAddTokenClick={ handleClose }/> },
{
title: 'Verify new address ownership',
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];
......
......@@ -21,7 +21,7 @@ const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
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
{ ...field }
required
......
......@@ -28,7 +28,7 @@ const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
isInvalid={ Boolean(error) }
isDisabled
autoComplete="off"
maxH="105px"
maxH={{ base: '140px', lg: '80px' }}
/>
<InputPlaceholder text="Message to sign" error={ error } isInModal/>
</FormControl>
......
import { Alert, Box, Button, Flex, Link } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
......@@ -14,6 +15,7 @@ import type {
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import LinkInternal from 'ui/shared/LinkInternal';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
......@@ -46,24 +48,9 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => {
});
if (response.status !== 'SUCCESS') {
switch (response.status) {
case 'INVALID_ADDRESS_ERROR': {
return setError('root', { type: 'manual', message: 'Specified address either does not exist or is EOA' });
}
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' });
}
}
const type = typeof response.status === 'number' ? 'UNKNOWN_ERROR' : response.status;
const message = ('payload' in response ? response.payload?.message : undefined) || 'Oops! Something went wrong';
return setError('root', { type, message });
}
onContinue({ ...response.result, address: data.address });
......@@ -76,10 +63,40 @@ const AddressVerificationStepAddress = ({ onContinue }: Props) => {
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 (
<form noValidate onSubmit={ onSubmit }>
{ formState.errors.root?.type === 'manual' && <Alert status="warning" mb={ 6 }>{ formState.errors.root?.message }</Alert> }
<Box mb={ 8 }>Let’s check your address...</Box>
<Box>Let’s check your address...</Box>
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button size="lg" type="submit" isDisabled={ formState.isSubmitting }>
......
......@@ -16,7 +16,6 @@ import type {
import type { VerifiedAddress } from 'types/api/account';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import shortenString from 'lib/shortenString';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -65,35 +64,15 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
});
if (response.status !== 'SUCCESS') {
switch (response.status) {
case 'INVALID_SIGNATURE_ERROR': {
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' });
}
}
const type = typeof response.status === 'number' ? 'UNKNOWN_STATUS' : response.status;
return setError('root', { type, message: response.status === 'INVALID_SIGNER_ERROR' ? response.invalidSigner.signer : undefined });
}
onContinue(response.result.verifiedAddress);
} catch (_error: unknown) {
const error = _error as ResourceError<AddressVerificationResponseError>;
setError('root', { type: 'manual', message: error.payload?.message || 'Oops! Something went wrong' });
} catch (error) {
setError('root', { type: 'UNKNOWN_STATUS' });
}
}, [ address, apiFetch, contractCreator, contractOwner, onContinue, setError ]);
}, [ address, apiFetch, onContinue, setError ]);
const onSubmit = handleSubmit(onFormSubmit);
......@@ -103,7 +82,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
onSubmit();
},
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
);
})();
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 (
<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 }>
<span>Please select the address below you will use to sign, copy the message, and sign it using your preferred method. </span>
<Link>Additional instructions</Link>
<span>Please select the address to sign and copy the message and sign it using the Blockscout message provider of your choice. </span>
<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>
{ (contractOwner || contractCreator) && (
<Flex flexDir="column" rowGap={ 4 } mb={ 8 }>
<Flex flexDir="column" rowGap={ 4 } mb={ 4 }>
{ contractCreator && (
<Box>
<chakra.span fontWeight={ 600 }>Contract creator: </chakra.span>
......
......@@ -3,10 +3,10 @@ import React from 'react';
interface Props {
onShowListClick: () => void;
onAddTokenClick: () => void;
onAddTokenInfoClick: () => void;
}
const AddressVerificationStepSuccess = ({ onAddTokenClick, onShowListClick }: Props) => {
const AddressVerificationStepSuccess = ({ onAddTokenInfoClick, onShowListClick }: Props) => {
return (
<Box>
<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
<Button size="lg" variant="outline" onClick={ onShowListClick }>
View my verified addresses
</Button>
<Button size="lg" onClick={ onAddTokenClick }>
<Button size="lg" onClick={ onAddTokenInfoClick }>
Add token information
</Button>
</Flex>
......
......@@ -15,6 +15,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm';
import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem';
import VerifiedAddressesTable from 'ui/verifiedAddresses/VerifiedAddressesTable';
const VerifiedAddresses = () => {
......@@ -28,6 +29,14 @@ const VerifiedAddresses = () => {
});
const applicationsQuery = useApiQuery('token_info_applications', {
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();
......@@ -74,9 +83,11 @@ const VerifiedAddresses = () => {
}, [ queryClient ]);
const addButton = (
<Button size="lg" onClick={ modalProps.onOpen } marginTop={ 8 }>
<Box marginTop={ 8 }>
<Button size="lg" onClick={ modalProps.onOpen }>
Add address
</Button>
</Box>
);
const skeleton = (
......@@ -119,8 +130,15 @@ const VerifiedAddresses = () => {
const content = addressesQuery.data?.verifiedAddresses ? (
<>
<Show below="lg" key="content-mobile" ssr={ false }>
<div>mobile view</div>
{ addButton }
{ addressesQuery.data.verifiedAddresses.map((item) => (
<VerifiedAddressesListItem
key={ item.contractAddress }
item={ item }
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress === item.contractAddress) }
onAdd={ handleItemAdd }
onEdit={ handleItemEdit }
/>
)) }
</Show>
<Hide below="lg" key="content-desktop" ssr={ false }>
<VerifiedAddressesTable
......@@ -129,7 +147,6 @@ const VerifiedAddresses = () => {
onItemEdit={ handleItemEdit }
onItemAdd={ handleItemAdd }
/>
{ addButton }
</Hide>
</>
) : null;
......@@ -147,8 +164,8 @@ const VerifiedAddresses = () => {
<chakra.p fontWeight={ 600 } mt={ 5 }>
Before starting, make sure that:
</chakra.p>
<OrderedList>
<ListItem>The source code for the smart contract is deployed on “Network Name”.</ListItem>
<OrderedList ml={ 6 }>
<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>
</OrderedList>
<chakra.div mt={ 5 }>
......@@ -163,9 +180,15 @@ const VerifiedAddresses = () => {
emptyText=""
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>
);
};
export default VerifiedAddresses;
export default React.memo(VerifiedAddresses);
......@@ -68,7 +68,7 @@ const DataListDisplay = (props: Props) => {
}
if (!props.items?.length) {
return <Text as="span">{ props.emptyText }</Text>;
return props.emptyText ? <Text as="span">{ props.emptyText }</Text> : null;
}
return (
......
......@@ -32,7 +32,7 @@ test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => {
test.describe('auth', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
context: async({ context }, use) => {
authFixture(context);
use(context);
},
......
......@@ -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 {
coinGeckoTicker: data.ticker_coin_gecko,
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
<Tr>
<Th>Address</Th>
<Th w="232px">Token info</Th>
<Th w="94px"></Th>
<Th w="160px">Request status</Th>
<Th w="150px">Date</Th>
</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 type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
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 TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
import VerifiedAddressesStatus from './VerifiedAddressesStatus';
import VerifiedAddressesTokenSnippet from './VerifiedAddressesTokenSnippet';
interface Props {
item: VerifiedAddress;
......@@ -25,49 +27,19 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
onEdit(item.contractAddress);
}, [ 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 (
<Tr>
<Td>
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</Td>
<Td fontSize="sm">
<Td fontSize="sm" verticalAlign="middle">
{ application ? (
<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 }
/>
<VerifiedAddressesTokenSnippet application={ application }/>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
</Td>
<Td>{ application ? (
<Tooltip label="Edit">
<IconButton
aria-label="edit"
......@@ -79,13 +51,9 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</Flex>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
</Td>
<Td fontSize="sm">{ status }</Td>
<Td fontSize="sm"></Td>
) : 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>
);
};
......
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