Commit 1a24ebc9 authored by tom goriunov's avatar tom goriunov Committed by GitHub

More events for mixpanel analytics (#1160)

* color mode property for PageView and AccountAccess events

* account events

* contract events

* address events

* event for more button
parent d2e00dd1
import type { Route } from 'nextjs-routes';
const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/': 'Homepage',
'/txs': 'Transactions',
'/tx/[hash]': 'Transaction details',
......
import { useColorMode } from '@chakra-ui/react';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -16,6 +17,7 @@ export default function useLogPageView(isInited: boolean) {
const tab = getQueryParamString(router.query.tab);
const page = getQueryParamString(router.query.page);
const { colorMode } = useColorMode();
React.useEffect(() => {
if (!config.features.mixpanel.isEnabled || !isInited) {
......@@ -26,11 +28,12 @@ export default function useLogPageView(isInited: boolean) {
'Page type': getPageType(router.pathname),
Tab: getTabName(tab),
Page: page || undefined,
'Color mode': colorMode,
});
// these are only deps that should trigger the effect
// in some scenarios page type is not changing (e.g navigation from one address page to another),
// but we still want to log page view
// so we use pathname from 'next/navigation' instead of router.pathname from 'next/router' as deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isInited, page, pathname, tab ]);
}, [ isInited, page, pathname, tab, colorMode ]);
}
......@@ -4,6 +4,15 @@ export enum EventTypes {
PAGE_VIEW = 'Page view',
SEARCH_QUERY = 'Search query',
ADD_TO_WALLET = 'Add to wallet',
ACCOUNT_ACCESS = 'Account access',
PRIVATE_TAG = 'Private tag',
VERIFY_ADDRESS = 'Verify address',
VERIFY_TOKEN = 'Verify token',
WALLET_CONNECT = 'Wallet connect',
CONTRACT_INTERACTION = 'Contract interaction',
CONTRACT_VERIFICATION = 'Contract verification',
QR_CODE = 'QR code',
PAGE_WIDGET = 'Page widget',
}
/* eslint-disable @typescript-eslint/indent */
......@@ -13,6 +22,7 @@ Type extends EventTypes.PAGE_VIEW ?
'Page type': string;
'Tab': string;
'Page'?: string;
'Color mode': 'light' | 'dark';
} :
Type extends EventTypes.SEARCH_QUERY ? {
'Search query': string;
......@@ -29,5 +39,43 @@ Type extends EventTypes.ADD_TO_WALLET ? (
'Token': string;
}
) :
Type extends EventTypes.ACCOUNT_ACCESS ? {
'Action': 'Auth0 init' | 'Verification email resent' | 'Logged out';
} :
Type extends EventTypes.PRIVATE_TAG ? {
'Action': 'Form opened' | 'Submit';
'Page type': string;
'Tag type': 'Address' | 'Tx';
} :
Type extends EventTypes.VERIFY_ADDRESS ? (
{
'Action': 'Form opened' | 'Address entered';
'Page type': string;
} | {
'Action': 'Sign ownership';
'Page type': string;
'Sign method': 'wallet' | 'manual';
}
) :
Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit';
} :
Type extends EventTypes.WALLET_CONNECT ? {
'Status': 'Started' | 'Connected';
} :
Type extends EventTypes.CONTRACT_INTERACTION ? {
'Method type': 'Read' | 'Write';
'Method name': string;
} :
Type extends EventTypes.CONTRACT_VERIFICATION ? {
'Method': string;
'Status': 'Method selected' | 'Finished';
} :
Type extends EventTypes.QR_CODE ? {
'Page type': string;
} :
Type extends EventTypes.PAGE_WIDGET ? {
'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)';
} :
undefined;
/* eslint-enable @typescript-eslint/indent */
......@@ -4,11 +4,11 @@ import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
const ContractConnectWallet = () => {
const { open, isOpen } = useWeb3Modal();
const { address, isDisconnected } = useAccount();
const { disconnect } = useDisconnect();
const isMobile = useIsMobile();
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
......@@ -17,12 +17,19 @@ const ContractConnectWallet = () => {
setIsModalOpening(true);
await open();
setIsModalOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Started' });
}, [ open ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
!isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Connected' });
}, []);
const handleDisconnect = React.useCallback(() => {
disconnect();
}, [ disconnect ]);
const { address, isDisconnected } = useAccount({ onConnect: handleAccountConnected });
const content = (() => {
if (isDisconnected || !address) {
return (
......
......@@ -8,6 +8,7 @@ import type { MethodFormFields, ContractMethodCallResult } from './types';
import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract';
import arrowIcon from 'icons/arrows/down-right.svg';
import * as mixpanel from 'lib/mixpanel/index';
import ContractMethodField from './ContractMethodField';
......@@ -105,8 +106,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
.catch((error) => {
setResult(error?.error || error?.data || (error?.reason && { message: error.reason }) || error);
setLoading(false);
})
.finally(() => {
mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_INTERACTION, {
'Method type': isWrite ? 'Write' : 'Read',
'Method name': 'name' in data ? data.name : 'Fallback',
});
});
}, [ onSubmit, data, inputs ]);
}, [ inputs, onSubmit, data, isWrite ]);
return (
<Box>
......
......@@ -8,6 +8,7 @@ import starOutlineIcon from 'icons/star_outline.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import * as mixpanel from 'lib/mixpanel/index';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
......@@ -29,6 +30,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
return;
}
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
!watchListId && mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Add to watchlist' });
}, [ isAccountActionAllowed, watchListId, deleteModalProps, addModalProps ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => {
......
......@@ -14,11 +14,14 @@ import {
Skeleton,
} from '@chakra-ui/react';
import * as Sentry from '@sentry/react';
import { useRouter } from 'next/router';
import QRCode from 'qrcode';
import React from 'react';
import qrCodeIcon from 'icons/qr_code.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import getPageType from 'lib/mixpanel/getPageType';
import * as mixpanel from 'lib/mixpanel/index';
const SVG_OPTIONS = {
margin: 0,
......@@ -33,9 +36,13 @@ interface Props {
const AddressQrCode = ({ hash, className, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useIsMobile();
const router = useRouter();
const [ qr, setQr ] = React.useState('');
const [ error, setError ] = React.useState('');
const pageType = getPageType(router.pathname);
React.useEffect(() => {
if (isOpen) {
QRCode.toString(hash, SVG_OPTIONS, (error: Error | null | undefined, svg: string) => {
......@@ -47,9 +54,10 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
setError('');
setQr(svg);
mixpanel.logEvent(mixpanel.EventTypes.QR_CODE, { 'Page type': pageType });
});
}
}, [ hash, isOpen, onClose ]);
}, [ hash, isOpen, onClose, pageType ]);
if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
......
......@@ -11,6 +11,7 @@ import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -38,6 +39,11 @@ const TokenSelect = ({ onClick }: Props) => {
const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
const handleIconButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Tokens show all (icon)' });
onClick?.();
}, [ onClick ]);
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) {
refetch();
......@@ -97,7 +103,7 @@ const TokenSelect = ({ onClick }: Props) => {
pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
onClick={ onClick }
onClick={ handleIconButtonClick }
/>
</NextLink>
</Box>
......
......@@ -5,6 +5,7 @@ import type { FormattedData } from './types';
import arrowIcon from 'icons/arrows/east-mini.svg';
import tokensIcon from 'icons/tokens.svg';
import * as mixpanel from 'lib/mixpanel/index';
import { getTokensTotalInfo } from '../utils/tokenUtils';
......@@ -25,6 +26,8 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea
if (isLoading && !isOpen) {
return;
}
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Tokens dropdown' });
onClick();
}, [ isLoading, isOpen, onClick ]);
......
......@@ -5,6 +5,7 @@ import type { AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess
import type { VerifiedAddress } from 'types/api/account';
import eastArrowIcon from 'icons/arrows/east.svg';
import * as mixpanel from 'lib/mixpanel/index';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress';
......@@ -20,22 +21,38 @@ interface Props {
onAddTokenInfoClick: (address: string) => void;
onShowListClick: () => void;
defaultAddress?: string;
pageType: string;
}
const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, onAddTokenInfoClick, onShowListClick }: Props) => {
const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, onAddTokenInfoClick, onShowListClick, pageType }: Props) => {
const [ stepIndex, setStepIndex ] = React.useState(0);
const [ data, setData ] = React.useState<StateData>({ address: '', signingMessage: '' });
React.useEffect(() => {
isOpen && mixpanel.logEvent(
mixpanel.EventTypes.VERIFY_ADDRESS,
{ Action: 'Form opened', 'Page type': pageType },
);
}, [ isOpen, pageType ]);
const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => {
setData(firstStepResult);
setStepIndex((prev) => prev + 1);
}, []);
mixpanel.logEvent(
mixpanel.EventTypes.VERIFY_ADDRESS,
{ Action: 'Address entered', 'Page type': pageType },
);
}, [ pageType ]);
const handleGoToThirdStep = React.useCallback((address: VerifiedAddress) => {
const handleGoToThirdStep = React.useCallback((address: VerifiedAddress, signMethod: 'wallet' | 'manual') => {
onSubmit(address);
setStepIndex((prev) => prev + 1);
setData((prev) => ({ ...prev, isToken: Boolean(address.metadata.tokenName) }));
}, [ onSubmit ]);
mixpanel.logEvent(
mixpanel.EventTypes.VERIFY_ADDRESS,
{ Action: 'Sign ownership', 'Page type': pageType, 'Sign method': signMethod },
);
}, [ onSubmit, pageType ]);
const handleGoToPrevStep = React.useCallback(() => {
setStepIndex((prev) => prev - 1);
......
......@@ -26,13 +26,15 @@ import AddressVerificationFieldSignature from '../fields/AddressVerificationFiel
type Fields = RootFields & AddressVerificationFormSecondStepFields;
type SignMethod = 'wallet' | 'manual';
interface Props extends AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess{
onContinue: (newItem: VerifiedAddress) => void;
onContinue: (newItem: VerifiedAddress, signMethod: SignMethod) => void;
noWeb3Provider?: boolean;
}
const AddressVerificationStepSignature = ({ address, signingMessage, contractCreator, contractOwner, onContinue, noWeb3Provider }: Props) => {
const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>(noWeb3Provider ? 'manually' : 'wallet');
const [ signMethod, setSignMethod ] = React.useState<SignMethod>(noWeb3Provider ? 'manual' : 'wallet');
const { open: openWeb3Modal } = useWeb3Modal();
const { isConnected } = useAccount();
......@@ -70,11 +72,11 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
return setError('root', { type, message: response.status === 'INVALID_SIGNER_ERROR' ? response.invalidSigner.signer : undefined });
}
onContinue(response.result.verifiedAddress);
onContinue(response.result.verifiedAddress, signMethod);
} catch (error) {
setError('root', { type: 'UNKNOWN_STATUS' });
}
}, [ address, apiFetch, onContinue, setError ]);
}, [ address, apiFetch, onContinue, setError, signMethod ]);
const onSubmit = handleSubmit(onFormSubmit);
......@@ -115,7 +117,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
}, [ clearErrors, onSubmit ]);
const button = (() => {
if (signMethod === 'manually') {
if (signMethod === 'manual') {
return (
<Button
size="lg"
......@@ -220,7 +222,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
<Radio value="manually">Sign manually</Radio>
</RadioGroup>
) }
{ signMethod === 'manually' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
{ signMethod === 'manual' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
</Flex>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
{ button }
......
......@@ -12,6 +12,7 @@ import { route } from 'nextjs-routes';
import useApiFetch from 'lib/api/useApiFetch';
import delay from 'lib/delay';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -23,7 +24,7 @@ import ContractVerificationStandardInput from './methods/ContractVerificationSta
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import ContractVerificationVyperStandardInput from './methods/ContractVerificationVyperStandardInput';
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils';
import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS } from './utils';
interface Props {
method?: SmartContractVerificationMethod;
......@@ -38,6 +39,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
});
const { control, handleSubmit, watch, formState, setError, reset } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>();
const apiFetch = useApiFetch();
const toast = useToast();
......@@ -80,6 +82,12 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
isClosable: true,
});
mixpanel.logEvent(
mixpanel.EventTypes.CONTRACT_VERIFICATION,
{ Status: 'Finished', Method: methodNameRef.current || '' },
{ send_immediately: true },
);
window.location.assign(route({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }));
}, [ hash, setError, toast ]);
......@@ -135,6 +143,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
useUpdateEffect(() => {
if (methodValue) {
reset(getDefaultValues(methodValue, config));
const methodName = METHOD_LABELS[methodValue];
mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName });
methodNameRef.current = methodName;
}
// !!! should run only when method is changed
}, [ methodValue ]);
......
......@@ -7,6 +7,7 @@ import dayjs from 'lib/date/dayjs';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index';
interface Props {
email?: string; // TODO: obtain email from API
......@@ -22,8 +23,14 @@ const UnverifiedEmail = ({ email }: Props) => {
setIsLoading(true);
mixpanel.logEvent(
mixpanel.EventTypes.ACCOUNT_ACCESS,
{ Action: 'Verification email resent' },
);
try {
await apiFetch('email_resend');
toast({
id: toastId,
position: 'top-right',
......
......@@ -8,6 +8,7 @@ import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, Veri
import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
......@@ -194,6 +195,7 @@ const VerifiedAddresses = () => {
/>
{ addButton }
<AddressVerificationModal
pageType={ PAGE_TYPE_DICT['/account/verified-addresses'] }
isOpen={ modalProps.isOpen }
onClose={ modalProps.onClose }
onSubmit={ handleAddressSubmit }
......
......@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
import type { AddressTag } from 'types/api/account';
import * as mixpanel from 'lib/mixpanel/index';
import FormModal from 'ui/shared/FormModal';
import AddressForm from './AddressForm';
......@@ -11,17 +12,35 @@ type Props = {
onClose: () => void;
onSuccess: () => Promise<void>;
data?: Partial<AddressTag>;
pageType: string;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data }) => {
const AddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data, pageType }) => {
const title = data?.id ? 'Edit address tag' : 'New address tag';
const text = !data?.id ? 'Label any address with a private address tag (up to 35 chars) to customize your explorer experience.' : '';
const [ isAlertVisible, setAlertVisible ] = useState(false);
React.useEffect(() => {
isOpen && !data?.id && mixpanel.logEvent(
mixpanel.EventTypes.PRIVATE_TAG,
{ Action: 'Form opened', 'Page type': pageType, 'Tag type': 'Address' },
);
}, [ data?.id, isOpen, pageType ]);
const handleSuccess = React.useCallback(() => {
if (!data?.id) {
mixpanel.logEvent(
mixpanel.EventTypes.PRIVATE_TAG,
{ Action: 'Submit', 'Page type': pageType, 'Tag type': 'Address' },
);
}
return onSuccess();
}, [ data?.id, onSuccess, pageType ]);
const renderForm = useCallback(() => {
return <AddressForm data={ data } onClose={ onClose } onSuccess={ onSuccess } setAlertVisible={ setAlertVisible }/>;
}, [ data, onClose, onSuccess ]);
return <AddressForm data={ data } onClose={ onClose } onSuccess={ handleSuccess } setAlertVisible={ setAlertVisible }/>;
}, [ data, onClose, handleSuccess ]);
return (
<FormModal<AddressTag>
isOpen={ isOpen }
......
......@@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -94,7 +95,13 @@ const PrivateAddressTags = () => {
Add address tag
</Button>
</Skeleton>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData } onSuccess={ onAddOrEditSuccess }/>
<AddressModal
{ ...addressModalProps }
data={ addressModalData }
pageType={ PAGE_TYPE_DICT['/account/tag-address'] }
onClose={ onAddressModalClose }
onSuccess={ onAddOrEditSuccess }
/>
{ deleteModalData && (
<DeletePrivateTagModal
{ ...deleteModalProps }
......
......@@ -23,6 +23,7 @@ const TAG_MAX_LENGTH = 35;
type Props = {
data?: TransactionTag;
onClose: () => void;
onSuccess: () => Promise<void>;
setAlertVisible: (isAlertVisible: boolean) => void;
}
......@@ -31,7 +32,7 @@ type Inputs = {
tag: string;
}
const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const TransactionForm: React.FC<Props> = ({ data, onClose, onSuccess, setAlertVisible }) => {
const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
......@@ -74,11 +75,11 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
setAlertVisible(true);
}
},
onSuccess: () => {
queryClient.refetchQueries([ resourceKey('private_tags_tx') ]).then(() => {
onClose();
setPending(false);
});
onSuccess: async() => {
await queryClient.refetchQueries([ resourceKey('private_tags_tx') ]);
await onSuccess();
onClose();
setPending(false);
},
});
......
......@@ -2,6 +2,8 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTag } from 'types/api/account';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import * as mixpanel from 'lib/mixpanel/index';
import FormModal from 'ui/shared/FormModal';
import TransactionForm from './TransactionForm';
......@@ -18,9 +20,25 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const [ isAlertVisible, setAlertVisible ] = useState(false);
React.useEffect(() => {
isOpen && !data?.id && mixpanel.logEvent(
mixpanel.EventTypes.PRIVATE_TAG,
{ Action: 'Form opened', 'Page type': PAGE_TYPE_DICT['/account/tag-address'], 'Tag type': 'Tx' },
);
}, [ data?.id, isOpen ]);
const handleSuccess = React.useCallback(async() => {
if (!data?.id) {
mixpanel.logEvent(
mixpanel.EventTypes.PRIVATE_TAG,
{ Action: 'Submit', 'Page type': PAGE_TYPE_DICT['/account/tag-address'], 'Tag type': 'Tx' },
);
}
}, [ data?.id ]);
const renderForm = useCallback(() => {
return <TransactionForm data={ data } onClose={ onClose } setAlertVisible={ setAlertVisible }/>;
}, [ data, onClose ]);
return <TransactionForm data={ data } onClose={ onClose } onSuccess={ handleSuccess } setAlertVisible={ setAlertVisible }/>;
}, [ data, handleSuccess, onClose ]);
return (
<FormModal<TransactionTag>
isOpen={ isOpen }
......
......@@ -5,6 +5,7 @@ import React from 'react';
import config from 'configs/app';
import iconArrow from 'icons/arrows/east-mini.svg';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
import PrivateTagMenuItem from './PrivateTagMenuItem';
......@@ -22,6 +23,10 @@ const AddressActions = ({ isLoading }: Props) => {
const isTokenPage = router.pathname === '/token/[hash]';
const isAccountActionAllowed = useIsAccountActionAllowed();
const handleButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' });
}, []);
return (
<Menu>
<Skeleton isLoaded={ !isLoading } ml={ 2 } borderRadius="base">
......@@ -29,6 +34,7 @@ const AddressActions = ({ isLoading }: Props) => {
as={ Button }
size="sm"
variant="outline"
onClick={ handleButtonClick }
>
<Flex alignItems="center">
<span>More</span>
......
import { MenuItem, Icon, chakra, useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address } from 'types/api/address';
import iconPrivateTags from 'icons/privattags.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import getPageType from 'lib/mixpanel/getPageType';
import PrivateTagModal from 'ui/privateTags/AddressModal/AddressModal';
interface Props {
......@@ -17,6 +19,7 @@ interface Props {
const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const modal = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
const queryKey = getResourceKey('address', { pathParams: { hash } });
const addressData = queryClient.getQueryData<Address>(queryKey);
......@@ -44,13 +47,21 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
return null;
}
const pageType = getPageType(router.pathname);
return (
<>
<MenuItem className={ className } onClick={ handleClick }>
<Icon as={ iconPrivateTags } boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
<PrivateTagModal isOpen={ modal.isOpen } onClose={ modal.onClose } onSuccess={ handleAddPrivateTag } data={ formData }/>
<PrivateTagModal
data={ formData }
pageType={ pageType }
isOpen={ modal.isOpen }
onClose={ modal.onClose }
onSuccess={ handleAddPrivateTag }
/>
</>
);
};
......
......@@ -8,6 +8,7 @@ import config from 'configs/app';
import iconEdit from 'icons/edit.svg';
import useApiQuery from 'lib/api/useApiQuery';
import useHasAccount from 'lib/hooks/useHasAccount';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
interface Props {
......@@ -93,6 +94,7 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => {
{ content }
<AddressVerificationModal
defaultAddress={ hash }
pageType={ PAGE_TYPE_DICT['/token/[hash]'] }
isOpen={ modal.isOpen }
onClose={ modal.onClose }
onSubmit={ handleVerifiedAddressSubmit }
......
......@@ -5,6 +5,7 @@ import type { UserInfo } from 'types/api/account';
import config from 'configs/app';
import useNavItems from 'lib/hooks/useNavItems';
import * as mixpanel from 'lib/mixpanel/index';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/snippets/navigation/NavLink';
......@@ -18,6 +19,14 @@ const ProfileMenuContent = ({ data }: Props) => {
const { accountNavItems, profileItem } = useNavItems();
const primaryTextColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
const handleSingOutClick = React.useCallback(() => {
mixpanel.logEvent(
mixpanel.EventTypes.ACCOUNT_ACCESS,
{ Action: 'Logged out' },
{ send_immediately: true },
);
}, []);
if (!feature.isEnabled) {
return null;
}
......@@ -52,7 +61,9 @@ const ProfileMenuContent = ({ data }: Props) => {
</VStack>
</Box>
<Box mt={ 2 } pt={ 3 } borderTopColor="divider" borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<Button size="sm" width="full" variant="outline" as="a" href={ feature.logoutUrl }>Sign Out</Button>
<Button size="sm" width="full" variant="outline" as="a" href={ feature.logoutUrl } onClick={ handleSingOutClick }>
Sign Out
</Button>
</Box>
</Box>
);
......
......@@ -4,6 +4,7 @@ import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useLoginUrl from 'lib/hooks/useLoginUrl';
import * as mixpanel from 'lib/mixpanel/index';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
......@@ -18,6 +19,14 @@ const ProfileMenuDesktop = () => {
}
}, [ data, error?.status, isLoading ]);
const handleSignInClick = React.useCallback(() => {
mixpanel.logEvent(
mixpanel.EventTypes.ACCOUNT_ACCESS,
{ Action: 'Auth0 init' },
{ send_immediately: true },
);
}, []);
const buttonProps: Partial<ButtonProps> = (() => {
if (hasMenu || !loginUrl) {
return {};
......@@ -26,6 +35,7 @@ const ProfileMenuDesktop = () => {
return {
as: 'a',
href: loginUrl,
onClick: handleSignInClick,
};
})();
......
......@@ -4,6 +4,7 @@ import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useLoginUrl from 'lib/hooks/useLoginUrl';
import * as mixpanel from 'lib/mixpanel/index';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
......@@ -14,6 +15,14 @@ const ProfileMenuMobile = () => {
const loginUrl = useLoginUrl();
const [ hasMenu, setHasMenu ] = React.useState(false);
const handleSignInClick = React.useCallback(() => {
mixpanel.logEvent(
mixpanel.EventTypes.ACCOUNT_ACCESS,
{ Action: 'Auth0 init' },
{ send_immediately: true },
);
}, []);
React.useEffect(() => {
if (!isLoading) {
setHasMenu(Boolean(data));
......@@ -28,6 +37,7 @@ const ProfileMenuMobile = () => {
return {
as: 'a',
href: loginUrl,
onClick: handleSignInClick,
};
})();
......
......@@ -12,6 +12,7 @@ import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useToast from 'lib/hooks/useToast';
import useUpdateEffect from 'lib/hooks/useUpdateEffect';
import * as mixpanel from 'lib/mixpanel/index';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -44,6 +45,7 @@ interface Props {
const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) => {
const containerRef = React.useRef<HTMLFormElement>(null);
const openEventSent = React.useRef<boolean>(false);
const apiFetch = useApiFetch();
const toast = useToast();
......@@ -58,6 +60,13 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
});
const { handleSubmit, formState, control, trigger } = formApi;
React.useEffect(() => {
if (!application?.id && !openEventSent.current) {
mixpanel.logEvent(mixpanel.EventTypes.VERIFY_TOKEN, { Action: 'Form opened' });
openEventSent.current = true;
}
}, [ application?.id ]);
const onFormSubmit: SubmitHandler<Fields> = React.useCallback(async(data) => {
try {
const submission = prepareRequestBody(data);
......@@ -73,6 +82,11 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
if ('id' in result) {
onSubmit(result);
if (!application?.id) {
mixpanel.logEvent(mixpanel.EventTypes.VERIFY_TOKEN, { Action: 'Submit' });
}
} else {
throw result;
}
......
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