improvement(swap): progress bar and more minimal default UI, also fix custom add/remove (#1069)

* add progress bar and minimal UI updates on swap

* add hook to explicity check user added tokens, fixes add/remove bug

* update with latest

* remove confusing comment

* update styles on loading, update arrow placement, code cleanup

* fix typo on progress import
This commit is contained in:
Ian Lapham 2020-08-31 15:27:30 -04:00 committed by GitHub
parent d6aa0e98a4
commit 975570fa97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 132 additions and 44 deletions

@ -10,7 +10,7 @@ const Base = styled(RebassButton)<{
padding?: string
width?: string
borderRadius?: string
altDisbaledStyle?: boolean
altDisabledStyle?: boolean
}>`
padding: ${({ padding }) => (padding ? padding : '18px')};
width: ${({ width }) => (width ? width : '100%')};
@ -53,12 +53,13 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
}
&:disabled {
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)};
cursor: auto;
box-shadow: none;
border: 1px solid transparent;
outline: none;
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')};
}
`
@ -253,11 +254,15 @@ const ButtonErrorStyle = styled(Base)`
}
`
export function ButtonConfirmed({ confirmed, ...rest }: { confirmed?: boolean } & ButtonProps) {
export function ButtonConfirmed({
confirmed,
altDisabledStyle,
...rest
}: { confirmed?: boolean; altDisabledStyle?: boolean } & ButtonProps) {
if (confirmed) {
return <ButtonConfirmedStyle {...rest} />
} else {
return <ButtonPrimary {...rest} />
return <ButtonPrimary {...rest} altDisabledStyle={altDisabledStyle} />
}
}

@ -0,0 +1,79 @@
import React from 'react'
import styled from 'styled-components'
import { RowBetween } from '../Row'
import { AutoColumn } from '../Column'
import { transparentize } from 'polished'
const Wrapper = styled(AutoColumn)`
margin-top: 1.25rem;
`
const Grouping = styled(RowBetween)`
width: 50%;
`
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
min-width: 20px;
min-height: 20px;
background-color: ${({ theme, confirmed, disabled }) =>
disabled ? theme.bg4 : confirmed ? theme.green1 : theme.primary1};
border-radius: 50%;
color: ${({ theme }) => theme.white};
display: flex;
align-items: center;
justify-content: center;
line-height: 8px;
font-size: 12px;
`
const CircleRow = styled.div`
width: calc(100% - 20px);
display: flex;
align-items: center;
`
const Connector = styled.div<{ prevConfirmed?: boolean }>`
width: 100%;
height: 2px;
background-color: ;
background: linear-gradient(
90deg,
${({ theme, prevConfirmed }) => transparentize(0.5, prevConfirmed ? theme.green1 : theme.primary1)} 0%,
${({ theme, prevConfirmed }) => (prevConfirmed ? theme.primary1 : theme.bg4)} 80%
);
opacity: 0.6;
`
interface ProgressCirclesProps {
steps: boolean[]
}
/**
* Based on array of steps, create a step counter of circles.
* A circle can be enabled, disabled, or confirmed. States are derived
* from previous step.
*
* An extra circle is added to represent the ability to swap, add, or remove.
* This step will never be marked as complete (because no 'txn done' state in body ui).
*
* @param steps array of booleans where true means step is complete
*/
export default function ProgressCircles({ steps }: ProgressCirclesProps) {
return (
<Wrapper justify={'center'}>
<Grouping>
{steps.map((step, i) => {
return (
<CircleRow key={i}>
<Circle confirmed={step} disabled={!steps[i - 1] && i !== 0}>
{step ? '✓' : i + 1}
</Circle>
<Connector prevConfirmed={step} />
</CircleRow>
)
})}
<Circle disabled={!steps[steps.length - 1]}>{steps.length + 1}</Circle>
</Grouping>
</Wrapper>
)
}

@ -8,6 +8,7 @@ import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useCurrencyBalance } from '../../state/wallet/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { useIsUserAddedToken } from '../../hooks/Tokens'
import Column from '../Column'
import { RowFixed } from '../Row'
import CurrencyLogo from '../CurrencyLogo'
@ -96,12 +97,13 @@ function CurrencyRow({
const key = currencyKey(currency)
const selectedTokenList = useSelectedTokenList()
const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
const customAdded = Boolean(!isOnSelectedList && currency instanceof Token)
const customAdded = useIsUserAddedToken(currency)
const balance = useCurrencyBalance(account ?? undefined, currency)
const removeToken = useRemoveUserAddedToken()
const addToken = useAddUserToken()
// only show add or remove buttons if not on selected list
return (
<MenuItem
style={style}
@ -116,7 +118,7 @@ function CurrencyRow({
{currency.symbol}
</Text>
<FadedSpan>
{customAdded ? (
{!isOnSelectedList && customAdded ? (
<TYPE.main fontWeight={500}>
Added by user
<LinkStyledButton

@ -1,5 +1,5 @@
import React from 'react'
import { Currency, Price } from '@uniswap/sdk'
import { Price } from '@uniswap/sdk'
import { useContext } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
@ -8,27 +8,19 @@ import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps {
price?: Price
inputCurrency?: Currency
outputCurrency?: Currency
showInverted: boolean
setShowInverted: (showInverted: boolean) => void
}
export default function TradePrice({
price,
inputCurrency,
outputCurrency,
showInverted,
setShowInverted
}: TradePriceProps) {
export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
const theme = useContext(ThemeContext)
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
const show = Boolean(inputCurrency && outputCurrency)
const show = Boolean(price?.baseCurrency && price?.quoteCurrency)
const label = showInverted
? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}`
: `${inputCurrency?.symbol} per ${outputCurrency?.symbol}`
? `${price?.quoteCurrency?.symbol} per ${price?.baseCurrency?.symbol}`
: `${price?.baseCurrency?.symbol} per ${price?.quoteCurrency?.symbol}`
return (
<Text

@ -1,5 +1,5 @@
import { parseBytes32String } from '@ethersproject/strings'
import { Currency, ETHER, Token } from '@uniswap/sdk'
import { Currency, ETHER, Token, currencyEquals } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useSelectedTokenList } from '../state/lists/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
@ -32,6 +32,12 @@ export function useAllTokens(): { [address: string]: Token } {
}, [chainId, userAddedTokens, allTokens])
}
// Check if currency is included in custom list from user storage
export function useIsUserAddedToken(currency: Currency): boolean {
const userAddedTokens = useUserAddedTokens()
return !!userAddedTokens.find(token => currencyEquals(currency, token))
}
// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {

@ -5,7 +5,7 @@ import ReactGA from 'react-ga'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import AddressInputPanel from '../../components/AddressInputPanel'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import { ButtonError, ButtonLight, ButtonPrimary, ButtonConfirmed } from '../../components/Button'
import Card, { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal'
@ -15,9 +15,10 @@ import { AutoRow, RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import { ArrowWrapper, BottomGrouping, Dots, SwapCallbackError, Wrapper } from '../../components/swap/styleds'
import { ArrowWrapper, BottomGrouping, SwapCallbackError, Wrapper } from '../../components/swap/styleds'
import TradePrice from '../../components/swap/TradePrice'
import TokenWarningModal from '../../components/TokenWarningModal'
import ProgressSteps from '../../components/ProgressSteps'
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
@ -42,6 +43,7 @@ import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { ClickableText } from '../Pool/styleds'
import Loader from '../../components/Loader'
export default function Swap() {
const loadedUrlParams = useDefaultsFromURLSearch()
@ -294,7 +296,7 @@ export default function Swap() {
<AutoColumn gap={'md'}>
<CurrencyInputPanel
label={independentField === Field.OUTPUT && !showWrap ? 'From (estimated)' : 'From'}
label={independentField === Field.OUTPUT && !showWrap && trade ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
currency={currencies[Field.INPUT]}
@ -304,9 +306,8 @@ export default function Swap() {
otherCurrency={currencies[Field.OUTPUT]}
id="swap-currency-input"
/>
<AutoColumn justify="space-between">
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<AutoRow justify={isExpertMode ? 'space-between' : 'center'} style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable>
<ArrowDown
size="16"
@ -327,7 +328,7 @@ export default function Swap() {
<CurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={independentField === Field.INPUT && !showWrap ? 'To (estimated)' : 'To'}
label={independentField === Field.INPUT && !showWrap && trade ? 'To (estimated)' : 'To'}
showMaxButton={false}
currency={currencies[Field.OUTPUT]}
onCurrencySelect={handleOutputSelect}
@ -352,19 +353,18 @@ export default function Swap() {
{showWrap ? null : (
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
{Boolean(trade) && (
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<TradePrice
inputCurrency={currencies[Field.INPUT]}
outputCurrency={currencies[Field.OUTPUT]}
price={trade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</RowBetween>
)}
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
<RowBetween align="center">
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
@ -393,20 +393,23 @@ export default function Swap() {
</GreyCard>
) : showApproveFlow ? (
<RowBetween>
<ButtonPrimary
<ButtonConfirmed
onClick={approveCallback}
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
width="48%"
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
altDisabledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
confirmed={approval === ApprovalState.APPROVED}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
<AutoRow gap="6px" justify="center">
Approving <Loader stroke="white" />
</AutoRow>
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + currencies[Field.INPUT]?.symbol
)}
</ButtonPrimary>
</ButtonConfirmed>
<ButtonError
onClick={() => {
if (isExpertMode) {
@ -463,6 +466,7 @@ export default function Swap() {
</Text>
</ButtonError>
)}
{showApproveFlow && <ProgressSteps steps={[approval === ApprovalState.APPROVED]} />}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>