Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc81c6e37d | ||
|
|
9d5e0701e7 | ||
|
|
66bdfd8a94 | ||
|
|
935694630b | ||
|
|
f8399fd03c | ||
|
|
429ade5b20 | ||
|
|
5c21dd9852 | ||
|
|
2ce5990f60 | ||
|
|
d56851561b | ||
|
|
5325b5f8b4 | ||
|
|
27936cf3f5 | ||
|
|
ff6f43d7aa | ||
|
|
f1443671ef | ||
|
|
a95697daf8 | ||
|
|
0835744006 | ||
|
|
f5df2fed09 | ||
|
|
5e7f6333b1 | ||
|
|
2aa4ec6a38 | ||
|
|
a70ef4326d | ||
|
|
6edc73784c | ||
|
|
da6e13130b | ||
|
|
4ef4ea8f58 | ||
|
|
4438818f38 | ||
|
|
12eb337444 | ||
|
|
44163f54b1 | ||
|
|
4b282d7813 | ||
|
|
f862a3f975 | ||
|
|
48d5955185 | ||
|
|
dbf5c63ece | ||
|
|
37d2603406 | ||
|
|
9bb1ca2970 | ||
|
|
2abae0ee4c | ||
|
|
9f8355ed7b | ||
|
|
c5bed1c6fb | ||
|
|
1411a92146 | ||
|
|
d016bdd87c | ||
|
|
491ae578ab | ||
|
|
1df685f31e | ||
|
|
02aeb43e62 | ||
|
|
1d849927ef | ||
|
|
1893d258b5 | ||
|
|
ed7f126bd0 | ||
|
|
9a38c4e58d | ||
|
|
99f3998941 | ||
|
|
30fa88e3af | ||
|
|
d951172a81 | ||
|
|
eb35d3a2a0 | ||
|
|
87455fc096 | ||
|
|
054d92cb9c | ||
|
|
36109a1fe7 | ||
|
|
8f8fe9ddad | ||
|
|
2b279e00f9 | ||
|
|
9f5c588bdd | ||
|
|
415b3a1548 | ||
|
|
4e7b8264c3 | ||
|
|
0ef6d1625a | ||
|
|
0258460821 | ||
|
|
2246afcefb | ||
|
|
e0767b1cb7 | ||
|
|
15dd02fe6a | ||
|
|
562a386de7 | ||
|
|
99bea34f14 | ||
|
|
58f1c6ff84 | ||
|
|
b2481d6ba8 | ||
|
|
eaa9b51913 |
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -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:
|
||||
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -16,4 +16,4 @@
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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> </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> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,4 +27,4 @@
|
||||
"short_name": "Uniswap",
|
||||
"start_url": ".",
|
||||
"theme_color": "#FC72FFs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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 |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
`
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
31
src/components/Common/index.tsx
Normal file
31
src/components/Common/index.tsx
Normal 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;
|
||||
}
|
||||
`
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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't have chart data because it hasn'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
76
src/components/Tokens/TokenDetails/TimeSelector.tsx
Normal file
76
src/components/Tokens/TokenDetails/TimeSelector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
215
src/components/Tokens/TokenDetails/index.tsx
Normal file
215
src/components/Tokens/TokenDetails/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -3,5 +3,4 @@ export enum FeatureFlag {
|
||||
nft = 'nfts',
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
multiNetworkBalances = 'multiNetworkBalances',
|
||||
nftGraphQl = 'nftGraphQl',
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useNftGraphQlFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.nftGraphQl)
|
||||
}
|
||||
|
||||
export { BaseVariant as NftGraphQlVariant }
|
||||
@@ -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
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
181
src/graphql/data/nft/NftBalance.ts
Normal file
181
src/graphql/data/nft/NftBalance.ts
Normal 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 }
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
16
src/hooks/useIsNftPage.ts
Normal 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')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
41
src/lib/hooks/useInterval.test.tsx
Normal file
41
src/lib/hooks/useInterval.test.tsx
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -115,6 +115,7 @@ export const removeButton = style([
|
||||
export const bagRowImage = sprinkles({
|
||||
width: '56',
|
||||
height: '56',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '8',
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
@@ -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}
|
||||
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}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export const ListingSection = ({
|
||||
color="textPrimary"
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
maxWidth={{
|
||||
sm: 'max',
|
||||
md:
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user