diff --git a/src/components/FeeControlGroup/CustomFeeInput.tsx b/src/components/FeeControlGroup/CustomFeeInput.tsx new file mode 100644 index 0000000000..1cfc7a9e86 --- /dev/null +++ b/src/components/FeeControlGroup/CustomFeeInput.tsx @@ -0,0 +1,156 @@ +import { t } from '@lingui/macro' +import React, { useEffect, useRef, useState } from 'react' +import { Text } from 'rebass' +import styled, { css } from 'styled-components' + +import { DEFAULT_TIPS } from 'constants/index' +import { formatSlippage } from 'utils/slippage' + +const parseTipInput = (str: string): number => Math.round(Number.parseFloat(str) * 100) + +const getFeeText = (fee: number) => { + const isCustom = !DEFAULT_TIPS.includes(fee) + if (!isCustom) { + return '' + } + return formatSlippage(fee, false) +} + +const feeOptionCSS = css` + height: 100%; + padding: 0; + border-radius: 20px; + border: 1px solid transparent; + + background-color: ${({ theme }) => theme.tabBackground}; + color: ${({ theme }) => theme.subText}; + text-align: center; + + font-size: 12px; + font-weight: 400; + line-height: 16px; + + outline: none; + cursor: pointer; + + :hover { + border-color: ${({ theme }) => theme.bg4}; + } + :focus { + border-color: ${({ theme }) => theme.bg4}; + } + + &[data-active='true'] { + background-color: ${({ theme }) => theme.tabActive}; + color: ${({ theme }) => theme.text}; + border-color: ${({ theme }) => theme.primary}; + + font-weight: 500; + } +` + +const CustomFeeOption = styled.div` + ${feeOptionCSS}; + + flex: 0 0 24%; + + display: inline-flex; + align-items: center; + padding: 0 4px; + column-gap: 2px; + flex: 1; + + transition: all 150ms linear; + + &[data-active='true'] { + color: ${({ theme }) => theme.text}; + font-weight: 500; + } +` + +const CustomInput = styled.input` + width: 100%; + height: 100%; + border: 0px; + border-radius: inherit; + + color: inherit; + background: transparent; + outline: none; + text-align: right; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + } + &::placeholder { + font-size: 12px; + } + @media only screen and (max-width: 375px) { + font-size: 10px; + } +` + +export type Props = { + fee: number + onFeeChange: (value: number) => void +} + +const CustomFeeInput = ({ fee, onFeeChange }: Props) => { + const [text, setText] = useState(getFeeText(fee)) + + const inputRef = useRef(null) + const isCustomOptionActive = !DEFAULT_TIPS.includes(fee) + + const handleChangeInput = (e: React.ChangeEvent) => { + const value = e.target.value + + if (value === '') { + setText(value) + onFeeChange(50) + return + } + + const numberRegex = /^(\d+)\.?(\d{1,2})?$/ + if (!value.match(numberRegex)) { + e.preventDefault() + return + } + + const parsedValue = parseTipInput(value) + if (Number.isNaN(parsedValue)) { + e.preventDefault() + return + } + + setText(value) + onFeeChange(parsedValue) + } + + const handleCommitChange = () => { + setText(getFeeText(fee)) + } + + useEffect(() => { + if (inputRef.current !== document.activeElement) { + setText(getFeeText(fee)) + } + }, [fee]) + + return ( + + + + % + + + ) +} + +export default CustomFeeInput diff --git a/src/components/FeeControlGroup/index.tsx b/src/components/FeeControlGroup/index.tsx new file mode 100644 index 0000000000..514b864ed9 --- /dev/null +++ b/src/components/FeeControlGroup/index.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { Flex, Text } from 'rebass' +import styled, { css } from 'styled-components' + +import useGetFeeConfig from 'components/SwapForm/hooks/useGetFeeConfig' +import { DEFAULT_TIPS } from 'constants/index' +import useTheme from 'hooks/useTheme' + +import CustomFeeInput from './CustomFeeInput' + +const feeOptionCSS = css` + height: 100%; + padding: 0; + border-radius: 20px; + border: 1px solid transparent; + + background-color: ${({ theme }) => theme.tabBackground}; + color: ${({ theme }) => theme.subText}; + text-align: center; + + font-size: 12px; + font-weight: 400; + line-height: 16px; + + outline: none; + cursor: pointer; + + :hover { + border-color: ${({ theme }) => theme.bg4}; + } + :focus { + border-color: ${({ theme }) => theme.bg4}; + } + + &[data-active='true'] { + background-color: ${({ theme }) => theme.tabActive}; + color: ${({ theme }) => theme.text}; + border-color: ${({ theme }) => theme.primary}; + + font-weight: 500; + } +` + +const DefaultFeeOption = styled.button` + ${feeOptionCSS}; + flex: 0 0 18%; + + @media only screen and (max-width: 375px) { + font-size: 10px; + flex: 0 0 15%; + } +` + +const FeeControlGroup = () => { + const theme = useTheme() + const { feeAmount, enableTip } = useGetFeeConfig() ?? {} + const [searchParams, setSearchParams] = useSearchParams() + + const [feeValue, setFee] = useState(Math.round(Number.parseFloat(feeAmount ?? '0')) || 0) + + useEffect(() => { + if (enableTip) { + searchParams.set('feeAmount', feeValue.toString()) + setSearchParams(searchParams) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enableTip, feeValue]) + + if (!enableTip) { + return null + } + + return ( + + + Tip : + + + No hidden fees - Your optional tips support DEX Screener! + + + + {DEFAULT_TIPS.map(tip => ( + { + setFee(tip) + }} + data-active={tip === feeValue} + > + {tip ? `${tip / 100}%` : 'No tip'} + + ))} + + + + ) +} + +export default FeeControlGroup diff --git a/src/components/SlippageControl/CustomSlippageInput.tsx b/src/components/SlippageControl/CustomSlippageInput.tsx index a3bd47c46a..d370b21ca3 100644 --- a/src/components/SlippageControl/CustomSlippageInput.tsx +++ b/src/components/SlippageControl/CustomSlippageInput.tsx @@ -171,20 +171,6 @@ const CustomSlippageInput: React.FC = ({ rawSlippage, setRawSlippage, isW mixpanelHandler(MIXPANEL_TYPE.SLIPPAGE_CHANGED, { new_slippage: Number(formatSlippage(rawSlippage, false)) }) } - const handleKeyPressInput = (e: React.KeyboardEvent) => { - const key = e.key - if (key === '.' || ('0' <= key && key <= '9')) { - return - } - - if (key === 'Enter') { - inputRef.current?.blur() - return - } - - e.preventDefault() - } - useEffect(() => { if (inputRef.current !== document.activeElement) { setRawText(getSlippageText(rawSlippage)) @@ -207,7 +193,6 @@ const CustomSlippageInput: React.FC = ({ rawSlippage, setRawSlippage, isW placeholder={t`Custom`} value={rawText} onChange={handleChangeInput} - onKeyPress={handleKeyPressInput} onBlur={handleCommitChange} /> { + const [searchParams] = useSearchParams() + + const feeAmount = searchParams.get('feeAmount') || '' + const chargeFeeBy = (searchParams.get('chargeFeeBy') as ChargeFeeBy) || ChargeFeeBy.NONE + const enableTip = searchParams.get('enableTip') || '' + const isInBps = searchParams.get('isInBps') || '' + const feeReceiver = searchParams.get('feeReceiver') || '' + + const feeConfigFromUrl = useMemo(() => { + if (feeAmount && chargeFeeBy && (enableTip || isInBps) && feeReceiver) + return { + feeAmount, + chargeFeeBy, + enableTip, + isInBps: enableTip ? '1' : isInBps, + feeReceiver, + } + return null + }, [feeAmount, chargeFeeBy, enableTip, isInBps, feeReceiver]) + + return feeConfigFromUrl +} + +export default useGetFeeConfig diff --git a/src/components/SwapForm/hooks/useGetRoute.ts b/src/components/SwapForm/hooks/useGetRoute.ts index 9e7e86bea6..f0d02e35ac 100644 --- a/src/components/SwapForm/hooks/useGetRoute.ts +++ b/src/components/SwapForm/hooks/useGetRoute.ts @@ -1,10 +1,10 @@ import { ChainId, Currency, CurrencyAmount } from '@kyberswap/ks-sdk-core' import debounce from 'lodash/debounce' import { useCallback, useEffect, useMemo, useRef } from 'react' -import { useSearchParams } from 'react-router-dom' import routeApi from 'services/route' import { GetRouteParams } from 'services/route/types/getRoute' +import useGetFeeConfig from 'components/SwapForm/hooks/useGetFeeConfig' import useGetSwapFeeConfig, { SwapFeeConfig } from 'components/SwapForm/hooks/useGetSwapFeeConfig' import useSelectedDexes from 'components/SwapForm/hooks/useSelectedDexes' import { AGGREGATOR_API } from 'constants/env' @@ -80,25 +80,7 @@ const useGetRoute = (args: ArgsGetRoute) => { const { chainId: currentChain } = useActiveWeb3React() const chainId = customChain || currentChain - const [searchParams] = useSearchParams() - - const feeAmount = searchParams.get('feeAmount') || '' - const chargeFeeBy = (searchParams.get('chargeFeeBy') as ChargeFeeBy) || ChargeFeeBy.NONE - const enableTip = searchParams.get('enableTip') || '' - const isInBps = searchParams.get('isInBps') || '' - const feeReceiver = searchParams.get('feeReceiver') || '' - - const feeConfigFromUrl = useMemo(() => { - if (feeAmount && chargeFeeBy && (enableTip || isInBps) && feeReceiver) - return { - feeAmount, - chargeFeeBy, - enableTip, - isInBps: enableTip ? '1' : isInBps, - feeReceiver, - } - return null - }, [feeAmount, chargeFeeBy, enableTip, isInBps, feeReceiver]) + const feeConfigFromUrl = useGetFeeConfig() const [trigger, _result] = routeApi.useLazyGetRouteQuery() const aggregatorDomain = useRouteApiDomain() diff --git a/src/components/SwapForm/index.tsx b/src/components/SwapForm/index.tsx index d3be063070..aa3ef684f1 100644 --- a/src/components/SwapForm/index.tsx +++ b/src/components/SwapForm/index.tsx @@ -10,6 +10,7 @@ import { parseGetRouteResponse } from 'services/route/utils' import styled from 'styled-components' import AddressInputPanel from 'components/AddressInputPanel' +import FeeControlGroup from 'components/FeeControlGroup' import { Clock } from 'components/Icons' import { NetworkSelector } from 'components/NetworkSelector' import { AutoRow } from 'components/Row' @@ -261,6 +262,7 @@ const SwapForm: React.FC = props => { )} + diff --git a/src/constants/index.ts b/src/constants/index.ts index 1703e63178..60ef14fd15 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -134,6 +134,7 @@ export const SWR_KEYS = { export const MAX_NORMAL_SLIPPAGE_IN_BIPS = 1999 export const MAX_DEGEN_SLIPPAGE_IN_BIPS = 5000 export const DEFAULT_SLIPPAGES = [5, 10, 50, 100] +export const DEFAULT_TIPS = [0, 10, 50, 100] export const DEFAULT_SLIPPAGE = 50 export const DEFAULT_SLIPPAGE_STABLE_PAIR_SWAP = 5