mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-02 06:07:06 +01:00
381 lines
14 KiB
JavaScript
381 lines
14 KiB
JavaScript
|
import React, { useState, useEffect, useContext } from 'react';
|
||
|
import { useDispatch, useSelector } from 'react-redux';
|
||
|
import PropTypes from 'prop-types';
|
||
|
import classnames from 'classnames';
|
||
|
|
||
|
import { I18nContext } from '../../../contexts/i18n';
|
||
|
import ButtonGroup from '../../../components/ui/button-group';
|
||
|
import Button from '../../../components/ui/button';
|
||
|
import InfoTooltip from '../../../components/ui/info-tooltip';
|
||
|
import ToggleButton from '../../../components/ui/toggle-button';
|
||
|
import Box from '../../../components/ui/box';
|
||
|
import Typography from '../../../components/ui/typography';
|
||
|
import {
|
||
|
TypographyVariant,
|
||
|
AlignItems,
|
||
|
JustifyContent,
|
||
|
DISPLAY,
|
||
|
SEVERITIES,
|
||
|
FlexDirection,
|
||
|
} from '../../../helpers/constants/design-system';
|
||
|
import { getTranslatedStxErrorMessage } from '../swaps.util';
|
||
|
import {
|
||
|
Slippage,
|
||
|
SLIPPAGE_OVER_LIMIT_ERROR,
|
||
|
SLIPPAGE_NEGATIVE_ERROR,
|
||
|
} from '../../../../shared/constants/swaps';
|
||
|
import {
|
||
|
BannerAlert,
|
||
|
Modal,
|
||
|
ModalOverlay,
|
||
|
ModalContent,
|
||
|
ModalHeader,
|
||
|
ButtonPrimary,
|
||
|
} from '../../../components/component-library';
|
||
|
import { setSwapsErrorKey } from '../../../store/actions';
|
||
|
import { getSwapsErrorKey } from '../../../ducks/swaps/swaps';
|
||
|
|
||
|
export default function TransactionSettings({
|
||
|
onSelect,
|
||
|
onModalClose,
|
||
|
maxAllowedSlippage,
|
||
|
currentSlippage,
|
||
|
smartTransactionsEnabled,
|
||
|
smartTransactionsOptInStatus,
|
||
|
setSmartTransactionsOptInStatus,
|
||
|
currentSmartTransactionsError,
|
||
|
isDirectWrappingEnabled,
|
||
|
}) {
|
||
|
const t = useContext(I18nContext);
|
||
|
const dispatch = useDispatch();
|
||
|
const swapsErrorKey = useSelector(getSwapsErrorKey);
|
||
|
const [customValue, setCustomValue] = useState(() => {
|
||
|
if (
|
||
|
typeof currentSlippage === 'number' &&
|
||
|
!Object.values(Slippage).includes(currentSlippage)
|
||
|
) {
|
||
|
return currentSlippage.toString();
|
||
|
}
|
||
|
return '';
|
||
|
});
|
||
|
const [enteringCustomValue, setEnteringCustomValue] = useState(false);
|
||
|
const [activeButtonIndex, setActiveButtonIndex] = useState(() => {
|
||
|
if (currentSlippage === Slippage.high) {
|
||
|
return 1; // 3% slippage.
|
||
|
} else if (currentSlippage === Slippage.default) {
|
||
|
return 0; // 2% slippage.
|
||
|
} else if (typeof currentSlippage === 'number') {
|
||
|
return 2; // Custom slippage.
|
||
|
}
|
||
|
return 0;
|
||
|
});
|
||
|
const [inputRef, setInputRef] = useState(null);
|
||
|
const [newSlippage, setNewSlippage] = useState(currentSlippage);
|
||
|
const [newSmartTransactionsOptInStatus, setNewSmartTransactionsOptInStatus] =
|
||
|
useState(smartTransactionsOptInStatus);
|
||
|
|
||
|
const didFormChange =
|
||
|
newSlippage !== currentSlippage ||
|
||
|
newSmartTransactionsOptInStatus !== smartTransactionsOptInStatus;
|
||
|
|
||
|
const updateTransactionSettings = () => {
|
||
|
if (newSlippage !== currentSlippage) {
|
||
|
onSelect(newSlippage);
|
||
|
}
|
||
|
if (newSmartTransactionsOptInStatus !== smartTransactionsOptInStatus) {
|
||
|
setSmartTransactionsOptInStatus(newSmartTransactionsOptInStatus);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let notificationText = '';
|
||
|
let notificationTitle = '';
|
||
|
let notificationSeverity = SEVERITIES.INFO;
|
||
|
if (customValue) {
|
||
|
// customValue is a string, e.g. '0'
|
||
|
if (Number(customValue) < 0) {
|
||
|
notificationSeverity = SEVERITIES.DANGER;
|
||
|
notificationText = t('swapSlippageNegativeDescription');
|
||
|
notificationTitle = t('swapSlippageNegativeTitle');
|
||
|
dispatch(setSwapsErrorKey(SLIPPAGE_NEGATIVE_ERROR));
|
||
|
} else if (Number(customValue) > 0 && Number(customValue) <= 1) {
|
||
|
// We will not show this warning for 0% slippage, because we will only
|
||
|
// return non-slippage quotes from off-chain makers.
|
||
|
notificationSeverity = SEVERITIES.WARNING;
|
||
|
notificationText = t('swapSlippageTooLowDescription');
|
||
|
notificationTitle = t('swapSlippageTooLowTitle');
|
||
|
} else if (
|
||
|
Number(customValue) >= 5 &&
|
||
|
Number(customValue) <= maxAllowedSlippage
|
||
|
) {
|
||
|
notificationSeverity = SEVERITIES.WARNING;
|
||
|
notificationText = t('swapSlippageVeryHighDescription');
|
||
|
notificationTitle = t('swapSlippageVeryHighTitle');
|
||
|
} else if (Number(customValue) > maxAllowedSlippage) {
|
||
|
notificationSeverity = SEVERITIES.DANGER;
|
||
|
notificationText = t('swapSlippageOverLimitDescription');
|
||
|
notificationTitle = t('swapSlippageOverLimitTitle');
|
||
|
dispatch(setSwapsErrorKey(SLIPPAGE_OVER_LIMIT_ERROR));
|
||
|
} else if (Number(customValue) === 0) {
|
||
|
notificationSeverity = SEVERITIES.INFO;
|
||
|
notificationText = t('swapSlippageZeroDescription');
|
||
|
notificationTitle = t('swapSlippageZeroTitle');
|
||
|
} else if (swapsErrorKey) {
|
||
|
dispatch(setSwapsErrorKey(''));
|
||
|
}
|
||
|
}
|
||
|
const isDangerSeverity = notificationSeverity === SEVERITIES.DANGER;
|
||
|
|
||
|
const customValueText = customValue || t('swapCustom');
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (
|
||
|
inputRef &&
|
||
|
enteringCustomValue &&
|
||
|
window.document.activeElement !== inputRef
|
||
|
) {
|
||
|
inputRef.focus();
|
||
|
}
|
||
|
}, [inputRef, enteringCustomValue]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (activeButtonIndex !== 2) {
|
||
|
// If it's not a custom slippage, remove an error key.
|
||
|
dispatch(setSwapsErrorKey(''));
|
||
|
}
|
||
|
}, [dispatch, activeButtonIndex]);
|
||
|
|
||
|
return (
|
||
|
<Modal
|
||
|
onClose={onModalClose}
|
||
|
isOpen
|
||
|
isClosedOnOutsideClick
|
||
|
isClosedOnEscapeKey
|
||
|
className="mm-modal__custom-scrollbar"
|
||
|
>
|
||
|
<ModalOverlay />
|
||
|
<ModalContent>
|
||
|
<ModalHeader onClose={onModalClose}>
|
||
|
{t('transactionSettings')}
|
||
|
</ModalHeader>
|
||
|
<Box
|
||
|
display={DISPLAY.FLEX}
|
||
|
flexDirection={FlexDirection.Column}
|
||
|
justifyContent={JustifyContent.spaceBetween}
|
||
|
alignItems={AlignItems.stretch}
|
||
|
className="transaction-settings__content"
|
||
|
>
|
||
|
<Box marginTop={7} marginBottom={5}>
|
||
|
<>
|
||
|
{smartTransactionsEnabled && (
|
||
|
<Box
|
||
|
marginTop={2}
|
||
|
marginBottom={6}
|
||
|
display={DISPLAY.FLEX}
|
||
|
justifyContent={JustifyContent.spaceBetween}
|
||
|
>
|
||
|
<Box
|
||
|
display={DISPLAY.FLEX}
|
||
|
alignItems={AlignItems.center}
|
||
|
paddingRight={3}
|
||
|
>
|
||
|
<Typography
|
||
|
variant={TypographyVariant.H6}
|
||
|
boxProps={{ paddingRight: 2 }}
|
||
|
>
|
||
|
{t('smartSwap')}
|
||
|
</Typography>
|
||
|
{currentSmartTransactionsError ? (
|
||
|
<InfoTooltip
|
||
|
position="top"
|
||
|
iconFillColor="var(--color-icon-muted)"
|
||
|
contentText={getTranslatedStxErrorMessage(
|
||
|
currentSmartTransactionsError,
|
||
|
t,
|
||
|
)}
|
||
|
/>
|
||
|
) : (
|
||
|
<InfoTooltip
|
||
|
position="top"
|
||
|
contentText={t('stxTooltip')}
|
||
|
iconFillColor="var(--color-icon-muted)"
|
||
|
/>
|
||
|
)}
|
||
|
</Box>
|
||
|
<ToggleButton
|
||
|
value={newSmartTransactionsOptInStatus}
|
||
|
onToggle={(value) => {
|
||
|
setNewSmartTransactionsOptInStatus(!value, value);
|
||
|
}}
|
||
|
offLabel={t('off')}
|
||
|
onLabel={t('on')}
|
||
|
disabled={Boolean(currentSmartTransactionsError)}
|
||
|
/>
|
||
|
</Box>
|
||
|
)}
|
||
|
{!isDirectWrappingEnabled && (
|
||
|
<>
|
||
|
<Box display={DISPLAY.FLEX} alignItems={AlignItems.center}>
|
||
|
<Typography
|
||
|
variant={TypographyVariant.H6}
|
||
|
boxProps={{ paddingRight: 2 }}
|
||
|
>
|
||
|
{t('swapsMaxSlippage')}
|
||
|
</Typography>
|
||
|
{currentSmartTransactionsError ? (
|
||
|
<InfoTooltip
|
||
|
position="top"
|
||
|
iconFillColor="var(--color-icon-muted)"
|
||
|
contentText={getTranslatedStxErrorMessage(
|
||
|
currentSmartTransactionsError,
|
||
|
t,
|
||
|
)}
|
||
|
/>
|
||
|
) : (
|
||
|
<InfoTooltip
|
||
|
position="top"
|
||
|
iconFillColor="var(--color-icon-muted)"
|
||
|
contentText={t('swapSlippageTooltip')}
|
||
|
/>
|
||
|
)}
|
||
|
</Box>
|
||
|
<Box display={DISPLAY.FLEX}>
|
||
|
<ButtonGroup
|
||
|
defaultActiveButtonIndex={
|
||
|
activeButtonIndex === 2 && !customValue
|
||
|
? 1
|
||
|
: activeButtonIndex
|
||
|
}
|
||
|
variant="radiogroup"
|
||
|
newActiveButtonIndex={activeButtonIndex}
|
||
|
className={classnames(
|
||
|
'button-group',
|
||
|
'transaction-settings__button-group',
|
||
|
)}
|
||
|
>
|
||
|
<Button
|
||
|
onClick={() => {
|
||
|
setCustomValue('');
|
||
|
setEnteringCustomValue(false);
|
||
|
setActiveButtonIndex(0);
|
||
|
setNewSlippage(Slippage.default);
|
||
|
}}
|
||
|
>
|
||
|
{t('swapSlippagePercent', [Slippage.default])}
|
||
|
</Button>
|
||
|
<Button
|
||
|
onClick={() => {
|
||
|
setCustomValue('');
|
||
|
setEnteringCustomValue(false);
|
||
|
setActiveButtonIndex(1);
|
||
|
setNewSlippage(Slippage.high);
|
||
|
}}
|
||
|
>
|
||
|
{t('swapSlippagePercent', [Slippage.high])}
|
||
|
</Button>
|
||
|
<Button
|
||
|
className={classnames(
|
||
|
'transaction-settings__button-group-custom-button',
|
||
|
{
|
||
|
'radio-button--danger': isDangerSeverity,
|
||
|
},
|
||
|
)}
|
||
|
onClick={() => {
|
||
|
setActiveButtonIndex(2);
|
||
|
setEnteringCustomValue(true);
|
||
|
}}
|
||
|
>
|
||
|
{enteringCustomValue ? (
|
||
|
<div
|
||
|
className={classnames(
|
||
|
'transaction-settings__custom-input',
|
||
|
{
|
||
|
'transaction-settings__custom-input--danger':
|
||
|
isDangerSeverity,
|
||
|
},
|
||
|
)}
|
||
|
>
|
||
|
<input
|
||
|
data-testid="transaction-settings-custom-slippage"
|
||
|
onChange={(event) => {
|
||
|
const { value } = event.target;
|
||
|
const isValueNumeric = !isNaN(Number(value));
|
||
|
if (isValueNumeric) {
|
||
|
setCustomValue(value);
|
||
|
setNewSlippage(Number(value));
|
||
|
}
|
||
|
}}
|
||
|
type="text"
|
||
|
maxLength="4"
|
||
|
ref={setInputRef}
|
||
|
onBlur={() => {
|
||
|
setEnteringCustomValue(false);
|
||
|
}}
|
||
|
onKeyDown={(event) => {
|
||
|
if (event.key === 'Enter') {
|
||
|
setEnteringCustomValue(false);
|
||
|
}
|
||
|
}}
|
||
|
value={customValue || ''}
|
||
|
/>
|
||
|
</div>
|
||
|
) : (
|
||
|
customValueText
|
||
|
)}
|
||
|
{(customValue || enteringCustomValue) && (
|
||
|
<div className="transaction-settings__percentage-suffix">
|
||
|
%
|
||
|
</div>
|
||
|
)}
|
||
|
</Button>
|
||
|
</ButtonGroup>
|
||
|
</Box>
|
||
|
</>
|
||
|
)}
|
||
|
</>
|
||
|
{notificationText && (
|
||
|
<Box marginTop={5}>
|
||
|
<BannerAlert
|
||
|
severity={notificationSeverity}
|
||
|
title={notificationTitle}
|
||
|
>
|
||
|
<Typography
|
||
|
variant={TypographyVariant.H6}
|
||
|
testId="mm-banner-alert-notification-text"
|
||
|
>
|
||
|
{notificationText}
|
||
|
</Typography>
|
||
|
</BannerAlert>
|
||
|
</Box>
|
||
|
)}
|
||
|
</Box>
|
||
|
<Box marginTop={5}>
|
||
|
<ButtonPrimary
|
||
|
onClick={() => {
|
||
|
updateTransactionSettings();
|
||
|
onModalClose();
|
||
|
}}
|
||
|
block
|
||
|
disabled={!didFormChange}
|
||
|
data-testid="update-transaction-settings-button"
|
||
|
>
|
||
|
{t('update')}
|
||
|
</ButtonPrimary>
|
||
|
</Box>
|
||
|
</Box>
|
||
|
</ModalContent>
|
||
|
</Modal>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
TransactionSettings.propTypes = {
|
||
|
onSelect: PropTypes.func.isRequired,
|
||
|
onModalClose: PropTypes.func.isRequired,
|
||
|
maxAllowedSlippage: PropTypes.number.isRequired,
|
||
|
currentSlippage: PropTypes.number,
|
||
|
smartTransactionsEnabled: PropTypes.bool.isRequired,
|
||
|
smartTransactionsOptInStatus: PropTypes.bool,
|
||
|
setSmartTransactionsOptInStatus: PropTypes.func,
|
||
|
currentSmartTransactionsError: PropTypes.string,
|
||
|
isDirectWrappingEnabled: PropTypes.bool,
|
||
|
};
|