Skip to content

Commit

Permalink
Custom tip for dex screener (#2440)
Browse files Browse the repository at this point in the history
* Receive "enableTip"

* Add FeeControlGroup

* Resolve comment

* Update max fee

* Update slippage when swap stable coin

* Add Trans

* Remove handle state

* Remove useCallback
  • Loading branch information
neikop authored Dec 12, 2023
1 parent e292fab commit 164e3e8
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 52 deletions.
169 changes: 169 additions & 0 deletions src/components/FeeControlGroup/CustomFeeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { t } from '@lingui/macro'
import React, { useEffect, useRef, useState } from 'react'
import { Text } from 'rebass'
import styled, { css } from 'styled-components'

import Tooltip from 'components/Tooltip'
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 [tooltip, setTooltip] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const isCustomOptionActive = !DEFAULT_TIPS.includes(fee)

const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setTooltip('')
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
}

const maxCustomFee = 9999
if (parsedValue > maxCustomFee) {
const format = formatSlippage(maxCustomFee)
setTooltip(t`Max is ${format}`)
e.preventDefault()
return
}

setText(value)
onFeeChange(parsedValue)
}

const handleCommitChange = () => {
setTooltip('')
setText(getFeeText(fee))
}

useEffect(() => {
if (inputRef.current !== document.activeElement) {
setText(getFeeText(fee))
}
}, [fee])

return (
<Tooltip text={tooltip} show={!!tooltip} placement="bottom" width="fit-content">
<CustomFeeOption data-active={isCustomOptionActive}>
<CustomInput
ref={inputRef}
placeholder={t`Custom`}
value={text}
onChange={handleChangeInput}
onBlur={handleCommitChange}
/>
<Text as="span" sx={{ flex: '0 0 12px' }}>
%
</Text>
</CustomFeeOption>
</Tooltip>
)
}

export default CustomFeeInput
116 changes: 116 additions & 0 deletions src/components/FeeControlGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Trans } from '@lingui/macro'
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 = Number.parseFloat(feeAmount ?? '0')

const handleFeeChange = (feeValue: number) => {
if (enableTip) {
searchParams.set('feeAmount', feeValue.toString())
setSearchParams(searchParams)
}
}

if (!enableTip) {
return null
}

return (
<Flex
sx={{
flexDirection: 'column',
width: '100%',
padding: '0 8px',
}}
>
<Text color={theme.subText} fontSize={12} fontWeight={500}>
<Trans>Tip</Trans>:
</Text>
<Text color={theme.text3} fontSize={12} fontWeight={500}>
<Trans>No hidden fees - Your optional tips support DEX Screener!</Trans>
</Text>

<Flex
sx={{
justifyContent: 'space-between',
width: '100%',
maxWidth: '100%',
height: '28px',
borderRadius: '20px',
background: theme.tabBackground,
padding: '2px',
marginTop: '8px',
}}
>
{DEFAULT_TIPS.map(tip => (
<DefaultFeeOption
key={tip}
onClick={() => {
handleFeeChange(tip)
}}
data-active={tip === feeValue}
>
{tip ? `${tip / 100}%` : <Trans>No tip</Trans>}
</DefaultFeeOption>
))}
<CustomFeeInput fee={feeValue} onFeeChange={handleFeeChange} />
</Flex>
</Flex>
)
}

export default FeeControlGroup
15 changes: 0 additions & 15 deletions src/components/SlippageControl/CustomSlippageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,6 @@ const CustomSlippageInput: React.FC<Props> = ({ rawSlippage, setRawSlippage, isW
mixpanelHandler(MIXPANEL_TYPE.SLIPPAGE_CHANGED, { new_slippage: Number(formatSlippage(rawSlippage, false)) })
}

const handleKeyPressInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
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))
Expand All @@ -207,7 +193,6 @@ const CustomSlippageInput: React.FC<Props> = ({ rawSlippage, setRawSlippage, isW
placeholder={t`Custom`}
value={rawText}
onChange={handleChangeInput}
onKeyPress={handleKeyPressInput}
onBlur={handleCommitChange}
/>
<Text
Expand Down
3 changes: 1 addition & 2 deletions src/components/SwapForm/SlippageSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,9 @@ const SlippageSetting = ({ isStablePairSwap, rightComponent, tooltip }: Props) =
)
}
>
<Trans>Max Slippage</Trans>
<Trans>Max Slippage</Trans>:
</MouseoverTooltip>
</TextDashed>
:
<Flex
sx={{
alignItems: 'center',
Expand Down
31 changes: 31 additions & 0 deletions src/components/SwapForm/hooks/useGetFeeConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'

import { ChargeFeeBy } from 'types/route'
import { convertStringToBoolean } from 'utils/string'

const useGetFeeConfig = () => {
const [searchParams] = useSearchParams()

const feeAmount = searchParams.get('feeAmount') || ''
const chargeFeeBy = (searchParams.get('chargeFeeBy') as ChargeFeeBy) || ChargeFeeBy.NONE
const enableTip = convertStringToBoolean(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
20 changes: 2 additions & 18 deletions src/components/SwapForm/hooks/useGetRoute.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -80,23 +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 isInBps = searchParams.get('isInBps') || ''
const feeReceiver = searchParams.get('feeReceiver') || ''

const feeConfigFromUrl = useMemo(() => {
if (feeAmount && chargeFeeBy && isInBps && feeReceiver)
return {
feeAmount,
chargeFeeBy,
isInBps,
feeReceiver,
}
return null
}, [feeAmount, chargeFeeBy, isInBps, feeReceiver])
const feeConfigFromUrl = useGetFeeConfig()

const [trigger, _result] = routeApi.useLazyGetRouteQuery()
const aggregatorDomain = useRouteApiDomain()
Expand Down
Loading

0 comments on commit 164e3e8

Please sign in to comment.