-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Custom Gas Modal Component (#6287)
* custom gas modal component * failing snapshot * validate for error txn with data * initialbackgroundstate in unit test
- Loading branch information
1 parent
6677a5f
commit 2c462b1
Showing
10 changed files
with
1,856 additions
and
263 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { StyleSheet } from 'react-native'; | ||
|
||
const createStyles = () => | ||
StyleSheet.create({ | ||
bottomModal: { | ||
justifyContent: 'flex-end', | ||
margin: 0, | ||
}, | ||
keyboardAwareWrapper: { | ||
flex: 1, | ||
justifyContent: 'flex-end', | ||
}, | ||
}); | ||
|
||
export default createStyles; |
140 changes: 140 additions & 0 deletions
140
app/components/UI/CustomGasModal/CustomGasModal.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import React from 'react'; | ||
|
||
import { fireEvent } from '@testing-library/react-native'; | ||
|
||
import Engine from '../../../core/Engine'; | ||
import initialBackgroundState from '../../../util/test/initial-background-state.json'; | ||
import renderWithProvider from '../../../util/test/renderWithProvider'; | ||
import CustomGasModal from './'; | ||
|
||
Engine.init({}); | ||
jest.mock('@react-navigation/native', () => ({ | ||
useNavigation: () => ({ | ||
navigation: {}, | ||
}), | ||
createNavigatorFactory: () => ({}), | ||
})); | ||
|
||
jest.mock('react-native-keyboard-aware-scroll-view', () => { | ||
const KeyboardAwareScrollView = jest.requireActual('react-native').ScrollView; | ||
return { KeyboardAwareScrollView }; | ||
}); | ||
|
||
const gasSelected = 'high'; | ||
|
||
const mockInitialState = { | ||
settings: {}, | ||
transaction: { | ||
selectedAsset: {}, | ||
transaction: { | ||
gas: '0x0', | ||
gasPrice: '0x0', | ||
data: '0x0', | ||
}, | ||
}, | ||
engine: { | ||
backgroundState: { | ||
...initialBackgroundState, | ||
}, | ||
}, | ||
}; | ||
|
||
jest.mock('react-redux', () => ({ | ||
...jest.requireActual('react-redux'), | ||
useSelector: (fn: any) => fn(mockInitialState), | ||
})); | ||
|
||
const mockedAction = jest.fn(); | ||
const updateGasState = jest.fn(); | ||
const eip1559GasData = { | ||
maxFeePerGas: '0x0', | ||
maxPriorityFeePerGas: '0x1', | ||
suggestedMaxFeePerGas: '0x2', | ||
suggestedMaxPriorityFeePerGas: '0x3', | ||
suggestedGasLimit: '0x4', | ||
}; | ||
const eip1559GasTxn = { | ||
suggestedGasLimit: '0x5', | ||
totalMaxHex: '0x6', | ||
}; | ||
|
||
const legacyGasData = { | ||
legacyGasLimit: '', | ||
suggestedGasPrice: '', | ||
}; | ||
|
||
const customGasModalSharedProps = { | ||
gasSelected, | ||
onChange: mockedAction, | ||
onCancel: mockedAction, | ||
isAnimating: false, | ||
onlyGas: false, | ||
validateAmount: mockedAction, | ||
updateGasState, | ||
onGasChanged: (gas: string) => mockedAction(gas), | ||
onGasCanceled: (gas: string) => mockedAction(gas), | ||
}; | ||
|
||
describe('CustomGasModal', () => { | ||
it('should render correctly', () => { | ||
const wrapper = renderWithProvider( | ||
<CustomGasModal | ||
{...customGasModalSharedProps} | ||
legacy | ||
legacyGasData={legacyGasData} | ||
/>, | ||
{ state: mockInitialState }, | ||
false, | ||
); | ||
expect(wrapper).toMatchSnapshot(); | ||
}); | ||
|
||
it('should contain gas price if legacy', async () => { | ||
const { findByText } = renderWithProvider( | ||
<CustomGasModal | ||
{...customGasModalSharedProps} | ||
legacy | ||
legacyGasData={legacyGasData} | ||
/>, | ||
{ state: mockInitialState }, | ||
false, | ||
); | ||
|
||
expect(await findByText('Gas price')).toBeDefined(); | ||
expect(await findByText('Gas limit must be at least 21000')).toBeDefined(); | ||
}); | ||
|
||
it('should contain gas fee if EIP1559 if legacy is false', () => { | ||
const { queryByText } = renderWithProvider( | ||
<CustomGasModal | ||
{...customGasModalSharedProps} | ||
legacy={false} | ||
EIP1559GasData={eip1559GasData} | ||
EIP1559GasTxn={eip1559GasTxn} | ||
/>, | ||
{ state: mockInitialState }, | ||
false, | ||
); | ||
|
||
expect(queryByText('Max fee')).toBeDefined(); | ||
}); | ||
|
||
it('should call updateParentState when saved', () => { | ||
const { getByText } = renderWithProvider( | ||
<CustomGasModal | ||
{...customGasModalSharedProps} | ||
validateAmount={mockedAction} | ||
updateGasState={updateGasState} | ||
legacy | ||
legacyGasData={legacyGasData} | ||
/>, | ||
{ state: mockInitialState }, | ||
false, | ||
); | ||
|
||
const saveButton = getByText('Save'); | ||
|
||
fireEvent.press(saveButton); | ||
expect(updateGasState).toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; | ||
import Modal from 'react-native-modal'; | ||
import { useSelector } from 'react-redux'; | ||
|
||
import { selectChainId } from '../../../selectors/networkController'; | ||
import { useAppThemeFromContext } from '../../../util/theme'; | ||
import EditGasFee1559 from '../../UI/EditGasFee1559Update'; | ||
import EditGasFeeLegacy from '../../UI/EditGasFeeLegacyUpdate'; | ||
import createStyles from './CustomGasModal.styles'; | ||
import { CustomGasModalProps } from './CustomGasModal.types'; | ||
|
||
const CustomGasModal = ({ | ||
gasSelected, | ||
animateOnChange, | ||
isAnimating, | ||
onlyGas, | ||
validateAmount, | ||
legacy, | ||
legacyGasData, | ||
EIP1559GasData, | ||
EIP1559GasTxn, | ||
onGasChanged, | ||
onGasCanceled, | ||
updateGasState, | ||
}: CustomGasModalProps) => { | ||
const { colors } = useAppThemeFromContext(); | ||
const styles = createStyles(); | ||
const transaction = useSelector((state: any) => state.transaction); | ||
const gasFeeEstimate = useSelector( | ||
(state: any) => | ||
state.engine.backgroundState.GasFeeController.gasFeeEstimates, | ||
); | ||
const primaryCurrency = useSelector( | ||
(state: any) => state.settings.primaryCurrency, | ||
); | ||
const chainId = useSelector((state: any) => selectChainId(state)); | ||
const selectedAsset = useSelector( | ||
(state: any) => state.transaction.selectedAsset, | ||
); | ||
const gasEstimateType = useSelector( | ||
(state: any) => | ||
state.engine.backgroundState.GasFeeController.gasEstimateType, | ||
); | ||
|
||
const [selectedGas, setSelectedGas] = useState(gasSelected); | ||
const [eip1559Txn, setEIP1559Txn] = useState(EIP1559GasTxn); | ||
const [legacyGasObj, setLegacyGasObj] = useState(legacyGasData); | ||
const [eip1559GasObj, setEIP1559GasObj] = useState(EIP1559GasData); | ||
const [isViewAnimating, setIsViewAnimating] = useState(false); | ||
const [error, setError] = useState(''); | ||
|
||
useEffect(() => { | ||
setIsViewAnimating(isAnimating); | ||
}, [isAnimating]); | ||
|
||
const onGasAnimationStart = useCallback(() => setIsViewAnimating(true), []); | ||
const onGasAnimationEnd = useCallback(() => setIsViewAnimating(false), []); | ||
|
||
const getGasAnalyticsParams = () => ({ | ||
active_currency: { value: selectedAsset.symbol, anonymous: true }, | ||
gas_estimate_type: gasEstimateType, | ||
}); | ||
|
||
const onChangeGas = (gasValue: string) => { | ||
setSelectedGas(gasValue); | ||
onGasChanged(selectedGas); | ||
}; | ||
|
||
const onCancelGas = () => { | ||
onGasCanceled(selectedGas); | ||
}; | ||
|
||
const updatedTransactionFrom = useMemo( | ||
() => ({ | ||
...transaction, | ||
data: transaction?.transaction?.data, | ||
from: transaction?.transaction?.from, | ||
}), | ||
[transaction], | ||
); | ||
|
||
const onSaveLegacyGasOption = useCallback( | ||
(gasTxn, gasObj, gasSelect) => { | ||
gasTxn.error = validateAmount({ | ||
transaction: updatedTransactionFrom, | ||
total: gasTxn.totalHex, | ||
}); | ||
setLegacyGasObj(gasObj); | ||
setError(gasTxn?.error); | ||
updateGasState({ gasTxn, gasObj, gasSelect, txnType: legacy }); | ||
}, | ||
[validateAmount, updatedTransactionFrom, legacy, updateGasState], | ||
); | ||
|
||
const onSaveEIP1559GasOption = useCallback( | ||
(gasTxn, gasObj) => { | ||
gasTxn.error = validateAmount({ | ||
transaction: updatedTransactionFrom, | ||
total: gasTxn.totalMaxHex, | ||
}); | ||
|
||
setEIP1559Txn(gasTxn); | ||
setEIP1559GasObj(gasObj); | ||
setError(gasTxn?.error); | ||
updateGasState({ | ||
gasTxn, | ||
gasObj, | ||
gasSelect: selectedGas, | ||
txnType: legacy, | ||
}); | ||
}, | ||
[ | ||
validateAmount, | ||
selectedGas, | ||
updatedTransactionFrom, | ||
legacy, | ||
updateGasState, | ||
], | ||
); | ||
|
||
const legacyGasObject = { | ||
legacyGasLimit: legacyGasObj?.legacyGasLimit, | ||
suggestedGasPrice: legacyGasObj?.suggestedGasPrice, | ||
}; | ||
|
||
const eip1559GasObject = { | ||
suggestedMaxFeePerGas: | ||
eip1559GasObj?.suggestedMaxFeePerGas || | ||
eip1559GasObj?.[selectedGas]?.suggestedMaxFeePerGas, | ||
suggestedMaxPriorityFeePerGas: | ||
eip1559GasObj?.suggestedMaxPriorityFeePerGas || | ||
gasFeeEstimate[selectedGas]?.suggestedMaxPriorityFeePerGas, | ||
suggestedGasLimit: | ||
eip1559GasObj?.suggestedGasLimit || eip1559Txn?.suggestedGasLimit, | ||
}; | ||
|
||
return ( | ||
<Modal | ||
isVisible | ||
animationIn="slideInUp" | ||
animationOut="slideOutDown" | ||
style={styles.bottomModal} | ||
backdropColor={colors.overlay.default} | ||
backdropOpacity={1} | ||
animationInTiming={600} | ||
animationOutTiming={600} | ||
onBackdropPress={onCancelGas} | ||
onBackButtonPress={onCancelGas} | ||
onSwipeComplete={onCancelGas} | ||
swipeDirection={'down'} | ||
propagateSwipe | ||
> | ||
<KeyboardAwareScrollView | ||
contentContainerStyle={styles.keyboardAwareWrapper} | ||
> | ||
{legacy ? ( | ||
<EditGasFeeLegacy | ||
selected={selectedGas} | ||
gasEstimateType={gasEstimateType} | ||
gasOptions={gasFeeEstimate} | ||
onChange={onChangeGas} | ||
primaryCurrency={primaryCurrency} | ||
chainId={chainId} | ||
onCancel={onCancelGas} | ||
onSave={onSaveLegacyGasOption} | ||
animateOnChange={animateOnChange} | ||
isAnimating={isViewAnimating} | ||
analyticsParams={getGasAnalyticsParams()} | ||
view={'SendTo (Confirm)'} | ||
onlyGas={false} | ||
selectedGasObject={legacyGasObject} | ||
error={error} | ||
onUpdatingValuesStart={onGasAnimationStart} | ||
onUpdatingValuesEnd={onGasAnimationEnd} | ||
/> | ||
) : ( | ||
<EditGasFee1559 | ||
selectedGasValue={selectedGas} | ||
gasOptions={gasFeeEstimate} | ||
onChange={onChangeGas} | ||
primaryCurrency={primaryCurrency} | ||
chainId={chainId} | ||
onCancel={onCancelGas} | ||
onSave={onSaveEIP1559GasOption} | ||
animateOnChange={animateOnChange} | ||
isAnimating={isAnimating} | ||
analyticsParams={getGasAnalyticsParams()} | ||
view={'SendTo (Confirm)'} | ||
selectedGasObject={eip1559GasObject} | ||
onlyGas={onlyGas} | ||
error={error} | ||
/> | ||
)} | ||
</KeyboardAwareScrollView> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default CustomGasModal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
export interface CustomGasModalProps { | ||
gasSelected: string; | ||
onChange: (gas: string) => void; | ||
onCancel: () => void; | ||
animateOnChange?: boolean; | ||
isAnimating: any; | ||
onlyGas: boolean; | ||
validateAmount: ({ | ||
transaction, | ||
total, | ||
}: { | ||
transaction: any; | ||
total: string; | ||
}) => void; | ||
legacy: boolean; | ||
legacyGasData?: { | ||
legacyGasLimit: string; | ||
suggestedGasPrice: string; | ||
}; | ||
EIP1559GasData?: { | ||
maxFeePerGas: string; | ||
maxPriorityFeePerGas: string; | ||
suggestedMaxFeePerGas: string; | ||
suggestedMaxPriorityFeePerGas: string; | ||
suggestedGasLimit: string; | ||
}; | ||
EIP1559GasTxn?: { | ||
suggestedGasLimit: string; | ||
totalMaxHex: string; | ||
}; | ||
onGasChanged: (gas: string) => void; | ||
onGasCanceled: (gas: string) => void; | ||
updateGasState: (state: { | ||
gasTxn: any; | ||
gasObj: any; | ||
gasSelect: string; | ||
txnType: boolean; | ||
}) => void; | ||
} |
Oops, something went wrong.