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/limit UI upgrade 2 #5267

Open
wants to merge 47 commits into
base: feat/limit-ui-upgrade
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ed82912
feat: add global USD setting to settings menu
fairlighteth Jan 2, 2025
abb823f
feat: make prepend symbol non selectable
fairlighteth Jan 2, 2025
169ba62
feat: numericalinput allow decimals typing
fairlighteth Jan 2, 2025
c6f87f8
feat: conditionally show unfillable orders tab
fairlighteth Jan 3, 2025
42d94d6
feat: update status labels
fairlighteth Jan 7, 2025
c85cfd7
feat: update status labels
fairlighteth Jan 7, 2025
6226f33
feat: remove global usd button in header
fairlighteth Jan 7, 2025
f9252f1
feat: set limit price position to bottom by default
fairlighteth Jan 7, 2025
ecf0cb8
feat: update status labels
fairlighteth Jan 7, 2025
bedeaf0
feat: remove column layout selector code
fairlighteth Jan 7, 2025
e8cc999
Merge branch 'feat/limit-ui-upgrade' into feat/limit-ui-upgrade-2
fairlighteth Jan 7, 2025
2729492
feat: add underline
fairlighteth Jan 7, 2025
7846272
feat: add conditional label
fairlighteth Jan 9, 2025
58f6bf2
feat: hover styles for invert price
fairlighteth Jan 9, 2025
32970eb
feat: fix tooltip display
fairlighteth Jan 9, 2025
b1a17b9
feat: add cancel state and column styling
fairlighteth Jan 9, 2025
c8dca0e
feat: pending execution row logic
fairlighteth Jan 9, 2025
613d5ba
feat: fix lint
fairlighteth Jan 9, 2025
486b5bc
feat: add filled status text
fairlighteth Jan 9, 2025
dc5ce37
feat: show TWAP parent fill price for next scheduled order
fairlighteth Jan 9, 2025
bfba42e
feat: fix table layout scroll
fairlighteth Jan 9, 2025
93d7d07
feat: click ga track settings
fairlighteth Jan 10, 2025
4d4cf34
feat: refactor analytics settings
fairlighteth Jan 10, 2025
ef9112e
feat: analytics refactor
fairlighteth Jan 10, 2025
64aafae
feat: order expired text and show expiration time
fairlighteth Jan 10, 2025
4a83390
feat: add timestamp on hover
fairlighteth Jan 10, 2025
818a3a2
feat: navigate to all orders on limit order placement
fairlighteth Jan 10, 2025
3b3103e
feat: switch to all orders on TWAP order placement
fairlighteth Jan 10, 2025
a61ee5f
feat: add signing tab and statusbox label
fairlighteth Jan 10, 2025
8d56d9b
feat: signing tooltips
fairlighteth Jan 10, 2025
c33385e
feat: responsive table layout improvements
fairlighteth Jan 10, 2025
f3a57ae
feat: limit UI upgrade 2 - USD precision (#5274)
alfetopito Jan 10, 2025
0e6646d
fix(deadlines): update internal label to 'one year' (#5282)
alfetopito Jan 14, 2025
653f42f
feat: limit UI upgrade 3 (#5283)
fairlighteth Jan 16, 2025
687454c
feat: keep settings menu open (#5284)
fairlighteth Jan 16, 2025
9bc0d37
feat: limit UI upgrade 5 (#5285)
fairlighteth Jan 16, 2025
23c92f5
feat: clear order selection on change tabs (#5286)
fairlighteth Jan 16, 2025
f18c566
feat: fix unfillable order status (#5288)
fairlighteth Jan 16, 2025
63fab5a
feat: revert default deadline (#5294)
fairlighteth Jan 16, 2025
8e60f94
feat: limit UI upgrade 9 (#5296)
fairlighteth Jan 16, 2025
9bd216c
feat: fix mobile viewport layout (#5298)
fairlighteth Jan 16, 2025
14aa123
feat: limit UI upgrade 11 (#5299)
fairlighteth Jan 16, 2025
d3952e2
Feat/limit UI upgrade 12 (#5300)
fairlighteth Jan 16, 2025
a15e109
Merge branch 'feat/limit-ui-upgrade' into feat/limit-ui-upgrade-2
alfetopito Jan 16, 2025
4a5b08f
fix: amount locking (#5304)
alfetopito Jan 16, 2025
4110b34
test: fix unit tests
alfetopito Jan 16, 2025
9b7c816
feat: only show twap market price for non final states (#5302)
fairlighteth Jan 16, 2025
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
7 changes: 6 additions & 1 deletion apps/cowswap-frontend/src/common/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { Percent } from '@uniswap/sdk-core'

import ms from 'ms.macro'

export const HIGH_FEE_WARNING_PERCENTAGE = new Percent(1, 10)
export const HIGH_FEE_WARNING_PERCENTAGE = new Percent(1, 10) // 10%

// Price difference thresholds
export const PENDING_EXECUTION_THRESHOLD_PERCENTAGE = 0.01 // 0.01% - threshold for considering an order close enough to market price for execution
export const GOOD_PRICE_THRESHOLD_PERCENTAGE = 1.0 // 1% or less difference - good price
export const FAIR_PRICE_THRESHOLD_PERCENTAGE = 5.0 // 5% or less difference - fair price

export const MAX_ORDER_DEADLINE = ms`1y` // https://github.com/cowprotocol/infrastructure/blob/staging/services/Pulumi.yaml#L7

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TokenWithLogo, USDC } from '@cowprotocol/common-const'
import { USDC } from '@cowprotocol/common-const'
import { getWrappedToken, tryParseCurrencyAmount } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { Currency } from '@uniswap/sdk-core'

import { useUsdPrice } from 'modules/usdAmount'

Expand All @@ -15,19 +15,11 @@ export function useConvertUsdToTokenValue(
const usdcToken = USDC[currencyUsdcPrice.currency.chainId as SupportedChainId]
const usdAmount = tryParseCurrencyAmount(typedValue, usdcToken)

const tokenAmount = currencyUsdcPrice.price.invert().quote(hackyAdjustAmountDust(usdAmount))
const tokenAmount = currencyUsdcPrice.price.invert().quote(usdAmount)

return tokenAmount.toExact()
}

return typedValue
}
}

/**
* TODO: this is a hacky way to adjust the amount to avoid dust
* For some reason, when you enter for example $366, price.quote() returns 365,9999999999
*/
function hackyAdjustAmountDust(amount: CurrencyAmount<TokenWithLogo>): typeof amount {
return amount.add(tryParseCurrencyAmount('0.000001', amount.currency))
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,22 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) {
const convertUsdToTokenValue = useConvertUsdToTokenValue(currency)

const onUserInputDispatch = useCallback(
(typedValue: string) => {
const value = convertUsdToTokenValue(typedValue, isUsdValuesMode)
(typedValue: string, currencyValue?: string) => {
// Always pass through empty string to allow clearing
if (typedValue === '') {
setTypedValue('')
onUserInput(field, '')
return
}

setTypedValue(value)
setTypedValue(typedValue)
// Avoid converting from USD if currencyValue is already provided
const value = currencyValue || convertUsdToTokenValue(typedValue, isUsdValuesMode)
onUserInput(field, value)
},
[onUserInput, field, viewAmount, convertUsdToTokenValue, isUsdValuesMode],
[onUserInput, field, convertUsdToTokenValue, isUsdValuesMode],
)

const handleMaxInput = useCallback(() => {
if (!maxBalance) {
return
Expand All @@ -122,19 +130,23 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) {
const value = isUsdValuesMode ? maxBalanceUsdAmount : maxBalance

if (value) {
onUserInputDispatch(value.toExact())
onUserInputDispatch(value.toExact(), isUsdValuesMode ? maxBalance.toExact() : undefined)
setMaxSellTokensAnalytics()
}
}, [maxBalance, onUserInputDispatch, convertUsdToTokenValue, isUsdValuesMode, maxBalanceUsdAmount])
}, [maxBalance, onUserInputDispatch, isUsdValuesMode, maxBalanceUsdAmount])

useEffect(() => {
const areValuesSame = parseFloat(viewAmount) === parseFloat(typedValue)
// Compare the actual string values to preserve trailing decimals
if (viewAmount === typedValue) return

// Don't override empty input
if (viewAmount === '' && typedValue === '') return

// Don't override typedValue when, for example: viewAmount = 5 and typedValue = 5.
if (areValuesSame) return
// Don't override when typing a decimal
if (typedValue.endsWith('.')) return

// Don't override typedValue, when viewAmount from props and typedValue are zero (0 or 0. or 0.000)
if (!viewAmount && (!typedValue || parseFloat(typedValue) === 0)) return
// Don't override when the values are numerically equal (e.g., "5." and "5")
if (parseFloat(viewAmount || '0') === parseFloat(typedValue || '0')) return

setTypedValue(viewAmount)
// We don't need triggering from typedValue changes
Expand All @@ -151,7 +163,7 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) {
<styledEl.NumericalInput
className="token-amount-input"
prependSymbol={isUsdValuesMode ? '$' : ''}
value={isChainIdUnsupported ? '' : isUsdValuesMode ? viewAmount : typedValue}
value={isChainIdUnsupported ? '' : typedValue}
readOnly={inputDisabled}
onUserInput={onUserInputDispatch}
$loading={areCurrenciesLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export const CurrencyInputBox = styled.div`

> div {
display: flex;
flex-flow: row wrap;
align-items: center;
color: inherit;
}
Expand Down
7 changes: 6 additions & 1 deletion apps/cowswap-frontend/src/common/pure/RateInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,12 @@ export function RateInfo({
1 <TokenSymbol token={rateInputCurrency} /> ={' '}
</>
)}
<TokenAmount amount={currentActiveRate} tokenSymbol={rateOutputCurrency} opacitySymbol={opacitySymbol} />
<TokenAmount
amount={currentActiveRate}
tokenSymbol={rateOutputCurrency}
opacitySymbol={opacitySymbol}
clickable
/>
</span>{' '}
{!!fiatAmount && (
<FiatRate>
Expand Down
140 changes: 92 additions & 48 deletions apps/cowswap-frontend/src/legacy/components/NumericalInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
import React from 'react'

import { escapeRegExp } from '@cowprotocol/common-utils'
import { UI } from '@cowprotocol/ui'

import styled from 'styled-components/macro'
import styled, { css } from 'styled-components/macro'

import { autofocus } from 'common/utils/autofocus'

const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>`
const textStyle = css<{ error?: boolean; fontSize?: string }>`
color: ${({ error }) => (error ? `var(${UI.COLOR_DANGER})` : 'inherit')};
font-size: ${({ fontSize }) => fontSize ?? '28px'};
font-weight: 500;
`

const PrependSymbol = styled.span<{ error?: boolean; fontSize?: string }>`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
user-select: none;
${textStyle}
`

const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>`
${textStyle}
width: 0;
position: relative;
font-weight: 500;
outline: none;
border: none;
flex: 1 1 auto;
background-color: var(${UI.COLOR_PAPER});
font-size: ${({ fontSize }) => fontSize ?? '28px'};
text-align: ${({ align }) => align && align};
text-align: ${({ align }) => align || 'right'};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px;
appearance: textfield;
text-align: right;

::-webkit-search-decoration {
-webkit-appearance: none;
Expand All @@ -43,15 +54,15 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s
}
`

const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
// Allow decimal point at any position, including at the end
const inputRegex = /^(\d*\.?\d*)?$/

export const Input = React.memo(function InnerInput({
value,
readOnly,
onUserInput,
placeholder,
prependSymbol,
type,
onFocus,
...rest
}: {
Expand All @@ -63,51 +74,84 @@ export const Input = React.memo(function InnerInput({
align?: 'right' | 'left'
prependSymbol?: string | undefined
} & Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'>) {
// Keep the input strictly as a string
const stringValue = typeof value === 'string' ? value : String(value)

const enforcer = (nextUserInput: string) => {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
onUserInput(nextUserInput)
// Always allow empty input
if (nextUserInput === '') {
onUserInput('')
return
}

// Convert commas to dots
const sanitizedValue = nextUserInput.replace(/,/g, '.')

// Allow the value if it matches our number format
if (inputRegex.test(sanitizedValue)) {
onUserInput(sanitizedValue)
}
}

const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
const pastedText = event.clipboardData.getData('text')

// Clean up pasted content - only allow numbers and single decimal
const cleanedText = pastedText
.replace(/,/g, '.') // Convert commas to dots
.replace(/[^\d.]/g, '') // Remove non-numeric/dot chars
.replace(/(\..*)\./g, '$1') // Keep only the first decimal point
.replace(/\.$/, '') // Remove trailing decimal point

if (inputRegex.test(cleanedText)) {
event.preventDefault()
enforcer(cleanedText)
}
}

return (
<StyledInput
{...rest}
value={prependSymbol && value ? prependSymbol + value : value}
readOnly={readOnly}
onFocus={(event) => {
autofocus(event)
onFocus?.(event)
}}
onChange={(event) => {
if (prependSymbol) {
const value = event.target.value

// cut off prepended symbol
const formattedValue = value.toString().includes(prependSymbol)
? value.toString().slice(1, value.toString().length + 1)
: value

// replace commas with periods, because uniswap exclusively uses period as the decimal separator
enforcer(formattedValue.replace(/,/g, '.'))
} else {
enforcer(event.target.value.replace(/,/g, '.'))
}
}}
// universal input options
inputMode="decimal"
autoComplete="off"
autoCorrect="off"
// text-specific options
type={type || 'text'}
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder={placeholder || '0.0'}
minLength={1}
maxLength={32}
spellCheck="false"
/>
<>
{prependSymbol && (
<PrependSymbol error={rest.error} fontSize={rest.fontSize}>
{prependSymbol}
</PrependSymbol>
)}
<StyledInput
{...rest}
value={stringValue}
readOnly={readOnly}
onFocus={(event) => {
autofocus(event)
onFocus?.(event)
}}
onChange={(event) => {
const rawValue = event.target.value

if (prependSymbol) {
// Remove prepended symbol if it appears in rawValue
const formattedValue = rawValue.includes(prependSymbol) ? rawValue.slice(prependSymbol.length) : rawValue
enforcer(formattedValue)
} else {
enforcer(rawValue)
}
}}
onPaste={handlePaste}
// Use text inputMode so decimals can be typed
inputMode="decimal"
autoComplete="off"
autoCorrect="off"
// Keep type="text" to preserve trailing decimals
type="text"
// Remove pattern to prevent browser validation interference
pattern=""
placeholder={placeholder || '0'}
// minLength to 0 so empty strings are always valid
minLength={0}
maxLength={79}
spellCheck="false"
/>
</>
)
})

export default Input

// const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
55 changes: 55 additions & 0 deletions apps/cowswap-frontend/src/modules/analytics/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum Category {
SURPLUS_MODAL = 'Surplus Modal',
PROGRESS_BAR = 'Progress Bar',
NOTIFICATIONS = 'Notifications',
LIMIT_ORDER_SETTINGS = 'Limit Order Settings',
}

export function shareFortuneTwitterAnalytics() {
Expand Down Expand Up @@ -348,3 +349,57 @@ export function clickOnHooks(event: string) {
action: event,
})
}

enum LimitOrderSettingsAction {
TOGGLE_SETTINGS = 'Toggle Limit Order Settings',
CUSTOM_RECIPIENT = 'Custom Recipient',
PARTIAL_EXECUTIONS = 'Enable Partial Executions',
PRICE_POSITION = 'Limit Price Position',
LOCK_PRICE = 'Lock Limit Price',
USD_MODE = 'Global USD Mode',
TABLE_POSITION = 'Orders Table Position',
}

function sendLimitOrderSettingsAnalytics(action: string, label?: string) {
const event = {
category: Category.LIMIT_ORDER_SETTINGS,
action,
...(label && { label }),
}
cowAnalytics.sendEvent(event)
}

function sendToggleAnalytics(action: LimitOrderSettingsAction, enable: boolean, customLabels?: [string, string]) {
sendLimitOrderSettingsAnalytics(
action,
customLabels ? (enable ? customLabels[0] : customLabels[1]) : enable ? 'Enabled' : 'Disabled',
)
}

export function openLimitOrderSettingsAnalytics() {
sendLimitOrderSettingsAnalytics(LimitOrderSettingsAction.TOGGLE_SETTINGS)
}

export function toggleCustomRecipientAnalytics(enable: boolean) {
sendToggleAnalytics(LimitOrderSettingsAction.CUSTOM_RECIPIENT, enable)
}

export function togglePartialExecutionsAnalytics(enable: boolean) {
sendToggleAnalytics(LimitOrderSettingsAction.PARTIAL_EXECUTIONS, enable)
}

export function changeLimitPricePositionAnalytics(oldPosition: string, newPosition: string) {
sendLimitOrderSettingsAnalytics(LimitOrderSettingsAction.PRICE_POSITION, `${oldPosition} -> ${newPosition}`)
}

export function toggleLockLimitPriceAnalytics(enable: boolean) {
sendToggleAnalytics(LimitOrderSettingsAction.LOCK_PRICE, enable)
}

export function toggleGlobalUsdModeAnalytics(enable: boolean) {
sendToggleAnalytics(LimitOrderSettingsAction.USD_MODE, enable)
}

export function toggleOrdersTablePositionAnalytics(enable: boolean) {
sendToggleAnalytics(LimitOrderSettingsAction.TABLE_POSITION, enable, ['Left', 'Right'])
}
Loading
Loading