Commit 24ff3fa8 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into internal-link

parents dfcb4027 758af1c4
......@@ -27,7 +27,7 @@ export default function useNavItems() {
const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: true },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: true },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: true },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute.startsWith('token'), isNewUi: true },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: true },
isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute.startsWith('app'), isNewUi: true } : null,
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
......@@ -36,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, unknown>}/>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, ResourceError>}/>
</TestApp>,
{ hooksConfig },
);
......@@ -65,7 +66,7 @@ test('token', async({ mount, page }) => {
const component = await mount(
<TestApp>
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, unknown>}/>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, ResourceError>}/>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
......@@ -86,7 +87,7 @@ test('validator +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, unknown>}/>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, ResourceError>}/>
</TestApp>,
{ hooksConfig },
);
......
......@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
......@@ -30,7 +31,7 @@ import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect';
interface Props {
addressQuery: UseQueryResult<TAddress>;
addressQuery: UseQueryResult<TAddress, ResourceError>;
scrollRef?: React.RefObject<HTMLDivElement>;
}
......@@ -38,8 +39,10 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const addressHash = router.query.id?.toString();
const countersQuery = useApiQuery('address_counters', {
pathParams: { id: router.query.id?.toString() },
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
......@@ -52,33 +55,54 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
}, 500);
}, [ scrollRef ]);
if (addressQuery.isError) {
const errorData = React.useMemo(() => ({
hash: addressHash || '',
is_contract: false,
implementation_name: null,
implementation_address: null,
token: null,
watchlist_names: null,
creation_tx_hash: null,
block_number_balance_updated_at: null,
name: null,
exchange_rate: null,
coin_balance: null,
has_tokens: true,
has_token_transfers: true,
has_validated_blocks: false,
}), [ addressHash ]);
const is404Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 404;
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;
if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
}
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
if (addressQuery.isError && !is404Error) {
return <DataFetchAlert/>;
}
if (addressQuery.isError) {
return <DataFetchAlert/>;
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const data = addressQuery.isError ? errorData : addressQuery.data;
return (
<Box>
<Flex alignItems="center">
<AddressIcon address={ addressQuery.data }/>
<AddressIcon address={ data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash }
{ isMobile ? <HashStringShorten hash={ data.hash }/> : data.hash }
</Text>
<CopyToClipboard text={ addressQuery.data.hash }/>
{ addressQuery.data.is_contract && addressQuery.data.token && <AddressAddToMetaMask ml={ 2 } token={ addressQuery.data.token }/> }
{ !addressQuery.data.is_contract && (
<AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/>
<CopyToClipboard text={ data.hash }/>
{ data.is_contract && data.token && <AddressAddToMetaMask ml={ 2 } token={ data.token }/> }
{ !data.is_contract && (
<AddressFavoriteButton hash={ data.hash } isAdded={ Boolean(data.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/>
<AddressQrCode hash={ data.hash } ml={ 2 }/>
</Flex>
{ explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
......@@ -95,71 +119,79 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressNameInfo data={ addressQuery.data }/>
{ addressQuery.data.is_contract && addressQuery.data.creation_tx_hash && addressQuery.data.creator_address_hash && (
<AddressNameInfo data={ data }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation."
>
<AddressLink type="address" hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/>
<AddressLink hash={ data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem>
) }
{ addressQuery.data.is_contract && addressQuery.data.implementation_address && (
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract."
columnGap={ 1 }
>
<LinkInternal href={ link('address_index', { id: addressQuery.data.implementation_address }) }>
{ addressQuery.data.implementation_name }
<LinkInternal href={ link('address_index', { id: data.implementation_address }) }>
{ data.implementation_name }
</LinkInternal>
<Text variant="secondary" overflow="hidden">
<HashStringShortenDynamic hash={ `(${ addressQuery.data.implementation_address })` }/>
<HashStringShortenDynamic hash={ `(${ data.implementation_address })` }/>
</Text>
</DetailsInfoItem>
) }
<AddressBalance data={ addressQuery.data }/>
{ addressQuery.data.has_tokens && (
<AddressBalance data={ data }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
alignSelf="center"
py={ 0 }
>
<TokenSelect/>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address."
>
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
{ addressQuery.data.has_token_transfers && (
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address."
>
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
{ addressQuery.data.has_validated_blocks && (
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
>
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
) }
{ addressQuery.data.block_number_balance_updated_at && (
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated."
......@@ -167,12 +199,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
py={{ base: '2px', lg: 1 }}
>
<LinkInternal
href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) }
href={ link('block', { id: String(data.block_number_balance_updated_at) }) }
display="flex"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/>
{ addressQuery.data.block_number_balance_updated_at }
{ data.block_number_balance_updated_at }
</LinkInternal>
</DetailsInfoItem>
) }
......
......@@ -13,7 +13,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: Address;
data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>;
}
const AddressBalance = ({ data }: Props) => {
......@@ -63,10 +63,6 @@ const AddressBalance = ({ data }: Props) => {
handler: handleNewCoinBalanceMessage,
});
if (!data.coin_balance) {
return null;
}
return (
<DetailsInfoItem
title="Balance"
......@@ -82,7 +78,7 @@ const AddressBalance = ({ data }: Props) => {
fontSize="sm"
/>
<CurrencyValue
value={ data.coin_balance }
value={ data.coin_balance || '0' }
exchangeRate={ data.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) }
currency={ appConfig.network.currency.symbol }
......
......@@ -29,7 +29,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
const data = query.data?.[prop];
if (query.isError || data === null || data === undefined) {
return <span>no data</span>;
return <span>0</span>;
}
switch (prop) {
......
......@@ -7,7 +7,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
data: Address;
data: Pick<Address, 'name' | 'token' | 'is_contract'>;
}
const AddressNameInfo = ({ data }: Props) => {
......
......@@ -17,7 +17,11 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => {
interface Props {
onClick?: () => void;
}
const TokenSelect = ({ onClick }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
......@@ -88,6 +92,7 @@ const TokenSelect = () => {
pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
onClick={ onClick }
/>
</NextLink>
</Box>
......
......@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import notEmpty from 'lib/notEmpty';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
......@@ -37,6 +38,10 @@ const TOKEN_TABS = Object.values(tokenTabsByType);
const AddressPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const addressQuery = useApiQuery('address', {
......@@ -113,6 +118,8 @@ const AddressPageContent = () => {
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
return (
<Page>
<TextAd mb={ 6 }/>
......@@ -121,13 +128,15 @@ const AddressPageContent = () => {
) : (
<PageTitle
text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
additionals={ tagsNode }
additionalsRight={ tagsNode }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to top accounts list"
/>
) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
{ addressQuery.isLoading ? <SkeletonTabs/> : content }
</Page>
);
};
......
......@@ -22,6 +22,11 @@ const hooksConfig = {
// FIXME: idk why mobile test doesn't work (it's ok locally)
// test('base view +@mobile +@dark-mode', async({ mount, page }) => {
test('base view +@dark-mode', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(TOKEN_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenInfo),
......
import { Skeleton, Box } from '@chakra-ui/react';
import { Skeleton, Box, Flex, SkeletonCircle } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
......@@ -23,6 +26,10 @@ const TokenPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/tokens');
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenQuery = useApiQuery('token', {
......@@ -82,9 +89,24 @@ const TokenPageContent = () => {
return (
<Page>
{ tokenQuery.isLoading ?
<Skeleton w="500px" h={ 10 } mb={ 6 }/> :
<PageTitle text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }/> }
{ tokenQuery.isLoading ? (
<Flex alignItems="center" mb={ 6 }>
<SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/>
<Skeleton w="500px" h={ 10 }/>
</Flex>
) : (
<>
<TextAd mb={ 6 }/>
<PageTitle
text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to tokens list"
additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/>
) }
/>
</>
) }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/>
......
......@@ -69,7 +69,7 @@ const TransactionPageContent = () => {
<TextAd mb={ 6 }/>
<PageTitle
text="Transaction details"
additionals={ additionals }
additionalsRight={ additionals }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to transactions list"
/>
......
// import { Icon } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
// import plusIcon from 'icons/plus.svg';
import * as textAdMock from 'mocks/ad/textAd';
import TestApp from 'playwright/TestApp';
......@@ -32,6 +34,10 @@ test('default view +@mobile', async({ mount }) => {
});
test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount }) => {
// https://github.com/microsoft/playwright/issues/15620
// not possible to pass component as a prop in tests
// const left = <Icon as={ plusIcon }/>;
const component = await mount(
<TestApp>
<PageTitle
......@@ -39,7 +45,8 @@ test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount })
withTextAd
backLinkLabel="Back"
backLinkUrl="back"
additionals="Privet"
// additionalsLeft={ left }
additionalsRight="Privet"
/>
</TestApp>,
);
......@@ -55,7 +62,7 @@ test('long title with text ad, back link and addons +@mobile', async({ mount })
withTextAd
backLinkLabel="Back"
backLinkUrl="back"
additionals="Privet, kak dela?"
additionalsRight="Privet, kak dela?"
/>
</TestApp>,
);
......
import { Heading, Flex, Tooltip, Icon, chakra } from '@chakra-ui/react';
import { Heading, Flex, Grid, Tooltip, Icon, chakra } from '@chakra-ui/react';
import React from 'react';
import eastArrowIcon from 'icons/arrows/east.svg';
......@@ -7,23 +7,20 @@ import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
text: string;
additionals?: React.ReactNode;
additionalsLeft?: React.ReactNode;
additionalsRight?: React.ReactNode;
withTextAd?: boolean;
className?: string;
backLinkLabel?: string;
backLinkUrl?: string;
}
const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => {
const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => {
const title = (
<Heading
as="h1"
size="lg"
flex="none"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
width={ backLinkUrl ? 'calc(100% - 36px)' : '100%' }
>
{ text }
</Heading>
......@@ -40,22 +37,25 @@ const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel,
className={ className }
>
<Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' }>
<Flex
flexWrap="nowrap"
alignItems="center"
<Grid
templateColumns={ [ backLinkUrl && 'auto', additionalsLeft && 'auto', '1fr' ].filter(Boolean).join(' ') }
columnGap={ 3 }
overflow="hidden"
>
{ backLinkUrl && (
<Tooltip label={ backLinkLabel }>
<LinkInternal display="inline-flex" href={ backLinkUrl }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
<LinkInternal display="inline-flex" href={ backLinkUrl } h="40px">
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>
</LinkInternal>
</Tooltip>
) }
{ title }
{ additionalsLeft !== undefined && (
<Flex h="40px" alignItems="center">
{ additionalsLeft }
</Flex>
{ additionals }
) }
{ title }
</Grid>
{ additionalsRight }
</Flex>
{ withTextAd && <TextAd flexShrink={ 100 }/> }
</Flex>
......
......@@ -48,14 +48,14 @@ const TokenTransferListItem = ({
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
return (
<ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" flexWrap="wrap" rowGap={ 1 } position="relative">
<Flex w="100%" justifyContent="space-between">
<Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 }>
<TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag>
</Flex>
{ showTxInfo && txHash && (
<Flex position="absolute" top={ 0 } right={ 0 }>
<TxAdditionalInfo hash={ txHash } isMobile/>
</Flex>
) }
</Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
......
......@@ -69,11 +69,6 @@ const TokenDetails = ({ tokenQuery }: Props) => {
);
}
// we show error in parent component, this is only for TS
if (tokenQuery.isError) {
return null;
}
const {
exchange_rate: exchangeRate,
total_supply: totalSupply,
......
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