Skip to content

Commit

Permalink
feat: add max tooltip for staking with gas fee consideration (#12025)
Browse files Browse the repository at this point in the history
## **Description**

This PR renders the max input modal component when the user clicks on
the max button on the stake input screen. This is also responsible for
subtracting the gas fees from the available balance before letting the
user continue to the deposit flow.


This PR also makes some minor changes and refactors as described below:

- Fixes the misaligned staking navbar on Android devices.
- The claim stake button will now be disabled until the
transaction-controllers has closed. This prevents users from initiating
multiple claims simultaneously.


## **Related issues**

Fixes:
[STAKE-847](https://consensyssoftware.atlassian.net/jira/software/projects/STAKE/boards/550/backlog?selectedIssue=STAKE-847)

## **Manual testing steps**

1. Add export MM_POOLED_STAKING_UI_ENABLED=true to .js.env file.
2. Click on Earn CTA  
3. Click on Max button 
4. Max input bottom sheet should appear and clicking on use max will
calculate the maximum amount that can be staked by subtracting gas fees.

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **New : Max Input Modal** 



https://github.com/user-attachments/assets/a48ed3f2-f165-4fe7-9dba-133c63cf9d44




### **Navbar alignment Fix**

### **Before**

<!-- [screenshots/recordings] -->
<img width="408" alt="image"
src="https://github.com/user-attachments/assets/d8e5da18-d2f1-450b-9eac-a581af8ec24c">

### **After**

<!-- [screenshots/recordings] -->
<img width="408" alt="image"
src="https://github.com/user-attachments/assets/cdd16bc7-2249-4bd4-b1f4-deb210e50bce">

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


[STAKE-847]:
https://consensyssoftware.atlassian.net/browse/STAKE-847?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Matthew Grainger <matthewgrainger33@gmail.com>
Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent 627ed0c commit 82aec27
Show file tree
Hide file tree
Showing 30 changed files with 1,777 additions and 206 deletions.
16 changes: 12 additions & 4 deletions app/components/UI/Navbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -1849,6 +1848,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) {
fontSize: 14,
...fontStyles.normal,
},
headerTitle: {
alignItems: 'center',
},
});

function navigationPop() {
Expand All @@ -1857,7 +1859,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) {

return {
headerTitle: () => (
<MorphText variant={TextVariant.HeadingMD}>{title}</MorphText>
<View style={innerStyles.headerTitle}>
<MorphText variant={TextVariant.HeadingMD}>{title}</MorphText>
</View>
),
headerStyle: innerStyles.headerStyle,
headerLeft: () =>
Expand All @@ -1868,7 +1872,9 @@ export function getStakingNavbar(title, navigation, themeColors, options) {
onPress={navigationPop}
style={innerStyles.headerLeft}
/>
) : null,
) : (
<></>
),
headerRight: () =>
hasCancelButton ? (
<TouchableOpacity
Expand All @@ -1879,6 +1885,8 @@ export function getStakingNavbar(title, navigation, themeColors, options) {
{strings('navigation.cancel')}
</Text>
</TouchableOpacity>
) : null,
) : (
<></>
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
20 changes: 13 additions & 7 deletions app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -45,7 +43,9 @@ const StakeInputView = () => {
annualRewardsFiat,
annualRewardRate,
isLoadingVaultData,
} = useStakingInputHandlers(balanceWei);
handleMax,
balanceValue,
} = useStakingInputHandlers();

const navigateToLearnMoreModal = () => {
navigation.navigate('StakeModals', {
Expand Down Expand Up @@ -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
Expand All @@ -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, {
Expand Down Expand Up @@ -121,6 +126,7 @@ const StakeInputView = () => {
<QuickAmounts
amounts={percentageOptions}
onAmountPress={handleAmountPress}
onMaxPress={handleMaxButtonPress}
/>
<Keypad
value={isEth ? amountEth : fiatAmount}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,26 +102,49 @@ exports[`StakeInputView 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,
}
}
/>
<View
collapsable={false}
pointerEvents="box-none"
style={
{
"marginHorizontal": 72,
"opacity": 1,
}
}
>
<Text
accessibilityRole="text"
<View
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Bold",
"fontSize": 18,
"fontWeight": "700",
"letterSpacing": 0,
"lineHeight": 24,
"alignItems": "center",
}
}
>
Stake ETH
</Text>
<Text
accessibilityRole="text"
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Bold",
"fontSize": 18,
"fontWeight": "700",
"letterSpacing": 0,
"lineHeight": 24,
}
}
>
Stake ETH
</Text>
</View>
</View>
<View
collapsable={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ jest.mock('../../hooks/useBalance', () => ({
default: () => ({
stakedBalanceWei: mockPooledStakeData.assets,
stakedBalanceFiat: MOCK_STAKED_ETH_ASSET.balanceFiat,
formattedStakedBalanceETH: '5.79133 ETH',
}),
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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,
Expand All @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
/>
<View
collapsable={false}
pointerEvents="box-none"
style={
{
"marginHorizontal": 72,
"opacity": 1,
}
}
>
<Text
accessibilityRole="text"
<View
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Bold",
"fontSize": 18,
"fontWeight": "700",
"letterSpacing": 0,
"lineHeight": 24,
"alignItems": "center",
}
}
>
Unstake ETH
</Text>
<Text
accessibilityRole="text"
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Bold",
"fontSize": 18,
"fontWeight": "700",
"letterSpacing": 0,
"lineHeight": 24,
}
}
>
Unstake ETH
</Text>
</View>
</View>
<View
collapsable={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StyleSheet } from 'react-native';

const createMaxInputModalStyles = () =>
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;
Loading

0 comments on commit 82aec27

Please sign in to comment.