Skip to content

Commit

Permalink
feat: add max input modal component
Browse files Browse the repository at this point in the history
  • Loading branch information
amitabh94 committed Oct 25, 2024
1 parent 475e9bf commit 14b2fc1
Show file tree
Hide file tree
Showing 13 changed files with 486 additions and 24 deletions.
11 changes: 11 additions & 0 deletions app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const StakeInputView = () => {
annualRewardsFiat,
annualRewardRate,
isLoadingVaultData,
handleMaxPress,
} = useStakingInputHandlers(balanceWei);

const navigateToLearnMoreModal = () => {
Expand Down Expand Up @@ -73,6 +74,15 @@ const StakeInputView = () => {
annualRewardRate,
]);

const handleMaxButtonPress = () => {
navigation.navigate('StakeModals', {
screen: Routes.STAKING.MODALS.MAX_INPUT,
params: {
handleMaxPress,
},
});
};

const balanceText = strings('stake.balance');

const buttonLabel = !isNonZeroAmount
Expand Down Expand Up @@ -121,6 +131,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 @@ -16,11 +16,11 @@ 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');
Expand All @@ -43,7 +43,7 @@ const UnstakeInputView = () => {
handleAmountPress,
handleKeypadChange,
conversionRate,
} = useStakingInputHandlers(new BN(stakedBalanceWei));
} = useUnstakingInputHandlers(new BN(stakedBalanceWei));

const stakeBalanceInEth = renderFromWei(stakedBalanceWei, 5);
const stakeBalanceFiatNumber = weiToFiatNumber(
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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import MaxInputModal from '.';
import { SafeAreaProvider } from 'react-native-safe-area-context';

const mockNavigate = jest.fn();

jest.mock('@react-navigation/native', () => {
const actualReactNavigation = jest.requireActual('@react-navigation/native');
return {
...actualReactNavigation,
useNavigation: () => ({
navigate: mockNavigate,
}),
};
});

const renderMaxInputModal = () =>
renderWithProvider(
<SafeAreaProvider>
<MaxInputModal />,
</SafeAreaProvider>,
);

describe('MaxInputModal', () => {
it('render matches snapshot', () => {
const { toJSON } = renderMaxInputModal();
expect(toJSON()).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MaxInputModal render matches snapshot 1`] = `
<RNCSafeAreaProvider
onInsetsChange={[Function]}
style={
[
{
"flex": 1,
},
undefined,
]
}
/>
`;
79 changes: 79 additions & 0 deletions app/components/UI/Stake/components/MaxInputModal/index.tsx
Original file line number Diff line number Diff line change
@@ -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<RouteProp<{ params: MaxInputModalRouteParams }, 'params'>>();
const sheetRef = useRef<BottomSheetRef>(null);

const { handleMaxPress } = route.params;

const handleCancel = () => {
sheetRef.current?.onCloseBottomSheet();
};

const handleConfirm = () => {
sheetRef.current?.onCloseBottomSheet();
handleMaxPress();
};

return (
<BottomSheet ref={sheetRef}>
<View style={styles.container}>
<BottomSheetHeader onClose={handleCancel}>
<Text variant={TextVariant.HeadingMD}>
{strings('stake.max_modal.title')}
</Text>
</BottomSheetHeader>
<View style={styles.textContainer}>
<Text variant={TextVariant.BodyMD}>
{strings('stake.max_modal.description')}
</Text>
</View>
</View>
<View style={styles.buttonContainer}>
<View style={styles.button}>
<Button
onPress={handleCancel}
label={strings('stake.cancel')}
variant={ButtonVariants.Secondary}
width={ButtonWidthTypes.Full}
size={ButtonSize.Lg}
/>
</View>
<View style={styles.button}>
<Button
onPress={handleConfirm}
label={strings('stake.use_max')}
variant={ButtonVariants.Primary}
width={ButtonWidthTypes.Full}
size={ButtonSize.Lg}
/>
</View>
</View>
</BottomSheet>
);
};

export default MaxInputModal;
47 changes: 32 additions & 15 deletions app/components/UI/Stake/components/QuickAmounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,44 +39,61 @@ const createStyles = (colors: Colors) =>
interface AmountProps {
amount: QuickAmount;
onPress: (amount: QuickAmount) => void;

onMaxPress?: () => void;
disabled?: boolean;
}

const Amount = ({ amount, onPress }: AmountProps) => {
const Amount = ({ amount, onPress, onMaxPress }: AmountProps) => {
const { value, label } = amount;
const { colors } = useTheme();
const styles = createStyles(colors);

const handlePress = useCallback(() => {
if (value === 1 && onMaxPress) {
onMaxPress();
return;
}
onPress(amount);
}, [onPress, amount]);
}, [value, onMaxPress, amount, onPress]);

return (
<ButtonBase
onPress={handlePress}
size={ButtonSize.Md}
width={ButtonWidthTypes.Full}
label={label}
labelColor={TextColor.Default}
labelTextVariant={TextVariant.BodyMDMedium}
{...(value === 1 ? { startIconName: IconName.Sparkle } : {})}
style={styles.amount}
/>
<>
<ButtonBase
onPress={handlePress}
size={ButtonSize.Md}
width={ButtonWidthTypes.Full}
label={label}
labelColor={TextColor.Default}
labelTextVariant={TextVariant.BodyMDMedium}
{...(value === 1 ? { startIconName: IconName.Sparkle } : {})}
style={styles.amount}
/>
</>
);
};

interface QuickAmountsProps {
amounts: QuickAmount[];
onAmountPress: (amount: QuickAmount) => void;
onMaxPress?: () => void;
}

const QuickAmounts = ({ amounts, onAmountPress }: QuickAmountsProps) => {
const QuickAmounts = ({
amounts,
onAmountPress,
onMaxPress,
}: QuickAmountsProps) => {
const { colors } = useTheme();
const styles = createStyles(colors);
return (
<View style={styles.content}>
{amounts.map((amount, index: number) => (
<Amount amount={amount} onPress={onAmountPress} key={index} />
<Amount
amount={amount}
onPress={onAmountPress}
onMaxPress={onMaxPress}
key={index}
/>
))}
</View>
);
Expand Down
59 changes: 59 additions & 0 deletions app/components/UI/Stake/hooks/useStakingGasFee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState, useEffect } from 'react';
import { useStakeContext } from './useStakeContext';
import type { PooledStakingContract } from '@metamask/stake-sdk';
import useGasPriceEstimation from '../../Ramp/hooks/useGasPriceEstimation';
import { useSelector } from 'react-redux';
import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController';
import { formatEther } from 'ethers/lib/utils';

interface StakingGasFee {
estimatedGasFeeETH: string;
gasLimit: number;
}

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const DEFAULT_GAS_LIMIT = 21000;
const GAS_LIMIT_BUFFER = 1.3;

const useStakingGasFee = (depositValueWei: string): StakingGasFee => {
const sdk = useStakeContext();
const selectedAddress =
useSelector(selectSelectedInternalAccountChecksummedAddress) || '';
const pooledStakingContract = sdk.stakingContract as PooledStakingContract;
const [gasLimit, setGasLimit] = useState<number>(0);

useEffect(() => {
const fetchDepositGasLimit = async () => {
try {
const depositGasLimit = await pooledStakingContract.estimateDepositGas(
formatEther(depositValueWei),
selectedAddress,
ZERO_ADDRESS,
);

const gasLimitWithBuffer = Math.ceil(
depositGasLimit * GAS_LIMIT_BUFFER,
);

setGasLimit(gasLimitWithBuffer);
} catch (error) {
console.error('Error fetching gas price or gas limit:', error);
setGasLimit(DEFAULT_GAS_LIMIT);
}
};

fetchDepositGasLimit();
}, [depositValueWei, pooledStakingContract, selectedAddress]);

const gasPriceEstimation = useGasPriceEstimation({
gasLimit,
estimateRange: 'high',
});

const estimatedGasFeeETH =
gasPriceEstimation?.estimatedGasFee?.toString() || '0';

return { estimatedGasFeeETH, gasLimit };
};

export default useStakingGasFee;
Loading

0 comments on commit 14b2fc1

Please sign in to comment.