Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implemented ui state management in limit orders input #8041

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,15 @@
"previewOrder": "Preview Order",
"youGet": "You Get",
"whenPriceReaches": "When price reaches",
"expiry": "Expiry"
"expiry": "Expiry",
"expiryOption": {
"oneHour": "1 hour",
"oneDay": "1 day",
"threeDays": "3 days",
"sevenDays": "7 days",
"twentyEightDays": "28 days",
"custom": "Custom"
}
},
"modals": {
"assetSearch": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,71 @@ import {
} from '@chakra-ui/react'
import type { Asset } from '@shapeshiftoss/types'
import { bn, bnOrZero } from '@shapeshiftoss/utils'
import { noop } from 'lodash'
import { useCallback, useMemo, useState } from 'react'
import { Amount } from 'components/Amount/Amount'
import { useCallback, useMemo, useRef, useState } from 'react'
import type { NumberFormatValues } from 'react-number-format'
import NumberFormat from 'react-number-format'
import { StyledAssetMenuButton } from 'components/AssetSelection/components/AssetMenuButton'
import { SwapIcon } from 'components/Icons/SwapIcon'
import { RawText, Text } from 'components/Text'
import { Text } from 'components/Text'
import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter'
import { assertUnreachable } from 'lib/utils'
import { allowedDecimalSeparators } from 'state/slices/preferencesSlice/preferencesSlice'

const EXPIRY_TIME_PERIODS = ['1 hour', '1 day', '3 days', '7 days', '28 days'] as const
import { AmountInput } from '../../TradeAmountInput'

enum PriceDirection {
Default = 'default',
Reversed = 'reversed',
}

enum PresetLimit {
Market = 'market',
OnePercent = 'onePercent',
TwoPercent = 'twoPercent',
FivePercent = 'fivePercent',
TenPercent = 'tenPercent',
}

enum ExpiryOption {
OneHour = 'oneHour',
OneDay = 'oneDay',
ThreeDays = 'threeDays',
SevenDays = 'sevenDays',
TwentyEightDays = 'twentyEightDays',
// TODO: implement custom expiry
// Custom = 'custom',
}

const EXPIRY_OPTIONS = [
ExpiryOption.OneHour,
ExpiryOption.OneDay,
ExpiryOption.ThreeDays,
ExpiryOption.SevenDays,
ExpiryOption.TwentyEightDays,
] as const

const getExpiryOptionTranslation = (expiryOption: ExpiryOption) => {
switch (expiryOption) {
case ExpiryOption.OneHour:
return `limitOrder.expiryOption.${expiryOption}`
case ExpiryOption.OneDay:
return `limitOrder.expiryOption.${expiryOption}`
case ExpiryOption.ThreeDays:
return `limitOrder.expiryOption.${expiryOption}`
case ExpiryOption.SevenDays:
return `limitOrder.expiryOption.${expiryOption}`
case ExpiryOption.TwentyEightDays:
return `limitOrder.expiryOption.${expiryOption}`
// TODO: implement custom expiry
// case ExpiryOption.Custom:
// return `limitOrder.expiryOption.${expiryOption}`
default:
assertUnreachable(expiryOption)
}
}

const timePeriodRightIcon = <ChevronDownIcon />
const swapIcon = <SwapIcon />

const swapPriceButtonProps = { pr: 4 }

type LimitOrderConfigProps = {
Expand All @@ -34,91 +87,150 @@ type LimitOrderConfigProps = {
setLimitPriceBuyAssetCryptoPrecision: (priceBuyAssetCryptoPrecision: string) => void
}

enum PriceDirection {
Sell = 'sell',
Buy = 'buy',
}

enum PresetLimit {
Market = 'market',
OnePercent = 'onePercent',
TwoPercent = 'twoPercent',
FivePercent = 'fivePercent',
TenPercent = 'tenPercent',
}

export const LimitOrderConfig = ({
sellAsset,
buyAsset,
marketPriceBuyAssetCryptoPrecision,
limitPriceBuyAssetCryptoPrecision,
setLimitPriceBuyAssetCryptoPrecision,
}: LimitOrderConfigProps) => {
const [priceDirection, setPriceDirection] = useState(PriceDirection.Sell)
const [presetLimit, setPresetLimit] = useState(PresetLimit.Market)
const priceAmountRef = useRef<string | null>(null)

const renderedChains = useMemo(() => {
return EXPIRY_TIME_PERIODS.map(timePeriod => {
const [priceDirection, setPriceDirection] = useState(PriceDirection.Default)
const [presetLimit, setPresetLimit] = useState<PresetLimit | undefined>(PresetLimit.Market)
const [expiryOption, setExpiryOption] = useState(ExpiryOption.SevenDays)

const {
number: { localeParts },
} = useLocaleFormatter()

const expiryOptions = useMemo(() => {
return EXPIRY_OPTIONS.map(expiryOption => {
return (
<MenuItemOption value={timePeriod} key={timePeriod}>
<RawText>{timePeriod}</RawText>
<MenuItemOption value={expiryOption} key={expiryOption}>
<Text translation={getExpiryOptionTranslation(expiryOption)} />
</MenuItemOption>
)
})
}, [])

const priceAsset = useMemo(() => {
return priceDirection === PriceDirection.Sell ? sellAsset : buyAsset
return priceDirection === PriceDirection.Default ? buyAsset : sellAsset
}, [buyAsset, priceDirection, sellAsset])

const priceCryptoPrecision = useMemo(() => {
if (bnOrZero(limitPriceBuyAssetCryptoPrecision).isZero()) {
return '0'
}

return priceDirection === PriceDirection.Sell
? bn(1).div(limitPriceBuyAssetCryptoPrecision).toFixed()
: limitPriceBuyAssetCryptoPrecision
}, [limitPriceBuyAssetCryptoPrecision, priceDirection])
// Lower the decimal places when the integer is greater than 8 significant digits for better UI
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
const priceCryptoFormatted = useMemo(() => {
const cryptoAmountIntegerCount = bnOrZero(
bnOrZero(limitPriceBuyAssetCryptoPrecision).toFixed(0),
).precision(true)

const handleTogglePriceDirection = useCallback(() => {
setPriceDirection(
priceDirection === PriceDirection.Sell ? PriceDirection.Buy : PriceDirection.Sell,
)
}, [priceDirection])
return cryptoAmountIntegerCount <= 8
? limitPriceBuyAssetCryptoPrecision
: bnOrZero(limitPriceBuyAssetCryptoPrecision).toFixed(3)
}, [limitPriceBuyAssetCryptoPrecision])

const arrow = useMemo(() => {
return priceDirection === PriceDirection.Sell ? '↑' : '↓'
return priceDirection === PriceDirection.Default ? '↑' : '↓'
}, [priceDirection])

const handleSetPresetLimit = useCallback(
(presetLimit: PresetLimit, priceDirection: PriceDirection) => {
setPresetLimit(presetLimit)
const multiplier = (() => {
switch (presetLimit) {
case PresetLimit.Market:
return '1.00'
case PresetLimit.OnePercent:
return '1.01'
case PresetLimit.TwoPercent:
return '1.02'
case PresetLimit.FivePercent:
return '1.05'
case PresetLimit.TenPercent:
return '1.10'
default:
assertUnreachable(presetLimit)
}
})()
const adjustedLimitPrice = bn(marketPriceBuyAssetCryptoPrecision).times(multiplier).toFixed()
const maybeReversedPrice =
priceDirection === PriceDirection.Reversed
? bn(1).div(adjustedLimitPrice).toFixed()
: adjustedLimitPrice
setLimitPriceBuyAssetCryptoPrecision(maybeReversedPrice)
},
[marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision],
)

const handleSetMarketLimit = useCallback(() => {
setPresetLimit(PresetLimit.Market)
setLimitPriceBuyAssetCryptoPrecision(marketPriceBuyAssetCryptoPrecision)
}, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision])
handleSetPresetLimit(PresetLimit.Market, priceDirection)
}, [handleSetPresetLimit, priceDirection])

const handleSetOnePercentLimit = useCallback(() => {
setPresetLimit(PresetLimit.OnePercent)
const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.01').toFixed()
setLimitPriceBuyAssetCryptoPrecision(price)
}, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision])
handleSetPresetLimit(PresetLimit.OnePercent, priceDirection)
}, [handleSetPresetLimit, priceDirection])

const handleSetTwoPercentLimit = useCallback(() => {
setPresetLimit(PresetLimit.TwoPercent)
const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.02').toFixed()
setLimitPriceBuyAssetCryptoPrecision(price)
}, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision])
handleSetPresetLimit(PresetLimit.TwoPercent, priceDirection)
}, [handleSetPresetLimit, priceDirection])

const handleSetFivePercentLimit = useCallback(() => {
setPresetLimit(PresetLimit.FivePercent)
const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.05').toFixed()
setLimitPriceBuyAssetCryptoPrecision(price)
}, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision])
handleSetPresetLimit(PresetLimit.FivePercent, priceDirection)
}, [handleSetPresetLimit, priceDirection])

const handleSetTenPercentLimit = useCallback(() => {
setPresetLimit(PresetLimit.TenPercent)
const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.10').toFixed()
setLimitPriceBuyAssetCryptoPrecision(price)
}, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision])
handleSetPresetLimit(PresetLimit.TenPercent, priceDirection)
}, [handleSetPresetLimit, priceDirection])

const handleTogglePriceDirection = useCallback(() => {
const newPriceDirection =
priceDirection === PriceDirection.Default ? PriceDirection.Reversed : PriceDirection.Default
setPriceDirection(newPriceDirection)

const isCustomLimit = presetLimit === undefined

if (isCustomLimit) {
// For custom limit, just take the reciprocal as we don't know what the original input value was
setLimitPriceBuyAssetCryptoPrecision(bn(1).div(limitPriceBuyAssetCryptoPrecision).toFixed())
} else {
// Otherwise set it to the precise value based on the original market price
handleSetPresetLimit(presetLimit, newPriceDirection)
}
}, [
handleSetPresetLimit,
limitPriceBuyAssetCryptoPrecision,
presetLimit,
priceDirection,
setLimitPriceBuyAssetCryptoPrecision,
])

const handlePriceChange = useCallback(() => {
// onChange will send us the formatted value
// To get around this we need to get the value from the onChange using a ref
// Now when the max buttons are clicked the onChange will not fire
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
setLimitPriceBuyAssetCryptoPrecision(priceAmountRef.current ?? '0')

// Unset the preset limit, as this is a custom value
setPresetLimit(undefined)
}, [setLimitPriceBuyAssetCryptoPrecision])

const handleValueChange = useCallback(
(values: NumberFormatValues) => {
// This fires anytime value changes including setting it on max click
// Store the value in a ref to send when we actually want the onChange to fire
woodenfurniture marked this conversation as resolved.
Show resolved Hide resolved
priceAmountRef.current = values.value
setLimitPriceBuyAssetCryptoPrecision(values.value)
},
[setLimitPriceBuyAssetCryptoPrecision],
)

const expiryOptionTranslation = useMemo(() => {
return getExpiryOptionTranslation(expiryOption)
}, [expiryOption])

const handleChangeExpiryOption = useCallback((newExpiry: string | string[]) => {
setExpiryOption(newExpiry as ExpiryOption)
}, [])

return (
<Stack spacing={4} px={6} py={4}>
Expand All @@ -128,18 +240,33 @@ export const LimitOrderConfig = ({
<Text translation='limitOrder.expiry' mr={4} />
<Menu isLazy>
<MenuButton as={Button} rightIcon={timePeriodRightIcon}>
<RawText>1 hour</RawText>
<Text translation={expiryOptionTranslation} />
</MenuButton>
<MenuList zIndex='modal'>
<MenuOptionGroup type='radio' value={'1 hour'} onChange={noop}>
{renderedChains}
<MenuOptionGroup
type='radio'
value={expiryOption}
onChange={handleChangeExpiryOption}
>
{expiryOptions}
</MenuOptionGroup>
</MenuList>
</Menu>
</Flex>
</Flex>
<HStack width='full' justify='space-between'>
<Amount.Crypto value={priceCryptoPrecision} symbol='' size='lg' fontSize='xl' />
<NumberFormat
customInput={AmountInput}
decimalScale={priceAsset.precision}
isNumericString={true}
decimalSeparator={localeParts.decimal}
inputMode='decimal'
allowedDecimalSeparators={allowedDecimalSeparators}
thousandSeparator={localeParts.group}
value={priceCryptoFormatted}
onValueChange={handleValueChange}
onChange={handlePriceChange}
/>
<StyledAssetMenuButton
rightIcon={swapIcon}
assetId={priceAsset.assetId}
Expand Down
Loading
Loading