diff --git a/app/actions/transaction/index.js b/app/actions/transaction/index.js index 7cc0d95a3e0..0d327b73458 100644 --- a/app/actions/transaction/index.js +++ b/app/actions/transaction/index.js @@ -157,3 +157,17 @@ export function setProposedNonce(proposedNonce) { proposedNonce, }; } + +export function setMaxValueMode(maxValueMode) { + return { + type: 'SET_MAX_VALUE_MODE', + maxValueMode, + }; +} + +export function setTransactionValue(value) { + return { + type: 'SET_TRANSACTION_VALUE', + value, + }; +} diff --git a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts index 38e96d6d39e..3b389a7b7b2 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts +++ b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts @@ -2,7 +2,7 @@ // Third party dependencies. import { IconName } from '../../Icons/Icon'; - +import Device from '../../../../util/device'; // Internal dependencies. import { IconByTabBarIconKey, TabBarIconKey } from './TabBar.types'; @@ -13,3 +13,5 @@ export const ICON_BY_TAB_BAR_ICON_KEY: IconByTabBarIconKey = { [TabBarIconKey.Activity]: IconName.Activity, [TabBarIconKey.Setting]: IconName.Setting, }; + +export const TAB_BAR_HEIGHT = Device.isAndroid() ? 62 : 48; diff --git a/app/component-library/components/Navigation/TabBar/TabBar.styles.ts b/app/component-library/components/Navigation/TabBar/TabBar.styles.ts index f74fed8b87c..bbfbad0763b 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.styles.ts +++ b/app/component-library/components/Navigation/TabBar/TabBar.styles.ts @@ -7,7 +7,7 @@ import Device from '../../../../util/device'; // Internal dependencies. import { TabBarStyleSheetVars } from './TabBar.types'; - +import { TAB_BAR_HEIGHT } from './TabBar.constants'; /** * Style sheet function for TabBar component. * @@ -39,7 +39,7 @@ const styleSheet = (params: { vars: TabBarStyleSheetVars; theme: Theme }) => { base: { flexDirection: 'row', alignItems: 'center', - height: Device.isAndroid() ? 62 : 48, + height: TAB_BAR_HEIGHT, paddingHorizontal: 16, marginBottom: bottomInset, backgroundColor: colors.background.default, diff --git a/app/component-library/components/Toast/Toast.tsx b/app/component-library/components/Toast/Toast.tsx index 170b7543897..8c23d38b46d 100644 --- a/app/component-library/components/Toast/Toast.tsx +++ b/app/component-library/components/Toast/Toast.tsx @@ -40,6 +40,7 @@ import { import styles from './Toast.styles'; import { ToastSelectorsIDs } from '../../../../e2e/selectors/wallet/ToastModal.selectors'; import { ButtonProps } from '../Buttons/Button/Button.types'; +import { TAB_BAR_HEIGHT } from '../Navigation/TabBar/TabBar.constants'; const visibilityDuration = 2750; const animationDuration = 250; @@ -53,7 +54,7 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => { const { bottom: bottomNotchSpacing } = useSafeAreaInsets(); const translateYProgress = useSharedValue(screenHeight); const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: translateYProgress.value }], + transform: [{ translateY: translateYProgress.value - TAB_BAR_HEIGHT }], })); const baseStyle: StyleProp>> = useMemo( diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx index 9e1b944bdb6..c7270e4af75 100644 --- a/app/components/UI/AssetOverview/AssetOverview.tsx +++ b/app/components/UI/AssetOverview/AssetOverview.tsx @@ -11,8 +11,9 @@ import AppConstants from '../../../core/AppConstants'; import Engine from '../../../core/Engine'; import { selectChainId, - selectTicker, selectNativeCurrencyByChainId, + selectSelectedNetworkClientId, + selectTicker, } from '../../../selectors/networkController'; import { selectConversionRate, @@ -58,7 +59,7 @@ import Routes from '../../../constants/navigation/Routes'; import TokenDetails from './TokenDetails'; import { RootState } from '../../../reducers'; import useGoToBridge from '../Bridge/utils/useGoToBridge'; -import SwapsController, { swapsUtils } from '@metamask/swaps-controller'; +import { swapsUtils } from '@metamask/swaps-controller'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { getDecimalChainId, @@ -114,6 +115,7 @@ const AssetOverview: React.FC = ({ ? (asset.chainId as Hex) : selectedChainId; const ticker = isPortfolioViewEnabled() ? nativeCurrency : selectedTicker; + const selectedNetworkClientId = useSelector(selectSelectedNetworkClientId); let currentAddress: Hex; @@ -136,12 +138,12 @@ const AssetOverview: React.FC = ({ const dispatch = useDispatch(); useEffect(() => { - const { SwapsController: SwapsControllerFromEngine } = Engine.context as { - SwapsController: SwapsController; - }; + const { SwapsController } = Engine.context; const fetchTokenWithCache = async () => { try { - await SwapsControllerFromEngine.fetchTokenWithCache(); + await SwapsController.fetchTokenWithCache({ + networkClientId: selectedNetworkClientId, + }); // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { @@ -152,7 +154,7 @@ const AssetOverview: React.FC = ({ } }; fetchTokenWithCache(); - }, []); + }, [selectedNetworkClientId]); const onReceive = () => { navigation.navigate(Routes.QR_TAB_SWITCHER, { diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 8c8d941b7bb..435f30d5fe3 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -63,6 +63,7 @@ import { toChecksumHexAddress } from '@metamask/controller-utils'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import { isBtcAccount } from '../../../core/Multichain/utils'; ///: END:ONLY_INCLUDE_IF +import { withMetaMetrics } from '../Stake/utils/metaMetrics/withMetaMetrics'; const trackEvent = (event, params = {}) => { MetaMetrics.getInstance().trackEvent(event); @@ -1949,16 +1950,23 @@ export const getSettingsNavigationOptions = (title, themeColors) => { * @param {String} title - Navbar Title. * @param {NavigationProp} navigation Navigation object returned from useNavigation hook. * @param {ThemeColors} themeColors theme.colors returned from useStyles hook. - * @param {{ backgroundColor?: string, hasCancelButton?: boolean, hasBackButton?: boolean }} [options] - Optional options for navbar. + * @param {{ backgroundColor?: string, hasCancelButton?: boolean, hasBackButton?: boolean }} [navBarOptions] - Optional navbar options. + * @param {{ cancelButtonEvent?: { event: IMetaMetricsEvent, properties: Record }, backButtonEvent?: { event: IMetaMetricsEvent, properties: Record} }} [metricsOptions] - Optional metrics options. * @returns Staking Navbar Component. */ -export function getStakingNavbar(title, navigation, themeColors, options) { - const { hasBackButton = true, hasCancelButton = true } = options ?? {}; +export function getStakingNavbar( + title, + navigation, + themeColors, + navBarOptions, + metricsOptions, +) { + const { hasBackButton = true, hasCancelButton = true } = navBarOptions ?? {}; const innerStyles = StyleSheet.create({ headerStyle: { backgroundColor: - options?.backgroundColor ?? themeColors.background.default, + navBarOptions?.backgroundColor ?? themeColors.background.default, shadowOffset: null, }, headerLeft: { @@ -1978,6 +1986,28 @@ export function getStakingNavbar(title, navigation, themeColors, options) { navigation.goBack(); } + function handleBackPress() { + if (metricsOptions?.backButtonEvent) { + withMetaMetrics(navigationPop, { + event: metricsOptions.backButtonEvent.event, + properties: metricsOptions.backButtonEvent.properties, + }); + } else { + navigationPop(); + } + } + + function handleCancelPress() { + if (metricsOptions?.cancelButtonEvent) { + withMetaMetrics(navigationPop, { + event: metricsOptions.cancelButtonEvent.event, + properties: metricsOptions.cancelButtonEvent.properties, + }); + } else { + navigationPop(); + } + } + return { headerTitle: () => ( @@ -1990,7 +2020,7 @@ export function getStakingNavbar(title, navigation, themeColors, options) { ) : ( @@ -1999,7 +2029,7 @@ export function getStakingNavbar(title, navigation, themeColors, options) { headerRight: () => hasCancelButton ? ( navigation.dangerouslyGetParent()?.pop()} + onPress={handleCancelPress} style={styles.closeButton} > diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx index 0eb0e2ba8de..8a86705d0e8 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx +++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx @@ -13,6 +13,8 @@ import { strings } from '../../../../../../locales/i18n'; import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types'; import UnstakingTimeCard from '../../components/StakingConfirmation/UnstakeTimeCard/UnstakeTimeCard'; import { ScrollView } from 'react-native-gesture-handler'; +import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking'; @@ -23,10 +25,24 @@ const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => { useEffect(() => { navigation.setOptions( - getStakingNavbar(strings('stake.stake'), navigation, theme.colors, { - backgroundColor: theme.colors.background.alternative, - hasCancelButton: false, - }), + getStakingNavbar( + strings('stake.stake'), + navigation, + theme.colors, + { + backgroundColor: theme.colors.background.alternative, + hasCancelButton: false, + }, + { + backButtonEvent: { + event: MetaMetricsEvents.STAKE_CONFIRMATION_BACK_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKE_CONFIRMATION_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors]); diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx index 650343c3958..559c068e427 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx @@ -246,6 +246,8 @@ describe('StakeInputView', () => { annualRewardRate: '2.5%', annualRewardsETH: '0.00938 ETH', annualRewardsFiat: '18.75 USD', + estimatedGasFee: '0.25', + estimatedGasFeePercentage: '66%', }, }); }); diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index ead9d7016c8..3ae652a718b 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -20,6 +20,8 @@ import useStakingInputHandlers from '../../hooks/useStakingInput'; import InputDisplay from '../../components/InputDisplay'; import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; +import { formatEther } from 'ethers/lib/utils'; +import { EVENT_PROVIDERS, EVENT_LOCATIONS } from '../../constants/events'; const StakeInputView = () => { const title = strings('stake.stake_eth'); @@ -49,6 +51,8 @@ const StakeInputView = () => { handleMax, balanceValue, isHighGasCostImpact, + getDepositTxGasPercentage, + estimatedGasFeeWei, isLoadingStakingGasFee, } = useStakingInputHandlers(); @@ -60,6 +64,21 @@ const StakeInputView = () => { const handleStakePress = useCallback(() => { if (isHighGasCostImpact()) { + trackEvent( + createEventBuilder( + MetaMetricsEvents.STAKE_GAS_COST_IMPACT_WARNING_TRIGGERED, + ) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, + tokens_to_stake_native_value: amountEth, + tokens_to_stake_usd_value: fiatAmount, + estimated_gas_fee: formatEther(estimatedGasFeeWei.toString()), + estimated_gas_percentage_of_deposit: `${getDepositTxGasPercentage()}%`, + }) + .build(), + ); + navigation.navigate('StakeModals', { screen: Routes.STAKING.MODALS.GAS_IMPACT, params: { @@ -68,6 +87,8 @@ const StakeInputView = () => { annualRewardsETH, annualRewardsFiat, annualRewardRate, + estimatedGasFee: formatEther(estimatedGasFeeWei.toString()), + estimatedGasFeePercentage: `${getDepositTxGasPercentage()}%`, }, }); return; @@ -86,7 +107,7 @@ const StakeInputView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.REVIEW_STAKE_BUTTON_CLICKED) .addProperties({ - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, tokens_to_stake_native_value: amountEth, tokens_to_stake_usd_value: fiatAmount, }) @@ -103,6 +124,8 @@ const StakeInputView = () => { trackEvent, createEventBuilder, amountEth, + estimatedGasFeeWei, + getDepositTxGasPercentage, ]); const handleMaxButtonPress = () => { @@ -124,9 +147,23 @@ const StakeInputView = () => { useEffect(() => { navigation.setOptions( - getStakingNavbar(title, navigation, theme.colors, { - hasBackButton: false, - }), + getStakingNavbar( + title, + navigation, + theme.colors, + { + hasBackButton: false, + }, + { + cancelButtonEvent: { + event: MetaMetricsEvents.STAKE_CANCEL_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors, title]); @@ -148,9 +185,9 @@ const StakeInputView = () => { handleCurrencySwitch={withMetaMetrics(handleCurrencySwitch, { event: MetaMetricsEvents.STAKE_INPUT_CURRENCY_SWITCH_CLICKED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Currency Switch Trigger', - location: 'Stake Input View', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, // We want to track the currency switching to. Not the current currency. currency_type: isEth ? 'fiat' : 'native', }, @@ -163,9 +200,9 @@ const StakeInputView = () => { onIconPress={withMetaMetrics(navigateToLearnMoreModal, { event: MetaMetricsEvents.TOOLTIP_OPENED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Tooltip Opened', - location: 'Stake Input View', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, tooltip_name: 'MetaMask Pool Estimated Rewards', }, })} @@ -178,7 +215,7 @@ const StakeInputView = () => { withMetaMetrics(handleQuickAmountPress, { event: MetaMetricsEvents.STAKE_INPUT_QUICK_AMOUNT_CLICKED, properties: { - location: 'StakeInputView', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, amount: value, // onMaxPress is called instead when it's defined and the max is clicked. is_max: false, @@ -189,7 +226,7 @@ const StakeInputView = () => { onMaxPress={withMetaMetrics(handleMaxButtonPress, { event: MetaMetricsEvents.STAKE_INPUT_QUICK_AMOUNT_CLICKED, properties: { - location: 'StakeInputView', + location: EVENT_LOCATIONS.STAKE_INPUT_VIEW, is_max: true, mode: isEth ? 'native' : 'fiat', }, diff --git a/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx b/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx index 1a78eab35e5..4c3a94084a3 100644 --- a/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx +++ b/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx @@ -11,6 +11,8 @@ import TokenValueStack from '../../components/StakingConfirmation/TokenValueStac import AccountCard from '../../components/StakingConfirmation/AccountCard/AccountCard'; import ConfirmationFooter from '../../components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter'; import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types'; +import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking'; @@ -21,10 +23,24 @@ const UnstakeConfirmationView = ({ route }: UnstakeConfirmationViewProps) => { useEffect(() => { navigation.setOptions( - getStakingNavbar(strings('stake.unstake'), navigation, theme.colors, { - backgroundColor: theme.colors.background.alternative, - hasCancelButton: false, - }), + getStakingNavbar( + strings('stake.unstake'), + navigation, + theme.colors, + { + backgroundColor: theme.colors.background.alternative, + hasCancelButton: false, + }, + { + backButtonEvent: { + event: MetaMetricsEvents.UNSTAKE_CONFIRMATION_BACK_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.UNSTAKE_CONFIRMATION_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors]); diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx index b27bbbe07e0..1786301fb0e 100644 --- a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx +++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx @@ -20,6 +20,7 @@ import Routes from '../../../../../constants/navigation/Routes'; import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; import useUnstakingInputHandlers from '../../hooks/useUnstakingInput'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const UnstakeInputView = () => { const title = strings('stake.unstake_eth'); @@ -54,9 +55,23 @@ const UnstakeInputView = () => { useEffect(() => { navigation.setOptions( - getStakingNavbar(title, navigation, theme.colors, { - hasBackButton: false, - }), + getStakingNavbar( + title, + navigation, + theme.colors, + { + hasBackButton: false, + }, + { + cancelButtonEvent: { + event: MetaMetricsEvents.UNSTAKE_CANCEL_CLICKED, + properties: { + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.UNSTAKE_INPUT_VIEW, + }, + }, + }, + ), ); }, [navigation, theme.colors, title]); @@ -71,7 +86,7 @@ const UnstakeInputView = () => { trackEvent( createEventBuilder(MetaMetricsEvents.REVIEW_UNSTAKE_BUTTON_CLICKED) .addProperties({ - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, tokens_to_stake_native_value: amountEth, tokens_to_stake_usd_value: fiatAmount, }) @@ -100,9 +115,9 @@ const UnstakeInputView = () => { handleCurrencySwitch={withMetaMetrics(handleCurrencySwitch, { event: MetaMetricsEvents.UNSTAKE_INPUT_CURRENCY_SWITCH_CLICKED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Currency Switch Trigger', - location: 'Unstake Input View', + location: EVENT_LOCATIONS.UNSTAKE_INPUT_VIEW, // We want to track the currency switching to. Not the current currency. currency_type: isEth ? 'fiat' : 'native', }, @@ -116,7 +131,7 @@ const UnstakeInputView = () => { withMetaMetrics(handleQuickAmountPress, { event: MetaMetricsEvents.UNSTAKE_INPUT_QUICK_AMOUNT_CLICKED, properties: { - location: 'UnstakeInputView', + location: EVENT_LOCATIONS.UNSTAKE_INPUT_VIEW, amount: value, is_max: value === 1, mode: isEth ? 'native' : 'fiat', diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx index 7018981373c..f6eca2e2f22 100644 --- a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx @@ -29,6 +29,8 @@ const props: GasImpactModalProps = { annualRewardRate: '2.5%', annualRewardsETH: '2.5 ETH', annualRewardsFiat: '$5000', + estimatedGasFee: '0.009171428571428572', + estimatedGasFeePercentage: '35%', }, name: 'params', }, diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts index a00204cfbee..7a1d9441615 100644 --- a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts @@ -6,6 +6,8 @@ interface GasImpactModalRouteParams { annualRewardsETH: string; annualRewardsFiat: string; annualRewardRate: string; + estimatedGasFee: string; + estimatedGasFeePercentage: string; } export interface GasImpactModalProps { diff --git a/app/components/UI/Stake/components/GasImpactModal/index.tsx b/app/components/UI/Stake/components/GasImpactModal/index.tsx index 4e348f75426..dabbaca7c5d 100644 --- a/app/components/UI/Stake/components/GasImpactModal/index.tsx +++ b/app/components/UI/Stake/components/GasImpactModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useCallback, useRef } from 'react'; import BottomSheet, { BottomSheetRef, } from '../../../../../component-library/components/BottomSheets/BottomSheet'; @@ -22,27 +22,65 @@ import { useNavigation } from '@react-navigation/native'; import Routes from '../../../../../constants/navigation/Routes'; import { GasImpactModalProps } from './GasImpactModal.types'; import { strings } from '../../../../../../locales/i18n'; +import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { formatEther } from 'ethers/lib/utils'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const GasImpactModal = ({ route }: GasImpactModalProps) => { const { styles } = useStyles(styleSheet, {}); const { navigate } = useNavigation(); + const { trackEvent, createEventBuilder } = useMetrics(); + const sheetRef = useRef(null); + const { + amountWei, + annualRewardRate, + annualRewardsFiat, + annualRewardsETH, + amountFiat, + estimatedGasFee, + estimatedGasFeePercentage, + } = route.params; + + const metricsEvent = useCallback( + ( + eventName: + | typeof MetaMetricsEvents.STAKE_GAS_COST_IMPACT_CANCEL_CLICKED + | typeof MetaMetricsEvents.STAKE_GAS_COST_IMPACT_PROCEEDED_CLICKED, + ) => { + trackEvent( + createEventBuilder(eventName) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.GAS_IMPACT_MODAL, + tokens_to_stake_native_value: formatEther(amountWei), + tokens_to_stake_usd_value: amountFiat, + estimated_gas_fee: estimatedGasFee, + estimated_gas_percentage_of_deposit: estimatedGasFeePercentage, + }) + .build(), + ); + }, + [ + amountFiat, + amountWei, + createEventBuilder, + estimatedGasFee, + estimatedGasFeePercentage, + trackEvent, + ], + ); + const handleClose = () => { + metricsEvent(MetaMetricsEvents.STAKE_GAS_COST_IMPACT_CANCEL_CLICKED); sheetRef.current?.onCloseBottomSheet(); }; const handleNavigateToStakeReviewScreen = () => { - const { - amountWei, - annualRewardRate, - annualRewardsFiat, - annualRewardsETH, - amountFiat, - } = route.params; - + metricsEvent(MetaMetricsEvents.STAKE_GAS_COST_IMPACT_PROCEEDED_CLICKED); navigate('StakeScreens', { screen: Routes.STAKING.STAKE_CONFIRMATION, params: { diff --git a/app/components/UI/Stake/components/LearnMoreModal/index.tsx b/app/components/UI/Stake/components/LearnMoreModal/index.tsx index eb321f8d238..5d6d4b6677d 100644 --- a/app/components/UI/Stake/components/LearnMoreModal/index.tsx +++ b/app/components/UI/Stake/components/LearnMoreModal/index.tsx @@ -19,6 +19,7 @@ import { POOLED_STAKING_FAQ_URL } from '../../constants'; import createLearnMoreModalStyles from './LearnMoreModal.styles'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; const styles = createLearnMoreModalStyles(); @@ -99,9 +100,9 @@ const LearnMoreModal = () => { onPress={withMetaMetrics(handleLearnMoreBrowserRedirect, { event: MetaMetricsEvents.STAKE_LEARN_MORE_CLICKED, properties: { - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Learn More', - location: 'Learn More Modal', + location: EVENT_LOCATIONS.LEARN_MORE_MODAL, }, })} label={strings('stake.learn_more')} diff --git a/app/components/UI/Stake/components/StakeButton/index.tsx b/app/components/UI/Stake/components/StakeButton/index.tsx index ca578bef3bd..f197ed15dfc 100644 --- a/app/components/UI/Stake/components/StakeButton/index.tsx +++ b/app/components/UI/Stake/components/StakeButton/index.tsx @@ -25,6 +25,7 @@ import { strings } from '../../../../../../locales/i18n'; import { RootState } from '../../../../../reducers'; import useStakingEligibility from '../../hooks/useStakingEligibility'; import { StakeSDKProvider } from '../../sdk/stakeSdkProvider'; +import { EVENT_LOCATIONS } from '../../constants/events'; interface StakeButtonProps { asset: TokenI; @@ -69,7 +70,7 @@ const StakeButtonContent = ({ asset }: StakeButtonProps) => { createEventBuilder(MetaMetricsEvents.STAKE_BUTTON_CLICKED) .addProperties({ chain_id: getDecimalChainId(chainId), - location: 'Home Screen', + location: EVENT_LOCATIONS.HOME_SCREEN, text: 'Stake', token_symbol: asset.symbol, url: AppConstants.STAKE.URL, diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx index 1d41646bc3c..ce12aec48e7 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import Badge, { BadgeVariant, } from '../../../../../component-library/components/Badges/Badge'; @@ -44,6 +44,8 @@ import useBalance from '../../hooks/useBalance'; import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance'; import { selectChainId } from '../../../../../selectors/networkController'; import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; +import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; export interface StakingBalanceProps { asset: TokenI; @@ -51,6 +53,12 @@ export interface StakingBalanceProps { const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { styles } = useStyles(styleSheet, {}); + + const [ + hasSentViewingStakingRewardsMetric, + setHasSentViewingStakingRewardsMetric, + ] = useState(false); + const chainId = useSelector(selectChainId); const networkName = useSelector(selectNetworkName); @@ -58,6 +66,8 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const { isStakingSupportedChain } = useStakingChain(); + const { trackEvent, createEventBuilder } = useMetrics(); + const { pooledStakesData, exchangeRate, @@ -92,6 +102,28 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { const hasClaimableEth = !!Number(claimableEth); + useEffect(() => { + if (hasStakedPositions && !hasSentViewingStakingRewardsMetric) { + trackEvent( + createEventBuilder( + MetaMetricsEvents.VISITED_ETH_OVERVIEW_WITH_STAKED_POSITIONS, + ) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: EVENT_LOCATIONS.STAKING_BALANCE, + }) + .build(), + ); + + setHasSentViewingStakingRewardsMetric(true); + } + }, [ + createEventBuilder, + hasSentViewingStakingRewardsMetric, + hasStakedPositions, + trackEvent, + ]); + if (!isStakingSupportedChain) { return <>; } diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx index 3a13ee588ca..e99f93880c0 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx @@ -23,6 +23,7 @@ import { MetaMetricsEvents, useMetrics, } from '../../../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS } from '../../../../constants/events'; type StakeBannerProps = Pick & { claimableAmount: string; @@ -48,7 +49,7 @@ const ClaimBanner = ({ claimableAmount, style }: StakeBannerProps) => { trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_CLAIM_BUTTON_CLICKED) .addProperties({ - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, }) .build(), ); diff --git a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx index e6245f6506a..a87ea82fc06 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx @@ -11,6 +11,7 @@ import Routes from '../../../../../../constants/navigation/Routes'; import { useMetrics, MetaMetricsEvents } from '../../../../../hooks/useMetrics'; import { useSelector } from 'react-redux'; import { selectChainId } from '../../../../../../selectors/networkController'; +import { EVENT_LOCATIONS } from '../../../constants/events'; interface StakingButtonsProps extends Pick { hasStakedPositions: boolean; @@ -34,7 +35,7 @@ const StakingButtons = ({ trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_WITHDRAW_BUTTON_CLICKED) .addProperties({ - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, text: 'Unstake', token_symbol: 'ETH', chain_id: chainId, @@ -48,7 +49,7 @@ const StakingButtons = ({ trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_BUTTON_CLICKED) .addProperties({ - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, text: 'Stake', token_symbol: 'ETH', chain_id: chainId, diff --git a/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx b/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx index a6e2f4efca0..d292873ef5e 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx @@ -13,6 +13,7 @@ import { strings } from '../../../../../../../locales/i18n'; import { useNavigation } from '@react-navigation/native'; import Routes from '../../../../../../constants/navigation/Routes'; import { MetaMetricsEvents, useMetrics } from '../../../../../hooks/useMetrics'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../../constants/events'; interface StakingCtaProps extends Pick { estimatedRewardRate: string; @@ -30,9 +31,9 @@ const StakingCta = ({ estimatedRewardRate, style }: StakingCtaProps) => { trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_LEARN_MORE_CLICKED) .addProperties({ - selected_provider: 'consensys', + selected_provider: EVENT_PROVIDERS.CONSENSYS, text: 'Learn More', - location: 'Token Details', + location: EVENT_LOCATIONS.TOKEN_DETAILS, }) .build(), ); diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx index c60c393fb18..98c3e8fc987 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useNavigation } from '@react-navigation/native'; import { View } from 'react-native'; import { strings } from '../../../../../../../../locales/i18n'; @@ -24,6 +24,36 @@ import { import Routes from '../../../../../../../constants/navigation/Routes'; import usePoolStakedUnstake from '../../../../hooks/usePoolStakedUnstake'; import usePooledStakes from '../../../../hooks/usePooledStakes'; +import { + MetaMetricsEvents, + useMetrics, +} from '../../../../../../hooks/useMetrics'; +import { IMetaMetricsEvent } from '../../../../../../../core/Analytics'; +import { formatEther } from 'ethers/lib/utils'; +import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../../../constants/events'; + +const STAKING_TX_METRIC_EVENTS: Record< + FooterButtonGroupActions, + Record< + 'APPROVED' | 'REJECTED' | 'CONFIRMED' | 'FAILED' | 'SUBMITTED', + IMetaMetricsEvent + > +> = { + STAKE: { + APPROVED: MetaMetricsEvents.STAKE_TRANSACTION_APPROVED, + REJECTED: MetaMetricsEvents.STAKE_TRANSACTION_REJECTED, + CONFIRMED: MetaMetricsEvents.STAKE_TRANSACTION_CONFIRMED, + FAILED: MetaMetricsEvents.STAKE_TRANSACTION_FAILED, + SUBMITTED: MetaMetricsEvents.STAKE_TRANSACTION_SUBMITTED, + }, + UNSTAKE: { + APPROVED: MetaMetricsEvents.UNSTAKE_TRANSACTION_APPROVED, + REJECTED: MetaMetricsEvents.UNSTAKE_TRANSACTION_REJECTED, + CONFIRMED: MetaMetricsEvents.UNSTAKE_TRANSACTION_CONFIRMED, + FAILED: MetaMetricsEvents.UNSTAKE_TRANSACTION_FAILED, + SUBMITTED: MetaMetricsEvents.UNSTAKE_TRANSACTION_SUBMITTED, + }, +}; const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const { styles } = useStyles(styleSheet, {}); @@ -31,6 +61,8 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const navigation = useNavigation(); const { navigate } = navigation; + const { trackEvent, createEventBuilder } = useMetrics(); + const activeAccount = useSelector(selectSelectedInternalAccount); const { attemptDepositTransaction } = usePoolStakedDeposit(); @@ -40,13 +72,49 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const [didSubmitTransaction, setDidSubmitTransaction] = useState(false); + const isStaking = useMemo( + () => action === FooterButtonGroupActions.STAKE, + [action], + ); + + const submitTxMetaMetric = useCallback( + (txEventName: IMetaMetricsEvent) => { + const { STAKE_CONFIRMATION_VIEW, UNSTAKE_CONFIRMATION_VIEW } = + EVENT_LOCATIONS; + + const location = isStaking + ? STAKE_CONFIRMATION_VIEW + : UNSTAKE_CONFIRMATION_VIEW; + + return trackEvent( + createEventBuilder(txEventName) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location, + transaction_amount_eth: formatEther(valueWei), + }) + .build(), + ); + }, + [createEventBuilder, isStaking, trackEvent, valueWei], + ); + const listenForTransactionEvents = useCallback( (transactionId?: string) => { if (!transactionId) return; + Engine.controllerMessenger.subscribeOnceIf( + 'TransactionController:transactionApproved', + () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].APPROVED); + }, + ({ transactionMeta }) => transactionMeta.id === transactionId, + ); + Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionSubmitted', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].SUBMITTED); setDidSubmitTransaction(false); navigate(Routes.TRANSACTIONS_VIEW); }, @@ -56,6 +124,7 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionFailed', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].FAILED); setDidSubmitTransaction(false); }, ({ transactionMeta }) => transactionMeta.id === transactionId, @@ -64,6 +133,7 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionRejected', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].REJECTED); setDidSubmitTransaction(false); }, ({ transactionMeta }) => transactionMeta.id === transactionId, @@ -72,12 +142,13 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { Engine.controllerMessenger.subscribeOnceIf( 'TransactionController:transactionConfirmed', () => { + submitTxMetaMetric(STAKING_TX_METRIC_EVENTS[action].CONFIRMED); refreshPooledStakes(); }, (transactionMeta) => transactionMeta.id === transactionId, ); }, - [navigate, refreshPooledStakes], + [action, navigate, refreshPooledStakes, submitTxMetaMetric], ); const handleConfirmation = async () => { @@ -86,17 +157,36 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { setDidSubmitTransaction(true); + const metricsEvent = { + name: isStaking + ? MetaMetricsEvents.STAKE_TRANSACTION_INITIATED + : MetaMetricsEvents.UNSTAKE_TRANSACTION_INITIATED, + location: isStaking + ? 'StakeConfirmationView' + : 'UnstakeConfirmationView', + }; + + trackEvent( + createEventBuilder(metricsEvent.name) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: metricsEvent.location, + transaction_amount_eth: formatEther(valueWei), + }) + .build(), + ); + let transactionId: string | undefined; - if (action === FooterButtonGroupActions.STAKE) { + if (isStaking) { const txRes = await attemptDepositTransaction( valueWei, activeAccount.address, ); transactionId = txRes?.transactionMeta?.id; } - - if (action === FooterButtonGroupActions.UNSTAKE) { + // Unstaking + else { const txRes = await attemptUnstakeTransaction( valueWei, activeAccount.address, @@ -110,6 +200,26 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { } }; + const handleCancelPress = () => { + const metricsEvent = { + name: isStaking + ? MetaMetricsEvents.STAKE_CANCEL_CLICKED + : MetaMetricsEvents.UNSTAKE_CANCEL_CLICKED, + location: isStaking ? 'StakeConfirmationView' : 'UnstakeConfirmationView', + }; + + trackEvent( + createEventBuilder(metricsEvent.name) + .addProperties({ + selected_provider: EVENT_PROVIDERS.CONSENSYS, + location: metricsEvent.location, + }) + .build(), + ); + + navigation.goBack(); + }; + return (