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