Commit 8ef67799 authored by tom's avatar tom

add mixpanel events

parent d75b4b4e
......@@ -6,11 +6,13 @@ export enum EventTypes {
SEARCH_QUERY = 'Search query',
LOCAL_SEARCH = 'Local search',
ADD_TO_WALLET = 'Add to wallet',
ACCOUNT_ACCESS = 'Account access', // deprecated
ACCOUNT_ACCESS = 'Account access',
LOGIN = 'Login',
ACCOUNT_LINK_INFO = 'Account link info',
PRIVATE_TAG = 'Private tag',
VERIFY_ADDRESS = 'Verify address',
VERIFY_TOKEN = 'Verify token',
WALLET_CONNECT = 'Wallet connect', // 🦆
WALLET_CONNECT = 'Wallet connect',
WALLET_ACTION = 'Wallet action',
CONTRACT_INTERACTION = 'Contract interaction',
CONTRACT_VERIFICATION = 'Contract verification',
......@@ -54,7 +56,27 @@ Type extends EventTypes.ADD_TO_WALLET ? (
}
) :
Type extends EventTypes.ACCOUNT_ACCESS ? {
'Action': 'Logged out';
'Action': 'Dropdown open' | 'Logged out';
} :
Type extends EventTypes.LOGIN ? (
{
'Action': 'Started';
'Source': string;
} | {
'Action': 'Wallet' | 'Email';
'Source': 'Options selector';
} | {
'Action': 'OTP sent';
'Source': 'Email';
} | {
'Action': 'Success';
'Source': 'Email' | 'Wallet';
}
) :
Type extends EventTypes.ACCOUNT_LINK_INFO ? {
'Source': 'Profile' | 'Login modal' | 'Profile dropdown';
'Status': 'Started' | 'OTP sent' | 'Finished';
'Type': 'Email' | 'Wallet';
} :
Type extends EventTypes.PRIVATE_TAG ? {
'Action': 'Form opened' | 'Submit';
......@@ -75,7 +97,7 @@ Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit';
} :
Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts' | 'Swap button';
'Source': 'Header' | 'Login' | 'Profile' | 'Profile dropdown' | 'Smart contracts' | 'Swap button';
'Status': 'Started' | 'Connected';
} :
Type extends EventTypes.WALLET_ACTION ? (
......
......@@ -13,6 +13,15 @@ import AuthModal from 'ui/snippets/auth/AuthModal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
const MIXPANEL_CONFIG = {
wallet_connect: {
source: 'Profile' as const,
},
account_link_info: {
source: 'Profile' as const,
},
};
const MyProfile = () => {
const [ authInitialScreen, setAuthInitialScreen ] = React.useState<Screen>();
const authModal = useDisclosure();
......@@ -38,9 +47,11 @@ const MyProfile = () => {
<>
<Flex maxW="480px" mt={ 8 } flexDir="column" rowGap={ 12 }>
<MyProfileEmail profileQuery={ profileQuery }/>
{ config.features.blockchainInteraction.isEnabled && <MyProfileWallet profileQuery={ profileQuery } onAddWallet={ handleAddWalletClick }/> }
{ config.features.blockchainInteraction.isEnabled &&
<MyProfileWallet profileQuery={ profileQuery } onAddWallet={ handleAddWalletClick }/> }
</Flex>
{ authModal.isOpen && authInitialScreen && <AuthModal initialScreen={ authInitialScreen } onClose={ authModal.onClose }/> }
{ authModal.isOpen && authInitialScreen &&
<AuthModal initialScreen={ authInitialScreen } onClose={ authModal.onClose } mixpanelConfig={ MIXPANEL_CONFIG }/> }
</>
);
})();
......
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { Screen, ScreenSuccess } from './types';
import * as mixpanel from 'lib/mixpanel';
import IconSvg from 'ui/shared/IconSvg';
import AuthModalScreenConnectWallet from './screens/AuthModalScreenConnectWallet';
......@@ -16,13 +18,38 @@ import useProfileQuery from './useProfileQuery';
interface Props {
initialScreen: Screen;
onClose: (isSuccess?: boolean) => void;
mixpanelConfig?: {
'wallet_connect': {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
};
'account_link_info': {
source: mixpanel.EventPayload<mixpanel.EventTypes.ACCOUNT_LINK_INFO>['Source'];
};
};
}
const AuthModal = ({ initialScreen, onClose }: Props) => {
const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
const [ steps, setSteps ] = React.useState<Array<Screen>>([ initialScreen ]);
const [ isSuccess, setIsSuccess ] = React.useState(false);
const router = useRouter();
const profileQuery = useProfileQuery();
React.useEffect(() => {
if ('isAuth' in initialScreen && initialScreen.isAuth) {
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
Status: 'Started',
Type: initialScreen.type === 'connect_wallet' ? 'Wallet' : 'Email',
Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown',
});
} else {
mixpanel.logEvent(mixpanel.EventTypes.LOGIN, {
Action: 'Started',
Source: mixpanel.getPageType(router.pathname),
});
}
}, [ initialScreen, mixpanelConfig, router.pathname ]);
const onNextStep = React.useCallback((screen: Screen) => {
setSteps((prev) => [ ...prev, screen ]);
}, []);
......@@ -37,12 +64,26 @@ const AuthModal = ({ initialScreen, onClose }: Props) => {
const onAuthSuccess = React.useCallback(async(screen: ScreenSuccess) => {
setIsSuccess(true);
if ('isAuth' in initialScreen && initialScreen.isAuth) {
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
Status: 'Finished',
Type: screen.type === 'success_wallet' ? 'Wallet' : 'Email',
Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown',
});
} else {
mixpanel.logEvent(mixpanel.EventTypes.LOGIN, {
Action: 'Success',
Source: screen.type === 'success_wallet' ? 'Wallet' : 'Email',
});
}
const { data } = await profileQuery.refetch();
if (data) {
onNextStep({ ...screen, profile: data });
}
// TODO @tom2drum handle error case
}, [ onNextStep, profileQuery ]);
}, [ initialScreen, mixpanelConfig?.account_link_info.source, onNextStep, profileQuery ]);
const onModalClose = React.useCallback(() => {
onClose(isSuccess);
......@@ -71,9 +112,22 @@ const AuthModal = ({ initialScreen, onClose }: Props) => {
case 'select_method':
return <AuthModalScreenSelectMethod onSelectMethod={ onNextStep }/>;
case 'connect_wallet':
return <AuthModalScreenConnectWallet onSuccess={ onAuthSuccess } onError={ onReset } isAuth={ currentStep.isAuth }/>;
return (
<AuthModalScreenConnectWallet
onSuccess={ onAuthSuccess }
onError={ onReset }
isAuth={ currentStep.isAuth }
source={ mixpanelConfig?.wallet_connect.source }
/>
);
case 'email':
return <AuthModalScreenEmail onSubmit={ onNextStep } isAuth={ currentStep.isAuth }/>;
return (
<AuthModalScreenEmail
onSubmit={ onNextStep }
isAuth={ currentStep.isAuth }
mixpanelConfig={ mixpanelConfig }
/>
);
case 'otp_code':
return <AuthModalScreenOtpCode email={ currentStep.email } onSuccess={ onAuthSuccess } isAuth={ currentStep.isAuth }/>;
case 'success_email':
......
......@@ -3,15 +3,18 @@ import React from 'react';
import type { ScreenSuccess } from '../types';
import type * as mixpanel from 'lib/mixpanel';
import useSignInWithWallet from '../useSignInWithWallet';
interface Props {
onSuccess: (screen: ScreenSuccess) => void;
onError: (isAuth?: boolean) => void;
isAuth?: boolean;
source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
}
const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth }: Props) => {
const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source }: Props) => {
const isStartedRef = React.useRef(false);
const handleSignInSuccess = React.useCallback(({ address }: { address: string }) => {
......@@ -22,7 +25,7 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth }: Props) =>
onError(isAuth);
}, [ onError, isAuth ]);
const { start } = useSignInWithWallet({ onSuccess: handleSignInSuccess, onError: handleSignInError });
const { start } = useSignInWithWallet({ onSuccess: handleSignInSuccess, onError: handleSignInError, source });
React.useEffect(() => {
if (!isStartedRef.current) {
......
......@@ -8,6 +8,7 @@ import type { EmailFormFields, Screen } from '../types';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/errors/getErrorMessage';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import AuthModalFieldEmail from '../fields/AuthModalFieldEmail';
......@@ -15,9 +16,14 @@ import AuthModalFieldEmail from '../fields/AuthModalFieldEmail';
interface Props {
onSubmit: (screen: Screen) => void;
isAuth?: boolean;
mixpanelConfig?: {
account_link_info: {
source: mixpanel.EventPayload<mixpanel.EventTypes.ACCOUNT_LINK_INFO>['Source'];
};
};
}
const AuthModalScreenEmail = ({ onSubmit, isAuth }: Props) => {
const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
const apiFetch = useApiFetch();
const toast = useToast();
......@@ -40,6 +46,18 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth }: Props) => {
},
})
.then(() => {
if (isAuth) {
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown',
Status: 'OTP sent',
Type: 'Email',
});
} else {
mixpanel.logEvent(mixpanel.EventTypes.LOGIN, {
Action: 'OTP sent',
Source: 'Email',
});
}
onSubmit({ type: 'otp_code', email: formData.email, isAuth });
})
.catch((error) => {
......@@ -49,7 +67,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth }: Props) => {
description: getErrorMessage(error) || 'Something went wrong',
});
});
}, [ apiFetch, onSubmit, toast, isAuth ]);
}, [ apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]);
return (
<FormProvider { ...formApi }>
......
......@@ -3,6 +3,8 @@ import React from 'react';
import type { Screen } from '../types';
import * as mixpanel from 'lib/mixpanel';
interface Props {
onSelectMethod: (screen: Screen) => void;
}
......@@ -10,10 +12,18 @@ interface Props {
const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => {
const handleEmailClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.LOGIN, {
Action: 'Email',
Source: 'Options selector',
});
onSelectMethod({ type: 'email' });
}, [ onSelectMethod ]);
const handleConnectWalletClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.LOGIN, {
Action: 'Wallet',
Source: 'Options selector',
});
onSelectMethod({ type: 'connect_wallet' });
}, [ onSelectMethod ]);
......
......@@ -6,6 +6,7 @@ import type { Route } from 'nextjs-routes';
import { getResourceKey } from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
import * as mixpanel from 'lib/mixpanel';
const PROTECTED_ROUTES: Array<Route['pathname']> = [
'/account/api-key',
......@@ -28,6 +29,8 @@ export default function useLogout() {
exact: true,
});
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_ACCESS, { Action: 'Logged out' }, { send_immediately: true });
if (
PROTECTED_ROUTES.includes(router.pathname) ||
(router.pathname === '/txs' && router.query.tab === 'watchlist')
......
......@@ -6,14 +6,16 @@ import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/errors/getErrorMessage';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel';
import useAccount from 'lib/web3/useAccount';
interface Props {
onSuccess?: ({ address }: { address: string }) => void;
onError?: () => void;
source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
}
function useSignInWithWallet({ onSuccess, onError }: Props) {
function useSignInWithWallet({ onSuccess, onError, source = 'Login' }: Props) {
const [ isPending, setIsPending ] = React.useState(false);
const isConnectingWalletRef = React.useRef(false);
......@@ -54,15 +56,17 @@ function useSignInWithWallet({ onSuccess, onError }: Props) {
} else {
isConnectingWalletRef.current = true;
web3Modal.open();
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
}
}, [ address, proceedToAuth, web3Modal ]);
}, [ address, proceedToAuth, source, web3Modal ]);
React.useEffect(() => {
if (address && isConnectingWalletRef.current) {
isConnectingWalletRef.current = false;
proceedToAuth(address);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
}
}, [ address, isConnected, proceedToAuth ]);
}, [ address, isConnected, proceedToAuth, source ]);
return React.useMemo(() => ({ start, isPending }), [ start, isPending ]);
}
......
......@@ -63,16 +63,12 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending }:
);
}
if (data.email) {
return (
<HStack gap={ 2 }>
<IconSvg name="profile" boxSize={ 5 }/>
<Box display={{ base: 'none', md: 'block' }}>{ getUserHandle(data.email) }</Box>
</HStack>
);
}
return 'Connected';
return (
<HStack gap={ 2 }>
<IconSvg name="profile" boxSize={ 5 }/>
<Box display={{ base: 'none', md: 'block' }}>{ data.email ? getUserHandle(data.email) : 'Profile' }</Box>
</HStack>
);
})();
return (
......
......@@ -12,7 +12,7 @@ interface Props {
}
const UserProfileContentWallet = ({ onClose }: Props) => {
const web3Wallet = useWeb3Wallet({ source: 'Header' });
const web3Wallet = useWeb3Wallet({ source: 'Profile dropdown' });
const web3AccountWithDomain = useWeb3AccountWithDomain(true);
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { Screen } from 'ui/snippets/auth/types';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel';
import Popover from 'ui/shared/chakra/Popover';
import AuthModal from 'ui/snippets/auth/AuthModal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
......@@ -32,6 +33,7 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) =>
const handleProfileButtonClick = React.useCallback(() => {
if (profileQuery.data) {
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_ACCESS, { Action: 'Dropdown open' });
profileMenu.onOpen();
return;
}
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { Screen } from 'ui/snippets/auth/types';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel';
import AuthModal from 'ui/snippets/auth/AuthModal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import useSignInWithWallet from 'ui/snippets/auth/useSignInWithWallet';
......@@ -26,6 +27,7 @@ const UserProfileMobile = () => {
const handleProfileButtonClick = React.useCallback(() => {
if (profileQuery.data) {
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_ACCESS, { Action: 'Dropdown open' });
profileMenu.onOpen();
return;
}
......
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