diff --git a/app/components/UI/AssetOverview/Balance/Balance.tsx b/app/components/UI/AssetOverview/Balance/Balance.tsx index 02c89071051..fed53bd539a 100644 --- a/app/components/UI/AssetOverview/Balance/Balance.tsx +++ b/app/components/UI/AssetOverview/Balance/Balance.tsx @@ -34,7 +34,7 @@ interface BalanceProps { secondaryBalance?: string; } -const NetworkBadgeSource = (chainId: string, ticker: string) => { +export const NetworkBadgeSource = (chainId: string, ticker: string) => { const isMainnet = isMainnetByChainId(chainId); const isLineaMainnet = isLineaMainnetByChainId(chainId); @@ -88,7 +88,9 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { {asset.name || asset.symbol} - {isPooledStakingFeatureEnabled() && asset?.isETH && } + {isPooledStakingFeatureEnabled() && asset?.isETH && ( + + )} ); }; diff --git a/app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.test.tsx b/app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.test.tsx deleted file mode 100644 index f9a8c51b5a5..00000000000 --- a/app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import StakingEarnings from './'; -import renderWithProvider from '../../../../util/test/renderWithProvider'; -import { strings } from '../../../../../locales/i18n'; - -jest.mock('../../Stake/constants', () => ({ - isPooledStakingFeatureEnabled: jest.fn().mockReturnValue(true), -})); - -const mockNavigate = jest.fn(); - -jest.mock('@react-navigation/native', () => { - const actualReactNavigation = jest.requireActual('@react-navigation/native'); - return { - ...actualReactNavigation, - useNavigation: () => ({ - navigate: mockNavigate, - }), - }; -}); - -describe('Staking Earnings', () => { - it('should render correctly', () => { - const { toJSON, getByText } = renderWithProvider(); - - expect(getByText(strings('stake.your_earnings'))).toBeDefined(); - expect(getByText(strings('stake.annual_rate'))).toBeDefined(); - expect(getByText(strings('stake.lifetime_rewards'))).toBeDefined(); - expect(getByText(strings('stake.estimated_annual_earnings'))).toBeDefined(); - expect(toJSON()).toMatchSnapshot(); - }); -}); diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx index 476ab653953..368e2352d23 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx @@ -1,5 +1,5 @@ import { zeroAddress } from 'ethereumjs-util'; -import React, { useState } from 'react'; +import React from 'react'; import { View } from 'react-native'; import { useSelector } from 'react-redux'; import i18n from '../../../../../locales/i18n'; @@ -21,8 +21,8 @@ import Logger from '../../../../util/Logger'; import TokenDetailsList from './TokenDetailsList'; import MarketDetailsList from './MarketDetailsList'; import { TokenI } from '../../Tokens/types'; -import StakingEarnings from '../StakingEarnings'; import { isPooledStakingFeatureEnabled } from '../../Stake/constants'; +import StakingEarnings from '../../Stake/components/StakingEarnings'; export interface TokenDetails { contractAddress: string | null; @@ -52,9 +52,6 @@ const TokenDetails: React.FC = ({ asset }) => { const currentCurrency = useSelector(selectCurrentCurrency); const tokenContractAddress = safeToChecksumAddress(asset.address); - // TEMP: Remove once component has been implemented. - const [hasStakingPositions] = useState(true); - let tokenMetadata; let marketData; @@ -126,9 +123,7 @@ const TokenDetails: React.FC = ({ asset }) => { return ( - {asset.isETH && - hasStakingPositions && - isPooledStakingFeatureEnabled() && } + {asset.isETH && isPooledStakingFeatureEnabled() && } {(asset.isETH || tokenMetadata) && ( )} diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx index a0c36a05079..86c5c82e1e3 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx +++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx @@ -62,12 +62,25 @@ jest.mock('../../hooks/usePoolStakedDeposit', () => ({ }), })); +jest.mock('../../hooks/usePooledStakes', () => ({ + __esModule: true, + default: () => ({ + refreshPooledStakes: jest.fn(), + }), +})); + describe('StakeConfirmationView', () => { it('render matches snapshot', () => { const props: StakeConfirmationViewProps = { route: { key: '1', - params: { amountWei: '3210000000000000', amountFiat: '7.46' }, + params: { + amountWei: '3210000000000000', + amountFiat: '7.46', + annualRewardRate: '2.5%', + annualRewardsETH: '2.5 ETH', + annualRewardsFiat: '$5000', + }, name: 'params', }, }; diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx index 857752feafb..48a7277df07 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx +++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx @@ -9,17 +9,9 @@ import AccountHeaderCard from '../../components/StakingConfirmation/AccountHeade import RewardsCard from '../../components/StakingConfirmation/RewardsCard/RewardsCard'; import ConfirmationFooter from '../../components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter'; import { StakeConfirmationViewProps } from './StakeConfirmationView.types'; -import { MOCK_GET_VAULT_RESPONSE } from '../../components/StakingBalance/mockData'; import { strings } from '../../../../../../locales/i18n'; import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types'; -const MOCK_REWARD_DATA = { - REWARDS: { - ETH: '0.13 ETH', - FIAT: '$334.93', - }, -}; - const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking'; const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => { @@ -47,9 +39,9 @@ const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => { diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts index 8c723135f4f..20214a0fc52 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts +++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts @@ -3,6 +3,9 @@ import { RouteProp } from '@react-navigation/native'; interface StakeConfirmationViewRouteParams { amountWei: string; amountFiat: string; + annualRewardsETH: string; + annualRewardsFiat: string; + annualRewardRate: string; } export interface StakeConfirmationViewProps { diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap index 9d14c100f63..eedfba7fe99 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap +++ b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap @@ -989,7 +989,7 @@ exports[`StakeConfirmationView render matches snapshot 1`] = ` } testID="label" > - 2.8% + 2.5% @@ -1100,7 +1100,7 @@ exports[`StakeConfirmationView render matches snapshot 1`] = ` } } > - $334.93 + $5000 - 0.13 ETH + 2.5 ETH @@ -1415,7 +1415,7 @@ exports[`StakeConfirmationView render matches snapshot 1`] = ` } } > - Confirm + Continue diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx index a44c0971a53..90639128471 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx @@ -8,6 +8,7 @@ import { BN } from 'ethereumjs-util'; import { Stake } from '../../sdk/stakeSdkProvider'; import { ChainId, PooledStakingContract } from '@metamask/stake-sdk'; import { Contract } from 'ethers'; +import { MOCK_GET_VAULT_RESPONSE } from '../../__mocks__/mockData'; function render(Component: React.ComponentType) { return renderScreen( @@ -89,6 +90,31 @@ jest.mock('../../hooks/useBalance', () => ({ }), })); +const mockVaultData = MOCK_GET_VAULT_RESPONSE; +// Mock hooks + +jest.mock('../../hooks/useStakingEligibility', () => ({ + __esModule: true, + default: () => ({ + isEligible: true, + loading: false, + error: null, + refreshPooledStakingEligibility: jest.fn(), + }), +})); + +jest.mock('../../hooks/useVaultData', () => ({ + __esModule: true, + default: () => ({ + vaultData: mockVaultData, + loading: false, + error: null, + refreshVaultData: jest.fn(), + annualRewardRate: '2.5%', + annualRewardRateDecimal: 0.025, + }), +})); + describe('StakeInputView', () => { it('render matches snapshot', () => { render(StakeInputView); @@ -122,7 +148,7 @@ describe('StakeInputView', () => { fireEvent.press(screen.getByText('2')); - expect(screen.getByText('0.052 ETH')).toBeTruthy(); + expect(screen.getByText('0.05 ETH')).toBeTruthy(); }); }); diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index 0431e67a77f..9351d57b785 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -41,6 +41,10 @@ const StakeInputView = () => { handleKeypadChange, calculateEstimatedAnnualRewards, estimatedAnnualRewards, + annualRewardsETH, + annualRewardsFiat, + annualRewardRate, + isLoadingVaultData, } = useStakingInputHandlers(balanceWei); const navigateToLearnMoreModal = () => { @@ -55,9 +59,19 @@ const StakeInputView = () => { params: { amountWei: amountWei.toString(), amountFiat: fiatAmount, + annualRewardsETH, + annualRewardsFiat, + annualRewardRate, }, }); - }, [amountWei, fiatAmount, navigation]); + }, [ + navigation, + amountWei, + fiatAmount, + annualRewardsETH, + annualRewardsFiat, + annualRewardRate, + ]); const balanceText = strings('stake.balance'); @@ -101,6 +115,7 @@ const StakeInputView = () => { - 2.6% + 2.5% ({ selectCurrentCurrency: jest.fn(() => 'USD'), })); +const mockVaultData = MOCK_GET_VAULT_RESPONSE; +const mockPooledStakeData = MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0]; + +jest.mock('../../hooks/useStakingEligibility', () => ({ + __esModule: true, + default: () => ({ + isEligible: true, + loading: false, + error: null, + refreshPooledStakingEligibility: jest.fn(), + }), +})); + +jest.mock('../../hooks/useVaultData', () => ({ + __esModule: true, + default: () => ({ + vaultData: mockVaultData, + loading: false, + error: null, + annualRewardRate: '2.5%', + annualRewardRateDecimal: 0.025, + }), +})); + +jest.mock('../../hooks/useBalance', () => ({ + __esModule: true, + default: () => ({ + stakedBalanceWei: mockPooledStakeData.assets, + stakedBalanceFiat: MOCK_STAKED_ETH_ASSET.balanceFiat, + }), +})); + describe('UnstakeInputView', () => { it('render matches snapshot', () => { render(UnstakeInputView); @@ -81,7 +118,7 @@ describe('UnstakeInputView', () => { fireEvent.press(screen.getByText('25%')); - expect(screen.getByText('1.14999')).toBeTruthy(); + expect(screen.getByText('1.44783')).toBeTruthy(); }); }); @@ -96,13 +133,14 @@ describe('UnstakeInputView', () => { render(UnstakeInputView); fireEvent.press(screen.getByText('1')); + expect(screen.getByText('Review')).toBeTruthy(); }); it('displays `Not enough ETH` when input exceeds balance', () => { render(UnstakeInputView); - fireEvent.press(screen.getByText('6')); + fireEvent.press(screen.getByText('8')); expect(screen.queryAllByText('Not enough ETH')).toHaveLength(2); }); }); diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx index c109940d3a5..8adb580eade 100644 --- a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx +++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx @@ -19,13 +19,14 @@ import { View } from 'react-native'; import useStakingInputHandlers from '../../hooks/useStakingInput'; import styleSheet from './UnstakeInputView.styles'; import InputDisplay from '../../components/InputDisplay'; +import useBalance from '../../hooks/useBalance'; const UnstakeInputView = () => { const title = strings('stake.unstake_eth'); const navigation = useNavigation(); const { styles, theme } = useStyles(styleSheet, {}); - const stakeBalance = '4599964000000000000'; //TODO: Replace with actual balance - STAKE-806 + const { stakedBalanceWei } = useBalance(); const { isEth, @@ -40,10 +41,13 @@ const UnstakeInputView = () => { handleAmountPress, handleKeypadChange, conversionRate, - } = useStakingInputHandlers(new BN(stakeBalance)); + } = useStakingInputHandlers(new BN(stakedBalanceWei)); - const stakeBalanceInEth = renderFromWei(stakeBalance, 5); - const stakeBalanceFiatNumber = weiToFiatNumber(stakeBalance, conversionRate); + const stakeBalanceInEth = renderFromWei(stakedBalanceWei, 5); + const stakeBalanceFiatNumber = weiToFiatNumber( + stakedBalanceWei, + conversionRate, + ); const stakedBalanceText = strings('stake.staked_balance'); const stakedBalanceValue = isEth diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.types.ts b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.types.ts new file mode 100644 index 00000000000..e58d20f58af --- /dev/null +++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.types.ts @@ -0,0 +1,9 @@ +import { RouteProp } from '@react-navigation/native'; + +interface UnstakeInputViewRouteParams { + stakedBalanceWei: string; +} + +export interface UnstakeInputViewProps { + route: RouteProp<{ params: UnstakeInputViewRouteParams }, 'params'>; +} diff --git a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap index 5e7927b0b5c..0e1816f6621 100644 --- a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap +++ b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap @@ -386,7 +386,7 @@ exports[`UnstakeInputView render matches snapshot 1`] = ` > Staked balance : - 4.59996 ETH + 5.79133 ETH StyleSheet.create({ @@ -42,12 +43,14 @@ const createStyles = (colors: Colors) => interface EstimatedAnnualRewardsCardProps { estimatedAnnualRewards: string; + isLoading?: boolean; onIconPress: () => void; } const EstimatedAnnualRewardsCard = ({ estimatedAnnualRewards, onIconPress, + isLoading = false, }: EstimatedAnnualRewardsCardProps) => { const { colors } = useTheme(); const styles = createStyles(colors); @@ -67,9 +70,19 @@ const EstimatedAnnualRewardsCard = ({ - - {estimatedAnnualRewards} - + {isLoading ? ( + + + + ) : ( + + {estimatedAnnualRewards} + + )} { + const actualReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualReactNavigation, + useNavigation: () => ({ + navigate: mockNavigate, + }), + }; +}); + +jest.mock('../../constants', () => ({ + isPooledStakingFeatureEnabled: jest.fn().mockReturnValue(true), +})); + +jest.mock('../../../../hooks/useMetrics', () => ({ + MetaMetricsEvents: { + STAKE_BUTTON_CLICKED: 'Stake Button Clicked', + }, + useMetrics: () => ({ + trackEvent: jest.fn(), + }), +})); + +jest.mock('../../../../../core/Engine', () => ({ + context: { + NetworkController: { + getNetworkClientById: () => ({ + configuration: { + chainId: '0x1', + rpcUrl: 'https://mainnet.infura.io/v3', + ticker: 'ETH', + type: 'custom', + }, + }), + findNetworkClientIdByChainId: () => 'mainnet', + }, + }, +})); + +jest.mock('../../hooks/useStakingEligibility', () => ({ + __esModule: true, + default: () => ({ + isEligible: true, + loading: false, + error: null, + refreshPooledStakingEligibility: jest.fn(), + }), +})); + +const renderComponent = () => + renderWithProvider(); + +describe('StakeButton', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const { getByTestId } = renderComponent(); + expect(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON)).toBeDefined(); + }); + + it('navigates to Stake Input screen when stake button is pressed and user is eligible', async () => { + const { getByTestId } = renderComponent(); + + fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON)); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('StakeScreens', { + screen: Routes.STAKING.STAKE, + }); + }); + }); +}); diff --git a/app/components/UI/Tokens/TokenList/StakeButton/index.tsx b/app/components/UI/Stake/components/StakeButton/index.tsx similarity index 74% rename from app/components/UI/Tokens/TokenList/StakeButton/index.tsx rename to app/components/UI/Stake/components/StakeButton/index.tsx index b0decefd026..7a92f67d3cb 100644 --- a/app/components/UI/Tokens/TokenList/StakeButton/index.tsx +++ b/app/components/UI/Stake/components/StakeButton/index.tsx @@ -1,14 +1,11 @@ import React from 'react'; -import { TokenI, BrowserTab } from '../../types'; +import { TokenI, BrowserTab } from '../../../Tokens/types'; import { useNavigation } from '@react-navigation/native'; -import { isPooledStakingFeatureEnabled } from '../../../Stake/constants'; +import { isPooledStakingFeatureEnabled } from '../../constants'; import Routes from '../../../../../constants/navigation/Routes'; import { useSelector } from 'react-redux'; import AppConstants from '../../../../../core/AppConstants'; -import { - MetaMetricsEvents, - useMetrics, -} from '../../../../../components/hooks/useMetrics'; +import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; import { getDecimalChainId } from '../../../../../util/networks'; import { selectChainId } from '../../../../../selectors/networkController'; import { Pressable } from 'react-native'; @@ -18,7 +15,7 @@ import Text, { } from '../../../../../component-library/components/Texts/Text'; import { WalletViewSelectorsIDs } from '../../../../../../e2e/selectors/wallet/WalletView.selectors'; import { useTheme } from '../../../../../util/theme'; -import createStyles from '../../styles'; +import createStyles from '../../../Tokens/styles'; import Icon, { IconColor, IconName, @@ -26,12 +23,13 @@ import Icon, { } from '../../../../../component-library/components/Icons/Icon'; import { strings } from '../../../../../../locales/i18n'; import { RootState } from '../../../../../reducers'; +import useStakingEligibility from '../../hooks/useStakingEligibility'; +import { StakeSDKProvider } from '../../sdk/stakeSdkProvider'; interface StakeButtonProps { asset: TokenI; } - -export const StakeButton = ({ asset }: StakeButtonProps) => { +const StakeButtonContent = ({ asset }: StakeButtonProps) => { const { colors } = useTheme(); const styles = createStyles(colors); const navigation = useNavigation(); @@ -40,8 +38,12 @@ export const StakeButton = ({ asset }: StakeButtonProps) => { const browserTabs = useSelector((state: RootState) => state.browser.tabs); const chainId = useSelector(selectChainId); - const onStakeButtonPress = () => { - if (isPooledStakingFeatureEnabled()) { + const { isEligible, refreshPooledStakingEligibility } = + useStakingEligibility(); + + const onStakeButtonPress = async () => { + await refreshPooledStakingEligibility(); + if (isPooledStakingFeatureEnabled() && isEligible) { navigation.navigate('StakeScreens', { screen: Routes.STAKING.STAKE }); } else { const existingStakeTab = browserTabs.find((tab: BrowserTab) => @@ -82,7 +84,7 @@ export const StakeButton = ({ asset }: StakeButtonProps) => { {' • '} - {`${strings('stake.stake')} `} + {`${strings('stake.earn')} `} { ); }; + +export const StakeButton = (props: StakeButtonProps) => ( + + + +); + +export default StakeButton; diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx index 2aae28643bf..e02cd932b65 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx @@ -5,6 +5,11 @@ import StakingBalance from './StakingBalance'; import { strings } from '../../../../../../locales/i18n'; import Routes from '../../../../../constants/navigation/Routes'; import { Image } from 'react-native'; +import { + MOCK_GET_POOLED_STAKES_API_RESPONSE, + MOCK_GET_VAULT_RESPONSE, + MOCK_STAKED_ETH_ASSET, +} from '../../__mocks__/mockData'; jest.mock('../../../../hooks/useIpfsGateway', () => jest.fn()); @@ -24,20 +29,92 @@ jest.mock('@react-navigation/native', () => { }; }); +const mockPooledStakeData = MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0]; +const mockExchangeRate = MOCK_GET_POOLED_STAKES_API_RESPONSE.exchangeRate; + +const mockVaultData = MOCK_GET_VAULT_RESPONSE; +// Mock hooks +jest.mock('../../hooks/usePooledStakes', () => ({ + __esModule: true, + default: () => ({ + pooledStakesData: mockPooledStakeData, + exchangeRate: mockExchangeRate, + loading: false, + error: null, + refreshPooledStakes: jest.fn(), + hasStakedPositions: true, + hasEthToUnstake: true, + hasNeverStaked: false, + hasRewards: true, + hasRewardsOnly: false, + }), +})); + +jest.mock('../../hooks/useStakingEligibility', () => ({ + __esModule: true, + default: () => ({ + isEligible: true, + loading: false, + error: null, + refreshPooledStakingEligibility: jest.fn(), + }), +})); + +jest.mock('../../hooks/useVaultData', () => ({ + __esModule: true, + default: () => ({ + vaultData: mockVaultData, + loading: false, + error: null, + annualRewardRate: '2.5%', + annualRewardRateDecimal: 0.025, + }), +})); + +jest.mock('../../hooks/useBalance', () => ({ + __esModule: true, + default: () => ({ + stakedBalanceWei: MOCK_STAKED_ETH_ASSET.balance, + stakedBalanceFiat: MOCK_STAKED_ETH_ASSET.balanceFiat, + }), +})); + +jest.mock('../../../../../core/Engine', () => ({ + context: { + NetworkController: { + getNetworkClientById: () => ({ + configuration: { + chainId: '0x1', + rpcUrl: 'https://mainnet.infura.io/v3', + ticker: 'ETH', + type: 'custom', + }, + }), + findNetworkClientIdByChainId: () => 'mainnet', + }, + }, +})); + afterEach(() => { jest.clearAllMocks(); }); describe('StakingBalance', () => { - beforeEach(() => jest.resetAllMocks()); + beforeEach(() => { + jest.resetAllMocks(); + }); it('render matches snapshot', () => { - const { toJSON } = renderWithProvider(); + const { toJSON } = renderWithProvider( + , + ); expect(toJSON()).toMatchSnapshot(); }); it('redirects to StakeInputView on stake button click', () => { - const { getByText } = renderWithProvider(); + const { getByText } = renderWithProvider( + , + ); fireEvent.press(getByText(strings('stake.stake_more'))); @@ -48,7 +125,9 @@ describe('StakingBalance', () => { }); it('redirects to UnstakeInputView on unstake button click', () => { - const { getByText } = renderWithProvider(); + const { getByText } = renderWithProvider( + , + ); fireEvent.press(getByText(strings('stake.unstake'))); diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx index 8884688694b..e70630ab901 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, useState } from 'react'; +import React, { useMemo } from 'react'; import Badge, { BadgeVariant, } from '../../../../../component-library/components/Badges/Badge'; @@ -11,7 +11,6 @@ import AssetElement from '../../../AssetElement'; import NetworkMainAssetLogo from '../../../NetworkMainAssetLogo'; import { selectNetworkName } from '../../../../../selectors/networkInfos'; import { useSelector } from 'react-redux'; -import images from '../../../../../images/image-icons'; import styleSheet from './StakingBalance.styles'; import { View } from 'react-native'; import StakingButtons from './StakingButtons/StakingButtons'; @@ -35,28 +34,48 @@ import { } from '../../utils/value'; import { multiplyValueByPowerOfTen } from '../../utils/bignumber'; import StakingCta from './StakingCta/StakingCta'; -import { - MOCK_GET_POOLED_STAKES_API_RESPONSE, - MOCK_GET_VAULT_RESPONSE, - MOCK_STAKED_ETH_ASSET, -} from './mockData'; +import useStakingEligibility from '../../hooks/useStakingEligibility'; +import useStakingChain from '../../hooks/useStakingChain'; +import usePooledStakes from '../../hooks/usePooledStakes'; +import useVaultData from '../../hooks/useVaultData'; +import { StakeSDKProvider } from '../../sdk/stakeSdkProvider'; +import type { TokenI } from '../../../Tokens/types'; +import useBalance from '../../hooks/useBalance'; +import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance'; +import { selectChainId } from '../../../../../selectors/networkController'; -const StakingBalance = () => { - const { styles } = useStyles(styleSheet, {}); +export interface StakingBalanceProps { + asset: TokenI; +} +const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { + const { styles } = useStyles(styleSheet, {}); + const chainId = useSelector(selectChainId); const networkName = useSelector(selectNetworkName); - const [isGeoBlocked] = useState(false); - const [hasStakedPositions] = useState(false); + const { isEligible: isEligibleForPooledStaking } = useStakingEligibility(); - const { unstakingRequests, claimableRequests } = useMemo( - () => - filterExitRequests( - MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0].exitRequests, - MOCK_GET_POOLED_STAKES_API_RESPONSE.exchangeRate, - ), - [], - ); + const { isStakingSupportedChain } = useStakingChain(); + + const { + pooledStakesData, + exchangeRate, + hasStakedPositions, + hasEthToUnstake, + isLoadingPooledStakesData, + } = usePooledStakes(); + const { vaultData } = useVaultData(); + const annualRewardRate = vaultData?.apy || ''; + + const { + formattedStakedBalanceETH: stakedBalanceETH, + formattedStakedBalanceFiat: stakedBalanceFiat, + } = useBalance(); + + const { unstakingRequests, claimableRequests } = useMemo(() => { + const exitRequests = pooledStakesData?.exitRequests ?? []; + return filterExitRequests(exitRequests, exchangeRate); + }, [pooledStakesData, exchangeRate]); const claimableEth = useMemo( () => @@ -72,20 +91,24 @@ const StakingBalance = () => { const hasClaimableEth = !!Number(claimableEth); + if (!isStakingSupportedChain || isLoadingPooledStakesData) { + return <>; + } + return ( - {Boolean(MOCK_STAKED_ETH_ASSET.balance) && !isGeoBlocked && ( + {hasStakedPositions && isEligibleForPooledStaking && ( } @@ -93,13 +116,13 @@ const StakingBalance = () => { - {MOCK_STAKED_ETH_ASSET.name || MOCK_STAKED_ETH_ASSET.symbol} + {strings('stake.staked_ethereum')} )} - {isGeoBlocked ? ( + {!isEligibleForPooledStaking ? ( { {!hasStakedPositions && ( )} - + )} @@ -156,4 +180,10 @@ const StakingBalance = () => { ); }; +export const StakingBalance = ({ asset }: StakingBalanceProps) => ( + + + +); + export default StakingBalance; diff --git a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx index 51ba3ea15f9..2cec44d4baf 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import Button, { ButtonVariants, } from '../../../../../../component-library/components/Buttons/Button'; @@ -9,16 +9,23 @@ import styleSheet from './StakingButtons.styles'; import { useNavigation } from '@react-navigation/native'; import Routes from '../../../../../../constants/navigation/Routes'; -interface StakingButtonsProps extends Pick {} +interface StakingButtonsProps extends Pick { + hasStakedPositions: boolean; + hasEthToUnstake: boolean; +} -const StakingButtons = ({ style }: StakingButtonsProps) => { - const [hasStakedPosition] = useState(true); - const [hasEthToUnstake] = useState(true); +const StakingButtons = ({ + style, + hasStakedPositions, + hasEthToUnstake, +}: StakingButtonsProps) => { const { navigate } = useNavigation(); const { styles } = useStyles(styleSheet, {}); const onUnstakePress = () => - navigate('StakeScreens', { screen: Routes.STAKING.UNSTAKE }); + navigate('StakeScreens', { + screen: Routes.STAKING.UNSTAKE, + }); const onStakePress = () => navigate('StakeScreens', { screen: Routes.STAKING.STAKE }); @@ -37,7 +44,7 @@ const StakingButtons = ({ style }: StakingButtonsProps) => { style={styles.balanceActionButton} variant={ButtonVariants.Secondary} label={ - hasStakedPosition + hasStakedPositions ? strings('stake.stake_more') : strings('stake.stake') } diff --git a/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap b/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap index ee2fbde051b..c161fdb50d6 100644 --- a/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap +++ b/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap @@ -200,40 +200,7 @@ exports[`StakingBalance render matches snapshot 1`] = ` "flex": 1, } } - > - - $13,292.20 - - - 4.9999 ETH - - + /> - - - Stake ETH and earn - - - - Stake your ETH with MetaMask Pool and earn - - - 2.9% - - - annually. - - - - Learn more. - - - - ({ }), })); +jest.mock('../../../hooks/usePooledStakes', () => ({ + __esModule: true, + default: () => ({ + refreshPooledStakes: jest.fn(), + }), +})); + describe('ConfirmationFooter', () => { it('render matches snapshot', () => { const props: ConfirmationFooterProps = { diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx index ffaec56ae87..509c2054dcb 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx @@ -53,6 +53,13 @@ jest.mock('../../../../hooks/usePoolStakedDeposit', () => ({ }), })); +jest.mock('../../../../hooks/usePooledStakes', () => ({ + __esModule: true, + default: () => ({ + refreshPooledStakes: jest.fn(), + }), +})); + describe('FooterButtonGroup', () => { beforeEach(() => { jest.resetAllMocks(); @@ -70,7 +77,7 @@ describe('FooterButtonGroup', () => { ); expect(getByText(strings('stake.cancel'))).toBeDefined(); - expect(getByText(strings('stake.confirm'))).toBeDefined(); + expect(getByText(strings('stake.continue'))).toBeDefined(); expect(toJSON()).toMatchSnapshot(); }); @@ -89,8 +96,7 @@ describe('FooterButtonGroup', () => { fireEvent.press(getByText(strings('stake.cancel'))); - expect(mockNavigate).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith('Asset'); + expect(mockGoBack).toHaveBeenCalledTimes(1); expect(toJSON()).toMatchSnapshot(); }); 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 00464468daa..5a98d66c48b 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx @@ -22,15 +22,18 @@ import { FooterButtonGroupProps, } from './FooterButtonGroup.types'; import Routes from '../../../../../../../constants/navigation/Routes'; +import usePooledStakes from '../../../../hooks/usePooledStakes'; const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const { styles } = useStyles(styleSheet, {}); - const { navigate } = useNavigation(); + const navigation = useNavigation(); + const { navigate } = navigation; const activeAccount = useSelector(selectSelectedInternalAccount); const { attemptDepositTransaction } = usePoolStakedDeposit(); + const { refreshPooledStakes } = usePooledStakes(); const handleStake = async () => { if (!activeAccount?.address) return; @@ -51,19 +54,17 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { ({ transactionMeta }) => transactionMeta.id === transactionId, ); - // Engine.controllerMessenger.subscribeOnceIf( - // 'TransactionController:transactionConfirmed', - // () => { - // TODO: Call refreshPooledStakes(); - // refreshPooledStakes(); - // }, - // (transactionMeta) => transactionMeta.id === transactionId, - // ); + Engine.controllerMessenger.subscribeOnceIf( + 'TransactionController:transactionConfirmed', + () => { + refreshPooledStakes(); + }, + (transactionMeta) => transactionMeta.id === transactionId, + ); }; const handleConfirmation = () => { if (action === FooterButtonGroupActions.STAKE) return handleStake(); - // TODO: Add handler (STAKE-803) }; return ( @@ -78,12 +79,14 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { variant={ButtonVariants.Secondary} width={ButtonWidthTypes.Full} size={ButtonSize.Lg} - onPress={() => navigate('Asset')} + onPress={() => { + navigation.goBack(); + }} />