Skip to content

Commit

Permalink
chore: Custom Gas Modal Component (#6287)
Browse files Browse the repository at this point in the history
* custom gas modal component

* failing snapshot

* validate for error txn with data

* initialbackgroundstate in unit test
  • Loading branch information
blackdevelopa authored Jul 26, 2023
1 parent 6677a5f commit 2c462b1
Show file tree
Hide file tree
Showing 10 changed files with 1,856 additions and 263 deletions.
15 changes: 15 additions & 0 deletions app/components/UI/CustomGasModal/CustomGasModal.styles.ts
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 app/components/UI/CustomGasModal/CustomGasModal.test.tsx
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();
});
});
200 changes: 200 additions & 0 deletions app/components/UI/CustomGasModal/CustomGasModal.tsx
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;
39 changes: 39 additions & 0 deletions app/components/UI/CustomGasModal/CustomGasModal.types.ts
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;
}
Loading

0 comments on commit 2c462b1

Please sign in to comment.