Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: STAKE-898: build select token component for earn products #13258

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import StakeEarningsHistoryView from './StakeEarningsHistoryView';
import useStakingEarningsHistory from '../../hooks/useStakingEarningsHistory';
import { MOCK_STAKED_ETH_ASSET } from '../../__mocks__/mockData';
import { MOCK_STAKED_ETH_MAINNET_ASSET } from '../../__mocks__/mockData';
import { fireLayoutEvent } from '../../../../../util/testUtils/react-native-svg-charts';
import { getStakingNavbar } from '../../../Navbar';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
Expand All @@ -24,7 +24,7 @@ jest.mock('@react-navigation/native', () => {
useRoute: () => ({
key: '1',
name: 'params',
params: { asset: MOCK_STAKED_ETH_ASSET },
params: { asset: MOCK_STAKED_ETH_MAINNET_ASSET },
}),
};
});
Expand Down
123 changes: 72 additions & 51 deletions app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
import { fireEvent } from '@testing-library/react-native';
import StakeInputView from './StakeInputView';
import { renderScreen } from '../../../../../util/test/renderWithProvider';
import renderWithProvider, {
DeepPartial,
} from '../../../../../util/test/renderWithProvider';
import Routes from '../../../../../constants/navigation/Routes';
import { backgroundState } from '../../../../../util/test/initial-root-state';
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 {
MOCK_ETH_MAINNET_ASSET,
MOCK_GET_VAULT_RESPONSE,
} from '../../__mocks__/mockData';
import { toWei } from '../../../../../util/number';
import { strings } from '../../../../../../locales/i18n';
// eslint-disable-next-line import/no-namespace
import * as useStakingGasFee from '../../hooks/useStakingGasFee';

function render(Component: React.ComponentType) {
return renderScreen(
Component,
{
name: Routes.STAKING.STAKE,
},
{
state: {
engine: {
backgroundState,
},
},
},
);
}
import {
STAKE_INPUT_VIEW_ACTIONS,
StakeInputViewProps,
} from './StakeInputView.types';
import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../../../util/test/accountsControllerTestUtils';
import { RootState } from '../../../../../reducers';
import { backgroundState } from '../../../../../util/test/initial-root-state';

const mockSetOptions = jest.fn();
const mockNavigate = jest.fn();
Expand All @@ -40,9 +35,7 @@ jest.mock('@react-navigation/native', () => {
...actualReactNavigation,
useNavigation: () => ({
navigate: mockNavigate,
setOptions: mockSetOptions.mockImplementation(
actualReactNavigation.useNavigation().setOptions,
),
setOptions: mockSetOptions,
reset: mockReset,
dangerouslyGetParent: () => ({
pop: mockPop,
Expand Down Expand Up @@ -148,77 +141,105 @@ jest.mock('../../hooks/useVaultData', () => ({
}),
}));

const mockInitialState: DeepPartial<RootState> = {
settings: {},
engine: {
backgroundState: {
...backgroundState,
AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
},
},
};

describe('StakeInputView', () => {
const baseProps: StakeInputViewProps = {
route: {
params: {
action: STAKE_INPUT_VIEW_ACTIONS.STAKE,
token: MOCK_ETH_MAINNET_ASSET,
},
key: Routes.STAKING.STAKE,
name: 'params',
},
};

const renderComponent = () =>
renderWithProvider(<StakeInputView {...baseProps} />, {
state: mockInitialState,
});

it('render matches snapshot', () => {
render(StakeInputView);
expect(screen.toJSON()).toMatchSnapshot();
const { toJSON } = renderComponent();
expect(toJSON()).toMatchSnapshot();
});

describe('when values are entered in the keypad', () => {
it('updates ETH and fiat values', () => {
render(StakeInputView);
const { toJSON, getByText } = renderComponent();

expect(toJSON()).toMatchSnapshot();

fireEvent.press(screen.getByText('2'));
fireEvent.press(getByText('2'));

expect(screen.getByText('4000 USD')).toBeTruthy();
expect(getByText('4000 USD')).toBeTruthy();
});
});

describe('currency toggle functionality', () => {
it('switches between ETH and fiat correctly', () => {
render(StakeInputView);
const { getByText } = renderComponent();

expect(screen.getByText('ETH')).toBeTruthy();
fireEvent.press(screen.getByText('0 USD'));
expect(getByText('ETH')).toBeTruthy();
fireEvent.press(getByText('0 USD'));

expect(screen.getByText('USD')).toBeTruthy();
expect(getByText('USD')).toBeTruthy();
});
});

describe('when calculating rewards', () => {
it('calculates estimated annual rewards based on input', () => {
render(StakeInputView);
const { getByText } = renderComponent();

fireEvent.press(screen.getByText('2'));
fireEvent.press(getByText('2'));

expect(screen.getByText('0.05 ETH')).toBeTruthy();
expect(getByText('0.05 ETH')).toBeTruthy();
});
});

describe('quick amount buttons', () => {
it('handles 25% quick amount button press correctly', () => {
render(StakeInputView);
const { getByText } = renderComponent();

fireEvent.press(screen.getByText('25%'));
fireEvent.press(getByText('25%'));

expect(screen.getByText('0.375')).toBeTruthy();
expect(getByText('0.375')).toBeTruthy();
});
});

describe('stake button states', () => {
it('displays `Enter amount` if input is 0', () => {
render(StakeInputView);
const { getByText } = renderComponent();

expect(screen.getByText('Enter amount')).toBeTruthy();
expect(getByText('Enter amount')).toBeTruthy();
});

it('displays `Review` on stake button if input is valid', () => {
render(StakeInputView);
const { getByText } = renderComponent();

fireEvent.press(screen.getByText('1'));
expect(screen.getByText('Review')).toBeTruthy();
fireEvent.press(getByText('1'));
expect(getByText('Review')).toBeTruthy();
});

it('displays `Not enough ETH` when input exceeds balance', () => {
render(StakeInputView);
const { getByText, queryAllByText } = renderComponent();

fireEvent.press(screen.getByText('4'));
expect(screen.queryAllByText('Not enough ETH')).toHaveLength(2);
fireEvent.press(getByText('4'));
expect(queryAllByText('Not enough ETH')).toHaveLength(2);
});

it('navigates to Learn more modal when learn icon is pressed', () => {
render(StakeInputView);
fireEvent.press(screen.getByLabelText('Learn More'));
const { getByLabelText } = renderComponent();
fireEvent.press(getByLabelText('Learn More'));
expect(mockNavigate).toHaveBeenCalledWith('StakeModals', {
screen: Routes.STAKING.MODALS.LEARN_MORE,
});
Expand All @@ -232,11 +253,11 @@ describe('StakeInputView', () => {
refreshGasValues: jest.fn(),
});

render(StakeInputView);
const { getByText } = renderComponent();

fireEvent.press(screen.getByText('25%'));
fireEvent.press(getByText('25%'));

fireEvent.press(screen.getByText(strings('stake.review')));
fireEvent.press(getByText(strings('stake.review')));

expect(mockNavigate).toHaveBeenLastCalledWith('StakeModals', {
screen: Routes.STAKING.MODALS.GAS_IMPACT,
Expand Down
16 changes: 13 additions & 3 deletions app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import { formatEther } from 'ethers/lib/utils';
import { EVENT_PROVIDERS, EVENT_LOCATIONS } from '../../constants/events';
import { selectConfirmationRedesignFlags } from '../../../../../selectors/featureFlagController';
import { selectSelectedInternalAccount } from '../../../../../selectors/accountsController';
import { StakeInputViewProps } from './StakeInputView.types';
import { getStakeInputViewTitle } from './utils';
import { isStablecoinLendingFeatureEnabled } from '../../constants';

const StakeInputView = () => {
const title = strings('stake.stake_eth');
const StakeInputView = ({ route }: StakeInputViewProps) => {
const navigation = useNavigation();
const { styles, theme } = useStyles(styleSheet, {});
const { trackEvent, createEventBuilder } = useMetrics();
Expand Down Expand Up @@ -169,6 +171,14 @@ const StakeInputView = () => {
: strings('stake.review');

useEffect(() => {
const title = isStablecoinLendingFeatureEnabled()
? getStakeInputViewTitle(
route?.params?.action,
route?.params?.token.symbol,
route?.params?.token.isETH,
)
: strings('stake.stake_eth');

navigation.setOptions(
getStakingNavbar(
title,
Expand All @@ -188,7 +198,7 @@ const StakeInputView = () => {
},
),
);
}, [navigation, theme.colors, title]);
}, [navigation, route.params, theme.colors]);

useEffect(() => {
calculateEstimatedAnnualRewards();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RouteProp } from '@react-navigation/native';
import { TokenI } from '../../../Tokens/types';
import { strings } from '../../../../../../locales/i18n';

export enum STAKE_INPUT_VIEW_ACTIONS {
STAKE = 'STAKE',
LEND = 'LEND',
}

export const STAKE_INPUT_ACTION_TO_LABEL_MAP = {
[STAKE_INPUT_VIEW_ACTIONS.STAKE]: strings('stake.stake'),
[STAKE_INPUT_VIEW_ACTIONS.LEND]: strings('stake.deposit'),
};

interface StakeInputViewRouteParams {
token: TokenI;
action: STAKE_INPUT_VIEW_ACTIONS;
}

export interface StakeInputViewProps {
route: RouteProp<{ params: StakeInputViewRouteParams }, 'params'>;
}
Loading
Loading