Commit f0377c08 authored by Max Alekseenko's avatar Max Alekseenko

add A/B-experiment and mixpanel event

parent b794a05b
...@@ -8,6 +8,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts'; ...@@ -8,6 +8,7 @@ import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
export interface GrowthBookFeatures { export interface GrowthBookFeatures {
test_value: string; test_value: string;
security_score_exp: boolean; security_score_exp: boolean;
action_button_exp: boolean;
} }
export const growthBook = (() => { export const growthBook = (() => {
......
...@@ -105,6 +105,10 @@ Type extends EventTypes.PAGE_WIDGET ? ( ...@@ -105,6 +105,10 @@ Type extends EventTypes.PAGE_WIDGET ? (
} | { } | {
'Type': 'Security score'; 'Type': 'Security score';
'Source': 'Analyzed contracts popup'; 'Source': 'Analyzed contracts popup';
} | {
'Type': 'Action button';
'Info': string;
'Source': 'Txn' | 'NFT collection' | 'NFT item';
} }
) : ) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
......
...@@ -6,27 +6,32 @@ import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata'; ...@@ -6,27 +6,32 @@ import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from '../LinkExternal'; import LinkExternal from '../LinkExternal';
type Props = { type Props = {
data?: AddressMetadataTagFormatted['meta']; data: NonNullable<AddressMetadataTagFormatted['meta']>;
className?: string; className?: string;
txHash?: string; txHash?: string;
source: 'Txn' | 'NFT collection' | 'NFT item';
} }
const ActionButton = ({ data, className, txHash }: Props) => { const ActionButton = ({ data, className, txHash, source }: Props) => {
const defaultTextColor = useColorModeValue('blue.600', 'blue.300'); const defaultTextColor = useColorModeValue('blue.600', 'blue.300');
const defaultBg = useColorModeValue('gray.100', 'gray.700'); const defaultBg = useColorModeValue('gray.100', 'gray.700');
if (!data) {
return null;
}
const { appID, textColor, bgColor, text, logoURL } = data; const { appID, textColor, bgColor, text, logoURL } = data;
const actionURL = data.actionURL?.replace('{chainId}', config.chain.id || '').replace('{txHash}', txHash || ''); const actionURL = data.actionURL?.replace('{chainId}', config.chain.id || '').replace('{txHash}', txHash || '');
const handleClick = React.useCallback(() => {
const info = appID || actionURL;
if (info) {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Action button', Info: info, Source: source });
}
}, [ source, appID, actionURL ]);
const content = ( const content = (
<> <>
<Image <Image
...@@ -47,6 +52,7 @@ const ActionButton = ({ data, className, txHash }: Props) => { ...@@ -47,6 +52,7 @@ const ActionButton = ({ data, className, txHash }: Props) => {
className={ className } className={ className }
as="a" as="a"
href={ route({ pathname: '/apps/[id]', query: { id: appID, action: 'connect', ...(actionURL ? { url: actionURL } : {}) } }) } href={ route({ pathname: '/apps/[id]', query: { id: appID, action: 'connect', ...(actionURL ? { url: actionURL } : {}) } }) }
onClick={ handleClick }
display="flex" display="flex"
size="sm" size="sm"
px={ 2 } px={ 2 }
...@@ -61,6 +67,7 @@ const ActionButton = ({ data, className, txHash }: Props) => { ...@@ -61,6 +67,7 @@ const ActionButton = ({ data, className, txHash }: Props) => {
<LinkExternal <LinkExternal
className={ className } className={ className }
href={ actionURL } href={ actionURL }
onClick={ handleClick }
variant="subtle" variant="subtle"
display="flex" display="flex"
px={ 2 } px={ 2 }
......
...@@ -10,9 +10,10 @@ interface Props { ...@@ -10,9 +10,10 @@ interface Props {
children: React.ReactNode; children: React.ReactNode;
isLoading?: boolean; isLoading?: boolean;
variant?: 'subtle'; variant?: 'subtle';
onClick?: () => void;
} }
const LinkExternal = ({ href, children, className, isLoading, variant }: Props) => { const LinkExternal = ({ href, children, className, isLoading, variant, onClick }: Props) => {
const subtleLinkBg = useColorModeValue('gray.100', 'gray.700'); const subtleLinkBg = useColorModeValue('gray.100', 'gray.700');
const styleProps: ChakraProps = (() => { const styleProps: ChakraProps = (() => {
...@@ -57,7 +58,7 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props) ...@@ -57,7 +58,7 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props)
} }
return ( return (
<Link className={ className } { ...styleProps } target="_blank" href={ href }> <Link className={ className } { ...styleProps } target="_blank" href={ href } onClick={ onClick }>
{ children } { children }
<IconSvg name="arrows/north-east" boxSize={ 4 } verticalAlign="middle" color="gray.400" flexShrink={ 0 }/> <IconSvg name="arrows/north-east" boxSize={ 4 } verticalAlign="middle" color="gray.400" flexShrink={ 0 }/>
</Link> </Link>
......
...@@ -12,6 +12,7 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -12,6 +12,7 @@ import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMounted from 'lib/hooks/useIsMounted'; import useIsMounted from 'lib/hooks/useIsMounted';
import { TOKEN_COUNTERS } from 'stubs/token'; import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token'; import type { TokenTabs } from 'ui/pages/Token';
...@@ -30,6 +31,8 @@ interface Props { ...@@ -30,6 +31,8 @@ interface Props {
const TokenDetails = ({ tokenQuery }: Props) => { const TokenDetails = ({ tokenQuery }: Props) => {
const router = useRouter(); const router = useRouter();
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const { value: isExperiment } = useFeatureValue('action_button_exp', false);
const hash = router.query.hash?.toString(); const hash = router.query.hash?.toString();
const tokenCountersQuery = useApiQuery('token_counters', { const tokenCountersQuery = useApiQuery('token_counters', {
...@@ -172,16 +175,24 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -172,16 +175,24 @@ const TokenDetails = ({ tokenQuery }: Props) => {
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ type !== 'ERC-20' && <TokenNftMarketplaces hash={ hash } isLoading={ tokenQuery.isPlaceholderData } actionData={ actionData }/> } { type !== 'ERC-20' && (
<TokenNftMarketplaces
hash={ hash }
isLoading={ tokenQuery.isPlaceholderData }
actionData={ actionData }
source="NFT collection"
isExperiment={ isExperiment }
/>
) }
{ (type !== 'ERC-20' && config.UI.views.nft.marketplaces.length === 0 && actionData) && ( { (type !== 'ERC-20' && config.UI.views.nft.marketplaces.length === 0 && actionData && isExperiment) && (
<DetailsInfoItem <DetailsInfoItem
title="Dapp" title="Dapp"
hint="Link to the dapp" hint="Link to the dapp"
alignSelf="center" alignSelf="center"
py={ 1 } py={ 1 }
> >
<ActionButton data={ actionData } height="30px"/> <ActionButton data={ actionData } height="30px" source="NFT collection"/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
......
...@@ -13,9 +13,11 @@ interface Props { ...@@ -13,9 +13,11 @@ interface Props {
id?: string; id?: string;
isLoading?: boolean; isLoading?: boolean;
actionData?: AddressMetadataTagFormatted['meta']; actionData?: AddressMetadataTagFormatted['meta'];
source: 'NFT collection' | 'NFT item';
isExperiment?: boolean;
} }
const TokenNftMarketplaces = ({ hash, id, isLoading, actionData }: Props) => { const TokenNftMarketplaces = ({ hash, id, isLoading, actionData, source, isExperiment }: Props) => {
if (!hash || config.UI.views.nft.marketplaces.length === 0) { if (!hash || config.UI.views.nft.marketplaces.length === 0) {
return null; return null;
} }
...@@ -26,7 +28,7 @@ const TokenNftMarketplaces = ({ hash, id, isLoading, actionData }: Props) => { ...@@ -26,7 +28,7 @@ const TokenNftMarketplaces = ({ hash, id, isLoading, actionData }: Props) => {
hint="Marketplaces trading this NFT" hint="Marketplaces trading this NFT"
alignSelf="center" alignSelf="center"
isLoading={ isLoading } isLoading={ isLoading }
py={ actionData ? 1 : 2 } py={ (actionData && isExperiment) ? 1 : 2 }
> >
<Skeleton isLoaded={ !isLoading } display="flex" columnGap={ 3 } flexWrap="wrap" alignItems="center"> <Skeleton isLoaded={ !isLoading } display="flex" columnGap={ 3 } flexWrap="wrap" alignItems="center">
{ config.UI.views.nft.marketplaces.map((item) => { { config.UI.views.nft.marketplaces.map((item) => {
...@@ -47,10 +49,10 @@ const TokenNftMarketplaces = ({ hash, id, isLoading, actionData }: Props) => { ...@@ -47,10 +49,10 @@ const TokenNftMarketplaces = ({ hash, id, isLoading, actionData }: Props) => {
</Tooltip> </Tooltip>
); );
}) } }) }
{ actionData && ( { (actionData && isExperiment) && (
<> <>
<TextSeparator color="gray.500" margin={ 0 }/> <TextSeparator color="gray.500" margin={ 0 }/>
<ActionButton data={ actionData } height="30px"/> <ActionButton data={ actionData } height="30px" source={ source }/>
</> </>
) } ) }
</Skeleton> </Skeleton>
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import type { TokenInfo, TokenInstance } from 'types/api/token'; import type { TokenInfo, TokenInstance } from 'types/api/token';
import config from 'configs/app'; import config from 'configs/app';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import ActionButton from 'ui/shared/ActionButton/ActionButton'; import ActionButton from 'ui/shared/ActionButton/ActionButton';
import useActionData from 'ui/shared/ActionButton/useActionData'; import useActionData from 'ui/shared/ActionButton/useActionData';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -28,6 +29,7 @@ interface Props { ...@@ -28,6 +29,7 @@ interface Props {
const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => { const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
const actionData = useActionData(token?.address); const actionData = useActionData(token?.address);
const { value: isExperiment } = useFeatureValue('action_button_exp', false);
const handleCounterItemClick = React.useCallback(() => { const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => { window.setTimeout(() => {
...@@ -76,15 +78,22 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => { ...@@ -76,15 +78,22 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
</Flex> </Flex>
</DetailsInfoItem> </DetailsInfoItem>
<TokenInstanceTransfersCount hash={ isLoading ? '' : token.address } id={ isLoading ? '' : data.id } onClick={ handleCounterItemClick }/> <TokenInstanceTransfersCount hash={ isLoading ? '' : token.address } id={ isLoading ? '' : data.id } onClick={ handleCounterItemClick }/>
<TokenNftMarketplaces isLoading={ isLoading } hash={ token.address } id={ data.id } actionData={ actionData }/> <TokenNftMarketplaces
{ (config.UI.views.nft.marketplaces.length === 0 && actionData) && ( isLoading={ isLoading }
hash={ token.address }
id={ data.id }
actionData={ actionData }
source="NFT item"
isExperiment={ isExperiment }
/>
{ (config.UI.views.nft.marketplaces.length === 0 && actionData && isExperiment) && (
<DetailsInfoItem <DetailsInfoItem
title="Dapp" title="Dapp"
hint="Link to the dapp" hint="Link to the dapp"
alignSelf="center" alignSelf="center"
py={ 1 } py={ 1 }
> >
<ActionButton data={ actionData } height="30px"/> <ActionButton data={ actionData } height="30px" source="NFT item"/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
</Grid> </Grid>
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useFeatureValue from 'lib/growthbook/useFeatureValue';
import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate';
import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
...@@ -29,6 +30,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { ...@@ -29,6 +30,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves'; const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves';
const actionData = useActionData(txQuery.data?.to?.hash); const actionData = useActionData(txQuery.data?.to?.hash);
const { value: isExperiment } = useFeatureValue('action_button_exp', false);
const txInterpretationQuery = useApiQuery('tx_interpretation', { const txInterpretationQuery = useApiQuery('tx_interpretation', {
pathParams: { hash }, pathParams: { hash },
...@@ -120,7 +122,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { ...@@ -120,7 +122,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
mt={{ base: 3, lg: 0 }} mt={{ base: 3, lg: 0 }}
> >
{ !hasTag && <AccountActionsMenu/> } { !hasTag && <AccountActionsMenu/> }
<ActionButton data={ actionData } txHash={ hash }/> { (actionData && isExperiment) && <ActionButton data={ actionData } txHash={ hash } source="Txn"/> }
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 0, lg: 'auto' }}/> <NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 0, lg: 'auto' }}/>
</Flex> </Flex>
</Box> </Box>
......
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