Commit cb094a1f authored by lynn's avatar lynn Committed by GitHub

feat: implement token selector events (#4067)

* init commit

* add amplitude ts sdk to package.json

* add more comments and documentation

* respond to vm comments

* respond to cmcewen comments

* fix: remove unused constants

* init commit

* adapt to web

* add optional event properties to trace

* correct telemetry to analytics

* change telemetry to analytics in doc

* fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement

* init commit

* respond to zzmp comments

* add token selected event

* fixes

* eliminate unnecessary state

* respond to part of zzmp comments

* respond to zzmp comments round 2

* fixes

* respond to zzmp comments

* add imported token event and other fixes

* also log onKeyPress for suggested tokens

* respond to cmcewen comments
parent d05fefc2
......@@ -7,9 +7,11 @@ export interface ITraceContext {
// Highest order context: eg Swap or Explore.
page?: PageName
// Enclosed section name. Can be as wide or narrow as necessary to
// provide context.
section?: SectionName | ModalName
// Enclosed section name. For contexts with modals, refers to the
// section of the page from which the user triggered the modal.
section?: SectionName
modal?: ModalName
// Element name mostly used to identify events sources
// Does not need to be unique given the higher order page and section.
......
......@@ -30,7 +30,7 @@ export const TraceEvent = memo((props: PropsWithChildren<TraceEventProps>) => {
return child
}
// For each child, augment event handlers defined in `actionNames` with event tracing
// For each child, augment event handlers defined in `events` with event tracing.
return cloneElement(child, getEventHandlers(child, traceContext, events, name, properties))
})
}
......@@ -42,7 +42,7 @@ export const TraceEvent = memo((props: PropsWithChildren<TraceEventProps>) => {
TraceEvent.displayName = 'TraceEvent'
/**
* Given a set of child element and action props, returns a spreadable
* Given a set of child element and event props, returns a spreadable
* object of the event handlers augmented with analytics logging.
*/
function getEventHandlers(
......@@ -61,7 +61,7 @@ function getEventHandlers(
child.props[event]?.apply(child, args)
// augment handler with analytics logging
sendAnalyticsEvent(name, { ...traceContext, ...properties })
sendAnalyticsEvent(name, { ...traceContext, ...properties, action: event })
}
}
......
......@@ -7,6 +7,9 @@
export enum EventName {
PAGE_VIEWED = 'Page Viewed',
SWAP_SUBMITTED = 'Swap Submitted',
TOKEN_IMPORTED = 'Token Imported',
TOKEN_SELECTED = 'Token Selected',
TOKEN_SELECTOR_OPENED = 'Token Selector Opened',
// alphabetize additional event names.
}
......@@ -25,12 +28,14 @@ export const enum PageName {
*/
export const enum SectionName {
CURRENCY_INPUT_PANEL = 'swap-currency-input',
CURRENCY_OUTPUT_PANEL = 'swap-currency-output',
// alphabetize additional section names.
}
/** Known modals for analytics purposes. */
export const enum ModalName {
SWAP = 'swap-modal',
TOKEN_SELECTOR = 'token-selector-modal',
// alphabetize additional modal names.
}
......@@ -39,8 +44,11 @@ export const enum ModalName {
* Use to identify low-level components given a TraceContext
*/
export const enum ElementName {
COMMON_BASES_CURRENCY_BUTTON = 'common-bases-currency-button',
CONFIRM_SWAP_BUTTON = 'confirm-swap-or-send',
IMPORT_TOKEN_BUTTON = 'import-token-button',
SWAP_BUTTON = 'swap-button',
TOKEN_SELECTOR_ROW = 'token-selector-row',
// alphabetize additional element names.
}
......@@ -51,5 +59,7 @@ export const enum ElementName {
*/
export enum Event {
onClick = 'onClick',
onKeyPress = 'onKeyPress',
onSelect = 'onSelect',
// alphabetize additional events.
}
......@@ -34,7 +34,7 @@ export function initializeAnalytics(isDevEnvironment = process.env.NODE_ENV ===
/** Sends an event to Amplitude. */
export function sendAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
if (process.env.NODE_ENV === 'development') {
console.debug(`[amplitude(${eventName})]: ${JSON.stringify(eventProperties)}`)
console.log(`[amplitude(${eventName})]: ${JSON.stringify(eventProperties)}`)
return
}
......
import { Currency } from '@uniswap/sdk-core'
import { Currency, Token } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import { AutoRow } from 'components/Row'
......@@ -31,14 +33,35 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
filter: ${({ disable }) => disable && 'grayscale(1)'};
`
const formatAnalyticsEventProperties = (
currency: Currency,
tokenAddress: string | undefined,
searchQuery: string,
isAddressSearch: string | false
) => ({
token_symbol: currency?.symbol,
token_chain_id: currency?.chainId,
...(tokenAddress ? { token_address: tokenAddress } : {}),
is_suggested_token: true,
is_selected_from_list: false,
is_imported_by_user: false,
...(isAddressSearch === false
? { search_token_symbol_input: searchQuery }
: { search_token_address_input: isAddressSearch }),
})
export default function CommonBases({
chainId,
onSelect,
selectedCurrency,
searchQuery,
isAddressSearch,
}: {
chainId?: number
selectedCurrency?: Currency | null
onSelect: (currency: Currency) => void
searchQuery: string
isAddressSearch: string | false
}) {
const bases = typeof chainId !== 'undefined' ? COMMON_BASES[chainId] ?? [] : []
......@@ -47,19 +70,29 @@ export default function CommonBases({
<AutoRow gap="4px">
{bases.map((currency: Currency) => {
const isSelected = selectedCurrency?.equals(currency)
const tokenAddress = currency instanceof Token ? currency?.address : undefined
return (
<BaseWrapper
tabIndex={0}
onKeyPress={(e) => !isSelected && e.key === 'Enter' && onSelect(currency)}
onClick={() => !isSelected && onSelect(currency)}
disable={isSelected}
<TraceEvent
events={[Event.onClick, Event.onKeyPress]}
name={EventName.TOKEN_SELECTED}
properties={formatAnalyticsEventProperties(currency, tokenAddress, searchQuery, isAddressSearch)}
element={ElementName.COMMON_BASES_CURRENCY_BUTTON}
key={currencyId(currency)}
>
<CurrencyLogoFromList currency={currency} />
<Text fontWeight={500} fontSize={16}>
{currency.symbol}
</Text>
</BaseWrapper>
<BaseWrapper
tabIndex={0}
onKeyPress={(e) => !isSelected && e.key === 'Enter' && onSelect(currency)}
onClick={() => !isSelected && onSelect(currency)}
disable={isSelected}
key={currencyId(currency)}
>
<CurrencyLogoFromList currency={currency} />
<Text fontWeight={500} fontSize={16}>
{currency.symbol}
</Text>
</BaseWrapper>
</TraceEvent>
)
})}
</AutoRow>
......
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { LightGreyCard } from 'components/Card'
import QuestionHelper from 'components/QuestionHelper'
import useTheme from 'hooks/useTheme'
......@@ -106,6 +108,7 @@ function CurrencyRow({
otherSelected,
style,
showCurrencyAmount,
eventProperties,
}: {
currency: Currency
onSelect: () => void
......@@ -113,6 +116,7 @@ function CurrencyRow({
otherSelected: boolean
style: CSSProperties
showCurrencyAmount?: boolean
eventProperties: Record<string, unknown>
}) {
const { account } = useWeb3React()
const key = currencyKey(currency)
......@@ -123,35 +127,42 @@ function CurrencyRow({
// only show add or remove buttons if not on selected list
return (
<MenuItem
tabIndex={0}
style={style}
className={`token-item-${key}`}
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect() : null)}
onClick={() => (isSelected ? null : onSelect())}
disabled={isSelected}
selected={otherSelected}
<TraceEvent
events={[Event.onClick, Event.onKeyPress]}
name={EventName.TOKEN_SELECTED}
properties={{ is_imported_by_user: customAdded, ...eventProperties }}
element={ElementName.TOKEN_SELECTOR_ROW}
>
<CurrencyLogo currency={currency} size={'24px'} />
<Column>
<Text title={currency.name} fontWeight={500}>
{currency.symbol}
</Text>
<ThemedText.DarkGray ml="0px" fontSize={'12px'} fontWeight={300}>
{!currency.isNative && !isOnSelectedList && customAdded ? (
<Trans>{currency.name} • Added by user</Trans>
) : (
currency.name
)}
</ThemedText.DarkGray>
</Column>
<TokenTags currency={currency} />
{showCurrencyAmount && (
<RowFixed style={{ justifySelf: 'flex-end' }}>
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
</RowFixed>
)}
</MenuItem>
<MenuItem
tabIndex={0}
style={style}
className={`token-item-${key}`}
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect() : null)}
onClick={() => (isSelected ? null : onSelect())}
disabled={isSelected}
selected={otherSelected}
>
<CurrencyLogo currency={currency} size={'24px'} />
<Column>
<Text title={currency.name} fontWeight={500}>
{currency.symbol}
</Text>
<ThemedText.DarkGray ml="0px" fontSize={'12px'} fontWeight={300}>
{!currency.isNative && !isOnSelectedList && customAdded ? (
<Trans>{currency.name} • Added by user</Trans>
) : (
currency.name
)}
</ThemedText.DarkGray>
</Column>
<TokenTags currency={currency} />
{showCurrencyAmount && (
<RowFixed style={{ justifySelf: 'flex-end' }}>
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
</RowFixed>
)}
</MenuItem>
</TraceEvent>
)
}
......@@ -186,6 +197,25 @@ function BreakLineComponent({ style }: { style: CSSProperties }) {
)
}
const formatAnalyticsEventProperties = (
token: Token,
index: number,
data: any[],
searchQuery: string,
isAddressSearch: string | false
) => ({
token_symbol: token?.symbol,
token_address: token?.address,
is_suggested_token: false,
is_selected_from_list: true,
scroll_position: '',
token_list_index: index,
token_list_length: data.length,
...(isAddressSearch === false
? { search_token_symbol_input: searchQuery }
: { search_token_address_input: isAddressSearch }),
})
export default function CurrencyList({
height,
currencies,
......@@ -198,6 +228,8 @@ export default function CurrencyList({
setImportToken,
showCurrencyAmount,
isLoading,
searchQuery,
isAddressSearch,
}: {
height: number
currencies: Currency[]
......@@ -210,6 +242,8 @@ export default function CurrencyList({
setImportToken: (token: Token) => void
showCurrencyAmount?: boolean
isLoading: boolean
searchQuery: string
isAddressSearch: string | false
}) {
const itemData: (Currency | BreakLine)[] = useMemo(() => {
if (otherListTokens && otherListTokens?.length > 0) {
......@@ -257,6 +291,7 @@ export default function CurrencyList({
onSelect={handleSelect}
otherSelected={otherSelected}
showCurrencyAmount={showCurrencyAmount}
eventProperties={formatAnalyticsEventProperties(token, index, data, searchQuery, isAddressSearch)}
/>
)
} else {
......@@ -272,6 +307,8 @@ export default function CurrencyList({
showImportView,
showCurrencyAmount,
isLoading,
isAddressSearch,
searchQuery,
]
)
......
......@@ -2,6 +2,8 @@
import { t, Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { EventName, ModalName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import { sendEvent } from 'components/analytics'
import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
......@@ -189,76 +191,86 @@ export function CurrencySearch({
}, [])
return (
<ContentWrapper>
<PaddedColumn gap="16px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
<Trans>Select a token</Trans>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<Row>
<SearchInput
type="text"
id="token-search-input"
placeholder={t`Search name or paste address`}
autoComplete="off"
value={searchQuery}
ref={inputRef as RefObject<HTMLInputElement>}
onChange={handleInput}
onKeyDown={handleEnter}
/>
</Row>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={selectedCurrency} />
<Trace name={EventName.TOKEN_SELECTOR_OPENED} modal={ModalName.TOKEN_SELECTOR} shouldLogImpression={true}>
<ContentWrapper>
<PaddedColumn gap="16px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
<Trans>Select a token</Trans>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<Row>
<SearchInput
type="text"
id="token-search-input"
placeholder={t`Search name or paste address`}
autoComplete="off"
value={searchQuery}
ref={inputRef as RefObject<HTMLInputElement>}
onChange={handleInput}
onKeyDown={handleEnter}
/>
</Row>
{showCommonBases && (
<CommonBases
chainId={chainId}
onSelect={handleCurrencySelect}
selectedCurrency={selectedCurrency}
searchQuery={searchQuery}
isAddressSearch={isAddressSearch}
/>
)}
</PaddedColumn>
<Separator />
{searchToken && !searchTokenIsAdded ? (
<Column style={{ padding: '20px 0', height: '100%' }}>
<ImportRow token={searchToken} showImportView={showImportView} setImportToken={setImportToken} />
</Column>
) : filteredSortedTokens?.length > 0 || filteredInactiveTokens?.length > 0 ? (
<div style={{ flex: '1' }}>
<AutoSizer disableWidth>
{({ height }) => (
<CurrencyList
height={height}
currencies={disableNonToken ? filteredSortedTokens : filteredSortedTokensWithETH}
otherListTokens={filteredInactiveTokens}
onCurrencySelect={handleCurrencySelect}
otherCurrency={otherSelectedCurrency}
selectedCurrency={selectedCurrency}
fixedListRef={fixedList}
showImportView={showImportView}
setImportToken={setImportToken}
showCurrencyAmount={showCurrencyAmount}
isLoading={balancesIsLoading && !tokenLoaderTimerElapsed}
searchQuery={searchQuery}
isAddressSearch={isAddressSearch}
/>
)}
</AutoSizer>
</div>
) : (
<Column style={{ padding: '20px', height: '100%' }}>
<ThemedText.Main color={theme.text3} textAlign="center" mb="20px">
<Trans>No results found.</Trans>
</ThemedText.Main>
</Column>
)}
</PaddedColumn>
<Separator />
{searchToken && !searchTokenIsAdded ? (
<Column style={{ padding: '20px 0', height: '100%' }}>
<ImportRow token={searchToken} showImportView={showImportView} setImportToken={setImportToken} />
</Column>
) : filteredSortedTokens?.length > 0 || filteredInactiveTokens?.length > 0 ? (
<div style={{ flex: '1' }}>
<AutoSizer disableWidth>
{({ height }) => (
<CurrencyList
height={height}
currencies={disableNonToken ? filteredSortedTokens : filteredSortedTokensWithETH}
otherListTokens={filteredInactiveTokens}
onCurrencySelect={handleCurrencySelect}
otherCurrency={otherSelectedCurrency}
selectedCurrency={selectedCurrency}
fixedListRef={fixedList}
showImportView={showImportView}
setImportToken={setImportToken}
showCurrencyAmount={showCurrencyAmount}
isLoading={balancesIsLoading && !tokenLoaderTimerElapsed}
/>
)}
</AutoSizer>
</div>
) : (
<Column style={{ padding: '20px', height: '100%' }}>
<ThemedText.Main color={theme.text3} textAlign="center" mb="20px">
<Trans>No results found.</Trans>
</ThemedText.Main>
</Column>
)}
<Footer>
<Row justify="center">
<ButtonText onClick={showManageView} color={theme.primary1} className="list-token-manage-button">
<RowFixed>
<IconWrapper size="16px" marginRight="6px" stroke={theme.primaryText1}>
<Edit />
</IconWrapper>
<ThemedText.Main color={theme.primaryText1}>
<Trans>Manage Token Lists</Trans>
</ThemedText.Main>
</RowFixed>
</ButtonText>
</Row>
</Footer>
</ContentWrapper>
<Footer>
<Row justify="center">
<ButtonText onClick={showManageView} color={theme.primary1} className="list-token-manage-button">
<RowFixed>
<IconWrapper size="16px" marginRight="6px" stroke={theme.primaryText1}>
<Edit />
</IconWrapper>
<ThemedText.Main color={theme.primaryText1}>
<Trans>Manage Token Lists</Trans>
</ThemedText.Main>
</RowFixed>
</ButtonText>
</Row>
</Footer>
</ContentWrapper>
</Trace>
)
}
import { Plural, Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import { RowBetween } from 'components/Row'
......@@ -30,6 +32,12 @@ interface ImportProps {
handleCurrencySelect?: (currency: Currency) => void
}
const formatAnalyticsEventProperties = (tokens: Token[]) => ({
token_symbols: tokens.map((token) => token?.symbol),
token_addresses: tokens.map((token) => token?.address),
token_chain_ids: tokens.map((token) => token?.chainId),
})
export function ImportToken(props: ImportProps) {
const { tokens, list, onBack, onDismiss, handleCurrencySelect } = props
const theme = useTheme()
......@@ -42,6 +50,7 @@ export function ImportToken(props: ImportProps) {
if (intersection.size > 0) {
return <BlockedToken onBack={onBack} onDismiss={onDismiss} blockedTokens={Array.from(intersection)} />
}
return (
<Wrapper>
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
......@@ -67,18 +76,25 @@ export function ImportToken(props: ImportProps) {
{tokens.map((token) => (
<TokenImportCard token={token} list={list} key={'import' + token.address} />
))}
<ButtonPrimary
altDisabledStyle={true}
$borderRadius="20px"
padding="10px 1rem"
onClick={() => {
tokens.map((token) => addToken(token))
handleCurrencySelect && handleCurrencySelect(tokens[0])
}}
className=".token-dismiss-button"
<TraceEvent
events={[Event.onClick]}
name={EventName.TOKEN_IMPORTED}
properties={formatAnalyticsEventProperties(tokens)}
element={ElementName.IMPORT_TOKEN_BUTTON}
>
<Trans>Import</Trans>
</ButtonPrimary>
<ButtonPrimary
altDisabledStyle={true}
$borderRadius="20px"
padding="10px 1rem"
onClick={() => {
tokens.map((token) => addToken(token))
handleCurrencySelect && handleCurrencySelect(tokens[0])
}}
className=".token-dismiss-button"
>
<Trans>Import</Trans>
</ButtonPrimary>
</TraceEvent>
</AutoColumn>
</Wrapper>
)
......
......@@ -4,6 +4,8 @@ import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { PageName, SectionName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import { sendEvent } from 'components/analytics'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
......@@ -396,260 +398,273 @@ export default function Swap({ history }: RouteComponentProps) {
const priceImpactTooHigh = priceImpactSeverity > 3 && !isExpertMode
return (
<>
<TokenWarningModal
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
tokens={importTokensNotInDefault}
onConfirm={handleConfirmTokenWarning}
onDismiss={handleDismissTokenWarning}
/>
<AppBody>
<SwapHeader allowedSlippage={allowedSlippage} />
<Wrapper id="swap-page">
<ConfirmSwapModal
isOpen={showConfirm}
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
txHash={txHash}
recipient={recipient}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
/>
<AutoColumn gap={'sm'}>
<div style={{ display: 'relative' }}>
<CurrencyInputPanel
label={
independentField === Field.OUTPUT && !showWrap ? <Trans>From (at most)</Trans> : <Trans>From</Trans>
}
value={formattedAmounts[Field.INPUT]}
showMaxButton={showMaxButton}
currency={currencies[Field.INPUT] ?? null}
onUserInput={handleTypeInput}
onMax={handleMaxInput}
fiatValue={fiatValueInput ?? undefined}
onCurrencySelect={handleInputSelect}
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id="swap-currency-input"
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
<ArrowWrapper clickable>
<ArrowDown
size="16"
onClick={() => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.text1 : theme.text3}
<Trace page={PageName.SWAP_PAGE} shouldLogImpression={false}>
<>
<TokenWarningModal
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
tokens={importTokensNotInDefault}
onConfirm={handleConfirmTokenWarning}
onDismiss={handleDismissTokenWarning}
/>
<AppBody>
<SwapHeader allowedSlippage={allowedSlippage} />
<Wrapper id="swap-page">
<ConfirmSwapModal
isOpen={showConfirm}
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
txHash={txHash}
recipient={recipient}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
/>
<AutoColumn gap={'sm'}>
<div style={{ display: 'relative' }}>
<Trace section={SectionName.CURRENCY_INPUT_PANEL}>
<CurrencyInputPanel
label={
independentField === Field.OUTPUT && !showWrap ? (
<Trans>From (at most)</Trans>
) : (
<Trans>From</Trans>
)
}
value={formattedAmounts[Field.INPUT]}
showMaxButton={showMaxButton}
currency={currencies[Field.INPUT] ?? null}
onUserInput={handleTypeInput}
onMax={handleMaxInput}
fiatValue={fiatValueInput ?? undefined}
onCurrencySelect={handleInputSelect}
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id={SectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
</Trace>
<ArrowWrapper clickable>
<ArrowDown
size="16"
onClick={() => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.text1 : theme.text3}
/>
</ArrowWrapper>
<Trace section={SectionName.CURRENCY_OUTPUT_PANEL}>
<CurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={
independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>
}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput ?? undefined}
priceImpact={priceImpact}
currency={currencies[Field.OUTPUT] ?? null}
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
showCommonBases={true}
id={SectionName.CURRENCY_OUTPUT_PANEL}
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</Trace>
</div>
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.text2} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
<Trans>- Remove recipient</Trans>
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
{!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing) && (
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
showInverted={showInverted}
setShowInverted={setShowInverted}
allowedSlippage={allowedSlippage}
/>
</ArrowWrapper>
<CurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput ?? undefined}
priceImpact={priceImpact}
currency={currencies[Field.OUTPUT] ?? null}
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
showCommonBases={true}
id="swap-currency-output"
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</div>
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.text2} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
<Trans>- Remove recipient</Trans>
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
{!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing) && (
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
showInverted={showInverted}
setShowInverted={setShowInverted}
allowedSlippage={allowedSlippage}
/>
)}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
<ThemedText.Main mb="4px">
<Trans>Unsupported Asset</Trans>
</ThemedText.Main>
</ButtonPrimary>
) : !account ? (
<ButtonLight onClick={toggleWalletModal}>
<Trans>Connect Wallet</Trans>
</ButtonLight>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}>
{wrapInputError ? (
<WrapErrorText wrapInputError={wrapInputError} />
) : wrapType === WrapType.WRAP ? (
<Trans>Wrap</Trans>
) : wrapType === WrapType.UNWRAP ? (
<Trans>Unwrap</Trans>
) : null}
</ButtonPrimary>
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
<GreyCard style={{ textAlign: 'center' }}>
<ThemedText.Main mb="4px">
<Trans>Insufficient liquidity for this trade.</Trans>
</ThemedText.Main>
</GreyCard>
) : showApproveFlow ? (
<AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}>
<AutoColumn style={{ width: '100%' }} gap="12px">
<ButtonConfirmed
onClick={handleApprove}
disabled={
approvalState !== ApprovalState.NOT_APPROVED ||
approvalSubmitted ||
signatureState === UseERC20PermitState.SIGNED
}
width="100%"
altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
confirmed={
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED
}
>
<AutoRow justify="space-between" style={{ flexWrap: 'nowrap' }}>
<span style={{ display: 'flex', alignItems: 'center' }}>
<CurrencyLogo
currency={currencies[Field.INPUT]}
size={'20px'}
style={{ marginRight: '8px', flexShrink: 0 }}
/>
{/* we need to shorten this string on mobile */}
{approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED ? (
<Trans>You can now trade {currencies[Field.INPUT]?.symbol}</Trans>
)}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
<ThemedText.Main mb="4px">
<Trans>Unsupported Asset</Trans>
</ThemedText.Main>
</ButtonPrimary>
) : !account ? (
<ButtonLight onClick={toggleWalletModal}>
<Trans>Connect Wallet</Trans>
</ButtonLight>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}>
{wrapInputError ? (
<WrapErrorText wrapInputError={wrapInputError} />
) : wrapType === WrapType.WRAP ? (
<Trans>Wrap</Trans>
) : wrapType === WrapType.UNWRAP ? (
<Trans>Unwrap</Trans>
) : null}
</ButtonPrimary>
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
<GreyCard style={{ textAlign: 'center' }}>
<ThemedText.Main mb="4px">
<Trans>Insufficient liquidity for this trade.</Trans>
</ThemedText.Main>
</GreyCard>
) : showApproveFlow ? (
<AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}>
<AutoColumn style={{ width: '100%' }} gap="12px">
<ButtonConfirmed
onClick={handleApprove}
disabled={
approvalState !== ApprovalState.NOT_APPROVED ||
approvalSubmitted ||
signatureState === UseERC20PermitState.SIGNED
}
width="100%"
altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
confirmed={
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED
}
>
<AutoRow justify="space-between" style={{ flexWrap: 'nowrap' }}>
<span style={{ display: 'flex', alignItems: 'center' }}>
<CurrencyLogo
currency={currencies[Field.INPUT]}
size={'20px'}
style={{ marginRight: '8px', flexShrink: 0 }}
/>
{/* we need to shorten this string on mobile */}
{approvalState === ApprovalState.APPROVED ||
signatureState === UseERC20PermitState.SIGNED ? (
<Trans>You can now trade {currencies[Field.INPUT]?.symbol}</Trans>
) : (
<Trans>Allow the Uniswap Protocol to use your {currencies[Field.INPUT]?.symbol}</Trans>
)}
</span>
{approvalState === ApprovalState.PENDING ? (
<Loader stroke="white" />
) : (approvalSubmitted && approvalState === ApprovalState.APPROVED) ||
signatureState === UseERC20PermitState.SIGNED ? (
<CheckCircle size="20" color={theme.green1} />
) : (
<Trans>Allow the Uniswap Protocol to use your {currencies[Field.INPUT]?.symbol}</Trans>
<MouseoverTooltip
text={
<Trans>
You must give the Uniswap smart contracts permission to use your{' '}
{currencies[Field.INPUT]?.symbol}. You only have to do this once per token.
</Trans>
}
>
<HelpCircle size="20" color={'white'} style={{ marginLeft: '8px' }} />
</MouseoverTooltip>
)}
</span>
{approvalState === ApprovalState.PENDING ? (
<Loader stroke="white" />
) : (approvalSubmitted && approvalState === ApprovalState.APPROVED) ||
signatureState === UseERC20PermitState.SIGNED ? (
<CheckCircle size="20" color={theme.green1} />
) : (
<MouseoverTooltip
text={
<Trans>
You must give the Uniswap smart contracts permission to use your{' '}
{currencies[Field.INPUT]?.symbol}. You only have to do this once per token.
</Trans>
}
>
<HelpCircle size="20" color={'white'} style={{ marginLeft: '8px' }} />
</MouseoverTooltip>
)}
</AutoRow>
</ButtonConfirmed>
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
</AutoRow>
</ButtonConfirmed>
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
width="100%"
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
(approvalState !== ApprovalState.APPROVED && signatureState !== UseERC20PermitState.SIGNED) ||
priceImpactTooHigh
}
}}
width="100%"
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
(approvalState !== ApprovalState.APPROVED && signatureState !== UseERC20PermitState.SIGNED) ||
priceImpactTooHigh
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={500}>
{priceImpactTooHigh ? (
<Trans>High Price Impact</Trans>
) : trade && priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
</AutoColumn>
</AutoRow>
) : (
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={500}>
{priceImpactTooHigh ? (
<Trans>High Price Impact</Trans>
) : trade && priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
</AutoColumn>
</AutoRow>
) : (
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
id="swap-button"
disabled={!isValid || routeIsSyncing || routeIsLoading || priceImpactTooHigh || !!swapCallbackError}
error={isValid && priceImpactSeverity > 2 && !swapCallbackError}
>
<Text fontSize={20} fontWeight={500}>
{swapInputError ? (
swapInputError
) : routeIsSyncing || routeIsLoading ? (
<Trans>Swap</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
)}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
<AlertWrapper>
<NetworkAlert />
</AlertWrapper>
<SwitchLocaleLink />
{!swapIsUnsupported ? null : (
<UnsupportedCurrencyFooter
show={swapIsUnsupported}
currencies={[currencies[Field.INPUT], currencies[Field.OUTPUT]]}
/>
)}
</>
}}
id="swap-button"
disabled={!isValid || routeIsSyncing || routeIsLoading || priceImpactTooHigh || !!swapCallbackError}
error={isValid && priceImpactSeverity > 2 && !swapCallbackError}
>
<Text fontSize={20} fontWeight={500}>
{swapInputError ? (
swapInputError
) : routeIsSyncing || routeIsLoading ? (
<Trans>Swap</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
)}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
<AlertWrapper>
<NetworkAlert />
</AlertWrapper>
<SwitchLocaleLink />
{!swapIsUnsupported ? null : (
<UnsupportedCurrencyFooter
show={swapIsUnsupported}
currencies={[currencies[Field.INPUT], currencies[Field.OUTPUT]]}
/>
)}
</>
</Trace>
)
}
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