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