diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index aeda7315f1d..07f352786e0 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -52,7 +52,6 @@ import Icon, { IconColor, } from '../../../component-library/components/Icons/Icon'; import { AddContactViewSelectorsIDs } from '../../../../e2e/selectors/Settings/Contacts/AddContactView.selectors'; -import { ImportTokenViewSelectorsIDs } from '../../../../e2e/selectors/wallet/ImportTokenView.selectors'; const trackEvent = (event, params = {}) => { MetaMetrics.getInstance().trackEvent(event, params); @@ -1849,6 +1848,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) { fontSize: 14, ...fontStyles.normal, }, + headerTitle: { + alignItems: 'center', + }, }); function navigationPop() { @@ -1857,7 +1859,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) { return { headerTitle: () => ( - {title} + + {title} + ), headerStyle: innerStyles.headerStyle, headerLeft: () => @@ -1868,7 +1872,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) { onPress={navigationPop} style={innerStyles.headerLeft} /> - ) : null, + ) : ( + <> + ), headerRight: () => hasCancelButton ? ( - ) : null, + ) : ( + <> + ), }; } diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx index 90639128471..c7c3005ad89 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx @@ -4,11 +4,11 @@ import StakeInputView from './StakeInputView'; import { renderScreen } from '../../../../../util/test/renderWithProvider'; import Routes from '../../../../../constants/navigation/Routes'; import { backgroundState } from '../../../../../util/test/initial-root-state'; -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'; +import { toWei } from '../../../../../util/number'; function render(Component: React.ComponentType) { return renderScreen( @@ -54,7 +54,7 @@ jest.mock('../../../../../selectors/currencyRateController.ts', () => ({ selectCurrentCurrency: jest.fn(() => 'USD'), })); -const mockBalanceBN = new BN('1500000000000000000'); +const mockBalanceBN = toWei('1.5'); // 1.5 ETH const mockPooledStakingContractService: PooledStakingContract = { chainId: ChainId.ETHEREUM, @@ -84,12 +84,25 @@ jest.mock('../../hooks/useStakeContext.ts', () => ({ jest.mock('../../hooks/useBalance', () => ({ __esModule: true, default: () => ({ - balance: '1.5', + balanceETH: '1.5', balanceWei: mockBalanceBN, balanceFiatNumber: '3000', }), })); +const mockGasFee = toWei('0.0001'); + +jest.mock('../../hooks/useStakingGasFee', () => ({ + __esModule: true, + default: () => ({ + estimatedGasFeeWei: mockGasFee, + gasLimit: 70122, + isLoadingStakingGasFee: false, + isStakingGasFeeError: false, + refreshGasValues: jest.fn(), + }), +})); + const mockVaultData = MOCK_GET_VAULT_RESPONSE; // Mock hooks diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index 9351d57b785..9dd1fdfaff5 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -17,14 +17,12 @@ import EstimatedAnnualRewardsCard from '../../components/EstimatedAnnualRewardsC import Routes from '../../../../../constants/navigation/Routes'; import styleSheet from './StakeInputView.styles'; import useStakingInputHandlers from '../../hooks/useStakingInput'; -import useBalance from '../../hooks/useBalance'; import InputDisplay from '../../components/InputDisplay'; const StakeInputView = () => { const title = strings('stake.stake_eth'); const navigation = useNavigation(); const { styles, theme } = useStyles(styleSheet, {}); - const { balance, balanceFiatNumber, balanceWei } = useBalance(); const { isEth, @@ -45,7 +43,9 @@ const StakeInputView = () => { annualRewardsFiat, annualRewardRate, isLoadingVaultData, - } = useStakingInputHandlers(balanceWei); + handleMax, + balanceValue, + } = useStakingInputHandlers(); const navigateToLearnMoreModal = () => { navigation.navigate('StakeModals', { @@ -73,6 +73,15 @@ const StakeInputView = () => { annualRewardRate, ]); + const handleMaxButtonPress = () => { + navigation.navigate('StakeModals', { + screen: Routes.STAKING.MODALS.MAX_INPUT, + params: { + handleMaxPress: handleMax, + }, + }); + }; + const balanceText = strings('stake.balance'); const buttonLabel = !isNonZeroAmount @@ -81,10 +90,6 @@ const StakeInputView = () => { ? strings('stake.not_enough_eth') : strings('stake.review'); - const balanceValue = isEth - ? `${balance} ETH` - : `${balanceFiatNumber?.toString()} ${currentCurrency.toUpperCase()}`; - useEffect(() => { navigation.setOptions( getStakingNavbar(title, navigation, theme.colors, { @@ -121,6 +126,7 @@ const StakeInputView = () => { + - - Stake ETH - + + Stake ETH + + ({ default: () => ({ stakedBalanceWei: mockPooledStakeData.assets, stakedBalanceFiat: MOCK_STAKED_ETH_ASSET.balanceFiat, + formattedStakedBalanceETH: '5.79133 ETH', }), })); diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx index ddfa676d158..269151a431b 100644 --- a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx +++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx @@ -1,6 +1,5 @@ import { useNavigation } from '@react-navigation/native'; import React, { useCallback, useEffect } from 'react'; -import { BN } from 'ethereumjs-util'; import UnstakeInputViewBanner from './UnstakeBanner'; import { strings } from '../../../../../../locales/i18n'; import Button, { @@ -9,26 +8,22 @@ import Button, { ButtonWidthTypes, } from '../../../../../component-library/components/Buttons/Button'; import { TextVariant } from '../../../../../component-library/components/Texts/Text'; -import { renderFromWei, weiToFiatNumber } from '../../../../../util/number'; import Keypad from '../../../../Base/Keypad'; import { useStyles } from '../../../../hooks/useStyles'; import { getStakingNavbar } from '../../../Navbar'; import ScreenLayout from '../../../Ramp/components/ScreenLayout'; import QuickAmounts from '../../components/QuickAmounts'; 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'; import Routes from '../../../../../constants/navigation/Routes'; +import useUnstakingInputHandlers from '../../hooks/useUnstakingInput'; const UnstakeInputView = () => { const title = strings('stake.unstake_eth'); const navigation = useNavigation(); const { styles, theme } = useStyles(styleSheet, {}); - const { stakedBalanceWei } = useBalance(); - const { isEth, currentCurrency, @@ -42,19 +37,10 @@ const UnstakeInputView = () => { percentageOptions, handleAmountPress, handleKeypadChange, - conversionRate, - } = useStakingInputHandlers(new BN(stakedBalanceWei)); - - const stakeBalanceInEth = renderFromWei(stakedBalanceWei, 5); - const stakeBalanceFiatNumber = weiToFiatNumber( - stakedBalanceWei, - conversionRate, - ); + stakedBalanceValue, + } = useUnstakingInputHandlers(); const stakedBalanceText = strings('stake.staked_balance'); - const stakedBalanceValue = isEth - ? `${stakeBalanceInEth} ETH` - : `${stakeBalanceFiatNumber?.toString()} ${currentCurrency.toUpperCase()}`; const buttonLabel = !isNonZeroAmount ? strings('stake.enter_amount') 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 0e1816f6621..263d1be6b4e 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 @@ -102,26 +102,49 @@ exports[`UnstakeInputView render matches snapshot 1`] = ` pointerEvents="box-none" style={ { - "marginHorizontal": 16, + "alignItems": "flex-start", + "bottom": 0, + "justifyContent": "center", + "left": 0, + "opacity": 1, + "position": "absolute", + "top": 0, + } + } + /> + - - Unstake ETH - + + Unstake ETH + + + StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + textContainer: { + paddingBottom: 16, + paddingRight: 16, + }, + buttonContainer: { + flexDirection: 'row', + gap: 16, + paddingHorizontal: 16, + paddingBottom: 16, + }, + button: { + flex: 1, + }, + }); + +export default createMaxInputModalStyles; diff --git a/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.test.tsx b/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.test.tsx new file mode 100644 index 00000000000..8092f9d9fc5 --- /dev/null +++ b/app/components/UI/Stake/components/MaxInputModal/MaxInputModal.test.tsx @@ -0,0 +1,67 @@ +import { renderScreen } from '../../../../../util/test/renderWithProvider'; +import MaxInputModal from '.'; +import { fireEvent } from '@testing-library/react-native'; +import Routes from '../../../../../constants/navigation/Routes'; + +const mockNavigate = jest.fn(); +const mockGoBack = jest.fn(); +const mockHandleMaxPress = jest.fn(); + +jest.mock('@react-navigation/native', () => { + const actualReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualReactNavigation, + useRoute: () => ({ + params: { + handleMaxPress: mockHandleMaxPress, + }, + }), + useNavigation: () => ({ + navigate: mockNavigate, + goBack: mockGoBack, + }), + }; +}); + +const renderMaxInputModal = () => + renderScreen(MaxInputModal, { name: Routes.STAKING.MODALS.MAX_INPUT }); + +describe('MaxInputModal', () => { + it('render matches snapshot', () => { + const { toJSON } = renderMaxInputModal(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('calls handleMaxPress when "Use max" button is pressed', () => { + const { getByText } = renderMaxInputModal(); + + // Press the "Use Max" button + const useMaxButton = getByText('Use max'); + fireEvent.press(useMaxButton); + + // Check if handleMaxPress was called + expect(mockHandleMaxPress).toHaveBeenCalledTimes(1); + }); + + it('closes the BottomSheet when "Cancel" button is pressed', () => { + const { getByText } = renderMaxInputModal(); + + // Press the "Cancel" button + const cancelButton = getByText('Cancel'); + fireEvent.press(cancelButton); + + // Check if the BottomSheet's close function was called + expect(mockGoBack).toHaveBeenCalled(); + }); + + it('closes the BottomSheet when "Use Max" button is pressed', () => { + const { getByText } = renderMaxInputModal(); + + // Press the "Use Max" button + const useMaxButton = getByText('Use max'); + fireEvent.press(useMaxButton); + + // Check if the BottomSheet's close function was called + expect(mockGoBack).toHaveBeenCalled(); + }); +}); diff --git a/app/components/UI/Stake/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap b/app/components/UI/Stake/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap new file mode 100644 index 00000000000..c3aa8b2abf5 --- /dev/null +++ b/app/components/UI/Stake/components/MaxInputModal/__snapshots__/MaxInputModal.test.tsx.snap @@ -0,0 +1,656 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MaxInputModal render matches snapshot 1`] = ` + + + + + + + + + + + + + MaxInput + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Max + + + + + + + + + + + + + Max is the total amount of ETH you have, minus the gas fee required to stake. It’s a good idea to keep some extra ETH in your wallet for future transactions. + + + + + + + + Cancel + + + + + + + Use max + + + + + + + + + + + + + + + + + +`; diff --git a/app/components/UI/Stake/components/MaxInputModal/index.tsx b/app/components/UI/Stake/components/MaxInputModal/index.tsx new file mode 100644 index 00000000000..a7534a2214c --- /dev/null +++ b/app/components/UI/Stake/components/MaxInputModal/index.tsx @@ -0,0 +1,79 @@ +import React, { useRef } from 'react'; +import { View } from 'react-native'; +import BottomSheet, { + type BottomSheetRef, +} from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import Button, { + ButtonSize, + ButtonVariants, + ButtonWidthTypes, +} from '../../../../../component-library/components/Buttons/Button'; +import { strings } from '../../../../../../locales/i18n'; +import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import createMaxInputModalStyles from './MaxInputModal.styles'; +import { useRoute, RouteProp } from '@react-navigation/native'; + +const styles = createMaxInputModalStyles(); + +interface MaxInputModalRouteParams { + handleMaxPress: () => void; +} + +const MaxInputModal = () => { + const route = + useRoute>(); + const sheetRef = useRef(null); + + const { handleMaxPress } = route.params; + + const handleCancel = () => { + sheetRef.current?.onCloseBottomSheet(); + }; + + const handleConfirm = () => { + sheetRef.current?.onCloseBottomSheet(); + handleMaxPress(); + }; + + return ( + + + + + {strings('stake.max_modal.title')} + + + + + {strings('stake.max_modal.description')} + + + + + +