Commit 975570fa authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

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
parent d6aa0e98
...@@ -10,7 +10,7 @@ const Base = styled(RebassButton)<{ ...@@ -10,7 +10,7 @@ const Base = styled(RebassButton)<{
padding?: string padding?: string
width?: string width?: string
borderRadius?: string borderRadius?: string
altDisbaledStyle?: boolean altDisabledStyle?: boolean
}>` }>`
padding: ${({ padding }) => (padding ? padding : '18px')}; padding: ${({ padding }) => (padding ? padding : '18px')};
width: ${({ width }) => (width ? width : '100%')}; width: ${({ width }) => (width ? width : '100%')};
...@@ -53,12 +53,13 @@ export const ButtonPrimary = styled(Base)` ...@@ -53,12 +53,13 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => darken(0.1, theme.primary1)}; background-color: ${({ theme }) => darken(0.1, theme.primary1)};
} }
&:disabled { &:disabled {
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)}; background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)}; color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)};
cursor: auto; cursor: auto;
box-shadow: none; box-shadow: none;
border: 1px solid transparent; border: 1px solid transparent;
outline: none; outline: none;
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')};
} }
` `
...@@ -253,11 +254,15 @@ const ButtonErrorStyle = styled(Base)` ...@@ -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) { if (confirmed) {
return <ButtonConfirmedStyle {...rest} /> return <ButtonConfirmedStyle {...rest} />
} else { } else {
return <ButtonPrimary {...rest} /> return <ButtonPrimary {...rest} altDisabledStyle={altDisabledStyle} />
} }
} }
......
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' ...@@ -8,6 +8,7 @@ import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks' import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useCurrencyBalance } from '../../state/wallet/hooks' import { useCurrencyBalance } from '../../state/wallet/hooks'
import { LinkStyledButton, TYPE } from '../../theme' import { LinkStyledButton, TYPE } from '../../theme'
import { useIsUserAddedToken } from '../../hooks/Tokens'
import Column from '../Column' import Column from '../Column'
import { RowFixed } from '../Row' import { RowFixed } from '../Row'
import CurrencyLogo from '../CurrencyLogo' import CurrencyLogo from '../CurrencyLogo'
...@@ -96,12 +97,13 @@ function CurrencyRow({ ...@@ -96,12 +97,13 @@ function CurrencyRow({
const key = currencyKey(currency) const key = currencyKey(currency)
const selectedTokenList = useSelectedTokenList() const selectedTokenList = useSelectedTokenList()
const isOnSelectedList = isTokenOnList(selectedTokenList, currency) const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
const customAdded = Boolean(!isOnSelectedList && currency instanceof Token) const customAdded = useIsUserAddedToken(currency)
const balance = useCurrencyBalance(account ?? undefined, currency) const balance = useCurrencyBalance(account ?? undefined, currency)
const removeToken = useRemoveUserAddedToken() const removeToken = useRemoveUserAddedToken()
const addToken = useAddUserToken() const addToken = useAddUserToken()
// only show add or remove buttons if not on selected list
return ( return (
<MenuItem <MenuItem
style={style} style={style}
...@@ -116,7 +118,7 @@ function CurrencyRow({ ...@@ -116,7 +118,7 @@ function CurrencyRow({
{currency.symbol} {currency.symbol}
</Text> </Text>
<FadedSpan> <FadedSpan>
{customAdded ? ( {!isOnSelectedList && customAdded ? (
<TYPE.main fontWeight={500}> <TYPE.main fontWeight={500}>
Added by user Added by user
<LinkStyledButton <LinkStyledButton
......
import React from 'react' import React from 'react'
import { Currency, Price } from '@uniswap/sdk' import { Price } from '@uniswap/sdk'
import { useContext } from 'react' import { useContext } from 'react'
import { Repeat } from 'react-feather' import { Repeat } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
...@@ -8,27 +8,19 @@ import { StyledBalanceMaxMini } from './styleds' ...@@ -8,27 +8,19 @@ import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps { interface TradePriceProps {
price?: Price price?: Price
inputCurrency?: Currency
outputCurrency?: Currency
showInverted: boolean showInverted: boolean
setShowInverted: (showInverted: boolean) => void setShowInverted: (showInverted: boolean) => void
} }
export default function TradePrice({ export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
price,
inputCurrency,
outputCurrency,
showInverted,
setShowInverted
}: TradePriceProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6) 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 const label = showInverted
? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}` ? `${price?.quoteCurrency?.symbol} per ${price?.baseCurrency?.symbol}`
: `${inputCurrency?.symbol} per ${outputCurrency?.symbol}` : `${price?.baseCurrency?.symbol} per ${price?.quoteCurrency?.symbol}`
return ( return (
<Text <Text
......
import { parseBytes32String } from '@ethersproject/strings' 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 { useMemo } from 'react'
import { useSelectedTokenList } from '../state/lists/hooks' import { useSelectedTokenList } from '../state/lists/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks' import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
...@@ -32,6 +32,12 @@ export function useAllTokens(): { [address: string]: Token } { ...@@ -32,6 +32,12 @@ export function useAllTokens(): { [address: string]: Token } {
}, [chainId, userAddedTokens, allTokens]) }, [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 // parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/ const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string { function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
......
...@@ -5,7 +5,7 @@ import ReactGA from 'react-ga' ...@@ -5,7 +5,7 @@ import ReactGA from 'react-ga'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import AddressInputPanel from '../../components/AddressInputPanel' 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 Card, { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column' import { AutoColumn } from '../../components/Column'
import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal' import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal'
...@@ -15,9 +15,10 @@ import { AutoRow, RowBetween } from '../../components/Row' ...@@ -15,9 +15,10 @@ import { AutoRow, RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown' import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import BetterTradeLink from '../../components/swap/BetterTradeLink' import BetterTradeLink from '../../components/swap/BetterTradeLink'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee' 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 TradePrice from '../../components/swap/TradePrice'
import TokenWarningModal from '../../components/TokenWarningModal' import TokenWarningModal from '../../components/TokenWarningModal'
import ProgressSteps from '../../components/ProgressSteps'
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants' import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1' import { getTradeVersion, isTradeBetter } from '../../data/V1'
...@@ -42,6 +43,7 @@ import { maxAmountSpend } from '../../utils/maxAmountSpend' ...@@ -42,6 +43,7 @@ import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices' import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { ClickableText } from '../Pool/styleds' import { ClickableText } from '../Pool/styleds'
import Loader from '../../components/Loader'
export default function Swap() { export default function Swap() {
const loadedUrlParams = useDefaultsFromURLSearch() const loadedUrlParams = useDefaultsFromURLSearch()
...@@ -294,7 +296,7 @@ export default function Swap() { ...@@ -294,7 +296,7 @@ export default function Swap() {
<AutoColumn gap={'md'}> <AutoColumn gap={'md'}>
<CurrencyInputPanel <CurrencyInputPanel
label={independentField === Field.OUTPUT && !showWrap ? 'From (estimated)' : 'From'} label={independentField === Field.OUTPUT && !showWrap && trade ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]} value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput} showMaxButton={!atMaxAmountInput}
currency={currencies[Field.INPUT]} currency={currencies[Field.INPUT]}
...@@ -304,9 +306,8 @@ export default function Swap() { ...@@ -304,9 +306,8 @@ export default function Swap() {
otherCurrency={currencies[Field.OUTPUT]} otherCurrency={currencies[Field.OUTPUT]}
id="swap-currency-input" id="swap-currency-input"
/> />
<AutoColumn justify="space-between"> <AutoColumn justify="space-between">
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}> <AutoRow justify={isExpertMode ? 'space-between' : 'center'} style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable> <ArrowWrapper clickable>
<ArrowDown <ArrowDown
size="16" size="16"
...@@ -327,7 +328,7 @@ export default function Swap() { ...@@ -327,7 +328,7 @@ export default function Swap() {
<CurrencyInputPanel <CurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]} value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput} onUserInput={handleTypeOutput}
label={independentField === Field.INPUT && !showWrap ? 'To (estimated)' : 'To'} label={independentField === Field.INPUT && !showWrap && trade ? 'To (estimated)' : 'To'}
showMaxButton={false} showMaxButton={false}
currency={currencies[Field.OUTPUT]} currency={currencies[Field.OUTPUT]}
onCurrencySelect={handleOutputSelect} onCurrencySelect={handleOutputSelect}
...@@ -352,19 +353,18 @@ export default function Swap() { ...@@ -352,19 +353,18 @@ export default function Swap() {
{showWrap ? null : ( {showWrap ? null : (
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}> <Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px"> <AutoColumn gap="4px">
<RowBetween align="center"> {Boolean(trade) && (
<Text fontWeight={500} fontSize={14} color={theme.text2}> <RowBetween align="center">
Price <Text fontWeight={500} fontSize={14} color={theme.text2}>
</Text> Price
<TradePrice </Text>
inputCurrency={currencies[Field.INPUT]} <TradePrice
outputCurrency={currencies[Field.OUTPUT]} price={trade?.executionPrice}
price={trade?.executionPrice} showInverted={showInverted}
showInverted={showInverted} setShowInverted={setShowInverted}
setShowInverted={setShowInverted} />
/> </RowBetween>
</RowBetween> )}
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && ( {allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
<RowBetween align="center"> <RowBetween align="center">
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}> <ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
...@@ -393,20 +393,23 @@ export default function Swap() { ...@@ -393,20 +393,23 @@ export default function Swap() {
</GreyCard> </GreyCard>
) : showApproveFlow ? ( ) : showApproveFlow ? (
<RowBetween> <RowBetween>
<ButtonPrimary <ButtonConfirmed
onClick={approveCallback} onClick={approveCallback}
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted} disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
width="48%" 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 ? ( {approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots> <AutoRow gap="6px" justify="center">
Approving <Loader stroke="white" />
</AutoRow>
) : approvalSubmitted && approval === ApprovalState.APPROVED ? ( ) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved' 'Approved'
) : ( ) : (
'Approve ' + currencies[Field.INPUT]?.symbol 'Approve ' + currencies[Field.INPUT]?.symbol
)} )}
</ButtonPrimary> </ButtonConfirmed>
<ButtonError <ButtonError
onClick={() => { onClick={() => {
if (isExpertMode) { if (isExpertMode) {
...@@ -463,6 +466,7 @@ export default function Swap() { ...@@ -463,6 +466,7 @@ export default function Swap() {
</Text> </Text>
</ButtonError> </ButtonError>
)} )}
{showApproveFlow && <ProgressSteps steps={[approval === ApprovalState.APPROVED]} />}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null} {isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />} {betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping> </BottomGrouping>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment