Compare commits

...

65 Commits

Author SHA1 Message Date
unipadmini
fc81c6e37d chore: Change font weights for headlines from normal -> medium. (#5193)
Co-authored-by: Padmini Pyapali <padminipyapali@Padminis-MacBook-Pro.local>
2022-11-13 19:37:21 -05:00
Charles Bachmeier
9d5e0701e7 fix: disable polling after scroll (#5191)
disable polling after scroll
2022-11-13 13:33:38 -05:00
lynn
66bdfd8a94 fix: only disable erc 1155 in sell mode (#5183)
* init

* make height into variable

* respond charlie

* cleaner code

* dont include erc 1155 when selecting all

* fix

* disable select all for sus flag nfts too

* oops
2022-11-12 00:03:19 -05:00
Zach Pomerantz
935694630b fix: do not poll when loading more assets (#5184) 2022-11-11 14:54:26 -08:00
Zach Pomerantz
f8399fd03c fix: enforce uniform asset height (#5181)
* fix: show loading assets at uniform height

* fix: enforce the uniform asset height

* fix: memoize assets

* fix: be more lenient with uniformHeight

* fix: simplify mapping
2022-11-11 17:29:57 -05:00
Jordan Frankfurt
429ade5b20 fix: add back the border-radius for swap tx conf (#5164)
add back the border-radius for swap tx conf
2022-11-11 16:13:49 -05:00
Zach Pomerantz
5c21dd9852 fix: defer useInterval until callback resolves (#5096)
* fix: defer useInterval until callback resolves

* fix: avoid refs in useInterval
2022-11-11 13:08:29 -08:00
aballerr
2ce5990f60 fix: fixing header on light mode (#5178)
* fixing header on light mode
2022-11-11 15:29:31 -05:00
Charles Bachmeier
d56851561b feat: mobile filters menu (#5163)
* header and scroll

* allow sweep buy now off

* generic filter row header

* begin building sort dropdown

* add file

* working checkmark

* remove icons

* updating scroll to work with mobile

* prevent scorlling behind menu

* hover styles

* remove console.log

* respond to comments

* revert null

* styled component header

* filter item styled component

* padding for items, slider, and inputs

* fixed scroll on mobile

Co-authored-by: Alex Ball <alex.ball@uniswap.org>
2022-11-11 15:01:39 -05:00
Zach Pomerantz
5325b5f8b4 fix: nft waterfalls requests (#5168)
* fix: request all sweep data in parallel

* fix: trigger collection query from a wrapping screen

* load sweep for correct markets

* add preload logic for assets query

* add load query to explore table

* fix: cleanup AssetFetcherParams

* fix: preload trending collections

* fix: graphql array argument

* fix: actually use preloaded asset query

* fix: use network and suspense to actually parallelize

Co-authored-by: Charlie <charles@bachmeier.io>
2022-11-11 14:55:09 -05:00
lynn
27936cf3f5 feat: image cropping / sizing on view my nfts (#5177)
init
2022-11-11 14:54:37 -05:00
Jordan Frankfurt
ff6f43d7aa fix(bag-footer): remove double border top and excess margin (#5171) 2022-11-11 13:34:32 -05:00
lynn
f1443671ef feat: Web 1854 listed card (#5160)
* init

* it's working with jack's card.tsx components

* add nft details on cards for view my nfts

* listed cards ready for review

* remove unnecessary code

* updated radius

* first round charlie comments

* respond all comments

* init

* fix

* color

* remove floor price when not on sell mode

* remove floor when nft is listed

* feat: Web 1858 disabled card 1155 when sell mode is on (#5169)

* disabled states + tooltips

* remove collection asset changes

* popover offset changes

* respond to padmini comment

* respond to charlie
2022-11-11 13:22:19 -05:00
Zach Pomerantz
a95697daf8 fix: initialize analytics outside of react lifecycle (#5173) 2022-11-11 09:47:37 -08:00
cartcrom
0835744006 feat: replaced protocol disclaimer with privacy policy link (#5170)
copy updates
2022-11-11 11:28:27 -05:00
Danny Daniil
f5df2fed09 feat: de-148- Analytics package integration (#5093)
* integrate analytics sdk

* roll back vscode

* remove dup imports

* use dummy api key and url from env

* update analytics sdk for optional init

* yarn deduped

* update documentation

* add analytics events package

* getBrowser from analytics events

* remove token banner

* add origin app

* replace local analytics

* upgrade events version

* events 1.0.4

* remove importToken
2022-11-11 11:05:57 -05:00
aballerr
5e7f6333b1 fix: Activity updates (#5165)
* listing and styling updates
2022-11-11 09:34:55 -05:00
Zach Pomerantz
2aa4ec6a38 fix: clear or retain amounts on token select (#5161)
* fix: clear or retain amounts on token select

* lint: add curlies
2022-11-10 16:01:37 -08:00
lynn
a70ef4326d feat: unlisted view my nft cards (#5129)
* init

* it's working with jack's card.tsx components

* add nft details on cards for view my nfts

* listed cards ready for review

* remove unnecessary code

* updated radius

* first round charlie comments

* respond all comments

* fix

* remove floor price when not on sell mode
2022-11-10 15:28:17 -05:00
Zach Pomerantz
6edc73784c fix: show 6 sigFigs for tx history (#5135) 2022-11-10 11:23:03 -08:00
aballerr
da6e13130b fix: Several Listing Cleanup Items (#5119)
* Improving view my nfts and listing
2022-11-10 12:39:37 -05:00
Jordan Frankfurt
4ef4ea8f58 fix: give a bunch of list renders keys (#5158)
* fix: give a bunch of list renders keys

* pr feedback

* pr feedback
2022-11-10 11:51:04 -05:00
vignesh mohankumar
4438818f38 fix: don't fetch tokens on empty search (#5155) 2022-11-10 11:35:35 -05:00
cartcrom
12eb337444 fix: one point price charts + added suspense (#5030)
* Used suspense for graph queries
* cleaned up unused code
* updated skeleton
* fixed zach's pr comments
* removed console.log
* throw error on missing token details address
2022-11-10 11:00:32 -05:00
cartcrom
44163f54b1 refactor: remove unused selector list/import components (#5145)
* removed unnused components
2022-11-10 10:57:18 -05:00
vignesh mohankumar
4b282d7813 fix: don't fetch collections without flag (#5154)
* better way

* check if value
2022-11-10 09:21:21 -05:00
vignesh mohankumar
f862a3f975 chore: add accentAction to sprinkles (#5146)
* fix: update Checkbox to accentActive

* revert checkbox

* one definition
2022-11-09 20:10:38 -05:00
Greg Bugyis
48d5955185 feat: Log NFT Sell events (#5106)
* Log profile page view

* Log sell flow started

* Add Start Listing event

* Add constant for list modal + useTrace

* Log sell item added

* Log listing completed

* Fix usd_value property

* Move log to startListingFlow

* Use Set to remove duplicate marketplaces

* Move listing completed event
2022-11-10 01:57:30 +02:00
Charles Bachmeier
dbf5c63ece refactor: remove graphql flag and default to gql endpoints (#5151)
* remove graphql flag and old endpoints

* remove unused queries

* deprecate old sell order type

* better null checks

* merge conflict
2022-11-09 18:15:40 -05:00
vignesh mohankumar
37d2603406 fix: accentAction for FilterButton (#5147)
* fix: accentAction for FilterButton

* update
2022-11-09 17:15:36 -05:00
aballerr
9bb1ca2970 fix: Hiding block on nft (#5148)
* hiding block # and gwei estimate on nft pages since it often is behind other react components
2022-11-09 16:15:52 -05:00
Charles Bachmeier
2abae0ee4c refactor: replace sweep query with gql call (#5143)
replace sweep query with gql call
2022-11-09 16:05:57 -05:00
vignesh mohankumar
9f8355ed7b refactor: useIsNftPage hooks (#5142) 2022-11-09 16:01:34 -05:00
Greg Bugyis
c5bed1c6fb style: Font weight on Trending Collections table (#5121)
* Drop classnames on Trending Table TD cells (no longer used)

* Fix font-weight in Trending Collections table collection name to match Figma

* There was a newer Figma

* Fixes

* Extend maxWidth on collection name (mobile)

* Mobile/desktop tweak

* Fix mobile size and add constants

* Make truncatedText a styled component
2022-11-09 22:42:52 +02:00
Jordan Frankfurt
1411a92146 feat(sell-bag): closing link sell bag open/close state with sell mode activation (#5128)
* stash resolve

* toggling bag open/close also toggles sell mode
2022-11-09 15:18:52 -05:00
Jordan Frankfurt
d016bdd87c fix(identicon): fix wrapper border-radius (#5144)
* fix(identicon): fix wrapper border-radius

* pr feedback
2022-11-09 14:56:25 -05:00
aballerr
491ae578ab feat: Mobile status bar (#5141)
* Updating mobile status bar to shrink (nft enabled)
2022-11-09 14:48:23 -05:00
vignesh mohankumar
1df685f31e fix: default to buy now disabled (#5140) 2022-11-09 11:52:56 -05:00
Charles Bachmeier
02aeb43e62 fix: don't pluralize Filter Button with 1 result (#5139)
* don't pluralize results with 1 result

* pass in count for profile page
2022-11-09 11:15:34 -05:00
aballerr
1d849927ef fix: fixing loader (#5138)
Fixing bug in loader that would not use correct stroke color
2022-11-09 11:11:18 -05:00
Charles Bachmeier
1893d258b5 feat: add marketplace and trait counts to assets query (#5137)
* working total count

* trait counts

* marketplace counts

* carousel card

* undo count refactor

* Filter styles

* remove any cast and handle 0
2022-11-09 10:44:42 -05:00
Charles Bachmeier
ed7f126bd0 fix: updates to wallet asset schemas (#5132)
* updates to wallet asset schemas

* update map type and market check

* much better syntax
2022-11-09 10:31:30 -05:00
Jordan Frankfurt
9a38c4e58d fix(bag): don't close when the user switches pages (#5133) 2022-11-08 18:47:01 -06:00
aballerr
99f3998941 fix: Several bugs on nft, see comments for clarity (#5116)
* a number of minor fixes related to nfts
2022-11-08 17:39:49 -05:00
vignesh mohankumar
30fa88e3af fix: show transparent box on empty banner (#5131)
* fix: show transparent box on empty banner

* div
2022-11-08 16:20:12 -05:00
vignesh mohankumar
d951172a81 chore: update nft grays (#5124)
* chore: update nft grays

* forgot to change some out

* prettier

* add 700
2022-11-08 16:15:05 -05:00
aballerr
eb35d3a2a0 fix: price range and details screen sizing (#5094)
* adjustments to screen sizing and updating to work better with graphql
2022-11-08 15:36:52 -05:00
Charles Bachmeier
87455fc096 fix: sort explore table null values (#5130) 2022-11-08 14:41:21 -05:00
Greg Bugyis
054d92cb9c style: Fix Trending Table filter borders on Safari (mobile & desktop) (#5122) 2022-11-08 20:13:34 +02:00
Charles Bachmeier
36109a1fe7 refactor: update nft gql fetcher endpoint (#5126)
update gql fetcher for prod endpoint
2022-11-08 13:11:25 -05:00
Charles Bachmeier
8f8fe9ddad fix: lowercase markets for gql profile cards (#5127)
lowercase markets for gql profile cards
2022-11-08 13:11:00 -05:00
Charles Bachmeier
2b279e00f9 feat: virtual containers for collection pages (#5125)
* use fixedsizeList only

* add autosizer comments

* undo asset testing change

* init

* cleanup

* scrollbar styles

* scrollbar styles

Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2022-11-08 12:15:21 -05:00
Jordan Frankfurt
9f5c588bdd feat(sell-bag): patch open/close behavior (#5107)
keep bag link in nav bar at all times
2022-11-08 10:14:00 -06:00
Charles Bachmeier
415b3a1548 fix: add more null checks for Asset gql query (#5123)
add more null checks for Asset gql query
2022-11-08 10:54:11 -05:00
Charles Bachmeier
4e7b8264c3 refactor: Wallet Assets GraphQL Query (#5053)
* begin nft balance integration

* inf scroll works

* working list

* update comment

* connect collection filters

* use lazyload

* update schema

* working with new schema

* details for nfs assets

* cleanup

* more null checks

* unique index for loading cards

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-11-08 09:02:39 -05:00
lynn
0ef6d1625a fix: collection stat line fixes (#5112)
fix
2022-11-07 19:45:23 -05:00
yyip-dev
0258460821 refactor: remove token promo banner (#5114)
* Remove token promo banner

* Remove unused variable and import

Co-authored-by: Yannie Yip <yannie@UNISWAP-MAC-072.local>
2022-11-07 19:42:03 -05:00
Zach Pomerantz
2246afcefb fix: only poll every 12s (#5110)
* fix: only poll every 12s

* refactor: clarify polling interval

* doc: polling interval

* docs: further clarification

* fix: ctor super first
2022-11-07 15:35:31 -08:00
vignesh mohankumar
e0767b1cb7 build: Revert "build: deploy on friday AMs" (#5113)
Revert "build: deploy on friday AMs (#5085)"

This reverts commit a5cb1f05dc.
2022-11-07 18:34:03 -05:00
lynn
15dd02fe6a refactor: filter panel (#5103)
* init

* remove unnecessary chagnes

* fix comments
2022-11-07 17:13:08 -05:00
lynn
562a386de7 fix: release phase 1 nav bar (#5111)
* init

* just release phase 1 nav bar

* oops
2022-11-07 16:16:06 -05:00
vignesh mohankumar
99bea34f14 chore: add new background to theme (#5109)
* chore: add new background to theme

* update snapshot
2022-11-07 16:05:57 -05:00
Greg Bugyis
58f1c6ff84 fix: Event property fixes (#5108)
* Fix usd value on NFT buy bag success

* Fix bag quantity property on nft_buy_bag_changed
2022-11-07 19:35:48 +02:00
Jordan Frankfurt
b2481d6ba8 fix(asset-details): if an asset has no listings, don't try to render them (#5088)
* fix(asset-details): if an asset has no listings, don't try to render them

* add todo
2022-11-07 10:22:03 -06:00
Zach Pomerantz
eaa9b51913 feat: nft polling (#5083)
* feat: nft polling

* docs: document the poll

* chore: add todo for cursor tracking

* fix: poll all pages

* 5s polling

Co-authored-by: Charlie <charles@bachmeier.io>
2022-11-07 07:28:38 -08:00
198 changed files with 3226 additions and 3882 deletions

View File

@@ -1,7 +1,7 @@
name: Release
on:
schedule:
- cron: '0 12 * * 1-5' # every day 12:00 UTC Monday-Friday
- cron: '0 12 * * 1-4' # every day 12:00 UTC Monday-Thursday
# manual trigger
workflow_dispatch:

View File

@@ -16,4 +16,4 @@
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}

View File

@@ -116,7 +116,6 @@
"typescript": "^4.4.3"
},
"dependencies": {
"@amplitude/analytics-browser": "^1.1.4",
"@coinbase/wallet-sdk": "^3.3.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
@@ -132,6 +131,8 @@
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@types/react-relay": "^13.0.2",
"@uniswap/analytics": "1.0.3",
"@uniswap/analytics-events": "1.0.4",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",

View File

@@ -1,119 +1,120 @@
<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
<!--
%PUBLIC_URL% will be replaced with the URL of the `public` folder during build.
Only files inside the `public` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/images/512x512_App_Icon.png" />
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com 'unsafe-inline'" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com 'unsafe-inline'"
/>
<!--
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://www.google-analytics.com/" />
<link rel="preconnect" href="https://www.google-analytics.com/" />
<link rel="preload" href="%PUBLIC_URL%/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="%PUBLIC_URL%/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(%PUBLIC_URL%/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(%PUBLIC_URL%/fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(%PUBLIC_URL%/fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(%PUBLIC_URL%/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(%PUBLIC_URL%/fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(%PUBLIC_URL%/fonts/Inter-roman.var.woff2) format('woff2');
}
}
html,
body {
margin: 0;
padding: 0;
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
button {
user-select: none;
}
html,
body {
margin: 0;
padding: 0;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
button {
user-select: none;
}
#background-radial-gradient {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background-color: #212429;
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
#background-radial-gradient {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
}
@media (prefers-color-scheme: light) {
html {
background-color: #f7f8fa;
min-height: 100%;
}
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@media (prefers-color-scheme: dark) {
html {
background-color: #212429;
}
}
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
@media (prefers-color-scheme: light) {
html {
background-color: #f7f8fa;
}
}
</style>
</head>
<div id="background-radial-gradient"></div>
</body>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
</html>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>

View File

@@ -27,4 +27,4 @@
"short_name": "Uniswap",
"start_url": ".",
"theme_color": "#FC72FFs"
}
}

View File

@@ -1,76 +0,0 @@
import { createContext, memo, PropsWithChildren, useContext, useEffect, useMemo } from 'react'
import { sendAnalyticsEvent } from '.'
import { ElementName, EventName, ModalName, PageName, SectionName } from './constants'
export interface ITraceContext {
// Highest order context: eg Swap or Explore.
page?: PageName
// 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.
element?: ElementName
}
export const TraceContext = createContext<ITraceContext>({})
export function useTrace(trace?: ITraceContext): ITraceContext {
const parentTrace = useContext(TraceContext)
return useMemo(() => ({ ...parentTrace, ...trace }), [parentTrace, trace])
}
type TraceProps = {
shouldLogImpression?: boolean // whether to log impression on mount
name?: EventName
properties?: Record<string, unknown>
} & ITraceContext
/**
* Sends an analytics event on mount (if shouldLogImpression is set),
* and propagates the context to child traces.
*/
export const Trace = memo(
({
shouldLogImpression,
name,
children,
page,
section,
modal,
element,
properties,
}: PropsWithChildren<TraceProps>) => {
const parentTrace = useTrace()
const combinedProps = useMemo(
() => ({
...parentTrace,
...Object.fromEntries(Object.entries({ page, section, modal, element }).filter(([_, v]) => v !== undefined)),
}),
[element, parentTrace, page, modal, section]
)
useEffect(() => {
if (shouldLogImpression) {
const commitHash = process.env.REACT_APP_GIT_COMMIT_HASH
sendAnalyticsEvent(name ?? EventName.PAGE_VIEWED, {
...combinedProps,
...properties,
git_commit_hash: commitHash,
})
}
// Impressions should only be logged on mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return <TraceContext.Provider value={combinedProps}>{children}</TraceContext.Provider>
}
)
Trace.displayName = 'Trace'

View File

@@ -1,75 +0,0 @@
import { Children, cloneElement, isValidElement, memo, PropsWithChildren, SyntheticEvent } from 'react'
import { sendAnalyticsEvent } from '.'
import { Event, EventName } from './constants'
import { ITraceContext, Trace, TraceContext } from './Trace'
type TraceEventProps = {
events: Event[]
name: EventName
properties?: Record<string, unknown>
shouldLogImpression?: boolean
} & ITraceContext
/**
* Analytics instrumentation component that wraps event callbacks with logging logic.
*
* @example
* <TraceEvent events={[Event.onClick]} element={ElementName.SWAP_BUTTON}>
* <Button onClick={() => console.log('clicked')}>Click me</Button>
* </TraceEvent>
*/
export const TraceEvent = memo((props: PropsWithChildren<TraceEventProps>) => {
const { shouldLogImpression, name, properties, events, children, ...traceProps } = props
return (
<Trace {...traceProps}>
<TraceContext.Consumer>
{(traceContext) =>
Children.map(children, (child) => {
if (!isValidElement(child)) {
return child
}
// For each child, augment event handlers defined in `events` with event tracing.
return cloneElement(
child,
getEventHandlers(child, traceContext, events, name, properties, shouldLogImpression)
)
})
}
</TraceContext.Consumer>
</Trace>
)
})
TraceEvent.displayName = 'TraceEvent'
/**
* Given a set of child element and event props, returns a spreadable
* object of the event handlers augmented with analytics logging.
*/
function getEventHandlers(
child: React.ReactElement,
traceContext: ITraceContext,
events: Event[],
name: EventName,
properties?: Record<string, unknown>,
shouldLogImpression = true
) {
const eventHandlers: Partial<Record<Event, (e: SyntheticEvent<Element, Event>) => void>> = {}
for (const event of events) {
eventHandlers[event] = (eventHandlerArgs: unknown) => {
// call child event handler with original arguments, must be in array
const args = Array.isArray(eventHandlerArgs) ? eventHandlerArgs : [eventHandlerArgs]
child.props[event]?.apply(child, args)
// augment handler with analytics logging
if (shouldLogImpression) sendAnalyticsEvent(name, { ...traceContext, ...properties, action: event })
}
}
// return a spreadable event handler object
return eventHandlers
}

View File

@@ -1,177 +0,0 @@
/**
* Event names that can occur in this application.
*
* Subject to change as new features are added and new events are defined
* and logged.
*/
export enum EventName {
APP_LOADED = 'Application Loaded',
APPROVE_TOKEN_TXN_SUBMITTED = 'Approve Token Transaction Submitted',
CONNECT_WALLET_BUTTON_CLICKED = 'Connect Wallet Button Clicked',
EXPLORE_BANNER_CLICKED = 'Explore Banner Clicked',
EXPLORE_SEARCH_SELECTED = 'Explore Search Selected',
EXPLORE_TOKEN_ROW_CLICKED = 'Explore Token Row Clicked',
PAGE_VIEWED = 'Page Viewed',
NAVBAR_RESULT_SELECTED = 'Navbar Result Selected',
NAVBAR_SEARCH_SELECTED = 'Navbar Search Selected',
NAVBAR_SEARCH_EXITED = 'Navbar Search Exited',
NFT_ACTIVITY_SELECTED = 'NFT Activity Selected',
NFT_BUY_ADDED = 'NFT Buy Bag Added',
NFT_BUY_BAG_CHANGED = 'NFT Buy Bag Changed',
NFT_BUY_BAG_PAY = 'NFT Buy Bag Pay Clicked',
NFT_BUY_BAG_REFUNDED = 'NFT Buy Bag Refunded',
NFT_BUY_BAG_SIGNED = 'NFT Buy Bag Signed',
NFT_BUY_BAG_SUCCEEDED = 'NFT Buy Bag Succeeded',
NFT_FILTER_OPENED = 'NFT Collection Filter Opened',
NFT_FILTER_SELECTED = 'NFT Filter Selected',
NFT_TRENDING_ROW_SELECTED = 'Trending Row Selected',
SWAP_AUTOROUTER_VISUALIZATION_EXPANDED = 'Swap Autorouter Visualization Expanded',
SWAP_DETAILS_EXPANDED = 'Swap Details Expanded',
SWAP_MAX_TOKEN_AMOUNT_SELECTED = 'Swap Max Token Amount Selected',
SWAP_PRICE_UPDATE_ACKNOWLEDGED = 'Swap Price Update Acknowledged',
SWAP_QUOTE_RECEIVED = 'Swap Quote Received',
SWAP_SIGNED = 'Swap Signed',
SWAP_SUBMITTED_BUTTON_CLICKED = 'Swap Submit Button Clicked',
SWAP_TOKENS_REVERSED = 'Swap Tokens Reversed',
SWAP_TRANSACTION_COMPLETED = 'Swap Transaction Completed',
TOKEN_IMPORTED = 'Token Imported',
TOKEN_SELECTED = 'Token Selected',
TOKEN_SELECTOR_OPENED = 'Token Selector Opened',
WALLET_CONNECT_TXN_COMPLETED = 'Wallet Connect Transaction Completed',
WALLET_SELECTED = 'Wallet Selected',
WEB_VITALS = 'Web Vitals',
WRAP_TOKEN_TXN_INVALIDATED = 'Wrap Token Transaction Invalidated',
WRAP_TOKEN_TXN_SUBMITTED = 'Wrap Token Transaction Submitted',
// alphabetize additional event names.
}
export enum CUSTOM_USER_PROPERTIES {
ALL_WALLET_ADDRESSES_CONNECTED = 'all_wallet_addresses_connected',
ALL_WALLET_CHAIN_IDS = 'all_wallet_chain_ids',
USER_AGENT = 'user_agent',
BROWSER = 'browser',
DARK_MODE = 'is_dark_mode',
EXPERT_MODE = 'is_expert_mode',
SCREEN_RESOLUTION_HEIGHT = 'screen_resolution_height',
SCREEN_RESOLUTION_WIDTH = 'screen_resolution_width',
WALLET_ADDRESS = 'wallet_address',
WALLET_TYPE = 'wallet_type',
}
export enum BROWSER {
FIREFOX = 'Mozilla Firefox',
SAMSUNG = 'Samsung Internet',
OPERA = 'Opera',
INTERNET_EXPLORER = 'Microsoft Internet Explorer',
EDGE = 'Microsoft Edge (Legacy)',
EDGE_CHROMIUM = 'Microsoft Edge (Chromium)',
CHROME = 'Google Chrome or Chromium',
SAFARI = 'Apple Safari',
BRAVE = 'Brave',
UNKNOWN = 'unknown',
}
export enum WALLET_CONNECTION_RESULT {
SUCCEEDED = 'Succeeded',
FAILED = 'Failed',
}
export enum SWAP_PRICE_UPDATE_USER_RESPONSE {
ACCEPTED = 'Accepted',
REJECTED = 'Rejected',
}
/**
* Known pages in the app. Highest order context.
*/
export enum PageName {
NFT_COLLECTION_PAGE = 'nft-collection-page',
NFT_DETAILS_PAGE = 'nft-details-page',
NFT_EXPLORE_PAGE = 'nft-explore-page',
TOKEN_DETAILS_PAGE = 'token-details',
TOKENS_PAGE = 'tokens-page',
POOL_PAGE = 'pool-page',
SWAP_PAGE = 'swap-page',
VOTE_PAGE = 'vote-page',
// alphabetize additional page names.
}
/**
* Sections. Disambiguates low-level elements that may share a name.
* eg a `back` button in a modal will have the same `element`,
* but a different `section`.
*/
export enum SectionName {
CURRENCY_INPUT_PANEL = 'swap-currency-input',
CURRENCY_OUTPUT_PANEL = 'swap-currency-output',
NAVBAR_SEARCH = 'Navbar Search',
WIDGET = 'widget',
// alphabetize additional section names.
}
/** Known modals for analytics purposes. */
export enum ModalName {
CONFIRM_SWAP = 'confirm-swap-modal',
NFT_TX_COMPLETE = 'nft-tx-complete-modal',
TOKEN_SELECTOR = 'token-selector-modal',
// alphabetize additional modal names.
}
/**
* Known element names for analytics purposes.
* Use to identify low-level components given a TraceContext
*/
export enum ElementName {
AUTOROUTER_VISUALIZATION_ROW = 'expandable-autorouter-visualization-row',
COMMON_BASES_CURRENCY_BUTTON = 'common-bases-currency-button',
CONFIRM_SWAP_BUTTON = 'confirm-swap-or-send',
CONNECT_WALLET_BUTTON = 'connect-wallet-button',
EXPLORE_BANNER = 'explore-banner',
EXPLORE_SEARCH_INPUT = 'explore_search_input',
IMPORT_TOKEN_BUTTON = 'import-token-button',
MAX_TOKEN_AMOUNT_BUTTON = 'max-token-amount-button',
NAVBAR_SEARCH_INPUT = 'navbar-search-input',
NFT_ACTIVITY_TAB = 'nft-activity-tab',
NFT_BUY_BAG_PAY_BUTTON = 'nft-buy-bag-pay-button',
NFT_FILTER_BUTTON = 'nft-filter-button',
NFT_FILTER_OPTION = 'nft-filter-option',
NFT_TRENDING_ROW = 'nft-trending-row',
PRICE_UPDATE_ACCEPT_BUTTON = 'price-update-accept-button',
SWAP_BUTTON = 'swap-button',
SWAP_DETAILS_DROPDOWN = 'swap-details-dropdown',
SWAP_TOKENS_REVERSE_ARROW_BUTTON = 'swap-tokens-reverse-arrow-button',
TOKEN_SELECTOR_ROW = 'token-selector-row',
WALLET_TYPE_OPTION = 'wallet-type-option',
// alphabetize additional element names.
}
/**
* Known events that trigger callbacks.
* @example
* <TraceEvent events={[Event.onClick]} element={name}>
*/
export enum Event {
onClick = 'onClick',
onFocus = 'onFocus',
onKeyPress = 'onKeyPress',
onSelect = 'onSelect',
// alphabetize additional events.
}
/** Known navbar search result types */
export enum NavBarSearchTypes {
COLLECTION_SUGGESTION = 'collection-suggestion',
COLLECTION_TRENDING = 'collection-trending',
RECENT_SEARCH = 'recent',
TOKEN_SUGGESTION = 'token-suggestion',
TOKEN_TRENDING = 'token-trending',
}
/**
* Known Filter Types for NFTs
*/
export enum FilterTypes {
MARKETPLACE = 'Marketplace',
PRICE_RANGE = 'Price Range',
TRAIT = 'Trait',
}

View File

@@ -1,90 +0,0 @@
import { Identify, identify, init, track } from '@amplitude/analytics-browser'
import { isProductionEnv } from 'utils/env'
const DUMMY_KEY = '00000000000000000000000000000000'
const PROXY_URL = process.env.REACT_APP_AMPLITUDE_PROXY_URL
/**
* Initializes Amplitude SDK and configures it to send events to a Uniswap reverse proxy,
* which relays to events to relevant Amplitude endpoints. You must be a
* member of the organization on Amplitude to view logged events.
*/
export function initializeAnalytics() {
if (typeof PROXY_URL === 'undefined') {
console.error('REACT_APP_AMPLITUDE_PROXY_URL is undefined, Amplitude analytics will not run.')
return
}
init(
DUMMY_KEY,
/* userId= */ undefined, // User ID should be undefined to let Amplitude default to Device ID
/* options= */
{
// Configure the SDK to work with alternate endpoint
serverUrl: PROXY_URL,
// Disable tracking of private user information by Amplitude
trackingOptions: {
ipAddress: false,
carrier: false,
city: false,
region: false,
dma: false, // designated market area
},
}
)
}
/** Sends an event to Amplitude. */
export function sendAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
if (!PROXY_URL) {
console.log(`[analytics(${eventName})]: ${JSON.stringify(eventProperties)}`)
return
}
track(eventName, { ...eventProperties, origin })
}
type Value = string | number | boolean | string[] | number[]
/**
* Class that exposes methods to mutate the User Model's properties in
* Amplitude that represents the current session's user.
*
* See https://help.amplitude.com/hc/en-us/articles/115002380567-User-properties-and-event-properties
* for details.
*/
class UserModel {
private log(method: string, ...parameters: unknown[]) {
console.debug(`[amplitude(Identify)]: ${method}(${parameters})`)
}
private call(mutate: (event: Identify) => Identify) {
if (!isProductionEnv()) {
const log = (_: Identify, method: string) => this.log.bind(this, method)
mutate(new Proxy(new Identify(), { get: log }))
return
}
identify(mutate(new Identify()))
}
set(key: string, value: Value) {
this.call((event) => event.set(key, value))
}
setOnce(key: string, value: Value) {
this.call((event) => event.setOnce(key, value))
}
add(key: string, value: number) {
this.call((event) => event.add(key, value))
}
postInsert(key: string, value: string | number) {
this.call((event) => event.postInsert(key, value))
}
remove(key: string, value: string | number) {
this.call((event) => event.remove(key, value))
}
}
export const user = new UserModel()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -57,7 +57,6 @@ const FailedText = ({ transactionState }: { transactionState: TransactionState }
const FormattedCurrencyAmount = ({
rawAmount,
currencyId,
sigFigs = 2,
}: {
rawAmount: string
currencyId: string
@@ -67,7 +66,7 @@ const FormattedCurrencyAmount = ({
return currency ? (
<HighlightText>
{formatAmount(rawAmount, currency.decimals, sigFigs)} {currency.symbol}
{formatAmount(rawAmount, currency.decimals, /* sigFigs= */ 6)} {currency.symbol}
</HighlightText>
) : null
}

View File

@@ -14,15 +14,15 @@ export const LightCard = styled(Card)`
background-color: ${({ theme }) => theme.deprecated_bg1};
`
export const LightGreyCard = styled(Card)`
export const LightGrayCard = styled(Card)`
background-color: ${({ theme }) => theme.deprecated_bg2};
`
export const GreyCard = styled(Card)`
export const GrayCard = styled(Card)`
background-color: ${({ theme }) => theme.deprecated_bg3};
`
export const DarkGreyCard = styled(Card)`
export const DarkGrayCard = styled(Card)`
background-color: ${({ theme }) => theme.deprecated_bg2};
`

View File

@@ -1,7 +1,7 @@
import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
import { curveCardinal, scaleLinear } from 'd3'
import { PricePoint } from 'graphql/data/TokenPrice'
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util'
import { memo } from 'react'
import styled, { useTheme } from 'styled-components/macro'

View File

@@ -0,0 +1,31 @@
import { css } from 'styled-components/macro'
export const ScrollBarStyles = css<{ $isHorizontalScroll?: boolean }>`
// Firefox scrollbar styling
scrollbar-width: thin;
scrollbar-color: ${({ theme }) => `${theme.backgroundOutline} transparent`};
height: 100%;
// safari and chrome scrollbar styling
::-webkit-scrollbar {
background: transparent;
// Set height for horizontal scrolls
${({ $isHorizontalScroll }) => {
return $isHorizontalScroll
? css`
height: 4px;
overflow-x: scroll;
`
: css`
width: 4px;
overflow-y: scroll;
`
}}
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) => theme.backgroundOutline};
border-radius: 8px;
}
`

View File

@@ -1,9 +1,9 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import { isSupportedChain } from 'constants/chains'
@@ -325,7 +325,7 @@ export default function SwapCurrencyInputPanel({
</ThemedText.DeprecatedBody>
{showMaxButton && selectedCurrencyBalance ? (
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
>

View File

@@ -1,9 +1,9 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import { isSupportedChain } from 'constants/chains'
@@ -314,7 +314,7 @@ export default function CurrencyInputPanel({
</ThemedText.DeprecatedBody>
{showMaxButton && selectedCurrencyBalance ? (
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
>

View File

@@ -1,6 +1,5 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { NftGraphQlVariant, useNftGraphQlFlag } from 'featureFlags/flags/nftGraphQl'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
@@ -205,12 +204,6 @@ export default function FeatureFlagModal() {
</Header>
<FeatureFlagGroup name="Phase 1">
<FeatureFlagOption variant={NftVariant} value={useNftFlag()} featureFlag={FeatureFlag.nft} label="NFTs" />
<FeatureFlagOption
variant={NftGraphQlVariant}
value={useNftGraphQlFlag()}
featureFlag={FeatureFlag.nftGraphQl}
label="NFT GraphQL Endpoints"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption

View File

@@ -9,7 +9,7 @@ import sockImg from '../../assets/svg/socks.svg'
import { useHasSocks } from '../../hooks/useSocksBalance'
import Identicon from '../Identicon'
const IconWrapper = styled.div<{ size?: number }>`
export const IconWrapper = styled.div<{ size?: number }>`
position: relative;
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;

View File

@@ -1,13 +1,13 @@
import jazzicon from '@metamask/jazzicon'
import { useWeb3React } from '@web3-react/core'
import useENSAvatar from 'hooks/useENSAvatar'
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components/macro'
const StyledIdenticon = styled.div<{ iconSize: number }>`
height: ${({ iconSize }) => `${iconSize}px`};
width: ${({ iconSize }) => `${iconSize}px`};
border-radius: 1.125rem;
border-radius: 50%;
background-color: ${({ theme }) => theme.deprecated_bg4};
font-size: initial;
`
@@ -41,10 +41,12 @@ export default function Identicon({ size }: { size?: number }) {
return
}, [icon, iconRef])
const handleError = useCallback(() => setFetchable(false), [])
return (
<StyledIdenticon iconSize={iconSize}>
{avatar && fetchable ? (
<StyledAvatar alt="avatar" src={avatar} onError={() => setFetchable(false)}></StyledAvatar>
<StyledAvatar alt="avatar" src={avatar} onError={handleError}></StyledAvatar>
) : (
<span ref={iconRef} />
)}

View File

@@ -1,28 +0,0 @@
import React from 'react'
import styled from 'styled-components/macro'
import useHttpLocations from '../../hooks/useHttpLocations'
import Logo from '../Logo'
const StyledListLogo = styled(Logo)<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
`
export default function ListLogo({
logoURI,
style,
size = '24px',
alt,
symbol,
}: {
logoURI: string
size?: string
style?: React.CSSProperties
alt?: string
symbol?: string
}) {
const srcs: string[] = useHttpLocations(logoURI)
return <StyledListLogo alt={alt} size={size} symbol={symbol} srcs={srcs} style={style} />
}

View File

@@ -14,7 +14,7 @@ const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
height: ${({ size }) => size};
width: ${({ size }) => size};
path {
stroke: ${({ stroke, theme }) => theme.accentActive};
stroke: ${({ stroke, theme }) => stroke ?? theme.accentActive};
}
`

View File

@@ -1,13 +1,11 @@
// eslint-disable-next-line no-restricted-imports
import { t } from '@lingui/macro'
import { sendAnalyticsEvent } from 'analytics'
import { ElementName, Event, EventName, SectionName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { useTrace } from 'analytics/Trace'
import { TraceEvent } from 'analytics/TraceEvent'
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, SectionName } from '@uniswap/analytics-events'
import clsx from 'clsx'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import useDebounce from 'hooks/useDebounce'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { organizeSearchResults } from 'lib/utils/searchBar'
import { Box } from 'nft/components/Box'
@@ -47,6 +45,7 @@ export const SearchBar = () => {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!debouncedSearchValue.length && phase1Flag === NftVariant.Enabled,
}
)
@@ -57,10 +56,11 @@ export const SearchBar = () => {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!debouncedSearchValue.length,
}
)
const isNFTPage = pathname.includes('/nfts')
const isNFTPage = useIsNftPage()
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? [])
@@ -137,7 +137,7 @@ export const SearchBar = () => {
</Box>
</Box>
<TraceEvent
events={[Event.onFocus]}
events={[BrowserEvent.onFocus]}
name={EventName.NAVBAR_SEARCH_SELECTED}
element={ElementName.NAVBAR_SEARCH_INPUT}
properties={{ ...trace }}

View File

@@ -1,7 +1,8 @@
import { Trans } from '@lingui/macro'
import { NavBarSearchTypes, SectionName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import { useTrace } from '@uniswap/analytics'
import { NavBarSearchTypes, SectionName } from '@uniswap/analytics-events'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { subheadSmall } from 'nft/css/common.css'
@@ -47,7 +48,7 @@ export const SearchBarDropdownSection = ({
}: SearchBarDropdownSectionProps) => {
return (
<Column gap="12">
<Row paddingX="16" paddingY="4" gap="8" color="grey300" className={subheadSmall} style={{ lineHeight: '20px' }}>
<Row paddingX="16" paddingY="4" gap="8" color="gray300" className={subheadSmall} style={{ lineHeight: '20px' }}>
{headerIcon ? headerIcon : null}
<Box>{header}</Box>
</Row>
@@ -113,7 +114,7 @@ export const SearchBarDropdown = ({
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
const { pathname } = useLocation()
const isNFTPage = pathname.includes('/nfts')
const isNFTPage = useIsNftPage()
const isTokenPage = pathname.includes('/tokens')
const phase1Flag = useNftFlag()
const [resultsState, setResultsState] = useState<ReactNode>()

View File

@@ -1,46 +1,46 @@
import { NavIcon } from 'components/NavBar/NavIcon'
import * as styles from 'components/NavBar/ShoppingBag.css'
import { Box } from 'nft/components/Box'
import { BagIcon, HundredsOverflowIcon, TagIcon } from 'nft/components/icons'
import { BagIcon, HundredsOverflowIcon } from 'nft/components/icons'
import { useBag, useSellAsset } from 'nft/hooks'
import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useCallback, useEffect, useState } from 'react'
import shallow from 'zustand/shallow'
export const ShoppingBag = () => {
const itemsInBag = useBag((state) => state.itemsInBag)
const sellAssets = useSellAsset((state) => state.sellAssets)
const [bagQuantity, setBagQuantity] = useState(0)
const [sellQuantity, setSellQuantity] = useState(0)
const location = useLocation()
const toggleBag = useBag((s) => s.toggleBag)
const { bagExpanded, setBagExpanded } = useBag(
({ bagExpanded, setBagExpanded }) => ({ bagExpanded, setBagExpanded }),
shallow
)
const { isSellMode, resetSellAssets, setIsSellMode } = useSellAsset(
({ isSellMode, reset, setIsSellMode }) => ({
isSellMode,
resetSellAssets: reset,
setIsSellMode,
}),
shallow
)
const handleIconClick = useCallback(() => {
if (isSellMode && bagExpanded) {
resetSellAssets()
setIsSellMode(false)
}
setBagExpanded({ bagExpanded: !bagExpanded })
}, [bagExpanded, isSellMode, resetSellAssets, setBagExpanded, setIsSellMode])
useEffect(() => {
setBagQuantity(itemsInBag.length)
}, [itemsInBag])
useEffect(() => {
setSellQuantity(sellAssets.length)
}, [sellAssets])
const isProfilePage = location.pathname === '/nfts/profile'
const bagHasItems = bagQuantity > 0
return (
<NavIcon onClick={toggleBag}>
{isProfilePage ? (
<>
<TagIcon viewBox="0 0 20 20" width={24} height={24} />
{sellQuantity ? (
<Box className={styles.bagQuantity}>{sellQuantity > 99 ? <HundredsOverflowIcon /> : sellQuantity}</Box>
) : null}
</>
) : (
<>
<BagIcon viewBox="0 0 20 20" width={24} height={24} />
{bagQuantity ? (
<Box className={styles.bagQuantity}>{bagQuantity > 99 ? <HundredsOverflowIcon /> : bagQuantity}</Box>
) : null}
</>
<NavIcon onClick={handleIconClick}>
<BagIcon viewBox="0 0 20 20" width={24} height={24} />
{bagHasItems && (
<Box className={styles.bagQuantity}>{bagQuantity > 99 ? <HundredsOverflowIcon /> : bagQuantity}</Box>
)}
</NavIcon>
)

View File

@@ -1,6 +1,6 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import clsx from 'clsx'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'

View File

@@ -3,10 +3,12 @@ import { useWeb3React } from '@web3-react/core'
import Web3Status from 'components/Web3Status'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { chainIdToBackendName } from 'graphql/data/util'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { UniIcon } from 'nft/components/icons'
import { ReactNode } from 'react'
import { useIsMobile } from 'nft/hooks'
import { ReactNode, useMemo } from 'react'
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom'
import { ChainSelector } from './ChainSelector'
@@ -48,6 +50,8 @@ const PageTabs = () => {
pathname.startsWith('/increase') ||
pathname.startsWith('/find')
const isNftPage = useIsNftPage()
return (
<>
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
@@ -57,7 +61,7 @@ const PageTabs = () => {
<Trans>Tokens</Trans>
</MenuItem>
{nftFlag === NftVariant.Enabled && (
<MenuItem href="/nfts" isActive={pathname.startsWith('/nfts')}>
<MenuItem href="/nfts" isActive={isNftPage}>
<Trans>NFTs</Trans>
</MenuItem>
)}
@@ -68,9 +72,29 @@ const PageTabs = () => {
)
}
const Navbar = () => {
const useShouldHideNavbar = () => {
const { pathname } = useLocation()
const isNftPage = pathname.startsWith('/nfts')
const isMobile = useIsMobile()
const shouldHideNavbar = useMemo(() => {
const paths = ['/nfts/profile']
if (!isMobile) return false
for (const path of paths) {
if (pathname.includes(path)) return true
}
return false
}, [isMobile, pathname])
return shouldHideNavbar
}
const Navbar = () => {
const shouldHideNavbar = useShouldHideNavbar()
const isNftPage = useIsNftPage()
if (shouldHideNavbar) return null
return (
<>

View File

@@ -4,6 +4,7 @@ import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import useGasPrice from 'hooks/useGasPrice'
import { useIsNftPage } from 'hooks/useIsNftPage'
import useMachineTimeMs from 'hooks/useMachineTime'
import JSBI from 'jsbi'
import useBlockNumber from 'lib/hooks/useBlockNumber'
@@ -106,6 +107,7 @@ export default function Polling() {
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
const blockTime = useCurrentBlockTimestamp()
const theme = useTheme()
const isNftPage = useIsNftPage()
const ethGasPrice = useGasPrice()
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
@@ -135,7 +137,7 @@ export default function Polling() {
//TODO - chainlink gas oracle is really slow. Can we get a better data source?
return (
return isNftPage ? null : (
<>
<RowFixed>
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} warning={warning}>

View File

@@ -1,7 +1,7 @@
import { Options, Placement } from '@popperjs/core'
import Portal from '@reach/portal'
import useInterval from 'lib/hooks/useInterval'
import React, { useCallback, useMemo, useState } from 'react'
import React, { CSSProperties, useCallback, useMemo, useState } from 'react'
import { usePopper } from 'react-popper'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
@@ -76,9 +76,20 @@ export interface PopoverProps {
show: boolean
children: React.ReactNode
placement?: Placement
offsetX?: number
offsetY?: number
style?: CSSProperties
}
export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) {
export default function Popover({
content,
show,
children,
placement = 'auto',
offsetX = 8,
offsetY = 8,
style,
}: PopoverProps) {
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
@@ -88,12 +99,12 @@ export default function Popover({ content, show, children, placement = 'auto' }:
placement,
strategy: 'fixed',
modifiers: [
{ name: 'offset', options: { offset: [8, 8] } },
{ name: 'offset', options: { offset: [offsetX, offsetY] } },
{ name: 'arrow', options: { element: arrowElement } },
{ name: 'preventOverflow', options: { padding: 8 } },
],
}),
[arrowElement, placement]
[arrowElement, offsetX, offsetY, placement]
)
const { styles, update, attributes } = usePopper(referenceElement, popperElement, options)
@@ -105,7 +116,9 @@ export default function Popover({ content, show, children, placement = 'auto' }:
return (
<>
<ReferenceElement ref={setReferenceElement as any}>{children}</ReferenceElement>
<ReferenceElement style={style} ref={setReferenceElement as any}>
{children}
</ReferenceElement>
<Portal>
<PopoverContainer show={show} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
{content}

View File

@@ -18,7 +18,7 @@ import { ExternalLink, ThemedText } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import { unwrappedToken } from '../../utils/unwrappedToken'
import { ButtonEmpty, ButtonPrimary, ButtonSecondary } from '../Button'
import { GreyCard, LightCard } from '../Card'
import { GrayCard, LightCard } from '../Card'
import { AutoColumn } from '../Column'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
@@ -78,7 +78,7 @@ export function MinimalPositionCard({ pair, showUnwrapped = false, border }: Pos
return (
<>
{userPoolBalance && JSBI.greaterThan(userPoolBalance.quotient, JSBI.BigInt(0)) ? (
<GreyCard border={border}>
<GrayCard border={border}>
<AutoColumn gap="12px">
<FixedHeightRow>
<RowFixed>
@@ -139,7 +139,7 @@ export function MinimalPositionCard({ pair, showUnwrapped = false, border }: Pos
</FixedHeightRow>
</AutoColumn>
</AutoColumn>
</GreyCard>
</GrayCard>
) : (
<LightCard>
<ThemedText.DeprecatedSubHeader style={{ textAlign: 'center' }}>

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { sendEvent } from 'components/analytics'
import Card, { DarkGreyCard } from 'components/Card'
import Card, { DarkGrayCard } from 'components/Card'
import Row, { AutoRow, RowBetween } from 'components/Row'
import { useEffect, useRef } from 'react'
import { ArrowDown, Info, X } from 'react-feather'
@@ -137,12 +137,12 @@ export function PrivacyPolicy() {
</ExternalLink>
</StyledExternalCard>
<StyledExternalCard>
<ExternalLink href={'https://uniswap.org/disclaimer/'}>
<ExternalLink href={'https://uniswap.org/privacy-policy/'}>
<RowBetween>
<AutoRow gap="4px">
<Info size={20} />
<ThemedText.DeprecatedMain fontSize={14} color={'deprecated_primaryText1'}>
<Trans>Protocol Disclaimer</Trans>
<Trans>Privacy Policy</Trans>
</ThemedText.DeprecatedMain>
</AutoRow>
<StyledLinkOut size={20} />
@@ -155,7 +155,7 @@ export function PrivacyPolicy() {
</ThemedText.DeprecatedMain>
<AutoColumn gap="12px">
{EXTERNAL_APIS.map(({ name, description }, i) => (
<DarkGreyCard key={i}>
<DarkGrayCard key={i}>
<AutoColumn gap="8px">
<AutoRow gap="4px">
<Info size={18} />
@@ -165,7 +165,7 @@ export function PrivacyPolicy() {
</AutoRow>
<ThemedText.DeprecatedMain fontSize={14}>{description}</ThemedText.DeprecatedMain>
</AutoColumn>
</DarkGreyCard>
</DarkGrayCard>
))}
<ThemedText.DeprecatedBody fontSize={12}>
<Row justify="center" marginBottom="1rem">

View File

@@ -131,7 +131,7 @@ exports[`renders multi route 1`] = `
.c9 {
background-color: #B8C0DC;
border-radius: 4px;
color: #5D6785;
color: #7780A0;
font-size: 10px;
padding: 2px 4px;
z-index: 1021;
@@ -369,7 +369,7 @@ exports[`renders single route 1`] = `
.c9 {
background-color: #B8C0DC;
border-radius: 4px;
color: #5D6785;
color: #7780A0;
font-size: 10px;
padding: 2px 4px;
z-index: 1021;

View File

@@ -1,71 +0,0 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { ButtonPrimary } from 'components/Button'
import { AlertCircle, ArrowLeft } from 'react-feather'
import styled from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
import TokenImportCard from './TokenImportCard'
const Wrapper = styled.div`
align-items: center;
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 100%;
width: 100%;
`
const Button = styled(ButtonPrimary)`
margin-top: 1em;
padding: 10px 1em;
`
const Content = styled.div`
padding: 1em;
`
const Copy = styled(ThemedText.DeprecatedBody)`
text-align: center;
margin: 0 2em 1em !important;
font-weight: 400;
font-size: 16px;
`
const Header = styled.div`
align-items: center;
display: flex;
gap: 14px;
justify-content: space-between;
padding: 20px;
width: 100%;
`
const Icon = styled(AlertCircle)`
stroke: ${({ theme }) => theme.deprecated_text2};
width: 48px;
height: 48px;
`
interface BlockedTokenProps {
onBack: (() => void) | undefined
onDismiss: (() => void) | undefined
blockedTokens: Token[]
}
const BlockedToken = ({ onBack, onDismiss, blockedTokens }: BlockedTokenProps) => (
<Wrapper>
<Header>
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div />}
<ThemedText.DeprecatedMediumHeader>
<Trans>Token not supported</Trans>
</ThemedText.DeprecatedMediumHeader>
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div />}
</Header>
<Icon />
<Content>
<Copy>
<Trans>This token is not supported in the Uniswap Labs app</Trans>
</Copy>
<TokenImportCard token={blockedTokens[0]} />
<Button disabled>
<Trans>Import</Trans>
</Button>
</Content>
</Wrapper>
)
export default BlockedToken

View File

@@ -1,12 +1,12 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { getTokenAddress } from 'analytics/utils'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import { AutoRow } from 'components/Row'
import { COMMON_BASES } from 'constants/routing'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { getTokenAddress } from 'lib/utils/analytics'
import { Text } from 'rebass'
import styled from 'styled-components/macro'
import { currencyId } from 'utils/currencyId'
@@ -69,7 +69,7 @@ export default function CommonBases({
return (
<TraceEvent
events={[Event.onClick, Event.onKeyPress]}
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
name={EventName.TOKEN_SELECTED}
properties={formatAnalyticsEventProperties(currency, searchQuery, isAddressSearch)}
element={ElementName.COMMON_BASES_CURRENCY_BUTTON}

View File

@@ -1,7 +1,7 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { checkWarning } from 'constants/tokenSafety'
import { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
@@ -133,7 +133,7 @@ export function CurrencyRow({
// only show add or remove buttons if not on selected list
return (
<TraceEvent
events={[Event.onClick, Event.onKeyPress]}
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
name={EventName.TOKEN_SELECTED}
properties={{ is_imported_by_user: customAdded, ...eventProperties }}
element={ElementName.TOKEN_SELECTOR_ROW}

View File

@@ -1,9 +1,9 @@
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { EventName, ModalName } from '@uniswap/analytics-events'
import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { EventName, ModalName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { sendEvent } from 'components/analytics'
import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside'

View File

@@ -1,106 +0,0 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import ListLogo from 'components/ListLogo'
import { AutoRow, RowFixed } from 'components/Row'
import { useIsTokenActive, useIsUserAddedToken } from 'hooks/Tokens'
import { CSSProperties } from 'react'
import { CheckCircle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { WrappedTokenInfo } from '../../state/lists/wrappedTokenInfo'
const TokenSection = styled.div<{ dim?: boolean }>`
padding: 4px 20px;
height: 56px;
display: grid;
grid-template-columns: auto minmax(auto, 1fr) auto;
grid-gap: 16px;
align-items: center;
opacity: ${({ dim }) => (dim ? '0.4' : '1')};
`
const CheckIcon = styled(CheckCircle)`
height: 16px;
width: 16px;
margin-right: 6px;
stroke: ${({ theme }) => theme.deprecated_green1};
`
const NameOverflow = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
font-size: 12px;
`
export default function ImportRow({
token,
style,
dim,
showImportView,
setImportToken,
}: {
token: Token
style?: CSSProperties
dim?: boolean
showImportView: () => void
setImportToken: (token: Token) => void
}) {
const theme = useTheme()
// check if already active on list or local storage tokens
const isAdded = useIsUserAddedToken(token)
const isActive = useIsTokenActive(token)
const list = token instanceof WrappedTokenInfo ? token.list : undefined
return (
<TokenSection tabIndex={0} style={style}>
<CurrencyLogo currency={token} size={'24px'} style={{ opacity: dim ? '0.6' : '1' }} />
<AutoColumn gap="4px" style={{ opacity: dim ? '0.6' : '1' }}>
<AutoRow>
<ThemedText.DeprecatedBody fontWeight={500}>{token.symbol}</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedDarkGray ml="8px" fontWeight={300}>
<NameOverflow title={token.name}>{token.name}</NameOverflow>
</ThemedText.DeprecatedDarkGray>
</AutoRow>
{list && list.logoURI && (
<RowFixed>
<ThemedText.DeprecatedSmall mr="4px" color={theme.deprecated_text3}>
<Trans>via {list.name} </Trans>
</ThemedText.DeprecatedSmall>
<ListLogo logoURI={list.logoURI} size="12px" />
</RowFixed>
)}
</AutoColumn>
{!isActive && !isAdded ? (
<ButtonPrimary
width="fit-content"
padding="6px 12px"
fontWeight={500}
fontSize="14px"
onClick={() => {
setImportToken && setImportToken(token)
showImportView()
}}
>
<Trans>Import</Trans>
</ButtonPrimary>
) : (
<RowFixed style={{ minWidth: 'fit-content' }}>
<CheckIcon />
<ThemedText.DeprecatedMain color={theme.deprecated_green1}>
<Trans>Active</Trans>
</ThemedText.DeprecatedMain>
</RowFixed>
)}
</TokenSection>
)
}

View File

@@ -1,100 +0,0 @@
import { Plural, Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import { RowBetween } from 'components/Row'
import { SectionBreak } from 'components/swap/styleds'
import { useUnsupportedTokens } from 'hooks/Tokens'
import { AlertCircle, ArrowLeft } from 'react-feather'
import { useAddUserToken } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
import BlockedToken from './BlockedToken'
import { PaddedColumn } from './styleds'
import TokenImportCard from './TokenImportCard'
const Wrapper = styled.div`
position: relative;
width: 100%;
overflow: auto;
`
interface ImportProps {
tokens: Token[]
list?: TokenList
onBack?: () => void
onDismiss?: () => 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) {
const { tokens, list, onBack, onDismiss, handleCurrencySelect } = props
const theme = useTheme()
const addToken = useAddUserToken()
const unsupportedTokens = useUnsupportedTokens()
const unsupportedSet = new Set(Object.keys(unsupportedTokens))
const intersection = new Set(tokens.filter((token) => unsupportedSet.has(token.address)))
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' }}>
<RowBetween>
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div />}
<ThemedText.DeprecatedMediumHeader>
<Plural value={tokens.length} _1="Import token" other="Import tokens" />
</ThemedText.DeprecatedMediumHeader>
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div />}
</RowBetween>
</PaddedColumn>
<SectionBreak />
<AutoColumn gap="md" style={{ marginBottom: '32px', padding: '1rem' }}>
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', padding: '1rem' }}>
<AlertCircle size={48} stroke={theme.deprecated_text2} strokeWidth={1} />
<ThemedText.DeprecatedBody fontWeight={400} fontSize={16}>
<Trans>
This token doesn&apos;t appear on the active token list(s). Make sure this is the token that you want to
trade.
</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
{tokens.map((token) => (
<TokenImportCard token={token} list={list} key={'import' + token.address} />
))}
<TraceEvent
events={[Event.onClick]}
name={EventName.TOKEN_IMPORTED}
properties={formatAnalyticsEventProperties(tokens)}
element={ElementName.IMPORT_TOKEN_BUTTON}
>
<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>
)
}

View File

@@ -1,152 +0,0 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import Card from 'components/Card'
import Column from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import Row, { RowBetween, RowFixed } from 'components/Row'
import { useToken } from 'hooks/Tokens'
import { ChangeEvent, RefObject, useCallback, useMemo, useRef, useState } from 'react'
import { useRemoveUserAddedToken, useUserAddedTokens } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { ButtonText, ExternalLink, ExternalLinkIcon, ThemedText, TrashIcon } from 'theme'
import { isAddress } from 'utils'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { CurrencyModalView } from './CurrencySearchModal'
import ImportRow from './ImportRow'
import { PaddedColumn, SearchInput, Separator } from './styleds'
const Wrapper = styled.div`
width: 100%;
height: calc(100% - 60px);
position: relative;
padding-bottom: 80px;
`
const Footer = styled.div`
position: absolute;
bottom: 0;
width: 100%;
border-radius: 20px;
border-top-right-radius: 0;
border-top-left-radius: 0;
border-top: 1px solid ${({ theme }) => theme.deprecated_bg3};
padding: 20px;
text-align: center;
`
export default function ManageTokens({
setModalView,
setImportToken,
}: {
setModalView: (view: CurrencyModalView) => void
setImportToken: (token: Token) => void
}) {
const { chainId } = useWeb3React()
const [searchQuery, setSearchQuery] = useState<string>('')
const theme = useTheme()
// manage focus on modal show
const inputRef = useRef<HTMLInputElement>()
const handleInput = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const input = event.target.value
const checksummedInput = isAddress(input)
setSearchQuery(checksummedInput || input)
}, [])
// if they input an address, use it
const isAddressSearch = isAddress(searchQuery)
const searchToken = useToken(searchQuery)
// all tokens for local lisr
const userAddedTokens: Token[] = useUserAddedTokens()
const removeToken = useRemoveUserAddedToken()
const handleRemoveAll = useCallback(() => {
if (chainId && userAddedTokens) {
userAddedTokens.map((token) => {
return removeToken(chainId, token.address)
})
}
}, [removeToken, userAddedTokens, chainId])
const tokenList = useMemo(() => {
return (
chainId &&
userAddedTokens.map((token) => (
<RowBetween key={token.address} width="100%">
<RowFixed>
<CurrencyLogo currency={token} size={'20px'} />
<ExternalLink href={getExplorerLink(chainId, token.address, ExplorerDataType.ADDRESS)}>
<ThemedText.DeprecatedMain ml={'10px'} fontWeight={600}>
{token.symbol}
</ThemedText.DeprecatedMain>
</ExternalLink>
</RowFixed>
<RowFixed>
<TrashIcon onClick={() => removeToken(chainId, token.address)} />
<ExternalLinkIcon href={getExplorerLink(chainId, token.address, ExplorerDataType.ADDRESS)} />
</RowFixed>
</RowBetween>
))
)
}, [userAddedTokens, chainId, removeToken])
return (
<Wrapper>
<Column style={{ width: '100%', height: '100%', flex: '1 1' }}>
<PaddedColumn gap="14px">
<Row>
<SearchInput
type="text"
id="token-search-input"
placeholder={'0x0000'}
value={searchQuery}
autoComplete="off"
ref={inputRef as RefObject<HTMLInputElement>}
onChange={handleInput}
/>
</Row>
{searchQuery !== '' && !isAddressSearch && (
<ThemedText.DeprecatedError error={true}>
<Trans>Enter valid token address</Trans>
</ThemedText.DeprecatedError>
)}
{searchToken && (
<Card backgroundColor={theme.deprecated_bg2} padding="10px 0">
<ImportRow
token={searchToken}
showImportView={() => setModalView(CurrencyModalView.importToken)}
setImportToken={setImportToken}
style={{ height: 'fit-content' }}
/>
</Card>
)}
</PaddedColumn>
<Separator />
<PaddedColumn gap="lg" style={{ overflow: 'auto', marginBottom: '10px' }}>
<RowBetween>
<ThemedText.DeprecatedMain fontWeight={600}>
<Trans>{userAddedTokens?.length} Custom Tokens</Trans>
</ThemedText.DeprecatedMain>
{userAddedTokens.length > 0 && (
<ButtonText onClick={handleRemoveAll}>
<ThemedText.DeprecatedBlue>
<Trans>Clear all</Trans>
</ThemedText.DeprecatedBlue>
</ButtonText>
)}
</RowBetween>
{tokenList}
</PaddedColumn>
</Column>
<Footer>
<ThemedText.DeprecatedDarkGray>
<Trans>Tip: Custom tokens are stored locally in your browser</Trans>
</ThemedText.DeprecatedDarkGray>
</Footer>
</Wrapper>
)
}

View File

@@ -1,76 +0,0 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { useWeb3React } from '@web3-react/core'
import Card from 'components/Card'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import ListLogo from 'components/ListLogo'
import { RowFixed } from 'components/Row'
import { transparentize } from 'polished'
import { AlertCircle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
const WarningWrapper = styled(Card)<{ highWarning: boolean }>`
background-color: ${({ theme, highWarning }) =>
highWarning ? transparentize(0.8, theme.deprecated_red1) : transparentize(0.8, theme.deprecated_yellow2)};
width: fit-content;
`
const AddressText = styled(ThemedText.DeprecatedBlue)`
font-size: 12px;
word-break: break-all;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
font-size: 10px;
`}
`
interface TokenImportCardProps {
list?: TokenList
token: Token
}
const TokenImportCard = ({ list, token }: TokenImportCardProps) => {
const theme = useTheme()
const { chainId } = useWeb3React()
return (
<Card backgroundColor={theme.deprecated_bg2} padding="2rem">
<AutoColumn gap="10px" justify="center">
<CurrencyLogo currency={token} size={'32px'} />
<AutoColumn gap="4px" justify="center">
<ThemedText.DeprecatedBody ml="8px" mr="8px" fontWeight={500} fontSize={20}>
{token.symbol}
</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedDarkGray fontWeight={400} fontSize={14}>
{token.name}
</ThemedText.DeprecatedDarkGray>
</AutoColumn>
{chainId && (
<ExternalLink href={getExplorerLink(chainId, token.address, ExplorerDataType.ADDRESS)}>
<AddressText fontSize={12}>{token.address}</AddressText>
</ExternalLink>
)}
{list !== undefined ? (
<RowFixed>
{list.logoURI && <ListLogo logoURI={list.logoURI} size="16px" />}
<ThemedText.DeprecatedSmall ml="6px" fontSize={14} color={theme.deprecated_text3}>
<Trans>via {list.name} token list</Trans>
</ThemedText.DeprecatedSmall>
</RowFixed>
) : (
<WarningWrapper $borderRadius="4px" padding="4px" highWarning={true}>
<RowFixed>
<AlertCircle stroke={theme.deprecated_red1} size="10px" />
<ThemedText.DeprecatedBody color={theme.deprecated_red1} ml="4px" fontSize="10px" fontWeight={500}>
<Trans>Unknown Source</Trans>
</ThemedText.DeprecatedBody>
</RowFixed>
</WarningWrapper>
)}
</AutoColumn>
</Card>
)
}
export default TokenImportCard

View File

@@ -1,22 +0,0 @@
import { Token } from '@uniswap/sdk-core'
import { ImportToken } from 'components/SearchModal/ImportToken'
import Modal from '../Modal'
export default function TokenWarningModal({
isOpen,
tokens,
onConfirm,
onDismiss,
}: {
isOpen: boolean
tokens: Token[]
onConfirm: () => void
onDismiss: () => void
}) {
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={100}>
<ImportToken tokens={tokens} handleCurrencySelect={onConfirm} />
</Modal>
)
}

View File

@@ -1,12 +1,12 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { formatToDecimal } from 'analytics/utils'
import CurrencyLogo from 'components/CurrencyLogo'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { formatToDecimal } from 'lib/utils/analytics'
import { useMemo } from 'react'
import styled from 'styled-components/macro'
import { StyledInternalLink } from 'theme'

View File

@@ -1,112 +1,77 @@
import { Trans } from '@lingui/macro'
import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core'
import { ParentSize } from '@visx/responsive'
import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
import { TokenQueryData } from 'graphql/data/Token'
import { PriceDurations } from 'graphql/data/TokenPrice'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { ChartContainer, LoadingChart } from 'components/Tokens/TokenDetails/Skeleton'
import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
import { isPricePoint, PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import styled from 'styled-components/macro'
import { textFadeIn } from 'theme/animations'
import { startTransition, Suspense, useMemo, useState } from 'react'
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
import { filterTimeAtom } from '../state'
import { L2NetworkLogo, LogoContainer } from '../TokenTable/TokenRow'
import PriceChart from './PriceChart'
import ShareButton from './ShareButton'
import TimePeriodSelector from './TimeSelector'
export const ChartHeader = styled.div`
width: 100%;
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textPrimary};
gap: 4px;
margin-bottom: 24px;
`
export const TokenInfoContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
export const ChartContainer = styled.div`
display: flex;
height: 436px;
align-items: center;
`
export const TokenNameCell = styled.div`
display: flex;
gap: 8px;
font-size: 20px;
line-height: 28px;
align-items: center;
${textFadeIn}
`
const TokenSymbol = styled.span`
text-transform: uppercase;
color: ${({ theme }) => theme.textSecondary};
`
const TokenActions = styled.div`
display: flex;
gap: 16px;
color: ${({ theme }) => theme.textSecondary};
`
function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPriceQuery>): PricePoint[] | undefined {
const queryData = usePreloadedQuery(tokenPriceQuery, priceQueryReference)
export function useTokenLogoURI(
token: NonNullable<TokenQueryData> | NonNullable<TopToken>,
nativeCurrency?: Token | NativeCurrency
) {
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
return [
...useCurrencyLogoURIs(nativeCurrency),
...useCurrencyLogoURIs({ ...token, chainId }),
token.project?.logoUrl,
][0]
// Appends the current price to the end of the priceHistory array
const priceHistory = useMemo(() => {
const market = queryData.tokens?.[0]?.market
const priceHistory = market?.priceHistory?.filter(isPricePoint)
const currentPrice = market?.price?.value
if (Array.isArray(priceHistory) && currentPrice !== undefined) {
const timestamp = Date.now() / 1000
return [...priceHistory, { timestamp, value: currentPrice }]
}
return priceHistory
}, [queryData])
return priceHistory
}
export default function ChartSection({
token,
currency,
nativeCurrency,
prices,
priceQueryReference,
refetchTokenPrices,
}: {
token: NonNullable<TokenQueryData>
currency?: Currency | null
nativeCurrency?: Token | NativeCurrency
prices?: PriceDurations
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined
refetchTokenPrices: RefetchPricesFunction
}) {
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
const timePeriod = useAtomValue(filterTimeAtom)
const logoSrc = useTokenLogoURI(token, nativeCurrency)
if (!priceQueryReference) {
return <LoadingChart />
}
return (
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<LogoContainer>
<CurrencyLogo
src={logoSrc}
size={'32px'}
symbol={nativeCurrency?.symbol ?? token.symbol}
currency={nativeCurrency ? undefined : currency}
/>
<L2NetworkLogo networkUrl={L2Icon} size={'16px'} />
</LogoContainer>
{nativeCurrency?.name ?? token.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{nativeCurrency?.symbol ?? token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
</TokenNameCell>
<TokenActions>
{token.name && token.symbol && token.address && <ShareButton token={token} isNative={!!nativeCurrency} />}
</TokenActions>
</TokenInfoContainer>
<Suspense fallback={<LoadingChart />}>
<ChartContainer>
<ParentSize>
{({ width }) => <PriceChart prices={prices ? prices?.[timePeriod] : null} width={width} height={436} />}
</ParentSize>
<Chart priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} />
</ChartContainer>
</ChartHeader>
</Suspense>
)
}
export type RefetchPricesFunction = (t: TimePeriod) => void
function Chart({
priceQueryReference,
refetchTokenPrices,
}: {
priceQueryReference: PreloadedQuery<TokenPriceQuery>
refetchTokenPrices: RefetchPricesFunction
}) {
const prices = usePreloadedTokenPriceQuery(priceQueryReference)
// Initializes time period to global & maintain separate time period for subsequent changes
const [timePeriod, setTimePeriod] = useState(useAtomValue(filterTimeAtom))
return (
<ChartContainer>
<ParentSize>
{({ width }) => <PriceChart prices={prices ?? null} width={width} height={436} timePeriod={timePeriod} />}
</ParentSize>
<TimePeriodSelector
currentTimePeriod={timePeriod}
onTimeChange={(t: TimePeriod) => {
startTransition(() => refetchTokenPrices(t))
setTimePeriod(t)
}}
/>
</ChartContainer>
)
}

View File

@@ -5,12 +5,10 @@ import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape'
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
import { filterTimeAtom } from 'components/Tokens/state'
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
import { PricePoint } from 'graphql/data/TokenPrice'
import { PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useAtom } from 'jotai'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
@@ -24,9 +22,6 @@ import {
} from 'utils/formatChartTimes'
import { formatDollar } from 'utils/formatNumbers'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
export const DATA_EMPTY = { value: 0, timestamp: 0 }
export function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
@@ -52,9 +47,9 @@ export function getDeltaArrow(delta: number | null | undefined) {
if (delta === null || delta === undefined) {
return null
} else if (Math.sign(delta) < 0) {
return <StyledDownArrow size={16} key="arrow-down" />
return <StyledDownArrow size={24} key="arrow-down" />
}
return <StyledUpArrow size={16} key="arrow-up" />
return <StyledUpArrow size={24} key="arrow-up" />
}
export function formatDelta(delta: number | null | undefined) {
@@ -86,46 +81,6 @@ const ArrowCell = styled.div`
padding-left: 2px;
display: flex;
`
export const TimeOptionsWrapper = styled.div`
display: flex;
justify-content: flex-end;
`
export const TimeOptionsContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 4px;
gap: 4px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
height: 40px;
padding: 4px;
width: fit-content;
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
width: 100%;
justify-content: space-between;
border: none;
}
`
const TimeButton = styled.button<{ active: boolean }>`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: ${({ theme, active }) => (active ? theme.backgroundInteractive : 'transparent')};
font-weight: 600;
font-size: 16px;
padding: 6px 12px;
border-radius: 12px;
line-height: 20px;
border: none;
cursor: pointer;
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
transition-duration: ${({ theme }) => theme.transition.duration.fast};
:hover {
${({ active, theme }) => !active && `opacity: ${theme.opacity.hover};`}
}
`
const margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
@@ -134,10 +89,10 @@ interface PriceChartProps {
width: number
height: number
prices: PricePoint[] | undefined | null
timePeriod: TimePeriod
}
export function PriceChart({ width, height, prices }: PriceChartProps) {
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
export function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
const locale = useActiveLocale()
const theme = useTheme()
@@ -282,9 +237,7 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
width={width}
height={graphHeight}
message={
prices === null ? (
<Trans>Loading chart data</Trans>
) : prices?.length === 0 ? (
prices?.length === 0 ? (
<Trans>This token doesn&apos;t have chart data because it hasn&apos;t been traded on Uniswap v3</Trans>
) : (
<Trans>Missing chart data</Trans>
@@ -375,21 +328,6 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
/>
</svg>
)}
<TimeOptionsWrapper>
<TimeOptionsContainer>
{ORDERED_TIMES.map((time) => (
<TimeButton
key={DISPLAYS[time]}
active={timePeriod === time}
onClick={() => {
setTimePeriod(time)
}}
>
{DISPLAYS[time]}
</TimeButton>
))}
</TimeOptionsContainer>
</TimeOptionsWrapper>
</>
)
}

View File

@@ -3,11 +3,12 @@ import { WIDGET_WIDTH } from 'components/Widget'
import { ArrowLeft } from 'react-feather'
import { useParams } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import { textFadeIn } from 'theme/animations'
import { LoadingBubble } from '../loading'
import { LogoContainer } from '../TokenTable/TokenRow'
import { AboutContainer, AboutHeader } from './About'
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
import { ChartContainer, ChartHeader, TokenInfoContainer, TokenNameCell } from './ChartSection'
import { DeltaContainer, TokenPrice } from './PriceChart'
import { StatPair, StatsWrapper, StatWrapper } from './StatsSection'
@@ -49,12 +50,38 @@ export const RightPanel = styled.div`
display: flex;
}
`
const LoadingChartContainer = styled(ChartContainer)`
export const ChartContainer = styled.div`
display: flex;
flex-direction: column;
height: 436px;
margin-bottom: 24px;
align-items: flex-start;
width: 100%;
`
const LoadingChartContainer = styled.div`
display: flex;
flex-direction: row;
align-items: flex-end;
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
height: 313px; // save 1px for the border-bottom (ie y-axis)
height: 100%;
margin-bottom: 44px;
padding-bottom: 66px;
overflow: hidden;
`
export const TokenInfoContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
`
export const TokenNameCell = styled.div`
display: flex;
gap: 8px;
font-size: 20px;
line-height: 28px;
align-items: center;
${textFadeIn}
`
/* Loading state bubbles */
const DetailBubble = styled(LoadingBubble)`
height: 16px;
@@ -73,10 +100,13 @@ const TitleBubble = styled(DetailBubble)`
width: 140px;
`
const PriceBubble = styled(SquaredBubble)`
height: 40px;
margin-top: 2px;
height: 38px;
`
const DeltaBubble = styled(DetailBubble)`
margin-top: 6px;
width: 96px;
height: 20px;
`
const SectionBubble = styled(SquaredBubble)`
width: 96px;
@@ -105,6 +135,7 @@ const ChartAnimation = styled.div`
animation: wave 8s cubic-bezier(0.36, 0.45, 0.63, 0.53) infinite;
display: flex;
overflow: hidden;
margin-top: 90px;
@keyframes wave {
0% {
@@ -128,15 +159,9 @@ function Wave() {
)
}
function LoadingChart() {
export function LoadingChart() {
return (
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<TokenLogoBubble />
<TitleBubble />
</TokenNameCell>
</TokenInfoContainer>
<ChartContainer>
<TokenPrice>
<PriceBubble />
</TokenPrice>
@@ -155,7 +180,7 @@ function LoadingChart() {
</ChartAnimation>
</div>
</LoadingChartContainer>
</ChartHeader>
</ChartContainer>
)
}
@@ -197,8 +222,17 @@ export default function TokenDetailsSkeleton() {
<BreadcrumbNavLink to={{ chainName } ? `/tokens/${chainName}` : `/explore`}>
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<TokenInfoContainer>
<TokenNameCell>
<LogoContainer>
<TokenLogoBubble />
</LogoContainer>
<TitleBubble />
</TokenNameCell>
</TokenInfoContainer>
<LoadingChart />
<Space heightSize={45} />
<Space heightSize={4} />
<LoadingStats />
<Hr />
<AboutContainer>

View File

@@ -0,0 +1,76 @@
import { TimePeriod } from 'graphql/data/util'
import { startTransition, useState } from 'react'
import styled from 'styled-components/macro'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
export const TimeOptionsWrapper = styled.div`
display: flex;
width: 100%;
justify-content: flex-end;
`
export const TimeOptionsContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 4px;
gap: 4px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
height: 40px;
padding: 4px;
width: fit-content;
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
width: 100%;
justify-content: space-between;
border: none;
}
`
const TimeButton = styled.button<{ active: boolean }>`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: ${({ theme, active }) => (active ? theme.backgroundInteractive : 'transparent')};
font-weight: 600;
font-size: 16px;
padding: 6px 12px;
border-radius: 12px;
line-height: 20px;
border: none;
cursor: pointer;
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
transition-duration: ${({ theme }) => theme.transition.duration.fast};
:hover {
${({ active, theme }) => !active && `opacity: ${theme.opacity.hover};`}
}
`
export default function TimePeriodSelector({
currentTimePeriod,
onTimeChange,
}: {
currentTimePeriod: TimePeriod
onTimeChange: (t: TimePeriod) => void
}) {
const [timePeriod, setTimePeriod] = useState(currentTimePeriod)
return (
<TimeOptionsWrapper>
<TimeOptionsContainer>
{ORDERED_TIMES.map((time) => (
<TimeButton
key={DISPLAYS[time]}
active={timePeriod === time}
onClick={() => {
startTransition(() => onTimeChange(time))
setTimePeriod(time)
}}
>
{DISPLAYS[time]}
</TimeButton>
))}
</TimeOptionsContainer>
</TimeOptionsWrapper>
)
}

View File

@@ -0,0 +1,215 @@
import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { PageName } from '@uniswap/analytics-events'
import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core'
import CurrencyLogo from 'components/CurrencyLogo'
import { AboutSection } from 'components/Tokens/TokenDetails/About'
import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
import { BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
import MobileBalanceSummaryFooter from 'components/Tokens/TokenDetails/MobileBalanceSummaryFooter'
import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
import TokenDetailsSkeleton, {
Hr,
LeftPanel,
RightPanel,
TokenDetailsLayout,
TokenInfoContainer,
TokenNameCell,
} from 'components/Tokens/TokenDetails/Skeleton'
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget from 'components/Widget'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { DEFAULT_ERC20_DECIMALS, NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { TokenPriceQuery } from 'graphql/data/__generated__/TokenPriceQuery.graphql'
import { Chain, TokenQuery } from 'graphql/data/Token'
import { QueryToken, tokenQuery, TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { useCallback, useMemo, useState, useTransition } from 'react'
import { ArrowLeft } from 'react-feather'
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { RefetchPricesFunction } from './ChartSection'
const TokenSymbol = styled.span`
text-transform: uppercase;
color: ${({ theme }) => theme.textSecondary};
`
const TokenActions = styled.div`
display: flex;
gap: 16px;
color: ${({ theme }) => theme.textSecondary};
`
export function useTokenLogoURI(token?: TokenQueryData | TopToken, nativeCurrency?: Token | NativeCurrency) {
const chainId = token ? CHAIN_NAME_TO_CHAIN_ID[token.chain] : SupportedChainId.MAINNET
return [
...useCurrencyLogoURIs(nativeCurrency),
...useCurrencyLogoURIs({ ...token, chainId }),
token?.project?.logoUrl,
][0]
}
type TokenDetailsProps = {
tokenAddress: string | undefined
chain: Chain
tokenQueryReference: PreloadedQuery<TokenQuery>
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined
refetchTokenPrices: RefetchPricesFunction
}
export default function TokenDetails({
tokenAddress,
chain,
tokenQueryReference,
priceQueryReference,
refetchTokenPrices,
}: TokenDetailsProps) {
if (!tokenAddress) {
throw new Error(`Invalid token details route: tokenAddress param is undefined`)
}
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const nativeCurrency = nativeOnChain(pageChainId)
const isNative = tokenAddress === NATIVE_CHAIN_ID
const tokenQueryData = usePreloadedQuery(tokenQuery, tokenQueryReference).tokens?.[0]
const token = useMemo(() => {
if (isNative) return nativeCurrency
if (tokenQueryData) return new QueryToken(tokenQueryData)
return new Token(pageChainId, tokenAddress, DEFAULT_ERC20_DECIMALS)
}, [isNative, nativeCurrency, pageChainId, tokenAddress, tokenQueryData])
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
const isBlockedToken = tokenWarning?.canProceed === false
const navigate = useNavigate()
// Wrapping navigate in a transition prevents Suspense from unnecessarily showing fallbacks again.
const [isPending, startTokenTransition] = useTransition()
const navigateToTokenForChain = useCallback(
(chain: Chain) => {
const chainName = chain.toLowerCase()
const token = tokenQueryData?.project?.tokens.find((token) => token.chain === chain && token.address)
const address = isNative ? NATIVE_CHAIN_ID : token?.address
if (!address) return
startTokenTransition(() => navigate(`/tokens/${chainName}/${address}`))
},
[isNative, navigate, startTokenTransition, tokenQueryData?.project?.tokens]
)
useOnGlobalChainSwitch(navigateToTokenForChain)
const navigateToWidgetSelectedToken = useCallback(
(token: Currency) => {
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
startTokenTransition(() => navigate(`/tokens/${chain.toLowerCase()}/${address}`))
},
[chain, navigate]
)
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(tokenAddress, pageChainId) && tokenWarning !== null
const onReviewSwapClick = useCallback(
() => new Promise<boolean>((resolve) => (shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true))),
[shouldShowSpeedbump]
)
const onResolveSwap = useCallback(
(value: boolean) => {
continueSwap?.resolve(value)
setContinueSwap(undefined)
},
[continueSwap, setContinueSwap]
)
const logoSrc = useTokenLogoURI(tokenQueryData, isNative ? nativeCurrency : undefined)
const L2Icon = getChainInfo(pageChainId)?.circleLogoUrl
return (
<Trace page={PageName.TOKEN_DETAILS_PAGE} properties={{ tokenAddress, tokenName: token?.name }} shouldLogImpression>
<TokenDetailsLayout>
{tokenQueryData && !isPending ? (
<LeftPanel>
<BreadcrumbNavLink to={`/tokens/${chain.toLowerCase()}`}>
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<TokenInfoContainer>
<TokenNameCell>
<LogoContainer>
<CurrencyLogo
src={logoSrc}
size={'32px'}
symbol={isNative ? nativeCurrency?.symbol : token?.symbol}
currency={isNative ? nativeCurrency : token}
/>
<L2NetworkLogo networkUrl={L2Icon} size={'16px'} />
</LogoContainer>
{token?.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{token?.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
</TokenNameCell>
<TokenActions>
{tokenQueryData?.name && tokenQueryData.symbol && tokenQueryData.address && (
<ShareButton token={tokenQueryData} isNative={!!nativeCurrency} />
)}
</TokenActions>
</TokenInfoContainer>
<ChartSection priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} />
<StatsSection
TVL={tokenQueryData.market?.totalValueLocked?.value}
volume24H={tokenQueryData.market?.volume24H?.value}
priceHigh52W={tokenQueryData.market?.priceHigh52W?.value}
priceLow52W={tokenQueryData.market?.priceLow52W?.value}
/>
{!isNative && (
<>
<Hr />
<AboutSection
address={tokenQueryData.address ?? ''}
description={tokenQueryData.project?.description}
homepageUrl={tokenQueryData.project?.homepageUrl}
twitterName={tokenQueryData.project?.twitterName}
/>
<AddressSection address={tokenQueryData.address ?? ''} />
</>
)}
</LeftPanel>
) : (
<TokenDetailsSkeleton />
)}
<RightPanel>
<Widget
token={token ?? nativeCurrency}
onTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
{tokenWarning && <TokenSafetyMessage tokenAddress={tokenAddress ?? ''} warning={tokenWarning} />}
{token && <BalanceSummary token={token} />}
</RightPanel>
{token && <MobileBalanceSummaryFooter token={token} />}
{tokenAddress && (
<TokenSafetyModal
isOpen={isBlockedToken || !!continueSwap}
tokenAddress={tokenAddress}
onContinue={() => onResolveSwap(true)}
onBlocked={() => navigate(-1)}
onCancel={() => onResolveSwap(false)}
showCancel={true}
/>
)}
</TokenDetailsLayout>
</Trace>
)
}

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import searchIcon from 'assets/svg/search.svg'
import xIcon from 'assets/svg/x.svg'
import useDebounce from 'hooks/useDebounce'
@@ -79,7 +79,7 @@ export default function SearchBar() {
<Trans
render={({ translation }) => (
<TraceEvent
events={[Event.onFocus]}
events={[BrowserEvent.onFocus]}
name={EventName.EXPLORE_SEARCH_SELECTED}
element={ElementName.EXPLORE_SEARCH_INPUT}
>

View File

@@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { ParentSize } from '@visx/responsive'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
@@ -31,7 +31,7 @@ import {
TokenSortMethod,
useSetSortMethod,
} from '../state'
import { useTokenLogoURI } from '../TokenDetails/ChartSection'
import { useTokenLogoURI } from '../TokenDetails'
import InfoTip from '../TokenDetails/InfoTip'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'

View File

@@ -1,100 +0,0 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { chainIdToBackendName } from 'graphql/data/util'
import { X } from 'react-feather'
import { Link } from 'react-router-dom'
import { useShowTokensPromoBanner } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { opacify } from 'theme/utils'
import { Z_INDEX } from 'theme/zIndex'
import tokensPromoDark from '../../assets/images/tokensPromoDark.png'
import tokensPromoLight from '../../assets/images/tokensPromoLight.png'
const BackgroundColor = styled(Link)<{ show: boolean }>`
background-color: ${({ theme }) => (theme.darkMode ? theme.backgroundScrim : '#FDF0F8')};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
bottom: 48px;
box-shadow: ${({ theme }) => theme.deepShadow};
display: ${({ show }) => (show ? 'block' : 'none')};
height: 88px;
position: fixed;
right: clamp(0px, 1vw, 16px);
text-decoration: none;
width: 320px;
z-index: ${Z_INDEX.sticky};
`
const PopupContainer = styled.div`
background-color: ${({ theme }) => (theme.darkMode ? theme.backgroundScrim : opacify(60, '#FDF0F8'))};
background-image: url(${({ theme }) => (theme.darkMode ? `${tokensPromoDark}` : `${tokensPromoLight}`)});
background-size: cover;
background-blend-mode: overlay;
border-radius: 12px;
color: ${({ theme }) => theme.textPrimary};
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
padding: 12px 16px 12px 20px;
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.slow} opacity ${timing.in}`};
`
const Header = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
`
const HeaderText = styled.span`
font-weight: 600;
font-size: 14px;
line-height: 20px;
`
const Description = styled.span`
font-weight: 400;
font-size: 12px;
line-height: 16px;
width: max(212px, calc(100% - 36px));
`
export default function TokensBanner() {
const theme = useTheme()
const [showTokensPromoBanner, setShowTokensPromoBanner] = useShowTokensPromoBanner()
const { chainId: connectedChainId } = useWeb3React()
const chainName = chainIdToBackendName(connectedChainId).toLowerCase()
return (
<BackgroundColor show={showTokensPromoBanner} to={`/tokens/${chainName}`}>
<TraceEvent events={[Event.onClick]} name={EventName.EXPLORE_BANNER_CLICKED} element={ElementName.EXPLORE_BANNER}>
<PopupContainer>
<Header>
<HeaderText>
<Trans>Explore Top Tokens on Uniswap</Trans>
</HeaderText>
<X
size={20}
color={theme.textSecondary}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setShowTokensPromoBanner(false)
}}
style={{ cursor: 'pointer' }}
/>
</Header>
<Description>
<Trans>Sort and filter assets across networks on the new Tokens page.</Trans>
</Description>
</PopupContainer>
</TraceEvent>
</BackgroundColor>
)
}

View File

@@ -42,9 +42,10 @@ export function MouseoverTooltip({ text, disableHover, children, ...rest }: Omit
const [show, setShow] = useState(false)
const open = useCallback(() => setShow(true), [setShow])
const close = useCallback(() => setShow(false), [setShow])
const noOp = () => null
return (
<Tooltip {...rest} show={show} text={disableHover ? null : text}>
<div onMouseEnter={open} onMouseLeave={close}>
<div onMouseEnter={disableHover ? noOp : open} onMouseLeave={disableHover ? noOp : close}>
{children}
</div>
</Tooltip>

View File

@@ -1,11 +1,9 @@
import { useWeb3React } from '@web3-react/core'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import TokensBanner from 'components/Tokens/TokensBanner'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import { lazy } from 'react'
import { useLocation } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
@@ -18,7 +16,6 @@ export default function TopLevelModals() {
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React()
const location = useLocation()
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
@@ -26,7 +23,6 @@ export default function TopLevelModals() {
<>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} />
{(location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />}
<Bag />
{useNftFlag() === NftVariant.Enabled && <TransactionCompleteModal />}
</>

View File

@@ -25,7 +25,8 @@ import AnimatedConfirmation from './AnimatedConfirmation'
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
outline: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
border-radius: 20px;
outline: 1px solid ${({ theme }) => theme.backgroundOutline};
width: 100%;
padding: 1rem;
`

View File

@@ -1,3 +1,4 @@
import { ScrollBarStyles } from 'components/Common'
import { ChevronLeft } from 'react-feather'
import styled from 'styled-components/macro'
@@ -8,22 +9,11 @@ const Menu = styled.div`
overflow: auto;
max-height: 450px;
// Firefox scrollbar styling
scrollbar-width: thin;
scrollbar-color: ${({ theme }) => `${theme.backgroundOutline} transparent`};
${ScrollBarStyles}
// safari and chrome scrollbar styling
::-webkit-scrollbar {
background: transparent;
width: 4px;
}
::-webkit-scrollbar-track {
margin-top: 40px;
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) => theme.backgroundOutline};
border-radius: 8px;
}
`
const Header = styled.span`

View File

@@ -1,5 +1,5 @@
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import React from 'react'
import { Check } from 'react-feather'
import styled from 'styled-components/macro'
@@ -115,7 +115,7 @@ export default function Option({
}) {
const content = (
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
name={EventName.WALLET_SELECTED}
properties={{ wallet_type: header }}
element={ElementName.WALLET_TYPE_OPTION}

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, user } from '@uniswap/analytics'
import { CustomUserProperties, EventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { sendAnalyticsEvent, user } from 'analytics'
import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'analytics/constants'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
@@ -126,17 +126,17 @@ const sendAnalyticsEventAndUserInfo = (
isReconnect: boolean
) => {
sendAnalyticsEvent(EventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WALLET_CONNECTION_RESULT.SUCCEEDED,
result: WalletConnectionResult.SUCCEEDED,
wallet_address: account,
wallet_type: walletType,
is_reconnect: isReconnect,
})
user.set(CUSTOM_USER_PROPERTIES.WALLET_ADDRESS, account)
user.set(CUSTOM_USER_PROPERTIES.WALLET_TYPE, walletType)
user.set(CustomUserProperties.WALLET_ADDRESS, account)
user.set(CustomUserProperties.WALLET_TYPE, walletType)
if (chainId) {
user.postInsert(CUSTOM_USER_PROPERTIES.ALL_WALLET_CHAIN_IDS, chainId)
user.postInsert(CustomUserProperties.ALL_WALLET_CHAIN_IDS, chainId)
}
user.postInsert(CUSTOM_USER_PROPERTIES.ALL_WALLET_ADDRESSES_CONNECTED, account)
user.postInsert(CustomUserProperties.ALL_WALLET_ADDRESSES_CONNECTED, account)
}
export default function WalletModal({
@@ -232,7 +232,7 @@ export default function WalletModal({
dispatch(updateConnectionError({ connectionType, error: error.message }))
sendAnalyticsEvent(EventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WALLET_CONNECTION_RESULT.FAILED,
result: WalletConnectionResult.FAILED,
wallet_type: getConnectionName(connectionType, getIsMetaMask()),
})
}
@@ -315,32 +315,24 @@ export default function WalletModal({
function getTermsOfService(nftFlagEnabled: boolean, walletView: string) {
if (nftFlagEnabled && walletView === WALLET_VIEWS.PENDING) return null
const content = (
<Trans>
By connecting a wallet, you agree to Uniswap Labs{' '}
<ExternalLink href="https://uniswap.org/terms-of-service/">Terms of Service</ExternalLink> and consent to its{' '}
<ExternalLink href="https://uniswap.org/privacy-policy">Privacy Policy</ExternalLink>.
</Trans>
)
return nftFlagEnabled ? (
<AutoRow style={{ flexWrap: 'nowrap', padding: '4px 16px' }}>
<ThemedText.BodySecondary fontSize={16} lineHeight={'24px'}>
<Trans>
By connecting a wallet, you agree to Uniswap Labs{' '}
<ExternalLink href="https://uniswap.org/terms-of-service/">Terms of Service</ExternalLink> and consent to
its <ExternalLink href="https://uniswap.org/privacy-policy">Privacy Policy</ExternalLink>.
</Trans>
{content}
</ThemedText.BodySecondary>
</AutoRow>
) : (
<LightCard>
<AutoRow style={{ flexWrap: 'nowrap' }}>
<ThemedText.DeprecatedBody fontSize={12}>
<Trans>
By connecting a wallet, you agree to Uniswap Labs{' '}
<ExternalLink style={{ textDecoration: 'underline' }} href="https://uniswap.org/terms-of-service/">
Terms of Service
</ExternalLink>{' '}
and acknowledge that you have read and understand the Uniswap{' '}
<ExternalLink style={{ textDecoration: 'underline' }} href="https://uniswap.org/disclaimer/">
Protocol Disclaimer
</ExternalLink>
.
</Trans>
</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedBody fontSize={12}>{content}</ThemedText.DeprecatedBody>
</AutoRow>
</LightCard>
)

View File

@@ -1,9 +1,11 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { IconWrapper } from 'components/Identicon/StatusIcon'
import WalletDropdown from 'components/WalletDropdown'
import { getConnection } from 'connection/utils'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { Portal } from 'nft/components/common/Portal'
import { getIsValidSwapQuote } from 'pages/Swap'
import { darken } from 'polished'
@@ -92,7 +94,7 @@ const Web3StatusConnectWrapper = styled.div<{ faded?: boolean }>`
}
`
const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>`
const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean; isNftActive?: boolean }>`
background-color: ${({ pending, theme }) => (pending ? theme.deprecated_primary1 : theme.deprecated_bg1)};
border: 1px solid ${({ pending, theme }) => (pending ? theme.deprecated_primary1 : theme.deprecated_bg1)};
color: ${({ pending, theme }) => (pending ? theme.deprecated_white : theme.deprecated_text1)};
@@ -107,6 +109,22 @@ const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>`
pending ? darken(0.1, theme.deprecated_primary1) : darken(0.1, theme.deprecated_bg2)};
}
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
width: ${({ isNftActive, pending }) => isNftActive && !pending && '36px'};
${IconWrapper} {
margin-right: ${({ isNftActive }) => isNftActive && 0};
}
}
`
const AddressAndChevronContainer = styled.div<{ isNftActive?: boolean }>`
display: flex;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
display: ${({ isNftActive }) => isNftActive && 'none'};
}
`
const Text = styled.p`
@@ -185,6 +203,7 @@ function Web3StatusInner() {
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
const isNftActive = useNftFlag() === NftVariant.Enabled
const allTransactions = useAllTransactions()
@@ -214,8 +233,14 @@ function Web3StatusInner() {
...CHEVRON_PROPS,
color: theme.textSecondary,
}
return (
<Web3StatusConnected data-testid="web3-status-connected" onClick={toggleWallet} pending={hasPendingTransactions}>
<Web3StatusConnected
data-testid="web3-status-connected"
isNftActive={isNftActive}
onClick={toggleWallet}
pending={hasPendingTransactions}
>
{!hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
{hasPendingTransactions ? (
<RowBetween>
@@ -225,10 +250,10 @@ function Web3StatusInner() {
<Loader stroke="white" />
</RowBetween>
) : (
<>
<AddressAndChevronContainer isNftActive={isNftActive}>
<Text>{ENSName || shortenAddress(account)}</Text>
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
</>
</AddressAndChevronContainer>
)}
</Web3StatusConnected>
)
@@ -240,7 +265,7 @@ function Web3StatusInner() {
}
return (
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
name={EventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: validSwapQuote }}
element={ElementName.CONNECT_WALLET_BUTTON}

View File

@@ -1,3 +1,5 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { EventName, SectionName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk'
import { Currency, TradeType } from '@uniswap/sdk-core'
import {
@@ -8,9 +10,7 @@ import {
SwapWidgetSkeleton,
} from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, SectionName, SWAP_PRICE_UPDATE_USER_RESPONSE } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import { useActiveLocale } from 'hooks/useActiveLocale'
import {
formatPercentInBasisPointsNumber,
formatSwapQuoteReceivedEventProperties,
@@ -18,8 +18,7 @@ import {
getDurationFromDateMilliseconds,
getPriceUpdateBasisPoints,
getTokenAddress,
} from 'analytics/utils'
import { useActiveLocale } from 'hooks/useActiveLocale'
} from 'lib/utils/analytics'
import { useCallback, useState } from 'react'
import { useIsDarkMode } from 'state/user/hooks'
import { DARK_THEME, LIGHT_THEME } from 'theme/widget'
@@ -94,7 +93,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
(stale: Trade<Currency, Currency, TradeType>, update: Trade<Currency, Currency, TradeType>) => {
const eventProperties = {
chain_id: update.inputAmount.currency.chainId,
response: SWAP_PRICE_UPDATE_USER_RESPONSE.ACCEPTED,
response: SwapPriceUpdateUserResponse.ACCEPTED,
token_in_symbol: update.inputAmount.currency.symbol,
token_out_symbol: update.outputAmount.currency.symbol,
price_update_basis_points: getPriceUpdateBasisPoints(stale.executionPrice, update.executionPrice),

View File

@@ -1,7 +1,6 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { EventName, SectionName } from '@uniswap/analytics-events'
import { Currency, Field, SwapController, SwapEventHandlers, TradeType } from '@uniswap/widgets'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, SectionName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
import { useCallback, useEffect, useMemo, useState } from 'react'
@@ -48,7 +47,7 @@ export function useSyncWidgetInputs({
if (origin === 'max') {
sendAnalyticsEvent(EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED, { ...trace })
}
setType(toTradeType(field))
setType(field === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
setAmount(amount)
},
[trace]
@@ -71,20 +70,34 @@ export function useSyncWidgetInputs({
}, [])
const onTokenSelect = useCallback(
(token: Currency) => {
(selectingToken: Currency) => {
if (selectingField === undefined) return
setType(toTradeType(selectingField))
const otherField = invertField(selectingField)
let otherToken = tokens[otherField]
otherToken = otherToken?.equals(token) ? tokens[selectingField] : otherToken
const update = {
[selectingField]: token,
[otherField]: otherToken,
const isFlip = tokens[otherField]?.equals(selectingToken)
const update: SwapTokens = {
[selectingField]: selectingToken,
[otherField]: isFlip ? tokens[selectingField] : tokens[otherField],
default: tokens.default,
}
setType((type) => {
// If flipping the tokens, also flip the type/amount.
if (isFlip) {
return invertTradeType(type)
}
// Setting a new token should clear its amount, if it is set.
const activeField = type === TradeType.EXACT_INPUT ? Field.INPUT : Field.OUTPUT
if (selectingField === activeField) {
setAmount(() => EMPTY_AMOUNT)
}
return type
})
if (!includesDefaultToken(update)) {
onTokenChange?.(update[Field.OUTPUT] || update[Field.INPUT] || token)
onTokenChange?.(update[Field.OUTPUT] || selectingToken)
}
setTokens(update)
},
@@ -127,16 +140,6 @@ function invertField(field: Field) {
}
}
// TODO(zzmp): Move to @uniswap/widgets.
function toTradeType(modifiedField: Field) {
switch (modifiedField) {
case Field.INPUT:
return TradeType.EXACT_INPUT
case Field.OUTPUT:
return TradeType.EXACT_OUTPUT
}
}
// TODO(zzmp): Include in @uniswap/sdk-core (on TradeType, if possible).
function invertTradeType(tradeType: TradeType) {
switch (tradeType) {

View File

@@ -1,3 +1,5 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { EventName, SectionName } from '@uniswap/analytics-events'
import {
TradeType,
Transaction,
@@ -6,12 +8,8 @@ import {
TransactionType as WidgetTransactionType,
} from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, SectionName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import { formatToDecimal, getTokenAddress } from 'analytics/utils'
import { formatSwapSignedAnalyticsEventProperties } from 'analytics/utils'
import { WrapType } from 'hooks/useWrapCallback'
import { formatSwapSignedAnalyticsEventProperties, formatToDecimal, getTokenAddress } from 'lib/utils/analytics'
import { useCallback, useMemo } from 'react'
import { useTransactionAdder } from 'state/transactions/hooks'
import {

View File

@@ -1,11 +1,9 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace } from '@uniswap/analytics'
import { EventName, ModalName } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { sendAnalyticsEvent } from 'analytics'
import { ModalName } from 'analytics/constants'
import { EventName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { formatSwapSignedAnalyticsEventProperties } from 'analytics/utils'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import AnimatedDropdown from 'components/AnimatedDropdown'
import Card, { OutlineCard } from 'components/Card'
import { AutoColumn } from 'components/Column'
@@ -131,7 +131,7 @@ export default function SwapDetailsDropdown({
<Wrapper style={{ marginTop: '0' }}>
<AutoColumn gap={'8px'} style={{ width: '100%', marginBottom: '-8px' }}>
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
name={EventName.SWAP_DETAILS_EXPANDED}
element={ElementName.SWAP_DETAILS_DROPDOWN}
shouldLogImpression={!showDetails}

View File

@@ -1,7 +1,8 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import {
formatPercentInBasisPointsNumber,
formatPercentNumber,
@@ -9,8 +10,7 @@ import {
getDurationFromDateMilliseconds,
getDurationUntilTimestampSeconds,
getTokenAddress,
} from 'analytics/utils'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
} from 'lib/utils/analytics'
import { ReactNode } from 'react'
import { Text } from 'rebass'
import { InterfaceTrade } from 'state/routing/types'
@@ -130,7 +130,7 @@ export default function SwapModalFooter({
<>
<AutoRow>
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
element={ElementName.CONFIRM_SWAP_BUTTON}
name={EventName.SWAP_SUBMITTED_BUTTON_CLICKED}
properties={formatAnalyticsEventProperties({

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, SWAP_PRICE_UPDATE_USER_RESPONSE } from 'analytics/constants'
import { getPriceUpdateBasisPoints } from 'analytics/utils'
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
import { useEffect, useState } from 'react'
import { AlertTriangle, ArrowDown } from 'react-feather'
import { Text } from 'rebass'
@@ -44,7 +44,7 @@ const ArrowWrapper = styled.div`
const formatAnalyticsEventProperties = (
trade: InterfaceTrade<Currency, Currency, TradeType>,
priceUpdate: number | undefined,
response: SWAP_PRICE_UPDATE_USER_RESPONSE
response: SwapPriceUpdateUserResponse
) => ({
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
@@ -93,7 +93,7 @@ export default function SwapModalHeader({
if (shouldLogModalCloseEvent && showAcceptChanges)
sendAnalyticsEvent(
EventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED,
formatAnalyticsEventProperties(trade, priceUpdate, SWAP_PRICE_UPDATE_USER_RESPONSE.REJECTED)
formatAnalyticsEventProperties(trade, priceUpdate, SwapPriceUpdateUserResponse.REJECTED)
)
setShouldLogModalCloseEvent(false)
}, [shouldLogModalCloseEvent, showAcceptChanges, setShouldLogModalCloseEvent, trade, priceUpdate])

View File

@@ -1,11 +1,11 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { AutoColumn } from 'components/Column'
import { LoadingRows } from 'components/Loader/styled'
@@ -65,7 +65,7 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
return (
<Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen}>
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
name={EventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED}
element={ElementName.AUTOROUTER_VISUALIZATION_ROW}
shouldLogImpression={!open}

View File

@@ -3,10 +3,16 @@ import { deepCopy } from '@ethersproject/properties'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { isPlain } from '@reduxjs/toolkit'
import ms from 'ms.macro'
import { SupportedChainId } from './chains'
import { CHAIN_IDS_TO_NAMES, SupportedChainId } from './chains'
import { RPC_URLS } from './networks'
// NB: Third-party providers (eg MetaMask) will have their own polling intervals,
// which should be left as-is to allow operations (eg transaction confirmation) to resolve faster.
// Network providers (eg AppJsonRpcProvider) need to update less frequently to be considered responsive.
export const POLLING_INTERVAL = ms`12s` // mainnet block frequency - ok for other chains as it is a sane refresh rate
class AppJsonRpcProvider extends StaticJsonRpcProvider {
private _blockCache = new Map<string, Promise<any>>()
get blockCache() {
@@ -18,8 +24,10 @@ class AppJsonRpcProvider extends StaticJsonRpcProvider {
return this._blockCache
}
constructor(urls: string[]) {
super(urls[0])
constructor(chainId: SupportedChainId) {
// Including networkish allows ethers to skip the initial detectNetwork call.
super(RPC_URLS[chainId][0], /* networkish= */ { chainId, name: CHAIN_IDS_TO_NAMES[chainId] })
this.pollingInterval = POLLING_INTERVAL
}
send(method: string, params: Array<any>): Promise<any> {
@@ -50,17 +58,17 @@ class AppJsonRpcProvider extends StaticJsonRpcProvider {
* These are the only JsonRpcProviders used directly by the interface.
*/
export const RPC_PROVIDERS: { [key in SupportedChainId]: StaticJsonRpcProvider } = {
[SupportedChainId.MAINNET]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.MAINNET]),
[SupportedChainId.RINKEBY]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.RINKEBY]),
[SupportedChainId.ROPSTEN]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.ROPSTEN]),
[SupportedChainId.GOERLI]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.GOERLI]),
[SupportedChainId.KOVAN]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.KOVAN]),
[SupportedChainId.OPTIMISM]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.OPTIMISM]),
[SupportedChainId.OPTIMISM_GOERLI]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.OPTIMISM_GOERLI]),
[SupportedChainId.ARBITRUM_ONE]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.ARBITRUM_ONE]),
[SupportedChainId.ARBITRUM_RINKEBY]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.ARBITRUM_RINKEBY]),
[SupportedChainId.POLYGON]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.POLYGON]),
[SupportedChainId.POLYGON_MUMBAI]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.POLYGON_MUMBAI]),
[SupportedChainId.CELO]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.CELO]),
[SupportedChainId.CELO_ALFAJORES]: new AppJsonRpcProvider(RPC_URLS[SupportedChainId.CELO_ALFAJORES]),
[SupportedChainId.MAINNET]: new AppJsonRpcProvider(SupportedChainId.MAINNET),
[SupportedChainId.RINKEBY]: new AppJsonRpcProvider(SupportedChainId.RINKEBY),
[SupportedChainId.ROPSTEN]: new AppJsonRpcProvider(SupportedChainId.ROPSTEN),
[SupportedChainId.GOERLI]: new AppJsonRpcProvider(SupportedChainId.GOERLI),
[SupportedChainId.KOVAN]: new AppJsonRpcProvider(SupportedChainId.KOVAN),
[SupportedChainId.OPTIMISM]: new AppJsonRpcProvider(SupportedChainId.OPTIMISM),
[SupportedChainId.OPTIMISM_GOERLI]: new AppJsonRpcProvider(SupportedChainId.OPTIMISM_GOERLI),
[SupportedChainId.ARBITRUM_ONE]: new AppJsonRpcProvider(SupportedChainId.ARBITRUM_ONE),
[SupportedChainId.ARBITRUM_RINKEBY]: new AppJsonRpcProvider(SupportedChainId.ARBITRUM_RINKEBY),
[SupportedChainId.POLYGON]: new AppJsonRpcProvider(SupportedChainId.POLYGON),
[SupportedChainId.POLYGON_MUMBAI]: new AppJsonRpcProvider(SupportedChainId.POLYGON_MUMBAI),
[SupportedChainId.CELO]: new AppJsonRpcProvider(SupportedChainId.CELO),
[SupportedChainId.CELO_ALFAJORES]: new AppJsonRpcProvider(SupportedChainId.CELO_ALFAJORES),
}

View File

@@ -3,5 +3,4 @@ export enum FeatureFlag {
nft = 'nfts',
traceJsonRpc = 'traceJsonRpc',
multiNetworkBalances = 'multiNetworkBalances',
nftGraphQl = 'nftGraphQl',
}

View File

@@ -1,7 +0,0 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useNftGraphQlFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.nftGraphQl)
}
export { BaseVariant as NftGraphQlVariant }

View File

@@ -1,38 +1,16 @@
import ms from 'ms.macro'
import { Variables } from 'react-relay'
import { Environment, Network, RecordSource, RequestParameters, Store } from 'relay-runtime'
import RelayQueryResponseCache from 'relay-runtime/lib/network/RelayQueryResponseCache'
import { Environment, Network, RecordSource, Store } from 'relay-runtime'
import fetchGraphQL from './fetchGraphQL'
// max number of request in cache, least-recently updated entries purged first
const size = 250
// number in milliseconds, how long records stay valid in cache
const ttl = ms`5m`
export const cache = new RelayQueryResponseCache({ size, ttl })
const fetchQuery = async function wrappedFetchQuery(params: RequestParameters, variables: Variables) {
const queryID = params.name
const cachedData = cache.get(queryID, variables)
// This makes it possible (and more likely) to be able to reuse data when navigating back to a page,
// tab or piece of content that has been visited before. These settings together configure the cache
// to serve the last 250 records, so long as they are less than 5 minutes old:
const gcReleaseBufferSize = 250
const queryCacheExpirationTime = ms`5m`
if (cachedData !== null) return cachedData
return fetchGraphQL(params, variables).then((data) => {
if (params.operationKind !== 'mutation') {
cache.set(queryID, variables, data)
}
return data
})
}
// This property tells Relay to not immediately clear its cache when the user
// navigates around the app. Relay will hold onto the specified number of
// query results, allowing the user to return to recently visited pages
// and reusing cached data if its available/fresh.
const gcReleaseBufferSize = 10
const queryCacheExpirationTime = ms`1m`
const store = new Store(new RecordSource(), { gcReleaseBufferSize, queryCacheExpirationTime })
const network = Network.create(fetchQuery)
// Export a singleton instance of Relay Environment configured with our network function:
export default new Environment({
network,
store,
export const CachingEnvironment = new Environment({
network: Network.create(fetchGraphQL),
store: new Store(new RecordSource(), { gcReleaseBufferSize, queryCacheExpirationTime }),
})
export default CachingEnvironment

View File

@@ -1,11 +1,8 @@
import graphql from 'babel-plugin-relay/macro'
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
import { useMemo } from 'react'
import { useLazyLoadQuery } from 'react-relay'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { Chain } from './__generated__/TokenPriceQuery.graphql'
import { TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { CHAIN_NAME_TO_CHAIN_ID } from './util'
/*
@@ -16,7 +13,7 @@ The difference between Token and TokenProject:
TokenMarket is per-chain market data for contracts pulled from the graph.
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/
const tokenQuery = graphql`
export const tokenQuery = graphql`
query TokenQuery($contract: ContractInput!) {
tokens(contracts: [$contract]) {
id @required(action: LOG)
@@ -58,15 +55,10 @@ const tokenQuery = graphql`
}
}
`
export type { Chain, ContractInput, TokenQuery } from './__generated__/TokenQuery.graphql'
export type TokenQueryData = NonNullable<TokenQuery$data['tokens']>[number]
export function useTokenQuery(address: string, chain: Chain): TokenQueryData | undefined {
const contract = useMemo(() => ({ address: address.toLowerCase(), chain }), [address, chain])
const token = useLazyLoadQuery<TokenQuery>(tokenQuery, { contract }).tokens?.[0]
return token
}
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo {
constructor(data: NonNullable<TokenQueryData>) {

View File

@@ -1,84 +1,19 @@
import graphql from 'babel-plugin-relay/macro'
import { useEffect, useMemo, useState } from 'react'
import { fetchQuery } from 'react-relay'
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import environment from './RelayEnvironment'
import { TimePeriod } from './util'
const tokenPriceQuery = graphql`
query TokenPriceQuery($contract: ContractInput!) {
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
export const tokenPriceQuery = graphql`
query TokenPriceQuery($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) {
market(currency: USD) {
priceHistory1H: priceHistory(duration: HOUR) {
timestamp
value
market(currency: USD) @required(action: LOG) {
price {
value @required(action: LOG)
}
priceHistory1D: priceHistory(duration: DAY) {
timestamp
value
}
priceHistory1W: priceHistory(duration: WEEK) {
timestamp
value
}
priceHistory1M: priceHistory(duration: MONTH) {
timestamp
value
}
priceHistory1Y: priceHistory(duration: YEAR) {
timestamp
value
priceHistory(duration: $duration) {
timestamp @required(action: LOG)
value @required(action: LOG)
}
}
}
}
`
export type PricePoint = { timestamp: number; value: number }
export type PriceDurations = Partial<Record<TimePeriod, PricePoint[]>>
export function isPricePoint(p: { timestamp: number; value: number | null } | null): p is PricePoint {
return Boolean(p && p.value)
}
export function useTokenPriceQuery(address: string, chain: Chain): PriceDurations | undefined {
const contract = useMemo(() => ({ address: address.toLowerCase(), chain }), [address, chain])
const [prices, setPrices] = useState<PriceDurations>()
useEffect(() => {
const subscription = fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, { contract }).subscribe({
next: (response: TokenPriceQuery['response']) => {
const priceData = response.tokens?.[0]?.market
const prices = {
[TimePeriod.HOUR]: priceData?.priceHistory1H?.filter(isPricePoint),
[TimePeriod.DAY]: priceData?.priceHistory1D?.filter(isPricePoint),
[TimePeriod.WEEK]: priceData?.priceHistory1W?.filter(isPricePoint),
[TimePeriod.MONTH]: priceData?.priceHistory1M?.filter(isPricePoint),
[TimePeriod.YEAR]: priceData?.priceHistory1Y?.filter(isPricePoint),
}
// Ensure the latest price available is available for every TimePeriod.
const latests = Object.values(prices)
.map((prices) => prices?.slice(-1)?.[0] ?? null)
.filter(isPricePoint)
if (latests.length) {
const latest = latests.reduce((latest, pricePoint) =>
latest.timestamp > pricePoint.timestamp ? latest : pricePoint
)
Object.values(prices)
.filter((prices) => prices && prices.slice(-1)[0] !== latest)
.forEach((prices) => prices?.push(latest))
}
setPrices(prices)
},
})
return () => {
setPrices(undefined)
subscription.unsubscribe()
}
}, [contract])
return prices
}
export type { TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'

View File

@@ -12,7 +12,7 @@ import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import type { Chain, TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { TopTokensSparklineQuery } from './__generated__/TopTokensSparklineQuery.graphql'
import { isPricePoint, PricePoint } from './TokenPrice'
import { isPricePoint, PricePoint } from './util'
import { CHAIN_NAME_TO_CHAIN_ID, toHistoryDuration, unwrapToken } from './util'
const topTokens100Query = graphql`
@@ -54,8 +54,8 @@ const tokenSparklineQuery = graphql`
address
market(currency: USD) {
priceHistory(duration: $duration) {
timestamp
value
timestamp @required(action: LOG)
value @required(action: LOG)
}
}
}

View File

@@ -13,13 +13,21 @@ const baseHeaders = {
}
const nftHeaders = {
'Content-Type': 'application/json',
'from-x-api-key': process.env.REACT_APP_NFT_FROM_AWS_X_API_KEY ?? '',
'x-api-key': process.env.REACT_APP_NFT_AWS_X_API_KEY ?? '',
}
// The issue below prevented using a custom var in metadata to gate which queries are for the nft endpoint vs base endpoint
// This is a temporary solution before the two endpoints merge
// https://github.com/relay-tools/relay-hooks/issues/215
const NFT_QUERIES = ['AssetQuery', 'AssetPaginationQuery', 'CollectionQuery', 'DetailsQuery']
const NFT_QUERIES = [
'AssetQuery',
'AssetPaginationQuery',
'CollectionQuery',
'DetailsQuery',
'NftBalanceQuery',
'NftBalancePaginationQuery',
]
const fetchQuery = (params: RequestParameters, variables: Variables): Promise<GraphQLResponse> => {
const isNFT = NFT_QUERIES.includes(params.name)

View File

@@ -1,10 +1,21 @@
import graphql from 'babel-plugin-relay/macro'
import { parseEther } from 'ethers/lib/utils'
import { GenieAsset, Rarity, SellOrder } from 'nft/types'
import { useLazyLoadQuery, usePaginationFragment } from 'react-relay'
import useInterval from 'lib/hooks/useInterval'
import ms from 'ms.macro'
import { GenieAsset, Trait } from 'nft/types'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, usePaginationFragment, useQueryLoader, useRelayEnvironment } from 'react-relay'
import { AssetPaginationQuery } from './__generated__/AssetPaginationQuery.graphql'
import { AssetQuery, NftAssetsFilterInput, NftAssetSortableField } from './__generated__/AssetQuery.graphql'
import {
AssetQuery,
AssetQuery$variables,
NftAssetsFilterInput,
NftAssetSortableField,
NftAssetTraitInput,
NftMarketplace,
} from './__generated__/AssetQuery.graphql'
import { AssetQuery_nftAssets$data } from './__generated__/AssetQuery_nftAssets.graphql'
const assetPaginationQuery = graphql`
fragment AssetQuery_nftAssets on Query @refetchable(queryName: "AssetPaginationQuery") {
@@ -88,6 +99,7 @@ const assetPaginationQuery = graphql`
metadataUrl
}
}
totalCount
}
}
`
@@ -107,84 +119,191 @@ const assetQuery = graphql`
}
`
export function useAssetsQuery(
address: string,
orderBy: NftAssetSortableField,
asc: boolean,
filter: NftAssetsFilterInput,
first?: number,
after?: string,
last?: number,
type NftAssetsQueryAsset = NonNullable<
NonNullable<NonNullable<AssetQuery_nftAssets$data['nftAssets']>['edges']>[number]
>
function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: number) {
const asset = queryAsset.node
const ethPrice = parseEther(
asset.listings?.edges[0]?.node.price.value?.toLocaleString('fullwide', { useGrouping: false }) ?? '0'
).toString()
return {
id: asset.id,
address: asset?.collection?.nftContracts?.[0]?.address,
notForSale: asset.listings?.edges?.length === 0,
collectionName: asset.collection?.name,
collectionSymbol: asset.collection?.image?.url,
imageUrl: asset.image?.url,
animationUrl: asset.animationUrl,
marketplace: asset.listings?.edges[0]?.node?.marketplace?.toLowerCase(),
name: asset.name,
priceInfo: asset.listings
? {
ETHPrice: ethPrice,
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: ethPrice,
}
: undefined,
susFlag: asset.suspiciousFlag,
sellorders: asset.listings?.edges.map((listingNode) => {
return {
...listingNode.node,
protocolParameters: listingNode.node?.protocolParameters
? JSON.parse(listingNode.node?.protocolParameters.toString())
: undefined,
}
}),
smallImageUrl: asset.smallImage?.url,
tokenId: asset.tokenId,
tokenType: asset.collection?.nftContracts?.[0]?.standard,
totalCount,
collectionIsVerified: asset.collection?.isVerified,
rarity: {
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
providers: asset.rarities?.map((rarity) => {
return {
...rarity,
provider: 'Rarity Sniper',
}
}),
},
owner: asset.ownerAddress,
creator: {
profile_img_url: asset.collection?.creator?.profileImage?.url,
address: asset.collection?.creator?.address,
},
metadataUrl: asset.metadataUrl,
}
}
export const ASSET_PAGE_SIZE = 25
export interface AssetFetcherParams {
address: string
orderBy: NftAssetSortableField
asc: boolean
filter: NftAssetsFilterInput
first?: number
after?: string
last?: number
before?: string
) {
const queryData = useLazyLoadQuery<AssetQuery>(assetQuery, {
address,
orderBy,
asc,
filter,
first,
after,
last,
before,
})
}
const defaultAssetFetcherParams: Omit<AssetQuery$variables, 'address'> = {
orderBy: 'PRICE',
asc: true,
// tokenSearchQuery must be specified so that this exactly matches the initial query.
filter: { listed: false, tokenSearchQuery: '' },
first: ASSET_PAGE_SIZE,
}
export function useLoadAssetsQuery(address?: string) {
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery)
useEffect(() => {
if (address) {
loadQuery({ ...defaultAssetFetcherParams, address })
}
}, [address, loadQuery])
}
export function useLazyLoadAssetsQuery(params: AssetFetcherParams) {
const vars = useMemo(() => ({ ...defaultAssetFetcherParams, ...params }), [params])
const [fetchKey, setFetchKey] = useState(0)
// Use the store if it is available (eg from polling), or the network if it is not (eg from an incorrect preload).
const fetchPolicy = 'store-or-network'
const queryData = useLazyLoadQuery<AssetQuery>(assetQuery, vars, { fetchKey, fetchPolicy }) // this will suspend if not yet loaded
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<AssetPaginationQuery, any>(
assetPaginationQuery,
queryData
)
const assets: GenieAsset[] = data.nftAssets?.edges?.map((queryAsset: { node: any }) => {
const asset = queryAsset.node
const ethPrice = parseEther(
asset.listings?.edges[0]?.node.price.value?.toLocaleString('fullwide', { useGrouping: false }) ?? '0'
).toString()
return {
id: asset.id,
address: asset.collection.nftContracts[0]?.address,
notForSale: asset.listings?.edges.length === 0,
collectionName: asset.collection?.name,
collectionSymbol: asset.collection?.image?.url,
imageUrl: asset.image?.url,
animationUrl: asset.animationUrl,
marketplace: asset.listings?.edges[0]?.node.marketplace.toLowerCase(),
name: asset.name,
priceInfo: asset.listings
? {
ETHPrice: ethPrice,
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: ethPrice,
}
: undefined,
susFlag: asset.suspiciousFlag,
sellorders: asset.listings?.edges.map((listingNode: { node: SellOrder }) => {
return {
...listingNode.node,
protocolParameters: listingNode.node.protocolParameters
? JSON.parse(listingNode.node.protocolParameters.toString())
: undefined,
}
// Poll for updates.
const POLLING_INTERVAL = ms`5s`
const environment = useRelayEnvironment()
const poll = useCallback(async () => {
if (data.nftAssets?.edges?.length > ASSET_PAGE_SIZE) return
// Initiate a network request. When it resolves, refresh the UI from store (to avoid re-triggering Suspense);
// see: https://relay.dev/docs/guided-tour/refetching/refreshing-queries/#if-you-need-to-avoid-suspense-1.
await fetchQuery<AssetQuery>(environment, assetQuery, { ...vars }).toPromise()
setFetchKey((fetchKey) => fetchKey + 1)
}, [data.nftAssets?.edges?.length, environment, vars])
useInterval(poll, isLoadingNext ? null : POLLING_INTERVAL, /* leading= */ false)
// It is especially important for this to be memoized to avoid re-rendering from polling if data is unchanged.
const assets: GenieAsset[] = useMemo(
() =>
data.nftAssets?.edges?.map((queryAsset: NftAssetsQueryAsset) => {
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount)
}),
smallImageUrl: asset.smallImage?.url,
tokenId: asset.tokenId,
tokenType: asset.collection.nftContracts[0]?.standard,
// totalCount?: number, // TODO waiting for BE changes
collectionIsVerified: asset.collection?.isVerified,
rarity: {
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
providers: asset.rarities.map((rarity: Rarity) => {
return {
...rarity,
provider: 'Rarity Sniper',
}
}),
},
owner: asset.ownerAddress,
creator: {
profile_img_url: asset.collection?.creator?.profileImage?.url,
address: asset.collection?.creator?.address,
},
metadataUrl: asset.metadataUrl,
}
})
[data.nftAssets?.edges, data.nftAssets?.totalCount]
)
return { assets, hasNext, isLoadingNext, loadNext }
}
const DEFAULT_SWEEP_AMOUNT = 50
export interface SweepFetcherParams {
contractAddress: string
markets?: string[]
price?: { high?: number | string; low?: number | string; symbol: string }
traits?: Trait[]
}
function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepFetcherParams): AssetQuery$variables {
const filter: NftAssetsFilterInput = useMemo(
() => ({
listed: true,
maxPrice: price?.high?.toString(),
minPrice: price?.low?.toString(),
traits:
traits && traits.length > 0
? traits?.map((trait) => {
return { name: trait.trait_type, values: [trait.trait_value] } as unknown as NftAssetTraitInput
})
: undefined,
marketplaces:
markets && markets.length > 0 ? markets?.map((market) => market.toUpperCase() as NftMarketplace) : undefined,
}),
[markets, price?.high, price?.low, traits]
)
return useMemo(
() => ({
address: contractAddress,
orderBy: 'PRICE',
asc: true,
first: DEFAULT_SWEEP_AMOUNT,
filter,
}),
[contractAddress, filter]
)
}
export function useLoadSweepAssetsQuery(params: SweepFetcherParams, enabled = true) {
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery)
const vars = useSweepFetcherVars(params)
useEffect(() => {
if (enabled) {
loadQuery(vars)
}
}, [loadQuery, enabled, vars])
}
// Lazy-loads an already loaded AssetsQuery.
// This will *not* trigger a query - that must be done from a parent component to ensure proper query coalescing and to
// prevent waterfalling. Use useLoadSweepAssetsQuery to trigger the query.
export function useLazyLoadSweepAssetsQuery(params: SweepFetcherParams): GenieAsset[] {
const vars = useSweepFetcherVars(params)
const queryData = useLazyLoadQuery(assetQuery, vars, { fetchPolicy: 'store-only' }) // this will suspend if not yet loaded
const { data } = usePaginationFragment<AssetPaginationQuery, any>(assetPaginationQuery, queryData)
return useMemo<GenieAsset[]>(
() =>
data.nftAssets?.edges?.map((queryAsset: NftAssetsQueryAsset) => {
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount)
}),
[data.nftAssets?.edges, data.nftAssets?.totalCount]
)
}

View File

@@ -1,12 +1,13 @@
import graphql from 'babel-plugin-relay/macro'
import { GenieCollection, Trait } from 'nft/types'
import { useLazyLoadQuery } from 'react-relay'
import { useEffect } from 'react'
import { useLazyLoadQuery, useQueryLoader } from 'react-relay'
import { CollectionQuery } from './__generated__/CollectionQuery.graphql'
const collectionQuery = graphql`
query CollectionQuery($address: String!) {
nftCollections(filter: { addresses: [$address] }) {
query CollectionQuery($addresses: [String!]!) {
nftCollections(filter: { addresses: $addresses }) {
edges {
cursor
node {
@@ -68,6 +69,11 @@ const collectionQuery = graphql`
value
currency
}
marketplaces {
marketplace
listings
floorPrice
}
}
}
}
@@ -81,19 +87,36 @@ const collectionQuery = graphql`
}
`
export function useLoadCollectionQuery(address?: string | string[]): void {
const [, loadQuery] = useQueryLoader(collectionQuery)
useEffect(() => {
if (address) {
loadQuery({ addresses: Array.isArray(address) ? address : [address] })
}
}, [address, loadQuery])
}
// Lazy-loads an already loaded CollectionQuery.
// This will *not* trigger a query - that must be done from a parent component to ensure proper query coalescing and to
// prevent waterfalling. Use useLoadCollectionQuery to trigger the query.
export function useCollectionQuery(address: string): GenieCollection | undefined {
const queryData = useLazyLoadQuery<CollectionQuery>(collectionQuery, { address })
const queryData = useLazyLoadQuery<CollectionQuery>( // this will suspend if not yet loaded
collectionQuery,
{ addresses: [address] },
{ fetchPolicy: 'store-or-network' }
)
const queryCollection = queryData.nftCollections?.edges[0]?.node
const market = queryCollection?.markets && queryCollection?.markets[0]
const traits = {} as Record<string, Trait[]>
if (queryCollection?.traits) {
queryCollection?.traits.forEach((trait) => {
if (trait.name && trait.values) {
traits[trait.name] = trait.values.map((value) => {
if (trait.name && trait.stats) {
traits[trait.name] = trait.stats.map((stats) => {
return {
trait_type: trait.name,
trait_value: value,
trait_type: stats.name,
trait_value: stats.value,
trait_count: stats.assets,
} as Trait
})
}
@@ -105,7 +128,7 @@ export function useCollectionQuery(address: string): GenieCollection | undefined
name: queryCollection?.name ?? undefined,
description: queryCollection?.description ?? undefined,
standard: queryCollection?.nftContracts ? queryCollection?.nftContracts[0]?.standard ?? undefined : undefined,
bannerImageUrl: queryCollection?.bannerImage?.url,
bannerImageUrl: queryCollection?.bannerImage?.url ?? undefined,
stats: queryCollection?.markets
? {
num_owners: market?.owners ?? undefined,
@@ -120,7 +143,15 @@ export function useCollectionQuery(address: string): GenieCollection | undefined
}
: {},
traits,
// marketplaceCount: { marketplace: string; count: number }[], // TODO add when backend supports
marketplaceCount: queryCollection?.markets
? market?.marketplaces?.map((market) => {
return {
marketplace: market.marketplace?.toLowerCase() ?? '',
count: market.listings ?? 0,
floorPrice: market.floorPrice ?? 0,
}
})
: undefined,
imageUrl: queryCollection?.image?.url ?? '',
twitterUrl: queryCollection?.twitterName ?? '',
instagram: queryCollection?.instagramName ?? undefined,

View File

@@ -37,6 +37,9 @@ const detailsQuery = graphql`
name
isVerified
numAssets
twitterName
discordUrl
homepageUrl
image {
url
}
@@ -96,7 +99,8 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
const asset = queryData.nftAssets?.edges[0]?.node
const collection = asset?.collection
const ethPrice = parseEther(asset?.listings?.edges[0].node.price.value?.toString() ?? '0').toString()
const listing = asset?.listings?.edges[0]?.node
const ethPrice = parseEther(listing?.price?.value?.toString() ?? '0').toString()
return [
{
@@ -104,10 +108,11 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
address,
notForSale: asset?.listings === null,
collectionName: asset?.collection?.name ?? undefined,
collectionSymbol: asset?.collection?.image?.url,
imageUrl: asset?.image?.url,
collectionSymbol: asset?.collection?.image?.url ?? undefined,
imageUrl: asset?.image?.url ?? undefined,
animationUrl: asset?.animationUrl ?? undefined,
marketplace: asset?.listings?.edges[0]?.node.marketplace.toLowerCase() as any,
// todo: fix the back/frontend discrepency here and drop the any
marketplace: listing?.marketplace.toLowerCase() as any,
name: asset?.name ?? undefined,
priceInfo: {
ETHPrice: ethPrice,
@@ -124,7 +129,7 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
: undefined,
} as SellOrder
}),
smallImageUrl: asset?.smallImage?.url,
smallImageUrl: asset?.smallImage?.url ?? undefined,
tokenId,
tokenType: (asset?.collection?.nftContracts && asset?.collection.nftContracts[0]?.standard) as TokenType,
collectionIsVerified: asset?.collection?.isVerified ?? undefined,
@@ -151,11 +156,14 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
}),
},
{
collectionDescription: collection?.description,
collectionImageUrl: collection?.image?.url,
collectionDescription: collection?.description ?? undefined,
collectionImageUrl: collection?.image?.url ?? undefined,
collectionName: collection?.name ?? undefined,
isVerified: collection?.isVerified ?? undefined,
totalSupply: collection?.numAssets ?? undefined,
twitterUrl: collection?.twitterName ?? undefined,
discordUrl: collection?.discordUrl ?? undefined,
externalUrl: collection?.homepageUrl ?? undefined,
},
]
}

View File

@@ -0,0 +1,181 @@
import graphql from 'babel-plugin-relay/macro'
import { parseEther } from 'ethers/lib/utils'
import { WalletAsset } from 'nft/types'
import { useLazyLoadQuery, usePaginationFragment } from 'react-relay'
import { NftBalancePaginationQuery } from './__generated__/NftBalancePaginationQuery.graphql'
import { NftBalanceQuery } from './__generated__/NftBalanceQuery.graphql'
import { NftBalanceQuery_nftBalances$data } from './__generated__/NftBalanceQuery_nftBalances.graphql'
const nftBalancePaginationQuery = graphql`
fragment NftBalanceQuery_nftBalances on Query @refetchable(queryName: "NftBalancePaginationQuery") {
nftBalances(
ownerAddress: $ownerAddress
filter: $filter
first: $first
after: $after
last: $last
before: $before
) @connection(key: "NftBalanceQuery_nftBalances") {
edges {
node {
ownedAsset {
id
animationUrl
collection {
isVerified
image {
url
}
name
nftContracts {
address
chain
name
standard
symbol
totalSupply
}
markets(currencies: ETH) {
floorPrice {
value
}
}
}
description
flaggedBy
image {
url
}
originalImage {
url
}
name
ownerAddress
smallImage {
url
}
suspiciousFlag
tokenId
thumbnail {
url
}
listings(first: 1) {
edges {
node {
price {
value
currency
}
createdAt
marketplace
}
}
}
}
listedMarketplaces
listingFees {
payoutAddress
basisPoints
}
lastPrice {
currency
timestamp
value
}
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`
const nftBalanceQuery = graphql`
query NftBalanceQuery(
$ownerAddress: String!
$filter: NftBalancesFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
...NftBalanceQuery_nftBalances
}
`
type NftBalanceQueryAsset = NonNullable<
NonNullable<NonNullable<NftBalanceQuery_nftBalances$data['nftBalances']>['edges']>[number]
>
//
// export type TokenQueryData = NonNullable<TokenQuery$data['tokens']>[number]
export function useNftBalanceQuery(
ownerAddress: string,
collectionFilters?: string[],
first?: number,
after?: string,
last?: number,
before?: string
) {
const queryData = useLazyLoadQuery<NftBalanceQuery>(nftBalanceQuery, {
ownerAddress,
filter: {
addresses: collectionFilters,
},
first,
after,
last,
before,
})
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<NftBalancePaginationQuery, any>(
nftBalancePaginationQuery,
queryData
)
const walletAssets: WalletAsset[] = data.nftBalances?.edges?.map((queryAsset: NftBalanceQueryAsset) => {
const asset = queryAsset.node.ownedAsset
const ethPrice = parseEther(
asset?.listings?.edges[0]?.node.price.value?.toLocaleString('fullwide', { useGrouping: false }) ?? '0'
).toString()
return {
id: asset?.id,
imageUrl: asset?.image?.url,
smallImageUrl: asset?.smallImage?.url,
notForSale: asset?.listings?.edges?.length === 0,
animationUrl: asset?.animationUrl,
susFlag: asset?.suspiciousFlag,
priceInfo: asset?.listings
? {
ETHPrice: ethPrice,
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: ethPrice,
}
: undefined,
name: asset?.name,
tokenId: asset?.tokenId,
asset_contract: {
address: asset?.collection?.nftContracts?.[0]?.address,
schema_name: asset?.collection?.nftContracts?.[0]?.standard,
name: asset?.collection?.name,
description: asset?.description,
image_url: asset?.collection?.image?.url,
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
tokenType: asset?.collection?.nftContracts?.[0].standard,
},
collection: asset?.collection,
collectionIsVerified: asset?.collection?.isVerified,
lastPrice: queryAsset.node.lastPrice?.value,
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,
creatorPercentage: queryAsset?.node?.listingFees?.[0]?.basisPoints ?? 0 / 10000,
listing_date: asset?.listings?.edges?.[0]?.node?.createdAt,
date_acquired: queryAsset.node.lastPrice?.timestamp,
sellOrders: asset?.listings?.edges.map((edge: any) => edge.node),
floor_sell_order_price: asset?.listings?.edges?.[0]?.node?.price?.value,
}
})
return { walletAssets, hasNext, isLoadingNext, loadNext }
}

View File

@@ -27,6 +27,12 @@ export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
}
}
export type PricePoint = { timestamp: number; value: number }
export function isPricePoint(p: PricePoint | null): p is PricePoint {
return p !== null
}
export const CHAIN_ID_TO_BACKEND_NAME: { [key: number]: Chain } = {
[SupportedChainId.MAINNET]: 'ETHEREUM',
[SupportedChainId.GOERLI]: 'ETHEREUM_GOERLI',

View File

@@ -1,5 +1,5 @@
import { useWeb3React } from '@web3-react/core'
import { Chain } from 'graphql/data/__generated__/TokenQuery.graphql'
import { Chain } from 'graphql/data/Token'
import { chainIdToBackendName } from 'graphql/data/util'
import { useEffect, useRef } from 'react'

16
src/hooks/useIsNftPage.ts Normal file
View File

@@ -0,0 +1,16 @@
import { useLocation } from 'react-router-dom'
export function useIsNftPage() {
const { pathname } = useLocation()
return pathname.startsWith('/nfts')
}
export function useIsNftProfilePage() {
const { pathname } = useLocation()
return pathname.startsWith('/nfts/profile')
}
export function useIsNftDetailsPage() {
const { pathname } = useLocation()
return pathname.startsWith('/nfts/asset')
}

View File

@@ -1,12 +1,15 @@
import useInterval from 'lib/hooks/useInterval'
import { useState } from 'react'
import { useCallback, useState } from 'react'
const useMachineTimeMs = (updateInterval: number): number => {
const [now, setNow] = useState(Date.now())
useInterval(() => {
setNow(Date.now())
}, updateInterval)
useInterval(
useCallback(() => {
setNow(Date.now())
}, []),
updateInterval
)
return now
}

View File

@@ -1,10 +1,10 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import { formatToDecimal, getTokenAddress } from 'analytics/utils'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { formatToDecimal, getTokenAddress } from 'lib/utils/analytics'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo, useState } from 'react'

View File

@@ -1,12 +1,12 @@
import { MaxUint256 } from '@ethersproject/constants'
import type { TransactionResponse } from '@ethersproject/providers'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import { getTokenAddress } from 'analytics/utils'
import { useTokenContract } from 'hooks/useContract'
import { useTokenAllowance } from 'hooks/useTokenAllowance'
import { getTokenAddress } from 'lib/utils/analytics'
import { useCallback, useMemo } from 'react'
import { calculateGasMargin } from 'utils/calculateGasMargin'

View File

@@ -0,0 +1,41 @@
import { renderHook } from '@testing-library/react'
import useInterval from './useInterval'
describe('useInterval', () => {
const spy = jest.fn()
it('with no interval it does not run', () => {
renderHook(() => useInterval(spy, null))
expect(spy).toHaveBeenCalledTimes(0)
})
describe('with a synchronous function', () => {
it('it runs on an interval', () => {
jest.useFakeTimers()
renderHook(() => useInterval(spy, 100))
expect(spy).toHaveBeenCalledTimes(1)
jest.runTimersToTime(100)
expect(spy).toHaveBeenCalledTimes(2)
})
})
describe('with an async funtion', () => {
it('it runs on an interval exclusive of fn resolving', async () => {
jest.useFakeTimers()
spy.mockImplementation(() => Promise.resolve(undefined))
renderHook(() => useInterval(spy, 100))
expect(spy).toHaveBeenCalledTimes(1)
jest.runTimersToTime(100)
expect(spy).toHaveBeenCalledTimes(1)
await spy.mock.results[0].value
jest.runTimersToTime(100)
expect(spy).toHaveBeenCalledTimes(2)
})
})
})

View File

@@ -1,31 +1,36 @@
import { useEffect, useRef } from 'react'
import { useEffect } from 'react'
/**
* Invokes callback repeatedly over an interval defined by the delay
*
* @param callback
* @param delay if null, the callback will not be invoked
* @param leading if true, the callback will be invoked immediately (on the leading edge); otherwise, it will be invoked after delay
* @param leading by default, the callback will be invoked immediately (on the leading edge);
* if false, the callback will not be invoked until a first delay
*/
export default function useInterval(callback: () => void, delay: null | number, leading = true) {
const savedCallback = useRef<() => void>()
// Remember the latest callback.
export default function useInterval(callback: () => void | Promise<void>, delay: null | number, leading = true) {
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
const { current } = savedCallback
current && current()
if (delay === null) {
return
}
if (delay !== null) {
if (leading) tick()
const id = setInterval(tick, delay)
return () => clearInterval(id)
let timeout: ReturnType<typeof setTimeout>
tick(delay, /* skip= */ !leading)
return () => {
if (timeout) {
clearInterval(timeout)
}
}
return
}, [delay, leading])
async function tick(delay: number, skip = false) {
if (!skip) {
const promise = callback()
// Defer the next interval until the current callback has resolved.
if (promise) await promise
}
timeout = setTimeout(() => tick(delay), delay)
}
}, [callback, delay, leading])
}

View File

@@ -1,6 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { useWeb3React } from '@web3-react/core'
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter'
import ListingModal from 'nft/components/bag/profile/ListingModal'
import { Box } from 'nft/components/Box'
@@ -28,8 +29,8 @@ import {
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQuery, useQueryClient } from 'react-query'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import shallow from 'zustand/shallow'
import * as styles from './Bag.css'
import { BagContent } from './BagContent'
@@ -67,32 +68,43 @@ const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
const Bag = () => {
const { account, provider } = useWeb3React()
const bagStatus = useBag((s) => s.bagStatus)
const setBagStatus = useBag((s) => s.setBagStatus)
const didOpenUnavailableAssets = useBag((s) => s.didOpenUnavailableAssets)
const setDidOpenUnavailableAssets = useBag((s) => s.setDidOpenUnavailableAssets)
const bagIsLocked = useBag((s) => s.isLocked)
const setLocked = useBag((s) => s.setLocked)
const reset = useBag((s) => s.reset)
const resetSellAssets = useSellAsset((state) => state.reset)
const sellAssets = useSellAsset((state) => state.sellAssets)
const setProfilePageState = useProfilePageState((state) => state.setProfilePageState)
const profilePageState = useProfilePageState((state) => state.state)
const uncheckedItemsInBag = useBag((s) => s.itemsInBag)
const setItemsInBag = useBag((s) => s.setItemsInBag)
const bagExpanded = useBag((s) => s.bagExpanded)
const toggleBag = useBag((s) => s.toggleBag)
const setTotalEthPrice = useBag((s) => s.setTotalEthPrice)
const setTotalUsdPrice = useBag((s) => s.setTotalUsdPrice)
const setBagExpanded = useBag((state) => state.setBagExpanded)
const { resetSellAssets, sellAssets, setIsSellMode } = useSellAsset(
({ isSellMode, reset, sellAssets, setIsSellMode }) => ({
isSellMode,
resetSellAssets: reset,
sellAssets,
setIsSellMode,
}),
shallow
)
const { pathname } = useLocation()
const isProfilePage = pathname.startsWith('/nfts/profile')
const isNFTPage = pathname.startsWith('/nfts')
const { profilePageState, setProfilePageState } = useProfilePageState(
({ setProfilePageState, state }) => ({ profilePageState: state, setProfilePageState }),
shallow
)
const {
bagStatus,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
bagIsLocked,
setLocked,
reset,
setItemsInBag,
bagExpanded,
toggleBag,
setTotalEthPrice,
setTotalUsdPrice,
setBagExpanded,
} = useBag((state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }), shallow)
const { uncheckedItemsInBag } = useBag(({ itemsInBag }) => ({ uncheckedItemsInBag: itemsInBag }))
const isProfilePage = useIsNftProfilePage()
const isDetailsPage = useIsNftDetailsPage()
const isNFTPage = useIsNftPage()
const isMobile = useIsMobile()
const isDetailsPage = pathname.includes('/nfts/asset/')
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const transactionState = useSendTransaction((state) => state.state)
const setTransactionState = useSendTransaction((state) => state.setState)
@@ -101,9 +113,7 @@ const Bag = () => {
const queryClient = useQueryClient()
const itemsInBag = useMemo(() => {
return recalculateBagUsingPooledAssets(uncheckedItemsInBag)
}, [uncheckedItemsInBag])
const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag])
const [isOpen, setModalIsOpen] = useState(false)
const [userCanScroll, setUserCanScroll] = useState(false)
@@ -153,7 +163,10 @@ const Bag = () => {
}
}
const handleCloseBag = useCallback(() => setBagExpanded({ bagExpanded: false, manualClose: true }), [setBagExpanded])
const handleCloseBag = useCallback(() => {
setIsSellMode(false)
setBagExpanded({ bagExpanded: false, manualClose: true })
}, [setBagExpanded, setIsSellMode])
const fetchAssets = async () => {
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
@@ -223,11 +236,6 @@ const Bag = () => {
if (bagIsLocked && !isOpen) setModalIsOpen(true)
}, [bagIsLocked, isOpen])
useEffect(() => {
setBagExpanded({ bagExpanded: false })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
useEffect(() => {
if (transactionStateRef.current === TxStateType.Confirming) setBagStatus(BagStatus.PROCESSING_TRANSACTION)
if (transactionStateRef.current === TxStateType.Denied || transactionStateRef.current === TxStateType.Invalid) {
@@ -263,66 +271,70 @@ const Bag = () => {
(!isProfilePage && !isBuyingAssets && bagStatus === BagStatus.ADDING_TO_BAG) || (isProfilePage && !isSellingAssets)
)
const eventProperties = useMemo(
() => ({
usd_value: totalUsdPrice,
...formatAssetEventProperties(itemsInBag.map((item) => item.asset)),
}),
[itemsInBag, totalUsdPrice]
)
if (!bagExpanded || !isNFTPage) {
return null
}
return (
<>
{bagExpanded && isNFTPage ? (
<Portal>
<Column zIndex={isMobile || isOpen ? 'modal' : '3'} className={styles.bagContainer}>
{!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
<>
<BagHeader
numberOfAssets={isProfilePage ? sellAssets.length : itemsInBag.length}
closeBag={handleCloseBag}
resetFlow={isProfilePage ? resetSellAssets : reset}
isProfilePage={isProfilePage}
/>
{shouldRenderEmptyState && <EmptyState />}
<ScrollingIndicator top show={userCanScroll && scrollProgress > 0} />
<Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12">
{isProfilePage ? <ProfileBagContent /> : <BagContent />}
</Column>
<ScrollingIndicator show={userCanScroll && scrollProgress < 100} />
{hasAssetsToShow && !isProfilePage && (
<BagFooter
totalEthPrice={totalEthPrice}
totalUsdPrice={totalUsdPrice}
bagStatus={bagStatus}
fetchAssets={fetchAssets}
eventProperties={{
usd_value: totalUsdPrice,
...formatAssetEventProperties(itemsInBag.map((item) => item.asset)),
}}
/>
)}
{isSellingAssets && isProfilePage && (
<Box
marginTop="32"
marginX="28"
paddingY="10"
className={`${buttonTextMedium} ${commonButtonStyles}`}
backgroundColor="accentAction"
textAlign="center"
onClick={() => {
isMobile && toggleBag()
setProfilePageState(ProfilePageStateType.LISTING)
}}
>
Continue
</Box>
)}
</>
) : (
<ListingModal />
<Portal>
<Column zIndex={isMobile || isOpen ? 'modal' : '3'} className={styles.bagContainer}>
{!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
<>
<BagHeader
numberOfAssets={isProfilePage ? sellAssets.length : itemsInBag.length}
closeBag={handleCloseBag}
resetFlow={isProfilePage ? resetSellAssets : reset}
isProfilePage={isProfilePage}
/>
{shouldRenderEmptyState && <EmptyState />}
<ScrollingIndicator top show={userCanScroll && scrollProgress > 0} />
<Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12">
{isProfilePage ? <ProfileBagContent /> : <BagContent />}
</Column>
{hasAssetsToShow && !isProfilePage && (
<BagFooter
totalEthPrice={totalEthPrice}
totalUsdPrice={totalUsdPrice}
bagStatus={bagStatus}
fetchAssets={fetchAssets}
eventProperties={eventProperties}
/>
)}
</Column>
{isDetailsPage ? (
<DetailsPageBackground onClick={toggleBag} />
) : (
isOpen && <Overlay onClick={() => (!bagIsLocked ? setModalIsOpen(false) : undefined)} />
)}
</Portal>
) : null}
</>
{isSellingAssets && isProfilePage && (
<Box
marginTop="32"
marginX="28"
paddingY="10"
className={`${buttonTextMedium} ${commonButtonStyles}`}
backgroundColor="accentAction"
textAlign="center"
onClick={() => {
isMobile && toggleBag()
setProfilePageState(ProfilePageStateType.LISTING)
}}
>
Continue
</Box>
)}
</>
) : (
<ListingModal />
)}
</Column>
{isDetailsPage ? (
<DetailsPageBackground onClick={toggleBag} />
) : (
isOpen && <Overlay onClick={() => (!bagIsLocked ? setModalIsOpen(false) : undefined)} />
)}
</Portal>
)
}

View File

@@ -1,6 +1,5 @@
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { sendAnalyticsEvent, Trace } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { BagRow, PriceChangeBagRow, UnavailableAssetsHeaderRow } from 'nft/components/bag/BagRow'
import { Column } from 'nft/components/Flex'
import { useBag, useIsMobile } from 'nft/hooks'
@@ -69,7 +68,7 @@ export const BagContent = () => {
name={EventName.NFT_BUY_BAG_CHANGED}
properties={{
usd_value: fetchedPriceData,
bag_quantity: itemsInBag,
bag_quantity: itemsInBag.length,
...formatAssetEventProperties(unavailableAssets),
}}
shouldLogImpression

View File

@@ -3,7 +3,6 @@ import { body } from 'nft/css/common.css'
import { sprinkles } from 'nft/css/sprinkles.css'
export const footerContainer = sprinkles({
marginTop: '20',
paddingX: '16',
})

View File

@@ -1,9 +1,9 @@
import { BigNumber } from '@ethersproject/bignumber'
import { parseEther } from '@ethersproject/units'
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import Loader from 'components/Loader'
import { SupportedChainId } from 'constants/chains'
import { Box } from 'nft/components/Box'
@@ -169,7 +169,7 @@ export const BagFooter = ({
</Row>
</Column>
<TraceEvent
events={[Event.onClick]}
events={[BrowserEvent.onClick]}
name={EventName.NFT_BUY_BAG_PAY}
element={ElementName.NFT_BUY_BAG_PAY_BUTTON}
properties={{ ...eventProperties }}

View File

@@ -33,13 +33,19 @@ const IconWrapper = styled.button`
display: flex;
padding: 2px;
opacity: 1;
transition: 125ms ease opacity;
:hover {
opacity: 0.6;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
:active {
opacity: 0.4;
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `opacity ${duration.medium} ${timing.ease}`};
`
interface BagHeaderProps {
numberOfAssets: number

View File

@@ -115,6 +115,7 @@ export const removeButton = style([
export const bagRowImage = sprinkles({
width: '56',
height: '56',
objectFit: 'cover',
borderRadius: '8',
})

View File

@@ -40,7 +40,7 @@ const NoContentContainer = () => (
left="1/2"
top="1/2"
style={{ transform: 'translate3d(-50%, -50%, 0)' }}
color="grey500"
color="gray500"
fontSize="12"
fontWeight="normal"
>

View File

@@ -1,8 +1,8 @@
import { useIsNftProfilePage } from 'hooks/useIsNftPage'
import { Center, Column } from 'nft/components/Flex'
import { LargeBagIcon, LargeTagIcon } from 'nft/components/icons'
import { subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
const StyledColumn = styled(Column)<{ isProfilePage?: boolean }>`
@@ -14,8 +14,7 @@ const StyledColumn = styled(Column)<{ isProfilePage?: boolean }>`
`
const EmptyState = () => {
const { pathname } = useLocation()
const isProfilePage = pathname.startsWith('/nfts/profile')
const isProfilePage = useIsNftProfilePage()
return (
<StyledColumn isProfilePage={isProfilePage}>

View File

@@ -13,6 +13,7 @@ export const chevron = style([
export const chevronDown = style({
transform: 'rotate(180deg)',
cursor: 'pointer',
})
export const sectionDivider = style([
@@ -53,6 +54,7 @@ export const listingModalIcon = style([
{
boxSizing: 'border-box',
marginLeft: '-2px',
marginRight: '4px',
},
])

View File

@@ -1,4 +1,6 @@
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
import { EventName, ModalName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
@@ -8,6 +10,7 @@ import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useNFTList, useSellAsset } from 'nft/hooks'
import { logListing, looksRareNonceFetcher } from 'nft/queries'
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { Dispatch, useEffect, useMemo, useRef, useState } from 'react'
@@ -34,6 +37,7 @@ const ListingModal = () => {
const toggleCart = useBag((state) => state.toggleBag)
const looksRareNonceRef = useRef(looksRareNonce)
const isMobile = useIsMobile()
const trace = useTrace({ modal: ModalName.NFT_LISTING })
useEffect(() => {
useNFTList.subscribe((state) => (looksRareNonceRef.current = state.looksRareNonce))
@@ -41,6 +45,29 @@ const ListingModal = () => {
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
useEffect(() => {
fetchPrice().then((price) => {
setEthPriceInUSD(price || 0)
})
}, [])
const startListingEventProperties = {
collection_addresses: sellAssets.map((asset) => asset.asset_contract.address),
token_ids: sellAssets.map((asset) => asset.tokenId),
marketplaces: Array.from(new Set(listings.map((asset) => asset.marketplace.name))),
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
}
const approvalEventProperties = {
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
}
// when all collections have been approved, auto start the signing process
useEffect(() => {
collectionsRequiringApproval?.length &&
@@ -60,6 +87,7 @@ const ListingModal = () => {
const startListingFlow = async () => {
if (!signer) return
sendAnalyticsEvent(EventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
setListingStatus(ListingStatus.SIGNING)
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const signerAddress = await signer.getAddress()
@@ -111,6 +139,11 @@ const ListingModal = () => {
} else if (!paused) {
setListingStatus(ListingStatus.FAILED)
}
sendAnalyticsEvent(EventName.NFT_LISTING_COMPLETED, {
signatures_requested: listings.length,
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
...approvalEventProperties,
})
await logListing(listings, (await signer?.getAddress()) ?? '')
}
@@ -144,93 +177,100 @@ const ListingModal = () => {
const showSuccessScreen = useMemo(() => listingStatus === ListingStatus.APPROVED, [listingStatus])
return (
<Column paddingTop="20" paddingBottom="20" paddingLeft="12" paddingRight="12">
<Row className={headlineSmall} marginBottom="10">
{isMobile && !showSuccessScreen && (
<Box paddingTop="4" marginRight="4" onClick={toggleCart}>
<ChevronLeftIcon height={28} width={28} />
<Trace modal={ModalName.NFT_LISTING}>
<Column paddingTop="20" paddingBottom="20" paddingLeft="12" paddingRight="12">
<Row className={headlineSmall} marginBottom="10">
{isMobile && !showSuccessScreen && (
<Box paddingTop="4" marginRight="4" onClick={toggleCart}>
<ChevronLeftIcon height={28} width={28} />
</Box>
)}
{showSuccessScreen ? 'Success!' : `Listing ${sellAssets.length} NFTs`}
<Box
as="button"
border="none"
color="textSecondary"
backgroundColor="backgroundSurface"
marginLeft="auto"
marginRight="0"
paddingRight="0"
display={{ sm: 'flex', md: 'none' }}
cursor="pointer"
onClick={toggleCart}
>
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
</Box>
)}
{showSuccessScreen ? 'Success!' : `Listing ${sellAssets.length} NFTs`}
<Box
as="button"
border="none"
color="textSecondary"
backgroundColor="backgroundSurface"
marginLeft="auto"
marginRight="0"
paddingRight="0"
display={{ sm: 'flex', md: 'none' }}
cursor="pointer"
onClick={toggleCart}
>
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
</Box>
</Row>
<Column overflowX="hidden" overflowY="auto" style={{ maxHeight: '60vh' }}>
</Row>
<Column overflowX="hidden" overflowY="auto" style={{ maxHeight: '60vh' }}>
{showSuccessScreen ? (
<Trace
name={EventName.NFT_LISTING_COMPLETED}
properties={{ list_quantity: listings.length, usd_value: ethPriceInUSD * totalEthListingValue, ...trace }}
>
<ListingSection
sectionTitle={`Listed ${listings.length} item${pluralize(listings.length)} for sale`}
rows={listings}
index={0}
openIndex={openIndex}
isSuccessScreen={true}
/>
</Trace>
) : (
<>
<ListingSection
sectionTitle={`Approve ${collectionsRequiringApproval.length} collection${pluralize(
collectionsRequiringApproval.length
)}`}
title="COLLECTIONS"
rows={collectionsRequiringApproval}
index={1}
openIndex={openIndex}
/>
<ListingSection
sectionTitle={`Confirm ${listings.length} listing${pluralize(listings.length)}`}
caption="Now you can sign to list each item"
title="NFTS"
rows={listings}
index={2}
openIndex={openIndex}
/>
</>
)}
</Column>
<hr className={styles.sectionDivider} />
<Row className={subhead} marginTop="12" marginBottom={showSuccessScreen ? '8' : '20'}>
Return if sold
<Row className={subheadSmall} marginLeft="auto" marginRight="0">
{totalEthListingValue}
&nbsp;ETH
</Row>
</Row>
{showSuccessScreen ? (
<ListingSection
sectionTitle={`Listed ${listings.length} item${pluralize(listings.length)} for sale`}
rows={listings}
index={0}
openIndex={openIndex}
isSuccessScreen={true}
/>
<Box as="span" className={caption} color="textSecondary">
Status:{' '}
<Box as="span" color="green200">
Confirmed
</Box>
</Box>
) : (
<>
<ListingSection
sectionTitle={`Approve ${collectionsRequiringApproval.length} collection${pluralize(
collectionsRequiringApproval.length
)}`}
title="COLLECTIONS"
rows={collectionsRequiringApproval}
index={1}
openIndex={openIndex}
/>
<ListingSection
sectionTitle={`Confirm ${listings.length} listing${pluralize(listings.length)}`}
caption="Now you can sign to list each item"
title="NFTS"
rows={listings}
index={2}
openIndex={openIndex}
/>
</>
<ListingButton onClick={clickStartListingFlow} buttonText={'Start listing'} showWarningOverride={isMobile} />
)}
{(listingStatus === ListingStatus.PENDING || listingStatus === ListingStatus.SIGNING) && (
<Box
as="button"
border="none"
backgroundColor="backgroundSurface"
cursor="pointer"
color="orange"
className={styles.button}
onClick={clickStopListing}
type="button"
>
Stop listing
</Box>
)}
</Column>
<hr className={styles.sectionDivider} />
<Row className={subhead} marginTop="12" marginBottom={showSuccessScreen ? '8' : '20'}>
Return if sold
<Row className={subheadSmall} marginLeft="auto" marginRight="0">
{totalEthListingValue}
&nbsp;ETH
</Row>
</Row>
{showSuccessScreen ? (
<Box as="span" className={caption} color="textSecondary">
Status:{' '}
<Box as="span" color="green200">
Confirmed
</Box>
</Box>
) : (
<ListingButton onClick={clickStartListingFlow} buttonText={'Start listing'} showWarningOverride={isMobile} />
)}
{(listingStatus === ListingStatus.PENDING || listingStatus === ListingStatus.SIGNING) && (
<Box
as="button"
border="none"
backgroundColor="backgroundSurface"
cursor="pointer"
color="orange"
className={styles.button}
onClick={clickStopListing}
type="button"
>
Stop listing
</Box>
)}
</Column>
</Trace>
)
}

View File

@@ -110,6 +110,7 @@ export const ListingSection = ({
color="textPrimary"
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
maxWidth={{
sm: 'max',
md:

View File

@@ -27,7 +27,7 @@ const ProfileAssetRow = ({ asset }: { asset: WalletAsset }) => {
>
<img className={styles.removeIcon} src={'/nft/svgs/minusCircle.svg'} alt="Remove item" />
</Box>
<img className={styles.tagAssetImage} src={asset.image_url} alt={asset.name} />
<img className={styles.tagAssetImage} src={asset.imageUrl} alt={asset.name} />
</div>
<Column gap="4" overflow="hidden" flexWrap="nowrap">
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" className={subhead}>

View File

@@ -142,7 +142,7 @@ export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], Listin
sellAssets.forEach((asset) => {
asset.marketplaces?.forEach((marketplace: ListingMarket) => {
const newListing = {
images: [asset.image_preview_url, marketplace.icon],
images: [asset.smallImageUrl, marketplace.icon],
name: asset.name || `#${asset.tokenId}`,
status: ListingStatus.DEFINED,
asset,

View File

@@ -1,7 +1,6 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { EventName, PageName } from '@uniswap/analytics-events'
import { ChainId } from '@uniswap/smart-order-router'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, PageName } from 'analytics/constants'
import { useTrace } from 'analytics/Trace'
import { MouseoverTooltip } from 'components/Tooltip'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
@@ -40,6 +39,8 @@ import * as styles from './Activity.css'
const AddressLink = styled(ExternalLink)`
color: ${({ theme }) => theme.textPrimary};
text-decoration: none;
font-weight: 400;
line-height: 20px;
a {
color: ${({ theme }) => theme.textPrimary};
text-decoration: none;
@@ -295,7 +296,7 @@ const NoContentContainer = () => (
left="1/2"
top="1/2"
style={{ transform: 'translate3d(-50%, -50%, 0)' }}
color="grey500"
color="gray500"
fontSize="12"
fontWeight="normal"
>

Some files were not shown because too many files have changed in this diff Show More