Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8be9701700 | ||
|
|
d66002dc75 | ||
|
|
b12e5270fa | ||
|
|
a717818920 | ||
|
|
d9434a1a9c | ||
|
|
a920a93b3d | ||
|
|
0987a311cf | ||
|
|
a97a6b7fa8 | ||
|
|
414b221727 | ||
|
|
bab2f47ac9 | ||
|
|
0323725543 | ||
|
|
f6a7c8568e | ||
|
|
a21bbfd5a7 | ||
|
|
5f431a1e26 | ||
|
|
bf13b4a917 | ||
|
|
c7ea77d292 | ||
|
|
00d674376e | ||
|
|
afaa52e5e7 | ||
|
|
443cfe7540 | ||
|
|
6768e4f4f7 | ||
|
|
a0e9211b71 | ||
|
|
aeeb3a248a | ||
|
|
3586a2884c | ||
|
|
5e2bdc4e4b | ||
|
|
f2a33b6f6b | ||
|
|
5462526f53 | ||
|
|
4388bbe0a2 | ||
|
|
6f2c09adea | ||
|
|
f9aadbbbdb | ||
|
|
f8c0525512 | ||
|
|
175ffade5e | ||
|
|
cba30fb0b1 | ||
|
|
604b854ef7 | ||
|
|
332843f428 | ||
|
|
cee32f9751 | ||
|
|
cb480706a2 | ||
|
|
4f74267144 | ||
|
|
f6b08e8ed1 | ||
|
|
0faaa3f0c4 | ||
|
|
6acc9300c0 | ||
|
|
70d33fb255 | ||
|
|
c0db592ab5 | ||
|
|
a5c5567936 | ||
|
|
cdd5b66d1b | ||
|
|
b65fffc5f7 | ||
|
|
a3a3e934a1 | ||
|
|
8a4e07e6b2 | ||
|
|
7f4413c79c | ||
|
|
55beaf65a2 | ||
|
|
20fe76ad29 | ||
|
|
0d5bc753ca | ||
|
|
79507a4b03 | ||
|
|
3a1be04a36 | ||
|
|
ec523e5235 | ||
|
|
c7b1aa2948 | ||
|
|
9370383f64 | ||
|
|
9856c03566 | ||
|
|
ec686bcaa5 | ||
|
|
06291a15a6 | ||
|
|
3e40a6f5c6 | ||
|
|
f91b48e214 | ||
|
|
e340f405b4 | ||
|
|
24fc39b016 | ||
|
|
2fc3f3c00e | ||
|
|
21e0faeb1e | ||
|
|
4b71a8d5f4 | ||
|
|
4075965252 | ||
|
|
3538312769 | ||
|
|
2924f36970 | ||
|
|
bf16dfa09c | ||
|
|
910e86d6a2 | ||
|
|
537fea103e | ||
|
|
87a6e2709b | ||
|
|
d704e78223 | ||
|
|
8ceabd513c | ||
|
|
4806c69053 | ||
|
|
c9f333003b | ||
|
|
33fa32cb07 | ||
|
|
5d1377af80 | ||
|
|
a75e239fd2 | ||
|
|
a663482dc6 | ||
|
|
e8d6235529 | ||
|
|
d8677d8a6d | ||
|
|
351f66a83e | ||
|
|
79c7c01964 | ||
|
|
f51474b66d | ||
|
|
14f01905d7 | ||
|
|
23eb31e6a2 | ||
|
|
e8d4f00f49 | ||
|
|
978e3f945d | ||
|
|
072f394476 | ||
|
|
5926d7037d | ||
|
|
52b51ee7d0 | ||
|
|
54f831ede4 | ||
|
|
c9ead63ff6 | ||
|
|
9ca74cf8d2 | ||
|
|
2a0d455419 | ||
|
|
9e959ca455 | ||
|
|
d3f6796bb9 | ||
|
|
64d6eeabcb | ||
|
|
859258c25c | ||
|
|
2338255a54 | ||
|
|
843afa93c3 | ||
|
|
5441e63825 | ||
|
|
7bf741027e | ||
|
|
0017e2fcc8 | ||
|
|
5c9c8b4cb7 | ||
|
|
873d0ea2a3 | ||
|
|
db4987f557 | ||
|
|
b0b61f886d | ||
|
|
5d4b25f417 | ||
|
|
c88d7c880b | ||
|
|
a96d13978b | ||
|
|
22b26de78d | ||
|
|
53b57879a3 | ||
|
|
d794cef770 | ||
|
|
19f175ba89 | ||
|
|
aaf105ef51 | ||
|
|
974308f939 | ||
|
|
0ec738a48a | ||
|
|
904f6e22f4 | ||
|
|
66fad96e61 | ||
|
|
9037930e56 | ||
|
|
d62013177d | ||
|
|
fc08ede58a | ||
|
|
995a62985e | ||
|
|
67d5a00a0c | ||
|
|
84364c9df2 | ||
|
|
446eb9f9a4 | ||
|
|
a73e814167 | ||
|
|
7125562c9d | ||
|
|
1361f99639 | ||
|
|
d70a87a89a | ||
|
|
2cb0d9527e | ||
|
|
1839e145ec |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -21,20 +21,20 @@ describe('Add Liquidity', () => {
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
it.skip('token not in storage is loaded', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('single token can be selected', () => {
|
||||
it.skip('single token can be selected', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('loads fee tier distribution', () => {
|
||||
it.skip('loads fee tier distribution', () => {
|
||||
cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
if (hasQuery(req, 'FeeTierDistributionQuery')) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
describe('Link', () => {
|
||||
it('should update route', () => {
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="pool-nav-link"]').click()
|
||||
cy.contains('Pool').click()
|
||||
cy.get('[data-cy="join-pool-button"]').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
describe('Lists', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
// @TODO check if default lists are active when we have them
|
||||
it('change list', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('.list-token-manage-button').click()
|
||||
})
|
||||
})
|
||||
@@ -23,7 +23,7 @@ describe('Remove Liquidity', () => {
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
it.skip('token not in storage is loaded', () => {
|
||||
cy.visit('/remove/v2/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/ethereum'
|
||||
|
||||
describe('Wallet', () => {
|
||||
before(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('displays account details', () => {
|
||||
cy.get('[data-testid=web3-status-connected]').contains(TEST_ADDRESS_NEVER_USE_SHORTENED).click()
|
||||
})
|
||||
|
||||
it('displays account view in wallet modal', () => {
|
||||
cy.get('[data-testid=web3-account-identifier-row]').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
|
||||
it('changes back to the options grid', () => {
|
||||
cy.contains('Change').click()
|
||||
cy.get('[data-testid=option-grid]').should('exist')
|
||||
})
|
||||
|
||||
it('selects injected wallet option', () => {
|
||||
cy.contains('Injected').click()
|
||||
cy.get('[data-testid=web3-account-identifier-row]').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
|
||||
it('shows connect buttons after disconnect', () => {
|
||||
cy.get('[data-testid=web3-status-connected]').contains(TEST_ADDRESS_NEVER_USE_SHORTENED).click()
|
||||
cy.contains('Disconnect').click()
|
||||
cy.get('[data-testid=option-grid]').should('exist')
|
||||
})
|
||||
})
|
||||
@@ -34,7 +34,7 @@ Cypress.Commands.overwrite(
|
||||
cy.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 }).then(() => {
|
||||
original({
|
||||
...options,
|
||||
url: (url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url) + '?chain=rinkeby',
|
||||
url: (url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url) + '?chain=goerli',
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
win.localStorage.clear()
|
||||
|
||||
@@ -19,10 +19,10 @@ export const TEST_ADDRESS_NEVER_USE_SHORTENED = `${TEST_ADDRESS_NEVER_USE.substr
|
||||
6
|
||||
)}...${TEST_ADDRESS_NEVER_USE.substr(-4, 4)}`
|
||||
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
|
||||
const provider = new JsonRpcProvider('https://goerli.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
|
||||
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
|
||||
export const injected = new (class extends Eip1193Bridge {
|
||||
chainId = 4
|
||||
chainId = /* GOERLI= */ 5
|
||||
|
||||
async sendAsync(...args: any[]) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
|
||||
10
package.json
10
package.json
@@ -100,8 +100,8 @@
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-better-styled-components": "^1.1.2",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
@@ -135,7 +135,7 @@
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/redux-multicall": "^1.1.5",
|
||||
"@uniswap/redux-multicall": "^1.1.6",
|
||||
"@uniswap/router-sdk": "^1.3.0",
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.10.0",
|
||||
@@ -146,7 +146,7 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "^2.9.2",
|
||||
"@uniswap/widgets": "^2.16.2",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
@@ -169,7 +169,6 @@
|
||||
"@web3-react/types": "^8.0.20-beta.0",
|
||||
"@web3-react/url": "^8.0.25-beta.0",
|
||||
"@web3-react/walletconnect": "^8.0.35-beta.0",
|
||||
"ajv": "^6.12.3",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"cids": "^1.0.0",
|
||||
@@ -195,6 +194,7 @@
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"popper-max-size-modifier": "^0.2.0",
|
||||
"qs": "^6.9.4",
|
||||
"rc-slider": "^10.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-confetti": "^6.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -1,126 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!--
|
||||
<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="#ff007a" />
|
||||
<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');
|
||||
}
|
||||
@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;
|
||||
}
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#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 {
|
||||
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 {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
background: radial-gradient(50% 50% at 50% 50%, #fc077d10 0%, rgba(255, 255, 255, 0) 100%);
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
background-color: #212429;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
min-height: 100%;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #212429;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<!-- The root is the container of the app -->
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
<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>
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -2,7 +2,10 @@
|
||||
"background_color": "#fff",
|
||||
"display": "standalone",
|
||||
"homepage_url": "https://app.uniswap.org",
|
||||
"providedBy": { "name": "Uniswap", "url": "https://uniswap.org" },
|
||||
"providedBy": {
|
||||
"name": "Uniswap",
|
||||
"url": "https://uniswap.org"
|
||||
},
|
||||
"icons": [
|
||||
{
|
||||
"src": "./images/192x192_App_Icon.png",
|
||||
@@ -23,5 +26,5 @@
|
||||
"iconPath": "./images/256x256_App_Icon_Pink.svg",
|
||||
"short_name": "Uniswap",
|
||||
"start_url": ".",
|
||||
"theme_color": "#ff007a"
|
||||
}
|
||||
"theme_color": "#FC72FFs"
|
||||
}
|
||||
@@ -20,6 +20,11 @@ export interface ITraceContext {
|
||||
|
||||
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
|
||||
@@ -31,25 +36,32 @@ type TraceProps = {
|
||||
* and propagates the context to child traces.
|
||||
*/
|
||||
export const Trace = memo(
|
||||
({ shouldLogImpression, name, children, page, section, element, properties }: PropsWithChildren<TraceProps>) => {
|
||||
const parentTrace = useContext(TraceContext)
|
||||
({
|
||||
shouldLogImpression,
|
||||
name,
|
||||
children,
|
||||
page,
|
||||
section,
|
||||
modal,
|
||||
element,
|
||||
properties,
|
||||
}: PropsWithChildren<TraceProps>) => {
|
||||
const parentTrace = useTrace()
|
||||
|
||||
const combinedProps = useMemo(
|
||||
() => ({
|
||||
...parentTrace,
|
||||
...Object.fromEntries(Object.entries({ page, section, element }).filter(([_, v]) => v !== undefined)),
|
||||
...Object.fromEntries(Object.entries({ page, section, modal, element }).filter(([_, v]) => v !== undefined)),
|
||||
}),
|
||||
[element, parentTrace, page, section]
|
||||
[element, parentTrace, page, modal, section]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldLogImpression) {
|
||||
const origin = window.location.origin
|
||||
const commitHash = process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
sendAnalyticsEvent(name ?? EventName.PAGE_VIEWED, {
|
||||
...combinedProps,
|
||||
...properties,
|
||||
origin,
|
||||
git_commit_hash: commitHash,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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',
|
||||
@@ -64,8 +65,6 @@ export enum WALLET_CONNECTION_RESULT {
|
||||
FAILED = 'Failed',
|
||||
}
|
||||
|
||||
export const NATIVE_CHAIN_ID = 'NATIVE'
|
||||
|
||||
export enum SWAP_PRICE_UPDATE_USER_RESPONSE {
|
||||
ACCEPTED = 'Accepted',
|
||||
REJECTED = 'Rejected',
|
||||
@@ -75,6 +74,7 @@ export enum SWAP_PRICE_UPDATE_USER_RESPONSE {
|
||||
* Known pages in the app. Highest order context.
|
||||
*/
|
||||
export enum PageName {
|
||||
TOKEN_DETAILS_PAGE = 'token-details',
|
||||
TOKENS_PAGE = 'tokens-page',
|
||||
POOL_PAGE = 'pool-page',
|
||||
SWAP_PAGE = 'swap-page',
|
||||
@@ -90,6 +90,7 @@ export enum PageName {
|
||||
export enum SectionName {
|
||||
CURRENCY_INPUT_PANEL = 'swap-currency-input',
|
||||
CURRENCY_OUTPUT_PANEL = 'swap-currency-output',
|
||||
WIDGET = 'widget',
|
||||
// alphabetize additional section names.
|
||||
}
|
||||
|
||||
@@ -109,6 +110,7 @@ export enum ElementName {
|
||||
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',
|
||||
|
||||
@@ -35,12 +35,13 @@ export function initializeAnalytics() {
|
||||
|
||||
/** Sends an event to Amplitude. */
|
||||
export function sendAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
|
||||
const origin = window.location.origin
|
||||
if (!API_KEY) {
|
||||
console.log(`[analytics(${eventName})]: ${JSON.stringify(eventProperties)}`)
|
||||
return
|
||||
}
|
||||
|
||||
track(eventName, eventProperties)
|
||||
track(eventName, { ...eventProperties, origin })
|
||||
}
|
||||
|
||||
type Value = string | number | boolean | string[] | number[]
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
|
||||
import { NATIVE_CHAIN_ID } from './constants'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, Percent, Price, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
|
||||
export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => {
|
||||
if (!futureTimestampInSecondsSinceEpoch) return undefined
|
||||
return futureTimestampInSecondsSinceEpoch - new Date().getTime() / 1000
|
||||
}
|
||||
|
||||
export const getDurationFromDateMilliseconds = (start: Date): number => {
|
||||
export const getDurationFromDateMilliseconds = (start?: Date): number | undefined => {
|
||||
if (!start) return undefined
|
||||
return new Date().getTime() - start.getTime()
|
||||
}
|
||||
|
||||
@@ -21,3 +24,57 @@ export const getTokenAddress = (currency: Currency) => (currency.isNative ? NATI
|
||||
export const formatPercentInBasisPointsNumber = (percent: Percent): number => parseFloat(percent.toFixed(2)) * 100
|
||||
|
||||
export const formatPercentNumber = (percent: Percent): number => parseFloat(percent.toFixed(2))
|
||||
|
||||
export const getPriceUpdateBasisPoints = (
|
||||
prevPrice: Price<Currency, Currency>,
|
||||
newPrice: Price<Currency, Currency>
|
||||
): number => {
|
||||
const changeFraction = newPrice.subtract(prevPrice).divide(prevPrice)
|
||||
const changePercentage = new Percent(changeFraction.numerator, changeFraction.denominator)
|
||||
return formatPercentInBasisPointsNumber(changePercentage)
|
||||
}
|
||||
|
||||
export const formatSwapSignedAnalyticsEventProperties = ({
|
||||
trade,
|
||||
txHash,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | Trade<Currency, Currency, TradeType>
|
||||
txHash: string
|
||||
}) => ({
|
||||
transaction_hash: txHash,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
})
|
||||
|
||||
export const formatSwapQuoteReceivedEventProperties = (
|
||||
trade: Trade<Currency, Currency, TradeType>,
|
||||
gasUseEstimateUSD?: CurrencyAmount<Token>,
|
||||
fetchingSwapQuoteStartTime?: Date
|
||||
) => {
|
||||
return {
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined,
|
||||
estimated_network_fee_usd: gasUseEstimateUSD ? formatToDecimal(gasUseEstimateUSD, 2) : undefined,
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
quote_latency_milliseconds: fetchingSwapQuoteStartTime
|
||||
? getDurationFromDateMilliseconds(fetchingSwapQuoteStartTime)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
3
src/assets/svg/swap-arrows.svg
Normal file
3
src/assets/svg/swap-arrows.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.80333 4.8863C7.51044 5.17919 7.51044 5.65406 7.80333 5.94696C8.09622 6.23985 8.5711 6.23985 8.86399 5.94696L7.80333 4.8863ZM12.0837 1.66663L12.614 1.1363C12.3211 0.843403 11.8462 0.843403 11.5533 1.1363L12.0837 1.66663ZM15.3033 5.94696C15.5962 6.23985 16.0711 6.23985 16.364 5.94696C16.6569 5.65406 16.6569 5.17919 16.364 4.8863L15.3033 5.94696ZM11.3337 9.99996C11.3337 10.4142 11.6694 10.75 12.0837 10.75C12.4979 10.75 12.8337 10.4142 12.8337 9.99996H11.3337ZM12.1973 15.1136C12.4902 14.8207 12.4902 14.3459 12.1973 14.053C11.9044 13.7601 11.4296 13.7601 11.1367 14.053L12.1973 15.1136ZM7.91699 18.3333L7.38666 18.8636C7.52731 19.0043 7.71808 19.0833 7.91699 19.0833C8.1159 19.0833 8.30667 19.0043 8.44732 18.8636L7.91699 18.3333ZM4.69732 14.053C4.40443 13.7601 3.92956 13.7601 3.63666 14.053C3.34377 14.3459 3.34377 14.8207 3.63666 15.1136L4.69732 14.053ZM8.66699 10.8333C8.66699 10.4191 8.33121 10.0833 7.91699 10.0833C7.50278 10.0833 7.16699 10.4191 7.16699 10.8333H8.66699ZM8.86399 5.94696L12.614 2.19696L11.5533 1.1363L7.80333 4.8863L8.86399 5.94696ZM11.5533 2.19696L15.3033 5.94696L16.364 4.8863L12.614 1.1363L11.5533 2.19696ZM11.3337 1.66663V9.99996H12.8337V1.66663H11.3337ZM11.1367 14.053L7.38666 17.803L8.44732 18.8636L12.1973 15.1136L11.1367 14.053ZM8.44732 17.803L4.69732 14.053L3.63666 15.1136L7.38666 18.8636L8.44732 17.803ZM8.66699 18.3333L8.66699 10.8333H7.16699L7.16699 18.3333H8.66699Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,4 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.24453 18.0887C3.24331 19.0467 3.47372 19.7558 3.93576 20.2158C4.39658 20.6771 5.09574 20.904 6.03326 20.8967H8.11975C8.20693 20.8934 8.29386 20.9079 8.37521 20.9395C8.45656 20.9711 8.53062 21.019 8.5928 21.0802L10.0779 22.5484C10.7527 23.2226 11.4139 23.5578 12.0617 23.5541C12.7096 23.5504 13.3709 23.2152 14.0456 22.5484L15.5124 21.0802C15.5767 21.0182 15.6529 20.97 15.7365 20.9385C15.82 20.9069 15.9091 20.8927 15.9982 20.8967H18.0719C19.0192 20.8979 19.7251 20.6673 20.1896 20.2048C20.6541 19.7423 20.8864 19.0333 20.8864 18.0777V16.0021C20.8816 15.8222 20.9474 15.6476 21.0697 15.5157L22.5365 14.0475C23.2198 13.3758 23.559 12.7145 23.5541 12.0636C23.5492 11.4127 23.21 10.7508 22.5365 10.0779L21.0697 8.6097C20.9471 8.47802 20.8812 8.30329 20.8864 8.12336V6.04769C20.8851 5.09092 20.6547 4.3819 20.1951 3.92064C19.7355 3.45939 19.0278 3.22875 18.0719 3.22875H15.9982C15.9091 3.23242 15.8201 3.21807 15.7366 3.18653C15.6532 3.155 15.5769 3.10694 15.5124 3.04523L14.0456 1.57703C13.3709 0.902883 12.7096 0.567648 12.0617 0.571319C11.4139 0.574989 10.7527 0.910224 10.0779 1.57703L8.5928 3.04523C8.53043 3.10622 8.45638 3.15393 8.37508 3.18547C8.29377 3.21701 8.20689 3.23173 8.11975 3.22875H6.03326C5.08718 3.22998 4.38373 3.45877 3.92291 3.91513C3.4621 4.3715 3.23168 5.08235 3.23168 6.04769V8.12887C3.23683 8.3088 3.17096 8.48352 3.04833 8.6152L1.58154 10.0834C0.908042 10.7551 0.571289 11.417 0.571289 12.0691C0.571289 12.7213 0.912332 13.3844 1.59439 14.0585L3.06118 15.5267C3.18346 15.6586 3.24928 15.8332 3.24453 16.0131V18.0887Z" fill="#4C82FB"/>
|
||||
<path d="M3.24453 18.0887C3.24331 19.0467 3.47372 19.7558 3.93576 20.2158C4.39658 20.6771 5.09574 20.904 6.03326 20.8967H8.11975C8.20693 20.8934 8.29386 20.9079 8.37521 20.9395C8.45656 20.9711 8.53062 21.019 8.5928 21.0802L10.0779 22.5484C10.7527 23.2226 11.4139 23.5578 12.0617 23.5541C12.7096 23.5504 13.3709 23.2152 14.0456 22.5484L15.5124 21.0802C15.5767 21.0182 15.6529 20.97 15.7365 20.9385C15.82 20.9069 15.9091 20.8927 15.9982 20.8967H18.0719C19.0192 20.8979 19.7251 20.6673 20.1896 20.2048C20.6541 19.7423 20.8864 19.0333 20.8864 18.0777V16.0021C20.8816 15.8222 20.9474 15.6476 21.0697 15.5157L22.5365 14.0475C23.2198 13.3758 23.559 12.7145 23.5541 12.0636C23.5492 11.4127 23.21 10.7508 22.5365 10.0779L21.0697 8.6097C20.9471 8.47802 20.8812 8.30329 20.8864 8.12336V6.04769C20.8851 5.09092 20.6547 4.3819 20.1951 3.92064C19.7355 3.45939 19.0278 3.22875 18.0719 3.22875H15.9982C15.9091 3.23242 15.8201 3.21807 15.7366 3.18653C15.6532 3.155 15.5769 3.10694 15.5124 3.04523L14.0456 1.57703C13.3709 0.902883 12.7096 0.567648 12.0617 0.571319C11.4139 0.574989 10.7527 0.910224 10.0779 1.57703L8.5928 3.04523C8.53043 3.10622 8.45638 3.15393 8.37508 3.18547C8.29377 3.21701 8.20689 3.23173 8.11975 3.22875H6.03326C5.08718 3.22998 4.38373 3.45877 3.92291 3.91513C3.4621 4.3715 3.23168 5.08235 3.23168 6.04769V8.12887C3.23683 8.3088 3.17096 8.48352 3.04833 8.6152L1.58154 10.0834C0.908042 10.7551 0.571289 11.417 0.571289 12.0691C0.571289 12.7213 0.912332 13.3844 1.59439 14.0585L3.06118 15.5267C3.18346 15.6586 3.24928 15.8332 3.24453 16.0131V18.0887Z" fill="currentColor"/>
|
||||
<path d="M11.996 15.9909C11.7795 16.3208 11.4599 16.5064 11.0887 16.5064C10.7072 16.5064 10.4083 16.3517 10.1299 15.9909L7.69677 13.0216C7.5215 12.8051 7.42871 12.5783 7.42871 12.3309C7.42871 11.8154 7.82049 11.4133 8.32567 11.4133C8.63497 11.4133 8.8824 11.5267 9.12984 11.8463L11.0475 14.2897L15.1199 7.75329C15.3364 7.40275 15.6147 7.23779 15.924 7.23779C16.4086 7.23779 16.8622 7.57802 16.8622 8.0832C16.8622 8.32033 16.7385 8.56777 16.6045 8.78427L11.996 15.9909Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -1,51 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x7Db418b5D567A4e0E8c59Ad71BE1FcE48f3E6107',
|
||||
'0x72a5843cc08275C8171E582972Aa4fDa8C397B2A',
|
||||
'0x7F19720A857F834887FC9A7bC0a0fBe7Fc7f8102',
|
||||
'0xA7e5d5A720f06526557c513402f2e6B5fA20b008',
|
||||
'0x1da5821544e25c636c1417Ba96Ade4Cf6D2f9B5A',
|
||||
'0x9F4cda013E354b8fC285BF4b9A60460cEe7f7Ea9',
|
||||
'0x19Aa5Fe80D33a56D56c78e82eA5E50E5d80b4Dff',
|
||||
'0x2f389cE8bD8ff92De3402FFCe4691d17fC4f6535',
|
||||
'0xe7aa314c77F4233C18C6CC84384A9247c0cf367B',
|
||||
'0x7F367cC41522cE07553e823bf3be79A889DEbe1B',
|
||||
'0xd882cFc20F52f2599D84b8e8D58C7FB62cfE344b',
|
||||
'0x901bb9583b24D97e995513C6778dc6888AB6870e',
|
||||
'0x8576aCC5C05D6Ce88f4e49bf65BdF0C62F91353C',
|
||||
'0xC8a65Fadf0e0dDAf421F28FEAb69Bf6E2E589963',
|
||||
'0x308eD4B7b49797e1A98D3818bFF6fe5385410370',
|
||||
'0x67d40EE1A85bf4a4Bb7Ffae16De985e8427B',
|
||||
'0x6f1ca141a28907f78ebaa64fb83a9088b02a83',
|
||||
'0x6acdfba02d390b97ac2b2d42a63e85293bcc1',
|
||||
'0x48549a34ae37b12f6a30566245176994e17c6',
|
||||
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121',
|
||||
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
|
||||
'0x3CBdeD43EFdAf0FC77b9C55F6fC9988fCC9b757d',
|
||||
'0x67d40EE1A85bf4a4Bb7Ffae16De985e8427B6b45',
|
||||
'0x6F1cA141A28907F78Ebaa64fb83A9088b02A8352',
|
||||
'0x6aCDFBA02D390b97Ac2b2d42A63E85293BCc160e',
|
||||
'0x48549a34ae37b12f6a30566245176994e17c6b4a',
|
||||
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
|
||||
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
|
||||
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
|
||||
'0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9',
|
||||
'0x098B716B8Aaf21512996dC57EB0615e2383E2f96',
|
||||
'0xfEC8A60023265364D066a1212fDE3930F6Ae8da7',
|
||||
]
|
||||
|
||||
export default function Blocklist({ children }: { children: ReactNode }) {
|
||||
const { account } = useWeb3React()
|
||||
const blocked: boolean = useMemo(() => Boolean(account && BLOCKED_ADDRESSES.indexOf(account) !== -1), [account])
|
||||
if (blocked) {
|
||||
return (
|
||||
<div>
|
||||
<Trans>Blocked address</Trans>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { LineChartProps } from './LineChart'
|
||||
|
||||
type AnimatedInLineChartProps<T> = Omit<LineChartProps<T>, 'height' | 'width' | 'children'>
|
||||
|
||||
const config = {
|
||||
duration: 800,
|
||||
easing: easeCubicInOut,
|
||||
@@ -23,10 +25,7 @@ function AnimatedInLineChart<T>({
|
||||
curve,
|
||||
color,
|
||||
strokeWidth,
|
||||
width,
|
||||
height,
|
||||
children,
|
||||
}: LineChartProps<T>) {
|
||||
}: AnimatedInLineChartProps<T>) {
|
||||
const lineRef = useRef<SVGPathElement>(null)
|
||||
const [lineLength, setLineLength] = useState(0)
|
||||
const [shouldAnimate, setShouldAnimate] = useState(false)
|
||||
@@ -41,49 +40,54 @@ function AnimatedInLineChart<T>({
|
||||
},
|
||||
})
|
||||
|
||||
const effectDependency = lineRef.current
|
||||
// We need to check to see after the "invisble" line has been drawn
|
||||
// what the length is to be able to animate in the line for the first time
|
||||
// This will run on each render to see if there is a new line length
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
if (lineRef.current) {
|
||||
setLineLength(lineRef.current.getTotalLength())
|
||||
setShouldAnimate(true)
|
||||
const length = lineRef.current.getTotalLength()
|
||||
if (length !== lineLength) {
|
||||
setLineLength(length)
|
||||
}
|
||||
if (length > 0 && !shouldAnimate && !hasAnimatedIn) {
|
||||
setShouldAnimate(true)
|
||||
}
|
||||
}
|
||||
}, [effectDependency])
|
||||
})
|
||||
const theme = useTheme()
|
||||
const lineColor = color ?? theme.accentAction
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<Group top={marginTop}>
|
||||
<LinePath curve={curve} x={getX} y={getY}>
|
||||
{({ path }) => {
|
||||
const d = path(data) || ''
|
||||
return (
|
||||
<>
|
||||
<Group top={marginTop}>
|
||||
<LinePath curve={curve} x={getX} y={getY}>
|
||||
{({ path }) => {
|
||||
const d = path(data) || ''
|
||||
return (
|
||||
<>
|
||||
<animated.path
|
||||
d={d}
|
||||
ref={lineRef}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeOpacity={hasAnimatedIn ? 1 : 0}
|
||||
fill="none"
|
||||
stroke={lineColor}
|
||||
/>
|
||||
{shouldAnimate && lineLength !== 0 && (
|
||||
<animated.path
|
||||
d={d}
|
||||
ref={lineRef}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeOpacity={hasAnimatedIn ? 1 : 0}
|
||||
fill="none"
|
||||
stroke={lineColor}
|
||||
strokeDashoffset={spring.frame.to((v) => v * lineLength)}
|
||||
strokeDasharray={lineLength}
|
||||
/>
|
||||
{shouldAnimate && lineLength !== 0 && (
|
||||
<animated.path
|
||||
d={d}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
stroke={lineColor}
|
||||
strokeDashoffset={spring.frame.to((v) => v * lineLength)}
|
||||
strokeDasharray={lineLength}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</LinePath>
|
||||
</Group>
|
||||
{children}
|
||||
</svg>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</LinePath>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import { curveCardinal, scaleLinear } from 'd3'
|
||||
import { filterPrices } from 'graphql/data/Token'
|
||||
import { TopToken } from 'graphql/data/TopTokens'
|
||||
import { PricePoint } from 'graphql/data/Token'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import React from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import { memo } from 'react'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { DATA_EMPTY, getPriceBounds } from '../Tokens/TokenDetails/PriceChart'
|
||||
import { getPriceBounds } from '../Tokens/TokenDetails/PriceChart'
|
||||
import LineChart from './LineChart'
|
||||
|
||||
type PricePoint = { value: number; timestamp: number }
|
||||
const LoadingContainer = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
interface SparklineChartProps {
|
||||
width: number
|
||||
@@ -16,15 +22,32 @@ interface SparklineChartProps {
|
||||
tokenData: TopToken
|
||||
pricePercentChange: number | undefined | null
|
||||
timePeriod: TimePeriod
|
||||
sparklineMap: SparklineMap
|
||||
}
|
||||
|
||||
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) {
|
||||
function _SparklineChart({
|
||||
width,
|
||||
height,
|
||||
tokenData,
|
||||
pricePercentChange,
|
||||
timePeriod,
|
||||
sparklineMap,
|
||||
}: SparklineChartProps) {
|
||||
const theme = useTheme()
|
||||
// for sparkline
|
||||
const pricePoints = filterPrices(tokenData?.market?.priceHistory) ?? []
|
||||
const hasData = pricePoints.length !== 0
|
||||
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
|
||||
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
|
||||
const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null
|
||||
|
||||
// Don't display if there's one or less pricepoints
|
||||
if (!pricePoints || pricePoints.length <= 1) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<SparkLineLoadingBubble />
|
||||
</LoadingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const startingPrice = pricePoints[0]
|
||||
const endingPrice = pricePoints[pricePoints.length - 1]
|
||||
const widthScale = scaleLinear()
|
||||
.domain(
|
||||
// the range of possible input values
|
||||
@@ -35,9 +58,7 @@ function SparklineChart({ width, height, tokenData, pricePercentChange, timePeri
|
||||
[0, 110]
|
||||
)
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([30, 0])
|
||||
|
||||
/* Default curve doesn't look good for the ALL chart */
|
||||
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
|
||||
const curveTension = 0.9
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
@@ -54,4 +75,4 @@ function SparklineChart({ width, height, tokenData, pricePercentChange, timePeri
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(SparklineChart)
|
||||
export default memo(_SparklineChart)
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { useMemo } from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
|
||||
@@ -18,7 +17,6 @@ export function FiatValue({
|
||||
priceImpact?: Percent
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const redesignFlagEnabled = useRedesignFlag() === RedesignVariant.Enabled
|
||||
const priceImpactColor = useMemo(() => {
|
||||
if (!priceImpact) return undefined
|
||||
if (priceImpact.lessThan('0')) return theme.deprecated_green1
|
||||
@@ -31,14 +29,8 @@ export function FiatValue({
|
||||
const p = Number(fiatValue?.toFixed())
|
||||
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
|
||||
|
||||
const textColor = redesignFlagEnabled
|
||||
? theme.textSecondary
|
||||
: fiatValue
|
||||
? theme.deprecated_text3
|
||||
: theme.deprecated_text4
|
||||
|
||||
return (
|
||||
<ThemedText.DeprecatedBody fontSize={14} color={textColor}>
|
||||
<ThemedText.DeprecatedBody fontSize={14} color={theme.textSecondary}>
|
||||
{fiatValue && <>${fiatValue?.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })}</>}
|
||||
{priceImpact ? (
|
||||
<span style={{ color: priceImpactColor }}>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { darken } from 'polished'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { Lock } from 'react-feather'
|
||||
@@ -25,45 +24,31 @@ import { RowBetween, RowFixed } from '../Row'
|
||||
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
|
||||
import { FiatValue } from './FiatValue'
|
||||
|
||||
const InputPanel = styled.div<{ hideInput?: boolean; redesignFlag: boolean }>`
|
||||
const InputPanel = styled.div<{ hideInput?: boolean }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
position: relative;
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
|
||||
background-color: ${({ theme, redesignFlag, hideInput }) =>
|
||||
redesignFlag ? 'transparent' : hideInput ? 'transparent' : theme.deprecated_bg2};
|
||||
z-index: 1;
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
transition: height 1s ease;
|
||||
will-change: height;
|
||||
`
|
||||
|
||||
const FixedContainer = styled.div<{ redesignFlag: boolean }>`
|
||||
const FixedContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
border-radius: 20px;
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg2)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const Container = styled.div<{ hideInput: boolean; disabled: boolean; redesignFlag: boolean }>`
|
||||
min-height: ${({ redesignFlag }) => redesignFlag && '69px'};
|
||||
const Container = styled.div<{ hideInput: boolean }>`
|
||||
min-height: 44px;
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
|
||||
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg0)};
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg1)};
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
${({ theme, hideInput, disabled, redesignFlag }) =>
|
||||
!redesignFlag &&
|
||||
!disabled &&
|
||||
`
|
||||
:focus,
|
||||
:hover {
|
||||
border: 1px solid ${hideInput ? ' transparent' : theme.deprecated_bg3};
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const CurrencySelect = styled(ButtonGray)<{
|
||||
@@ -71,22 +56,14 @@ const CurrencySelect = styled(ButtonGray)<{
|
||||
selected: boolean
|
||||
hideInput?: boolean
|
||||
disabled?: boolean
|
||||
redesignFlag: boolean
|
||||
}>`
|
||||
align-items: center;
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
redesignFlag
|
||||
? selected
|
||||
? theme.stateOverlayPressed
|
||||
: theme.accentAction
|
||||
: selected
|
||||
? theme.deprecated_bg2
|
||||
: theme.deprecated_primary1};
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.backgroundInteractive : theme.accentAction)};
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')};
|
||||
color: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
|
||||
cursor: pointer;
|
||||
height: ${({ hideInput, redesignFlag }) => (redesignFlag ? 'unset' : hideInput ? '2.8rem' : '2.4rem')};
|
||||
height: unset;
|
||||
border-radius: 16px;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
@@ -94,48 +71,52 @@ const CurrencySelect = styled(ButtonGray)<{
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
padding: ${({ selected, redesignFlag }) =>
|
||||
redesignFlag ? (selected ? '4px 8px 4px 4px' : '6px 6px 6px 8px') : '0 8px'};
|
||||
gap: ${({ redesignFlag }) => (redesignFlag ? '8px' : '0px')};
|
||||
padding: ${({ selected }) => (selected ? '4px 8px 4px 4px' : '6px 6px 6px 8px')};
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
margin-left: ${({ hideInput }) => (hideInput ? '0' : '12px')};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
redesignFlag
|
||||
? theme.stateOverlayHover
|
||||
: selected
|
||||
? darken(0.05, theme.deprecated_primary1)
|
||||
: theme.deprecated_bg3};
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: ${({ theme, selected }) => (selected ? theme.backgroundInteractive : theme.accentAction)};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
redesignFlag
|
||||
? theme.stateOverlayPressed
|
||||
: selected
|
||||
? darken(0.05, theme.deprecated_primary1)
|
||||
: theme.deprecated_bg3};
|
||||
&:before {
|
||||
background-size: 100%;
|
||||
border-radius: inherit;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:hover:before {
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
|
||||
&:active:before {
|
||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||
}
|
||||
|
||||
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
|
||||
`
|
||||
|
||||
const InputRow = styled.div<{ selected: boolean; redesignFlag: boolean }>`
|
||||
const InputRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: ${({ selected, redesignFlag }) =>
|
||||
redesignFlag ? '0px' : selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem'};
|
||||
`
|
||||
|
||||
const LabelRow = styled.div<{ redesignFlag: boolean }>`
|
||||
const LabelRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textSecondary : theme.deprecated_text1)};
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
@@ -143,11 +124,10 @@ const LabelRow = styled.div<{ redesignFlag: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
const FiatRow = styled(LabelRow)<{ redesignFlag: boolean }>`
|
||||
const FiatRow = styled(LabelRow)`
|
||||
justify-content: flex-end;
|
||||
min-height: ${({ redesignFlag }) => redesignFlag && '32px'};
|
||||
padding: ${({ redesignFlag }) => redesignFlag && '8px 0px'};
|
||||
height: ${({ redesignFlag }) => !redesignFlag && '24px'};
|
||||
min-height: 20px;
|
||||
padding: 8px 0px 0px 0px;
|
||||
`
|
||||
|
||||
const Aligner = styled.span`
|
||||
@@ -157,34 +137,30 @@ const Aligner = styled.span`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const StyledDropDown = styled(DropDown)<{ selected: boolean; redesignFlag: boolean }>`
|
||||
const StyledDropDown = styled(DropDown)<{ selected: boolean }>`
|
||||
margin: 0 0.25rem 0 0.35rem;
|
||||
height: 35%;
|
||||
margin-left: ${({ redesignFlag }) => redesignFlag && '8px'};
|
||||
margin-left: 8px;
|
||||
|
||||
path {
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
|
||||
stroke-width: ${({ redesignFlag }) => (redesignFlag ? '2px' : '1.5px')};
|
||||
stroke-width: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledTokenName = styled.span<{ active?: boolean; redesignFlag: boolean }>`
|
||||
const StyledTokenName = styled.span<{ active?: boolean }>`
|
||||
${({ active }) => (active ? ' margin: 0 0.25rem 0 0.25rem;' : ' margin: 0 0.25rem 0 0.25rem;')}
|
||||
font-size: ${({ active }) => (active ? '18px' : '18px')};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boolean }>`
|
||||
const StyledBalanceMax = styled.button<{ disabled?: boolean }>`
|
||||
background-color: transparent;
|
||||
background-color: ${({ theme, redesignFlag }) => !redesignFlag && theme.deprecated_primary5};
|
||||
border: none;
|
||||
text-transform: ${({ redesignFlag }) => !redesignFlag && 'uppercase'};
|
||||
border-radius: ${({ redesignFlag }) => !redesignFlag && '12px'};
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentAction : theme.deprecated_primary1)};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
cursor: pointer;
|
||||
font-size: ${({ redesignFlag }) => (redesignFlag ? '14px' : '11px')};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
margin-left: ${({ redesignFlag }) => (redesignFlag ? '0px' : '0.25rem')};
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
padding: 4px 6px;
|
||||
pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
|
||||
@@ -198,12 +174,12 @@ const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boole
|
||||
}
|
||||
`
|
||||
|
||||
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean; redesignFlag: boolean }>`
|
||||
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
|
||||
${loadingOpacityMixin};
|
||||
text-align: left;
|
||||
font-size: ${({ redesignFlag }) => redesignFlag && '36px'};
|
||||
line-height: ${({ redesignFlag }) => redesignFlag && '44px'};
|
||||
font-variant: ${({ redesignFlag }) => redesignFlag && 'small-caps'};
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
font-variant: small-caps;
|
||||
`
|
||||
|
||||
interface SwapCurrencyInputPanelProps {
|
||||
@@ -253,8 +229,6 @@ export default function SwapCurrencyInputPanel({
|
||||
}: SwapCurrencyInputPanelProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account, chainId } = useWeb3React()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useTheme()
|
||||
|
||||
@@ -265,9 +239,9 @@ export default function SwapCurrencyInputPanel({
|
||||
const chainAllowed = isSupportedChain(chainId)
|
||||
|
||||
return (
|
||||
<InputPanel id={id} hideInput={hideInput} {...rest} redesignFlag={redesignFlagEnabled}>
|
||||
<InputPanel id={id} hideInput={hideInput} {...rest}>
|
||||
{locked && (
|
||||
<FixedContainer redesignFlag={redesignFlagEnabled}>
|
||||
<FixedContainer>
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Lock />
|
||||
<ThemedText.DeprecatedLabel fontSize="12px" textAlign="center" padding="0 12px">
|
||||
@@ -276,12 +250,8 @@ export default function SwapCurrencyInputPanel({
|
||||
</AutoColumn>
|
||||
</FixedContainer>
|
||||
)}
|
||||
<Container hideInput={hideInput} disabled={!chainAllowed} redesignFlag={redesignFlagEnabled}>
|
||||
<InputRow
|
||||
style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}
|
||||
selected={!onCurrencySelect}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
<Container hideInput={hideInput}>
|
||||
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}>
|
||||
{!hideInput && (
|
||||
<StyledNumericalInput
|
||||
className="token-amount-input"
|
||||
@@ -289,7 +259,6 @@ export default function SwapCurrencyInputPanel({
|
||||
onUserInput={onUserInput}
|
||||
disabled={!chainAllowed}
|
||||
$loading={loading}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -298,7 +267,6 @@ export default function SwapCurrencyInputPanel({
|
||||
visible={currency !== undefined}
|
||||
selected={!!currency}
|
||||
hideInput={hideInput}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (onCurrencySelect) {
|
||||
@@ -316,15 +284,11 @@ export default function SwapCurrencyInputPanel({
|
||||
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size={'24px'} />
|
||||
) : null}
|
||||
{pair ? (
|
||||
<StyledTokenName className="pair-name-container" redesignFlag={redesignFlagEnabled}>
|
||||
<StyledTokenName className="pair-name-container">
|
||||
{pair?.token0.symbol}:{pair?.token1.symbol}
|
||||
</StyledTokenName>
|
||||
) : (
|
||||
<StyledTokenName
|
||||
className="token-symbol-container"
|
||||
active={Boolean(currency && currency.symbol)}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
|
||||
{(currency && currency.symbol && currency.symbol.length > 20
|
||||
? currency.symbol.slice(0, 4) +
|
||||
'...' +
|
||||
@@ -333,12 +297,12 @@ export default function SwapCurrencyInputPanel({
|
||||
</StyledTokenName>
|
||||
)}
|
||||
</RowFixed>
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} redesignFlag={redesignFlagEnabled} />}
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
{!hideInput && !hideBalance && currency && (
|
||||
<FiatRow redesignFlag={redesignFlagEnabled}>
|
||||
<FiatRow>
|
||||
<RowBetween>
|
||||
<LoadingOpacityContainer $loading={loading}>
|
||||
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
|
||||
@@ -346,8 +310,8 @@ export default function SwapCurrencyInputPanel({
|
||||
{account ? (
|
||||
<RowFixed style={{ height: '17px' }}>
|
||||
<ThemedText.DeprecatedBody
|
||||
color={redesignFlag ? theme.textSecondary : theme.deprecated_text3}
|
||||
fontWeight={redesignFlag ? 400 : 500}
|
||||
color={theme.textSecondary}
|
||||
fontWeight={400}
|
||||
fontSize={14}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
@@ -365,7 +329,7 @@ export default function SwapCurrencyInputPanel({
|
||||
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||
>
|
||||
<StyledBalanceMax onClick={onMax} redesignFlag={redesignFlagEnabled}>
|
||||
<StyledBalanceMax onClick={onMax}>
|
||||
<Trans>Max</Trans>
|
||||
</StyledBalanceMax>
|
||||
</TraceEvent>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { darken } from 'polished'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { Lock } from 'react-feather'
|
||||
@@ -115,10 +114,10 @@ const LabelRow = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const FiatRow = styled(LabelRow)<{ redesignFlag: boolean }>`
|
||||
const FiatRow = styled(LabelRow)`
|
||||
justify-content: flex-end;
|
||||
padding: ${({ redesignFlag }) => redesignFlag && '0px 1rem 0.75rem'};
|
||||
height: ${({ redesignFlag }) => (redesignFlag ? '32px' : '16px')};
|
||||
padding: 0px 1rem 0.75rem;
|
||||
height: 32px;
|
||||
`
|
||||
|
||||
const Aligner = styled.span`
|
||||
@@ -140,7 +139,7 @@ const StyledDropDown = styled(DropDown)<{ selected: boolean }>`
|
||||
|
||||
const StyledTokenName = styled.span<{ active?: boolean }>`
|
||||
${({ active }) => (active ? ' margin: 0 0.25rem 0 0.25rem;' : ' margin: 0 0.25rem 0 0.25rem;')}
|
||||
font-size: ${({ active }) => (active ? '18px' : '18px')};
|
||||
font-size: 20px;
|
||||
`
|
||||
|
||||
const StyledBalanceMax = styled.button<{ disabled?: boolean }>`
|
||||
@@ -220,8 +219,6 @@ export default function CurrencyInputPanel({
|
||||
const { account, chainId } = useWeb3React()
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useTheme()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
@@ -293,7 +290,7 @@ export default function CurrencyInputPanel({
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
{!hideInput && !hideBalance && currency && (
|
||||
<FiatRow redesignFlag={redesignFlagEnabled}>
|
||||
<FiatRow>
|
||||
<RowBetween>
|
||||
<LoadingOpacityContainer $loading={loading}>
|
||||
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function CurrencyLogo({
|
||||
src?: string | null
|
||||
}) {
|
||||
const logoURIs = useCurrencyLogoURIs(currency)
|
||||
const srcs = useMemo(() => (src ? [src] : logoURIs), [src, logoURIs])
|
||||
const srcs = useMemo(() => (src ? [src, ...logoURIs] : logoURIs), [src, logoURIs])
|
||||
const props = {
|
||||
alt: `${currency?.symbol ?? 'token'} logo`,
|
||||
size,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
|
||||
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
|
||||
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'
|
||||
@@ -207,42 +203,14 @@ export default function FeatureFlagModal() {
|
||||
<X size={24} />
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<FeatureFlagGroup name="Phase 0">
|
||||
<FeatureFlagOption
|
||||
variant={RedesignVariant}
|
||||
value={useRedesignFlag()}
|
||||
featureFlag={FeatureFlag.redesign}
|
||||
label="Redesign"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={NavBarVariant}
|
||||
value={useNavBarFlag()}
|
||||
featureFlag={FeatureFlag.navBar}
|
||||
label="NavBar"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TokensVariant}
|
||||
value={useTokensFlag()}
|
||||
featureFlag={FeatureFlag.tokens}
|
||||
label="Tokens"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TokenSafetyVariant}
|
||||
value={useTokenSafetyFlag()}
|
||||
featureFlag={FeatureFlag.tokenSafety}
|
||||
label="Token Safety"
|
||||
/>
|
||||
</FeatureFlagGroup>
|
||||
<FeatureFlagGroup name="Phase 0 Follow-ups">
|
||||
<FeatureFlagOption
|
||||
variant={FavoriteTokensVariant}
|
||||
value={useFavoriteTokensFlag()}
|
||||
featureFlag={FeatureFlag.favoriteTokens}
|
||||
label="Favorite Tokens"
|
||||
/>
|
||||
</FeatureFlagGroup>
|
||||
<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
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import SantaHat from '../../assets/images/santa-hat.png'
|
||||
|
||||
const SantaHatImage = styled.img`
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
height: 18px;
|
||||
`
|
||||
|
||||
const Christmas = <SantaHatImage src={SantaHat} alt="Santa hat" />
|
||||
|
||||
const DATE_TO_ORNAMENT: { [date: string]: ReactElement } = {
|
||||
'12-24': Christmas,
|
||||
'12-25': Christmas,
|
||||
}
|
||||
|
||||
const HolidayOrnament = () => {
|
||||
// months in javascript are 0 indexed...
|
||||
const today = `${new Date().getMonth() + 1}-${new Date().getDate()}`
|
||||
return DATE_TO_ORNAMENT[today] || null
|
||||
}
|
||||
|
||||
export default HolidayOrnament
|
||||
@@ -1,345 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useSelectChain from 'hooks/useSelectChain'
|
||||
import useSyncChainQuery from 'hooks/useSyncChainQuery'
|
||||
import { darken } from 'polished'
|
||||
import { useRef } from 'react'
|
||||
import { AlertTriangle, ArrowDownCircle, ChevronDown } from 'react-feather'
|
||||
import { useCloseModal, useModalIsOpen, useOpenModal, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
const ActiveRowLinkList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 8px;
|
||||
& > a {
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.deprecated_text2};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
& > a:first-child {
|
||||
margin: 0;
|
||||
margin-top: 0px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
`
|
||||
const ActiveRowWrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.deprecated_bg1};
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
`
|
||||
const FlyoutHeader = styled.div`
|
||||
color: ${({ theme }) => theme.deprecated_text2};
|
||||
cursor: default;
|
||||
font-weight: 400;
|
||||
`
|
||||
const FlyoutMenu = styled.div`
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
width: 272px;
|
||||
z-index: 99;
|
||||
padding-top: 10px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
top: 40px;
|
||||
}
|
||||
`
|
||||
const FlyoutMenuContents = styled.div`
|
||||
align-items: flex-start;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg0};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
& > *:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`
|
||||
const FlyoutRow = styled.div<{ active: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ active, theme }) => (active ? theme.deprecated_bg1 : 'transparent')};
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
`
|
||||
const FlyoutRowActiveIndicator = styled.div`
|
||||
background-color: ${({ theme }) => theme.deprecated_green1};
|
||||
border-radius: 50%;
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
`
|
||||
|
||||
const CircleContainer = styled.div`
|
||||
width: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const LinkOutCircle = styled(ArrowDownCircle)`
|
||||
transform: rotate(230deg);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`
|
||||
const Logo = styled.img`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 8px;
|
||||
`
|
||||
const NetworkLabel = styled.div`
|
||||
flex: 1 1 auto;
|
||||
`
|
||||
const SelectorLabel = styled(NetworkLabel)`
|
||||
display: none;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
display: block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`
|
||||
const NetworkAlertLabel = styled(NetworkLabel)`
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0 0.5rem 0 0.4rem;
|
||||
font-size: 1rem;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
const SelectorControls = styled.div<{ supportedChain: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg0};
|
||||
border: 2px solid ${({ theme }) => theme.deprecated_bg0};
|
||||
border-radius: 16px;
|
||||
color: ${({ theme }) => theme.deprecated_text1};
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
${({ supportedChain, theme }) =>
|
||||
!supportedChain &&
|
||||
`
|
||||
color: ${theme.deprecated_white};
|
||||
background-color: ${theme.deprecated_red1};
|
||||
border: 2px solid ${theme.deprecated_red1};
|
||||
`}
|
||||
cursor: default;
|
||||
:focus {
|
||||
background-color: ${({ theme }) => darken(0.1, theme.deprecated_red1)};
|
||||
}
|
||||
`
|
||||
const SelectorLogo = styled(Logo)`
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`
|
||||
const SelectorWrapper = styled.div`
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
position: relative;
|
||||
}
|
||||
`
|
||||
const StyledChevronDown = styled(ChevronDown)`
|
||||
width: 16px;
|
||||
`
|
||||
|
||||
const NetworkIcon = styled(AlertTriangle)`
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`
|
||||
|
||||
const BridgeLabel = ({ chainId }: { chainId: SupportedChainId }) => {
|
||||
switch (chainId) {
|
||||
case SupportedChainId.ARBITRUM_ONE:
|
||||
case SupportedChainId.ARBITRUM_RINKEBY:
|
||||
return <Trans>Arbitrum Bridge</Trans>
|
||||
case SupportedChainId.OPTIMISM:
|
||||
case SupportedChainId.OPTIMISM_GOERLI:
|
||||
return <Trans>Optimism Bridge</Trans>
|
||||
case SupportedChainId.POLYGON:
|
||||
case SupportedChainId.POLYGON_MUMBAI:
|
||||
return <Trans>Polygon Bridge</Trans>
|
||||
case SupportedChainId.CELO:
|
||||
case SupportedChainId.CELO_ALFAJORES:
|
||||
return <Trans>Portal Bridge</Trans>
|
||||
default:
|
||||
return <Trans>Bridge</Trans>
|
||||
}
|
||||
}
|
||||
const ExplorerLabel = ({ chainId }: { chainId: SupportedChainId }) => {
|
||||
switch (chainId) {
|
||||
case SupportedChainId.ARBITRUM_ONE:
|
||||
case SupportedChainId.ARBITRUM_RINKEBY:
|
||||
return <Trans>Arbiscan</Trans>
|
||||
case SupportedChainId.OPTIMISM:
|
||||
case SupportedChainId.OPTIMISM_GOERLI:
|
||||
return <Trans>Optimistic Etherscan</Trans>
|
||||
case SupportedChainId.POLYGON:
|
||||
case SupportedChainId.POLYGON_MUMBAI:
|
||||
return <Trans>Polygonscan</Trans>
|
||||
case SupportedChainId.CELO:
|
||||
case SupportedChainId.CELO_ALFAJORES:
|
||||
return <Trans>Blockscout</Trans>
|
||||
default:
|
||||
return <Trans>Etherscan</Trans>
|
||||
}
|
||||
}
|
||||
|
||||
function Row({
|
||||
targetChain,
|
||||
onSelectChain,
|
||||
}: {
|
||||
targetChain: SupportedChainId
|
||||
onSelectChain: (targetChain: number) => void
|
||||
}) {
|
||||
const { provider, chainId } = useWeb3React()
|
||||
if (!provider || !chainId) {
|
||||
return null
|
||||
}
|
||||
const active = chainId === targetChain
|
||||
const { helpCenterUrl, explorer, bridge, label, logoUrl } = getChainInfo(targetChain)
|
||||
|
||||
const rowContent = (
|
||||
<FlyoutRow onClick={() => onSelectChain(targetChain)} active={active}>
|
||||
<Logo src={logoUrl} />
|
||||
<NetworkLabel>{label}</NetworkLabel>
|
||||
{chainId === targetChain && (
|
||||
<CircleContainer>
|
||||
<FlyoutRowActiveIndicator />
|
||||
</CircleContainer>
|
||||
)}
|
||||
</FlyoutRow>
|
||||
)
|
||||
|
||||
if (active) {
|
||||
return (
|
||||
<ActiveRowWrapper>
|
||||
{rowContent}
|
||||
<ActiveRowLinkList>
|
||||
{bridge && (
|
||||
<ExternalLink href={bridge}>
|
||||
<BridgeLabel chainId={chainId} />
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
)}
|
||||
{explorer && (
|
||||
<ExternalLink href={explorer}>
|
||||
<ExplorerLabel chainId={chainId} />
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
)}
|
||||
{helpCenterUrl && (
|
||||
<ExternalLink href={helpCenterUrl}>
|
||||
<Trans>Help Center</Trans>
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</ActiveRowLinkList>
|
||||
</ActiveRowWrapper>
|
||||
)
|
||||
}
|
||||
return rowContent
|
||||
}
|
||||
|
||||
const NETWORK_SELECTOR_CHAINS = [
|
||||
SupportedChainId.MAINNET,
|
||||
SupportedChainId.POLYGON,
|
||||
SupportedChainId.OPTIMISM,
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
SupportedChainId.CELO,
|
||||
]
|
||||
|
||||
export default function NetworkSelector() {
|
||||
const { chainId, provider } = useWeb3React()
|
||||
|
||||
const node = useRef<HTMLDivElement>(null)
|
||||
const isOpen = useModalIsOpen(ApplicationModal.NETWORK_SELECTOR)
|
||||
const openModal = useOpenModal(ApplicationModal.NETWORK_SELECTOR)
|
||||
const closeModal = useCloseModal(ApplicationModal.NETWORK_SELECTOR)
|
||||
const toggleModal = useToggleModal(ApplicationModal.NETWORK_SELECTOR)
|
||||
|
||||
const info = getChainInfo(chainId)
|
||||
|
||||
const selectChain = useSelectChain()
|
||||
useSyncChainQuery()
|
||||
|
||||
if (!chainId || !provider) {
|
||||
return null
|
||||
}
|
||||
|
||||
const onSupportedChain = info !== undefined
|
||||
|
||||
return (
|
||||
<SelectorWrapper
|
||||
ref={node}
|
||||
onMouseEnter={openModal}
|
||||
onMouseLeave={closeModal}
|
||||
onClick={isMobile ? toggleModal : undefined}
|
||||
>
|
||||
<SelectorControls supportedChain={onSupportedChain}>
|
||||
{onSupportedChain ? (
|
||||
<>
|
||||
<SelectorLogo src={info.logoUrl} />
|
||||
<SelectorLabel>{info.label}</SelectorLabel>
|
||||
<StyledChevronDown />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NetworkIcon />
|
||||
<NetworkAlertLabel>Switch Network</NetworkAlertLabel>
|
||||
<StyledChevronDown />
|
||||
</>
|
||||
)}
|
||||
</SelectorControls>
|
||||
{isOpen && (
|
||||
<FlyoutMenu>
|
||||
<FlyoutMenuContents>
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a {!onSupportedChain ? ' supported ' : ''}network</Trans>
|
||||
</FlyoutHeader>
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
|
||||
<Row
|
||||
onSelectChain={async (targetChainId: SupportedChainId) => {
|
||||
await selectChain(targetChainId)
|
||||
closeModal()
|
||||
}}
|
||||
targetChain={chainId}
|
||||
key={chainId}
|
||||
/>
|
||||
))}
|
||||
</FlyoutMenuContents>
|
||||
</FlyoutMenu>
|
||||
)}
|
||||
</SelectorWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import useScrollPosition from '@react-hook/window-scroll'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
|
||||
import { darken } from 'polished'
|
||||
import { NavLink, useLocation } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { useShowClaimPopup, useToggleSelfClaimModal } from 'state/application/hooks'
|
||||
import { useUserHasAvailableClaim } from 'state/claim/hooks'
|
||||
import { useNativeCurrencyBalances } from 'state/connection/hooks'
|
||||
import { useUserHasSubmittedClaim } from 'state/transactions/hooks'
|
||||
import { useDarkModeManager } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { ReactComponent as Logo } from '../../assets/svg/logo.svg'
|
||||
import { ExternalLink, ThemedText } from '../../theme'
|
||||
import ClaimModal from '../claim/ClaimModal'
|
||||
import { CardNoise } from '../earn/styled'
|
||||
import Menu from '../Menu'
|
||||
import Row from '../Row'
|
||||
import { Dots } from '../swap/styleds'
|
||||
import Web3Status from '../Web3Status'
|
||||
import HolidayOrnament from './HolidayOrnament'
|
||||
import NetworkSelector from './NetworkSelector'
|
||||
|
||||
const HeaderFrame = styled.div<{ showBackground: boolean }>`
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr 120px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
z-index: 21;
|
||||
position: relative;
|
||||
/* Background slide effect on scroll. */
|
||||
background-image: ${({ theme }) => `linear-gradient(to bottom, transparent 50%, ${theme.deprecated_bg0} 50% )}}`};
|
||||
background-position: ${({ showBackground }) => (showBackground ? '0 -100%' : '0 0')};
|
||||
background-size: 100% 200%;
|
||||
box-shadow: 0px 0px 0px 1px ${({ theme, showBackground }) => (showBackground ? theme.deprecated_bg2 : 'transparent;')};
|
||||
transition: background-position 0.1s, box-shadow 0.1s;
|
||||
background-blend-mode: hard-light;
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToLarge`
|
||||
grid-template-columns: 48px 1fr 1fr;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
padding: 1rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
padding: 1rem;
|
||||
grid-template-columns: 36px 1fr;
|
||||
`};
|
||||
`
|
||||
|
||||
const HeaderControls = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-self: flex-end;
|
||||
`
|
||||
|
||||
const HeaderElement = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
/* addresses safaris lack of support for "gap" */
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
align-items: center;
|
||||
`};
|
||||
`
|
||||
|
||||
const HeaderLinks = styled(Row)`
|
||||
justify-self: center;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg0};
|
||||
width: max-content;
|
||||
padding: 2px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 10px;
|
||||
overflow: auto;
|
||||
align-items: center;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToLarge`
|
||||
justify-self: start;
|
||||
`};
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
justify-self: center;
|
||||
`};
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
justify-self: center;
|
||||
z-index: 99;
|
||||
position: fixed;
|
||||
bottom: 0; right: 50%;
|
||||
transform: translate(50%,-50%);
|
||||
margin: 0 auto;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg0};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg2};
|
||||
box-shadow: 0px 6px 10px rgb(0 0 0 / 2%);
|
||||
`};
|
||||
`
|
||||
|
||||
const AccountElement = styled.div<{ active: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: ${({ theme, active }) => (!active ? theme.deprecated_bg0 : theme.deprecated_bg0)};
|
||||
border-radius: 16px;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
:focus {
|
||||
border: 1px solid blue;
|
||||
}
|
||||
`
|
||||
|
||||
const UNIAmount = styled(AccountElement)`
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg3};
|
||||
background: radial-gradient(174.47% 188.91% at 1.84% 0%, #ff007a 0%, #2172e5 100%), #edeef2;
|
||||
`
|
||||
|
||||
const UNIWrapper = styled.span`
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`
|
||||
|
||||
const BalanceText = styled(Text)`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const Title = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
justify-self: flex-start;
|
||||
margin-right: 12px;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
justify-self: center;
|
||||
`};
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const UniIcon = styled.div`
|
||||
transition: transform 0.3s ease;
|
||||
:hover {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
`
|
||||
|
||||
// can't be customized under react-router-dom v6
|
||||
// so we have to persist to the default one, i.e., .active
|
||||
const activeClassName = 'active'
|
||||
|
||||
const StyledNavLink = styled(NavLink)`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: left;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.deprecated_text2};
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
padding: 8px 12px;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
&.${activeClassName} {
|
||||
border-radius: 14px;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
color: ${({ theme }) => theme.deprecated_text1};
|
||||
background-color: ${({ theme }) => theme.deprecated_bg1};
|
||||
}
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
color: ${({ theme }) => darken(0.1, theme.deprecated_text1)};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledExternalLink = styled(ExternalLink)`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: left;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.deprecated_text2};
|
||||
font-size: 1rem;
|
||||
width: fit-content;
|
||||
margin: 0 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.${activeClassName} {
|
||||
border-radius: 14px;
|
||||
font-weight: 600;
|
||||
color: ${({ theme }) => theme.deprecated_text1};
|
||||
}
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
color: ${({ theme }) => darken(0.1, theme.deprecated_text1)};
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Header() {
|
||||
const tokensFlag = useTokensFlag()
|
||||
|
||||
const { account, chainId } = useWeb3React()
|
||||
|
||||
const userEthBalance = useNativeCurrencyBalances(account ? [account] : [])?.[account ?? '']
|
||||
const [darkMode] = useDarkModeManager()
|
||||
const { deprecated_white, deprecated_black } = useTheme()
|
||||
|
||||
const toggleClaimModal = useToggleSelfClaimModal()
|
||||
|
||||
const availableClaim: boolean = useUserHasAvailableClaim(account)
|
||||
|
||||
const { claimTxn } = useUserHasSubmittedClaim(account ?? undefined)
|
||||
|
||||
const showClaimPopup = useShowClaimPopup()
|
||||
|
||||
const scrollY = useScrollPosition()
|
||||
|
||||
const { pathname } = useLocation()
|
||||
|
||||
const {
|
||||
infoLink,
|
||||
nativeCurrency: { symbol: nativeCurrencySymbol },
|
||||
} = getChainInfoOrDefault(chainId)
|
||||
|
||||
// work around https://github.com/remix-run/react-router/issues/8161
|
||||
// as we can't pass function `({isActive}) => ''` to className with styled-components
|
||||
const isPoolActive =
|
||||
pathname.startsWith('/pool') ||
|
||||
pathname.startsWith('/add') ||
|
||||
pathname.startsWith('/remove') ||
|
||||
pathname.startsWith('/increase') ||
|
||||
pathname.startsWith('/find')
|
||||
|
||||
return (
|
||||
<HeaderFrame showBackground={scrollY > 45}>
|
||||
<ClaimModal />
|
||||
<Title href=".">
|
||||
<UniIcon>
|
||||
<Logo fill={darkMode ? deprecated_white : deprecated_black} width="24px" height="100%" title="logo" />
|
||||
<HolidayOrnament />
|
||||
</UniIcon>
|
||||
</Title>
|
||||
<HeaderLinks>
|
||||
<StyledNavLink id={`swap-nav-link`} to={'/swap'}>
|
||||
<Trans>Swap</Trans>
|
||||
</StyledNavLink>
|
||||
{tokensFlag === TokensVariant.Enabled && (
|
||||
<StyledNavLink id={`tokens-nav-link`} to={'/tokens'}>
|
||||
<Trans>Tokens</Trans>
|
||||
</StyledNavLink>
|
||||
)}
|
||||
<StyledNavLink
|
||||
data-cy="pool-nav-link"
|
||||
id={`pool-nav-link`}
|
||||
to={'/pool'}
|
||||
className={isPoolActive ? activeClassName : undefined}
|
||||
>
|
||||
<Trans>Pool</Trans>
|
||||
</StyledNavLink>
|
||||
{(!chainId || chainId === SupportedChainId.MAINNET) && (
|
||||
<StyledNavLink id={`vote-nav-link`} to={'/vote'}>
|
||||
<Trans>Vote</Trans>
|
||||
</StyledNavLink>
|
||||
)}
|
||||
<StyledExternalLink id={`charts-nav-link`} href={infoLink}>
|
||||
<Trans>Charts</Trans>
|
||||
<sup>↗</sup>
|
||||
</StyledExternalLink>
|
||||
</HeaderLinks>
|
||||
|
||||
<HeaderControls>
|
||||
<HeaderElement>
|
||||
<NetworkSelector />
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
{availableClaim && !showClaimPopup && (
|
||||
<UNIWrapper onClick={toggleClaimModal}>
|
||||
<UNIAmount active={!!account && !availableClaim} style={{ pointerEvents: 'auto' }}>
|
||||
<ThemedText.DeprecatedWhite padding="0 2px">
|
||||
{claimTxn && !claimTxn?.receipt ? (
|
||||
<Dots>
|
||||
<Trans>Claiming UNI</Trans>
|
||||
</Dots>
|
||||
) : (
|
||||
<Trans>Claim UNI</Trans>
|
||||
)}
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</UNIAmount>
|
||||
<CardNoise />
|
||||
</UNIWrapper>
|
||||
)}
|
||||
<AccountElement active={!!account}>
|
||||
{account && userEthBalance ? (
|
||||
<BalanceText style={{ flexShrink: 0, userSelect: 'none' }} pl="0.75rem" pr=".4rem" fontWeight={500}>
|
||||
<Trans>
|
||||
{userEthBalance?.toSignificant(3)} {nativeCurrencySymbol}
|
||||
</Trans>
|
||||
</BalanceText>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<Menu />
|
||||
</HeaderElement>
|
||||
</HeaderControls>
|
||||
</HeaderFrame>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ConnectionType } from 'connection'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
@@ -53,9 +52,8 @@ const Socks = () => {
|
||||
const useIcon = (connectionType: ConnectionType) => {
|
||||
const { account } = useWeb3React()
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
|
||||
if ((isNavbarEnabled && avatar) || connectionType === ConnectionType.INJECTED) {
|
||||
if (avatar || connectionType === ConnectionType.INJECTED) {
|
||||
return <Identicon />
|
||||
} else if (connectionType === ConnectionType.WALLET_CONNECT) {
|
||||
return <img src={WalletConnectIcon} alt="WalletConnect" />
|
||||
@@ -68,12 +66,11 @@ const useIcon = (connectionType: ConnectionType) => {
|
||||
|
||||
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
|
||||
const hasSocks = useHasSocks()
|
||||
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
const icon = useIcon(connectionType)
|
||||
|
||||
return (
|
||||
<IconWrapper size={size ?? 16}>
|
||||
{isNavbarEnabled && hasSocks && <Socks />}
|
||||
{hasSocks && <Socks />}
|
||||
{icon}
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import jazzicon from '@metamask/jazzicon'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -23,8 +22,7 @@ export default function Identicon({ size }: { size?: number }) {
|
||||
const { account } = useWeb3React()
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
const [fetchable, setFetchable] = useState(true)
|
||||
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
const iconSize = size ? size : isNavbarEnabled ? 24 : 16
|
||||
const iconSize = size ?? 24
|
||||
|
||||
const icon = useMemo(() => account && jazzicon(iconSize, parseInt(account.slice(2, 10), 16)), [account, iconSize])
|
||||
const iconRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { isMobile } from '../../utils/userAgent'
|
||||
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boolean }>`
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boolean; scrollOverlay?: boolean }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: ${Z_INDEX.modalBackdrop};
|
||||
background-color: transparent;
|
||||
@@ -18,6 +18,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boole
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
|
||||
justify-content: center;
|
||||
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundScrim : theme.deprecated_modalBG)};
|
||||
@@ -27,7 +28,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boole
|
||||
const AnimatedDialogContent = animated(DialogContent)
|
||||
// destructure to not pass custom props to Dialog DOM element
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, redesignFlag, ...rest }) => (
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, redesignFlag, scrollOverlay, ...rest }) => (
|
||||
<AnimatedDialogContent {...rest} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog',
|
||||
@@ -35,7 +36,7 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
|
||||
overflow-y: auto;
|
||||
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
margin: ${({ redesignFlag }) => (redesignFlag ? 'auto' : '0 0 2rem 0')};
|
||||
background-color: ${({ theme }) => theme.deprecated_bg0};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg1};
|
||||
box-shadow: ${({ theme, redesignFlag }) =>
|
||||
@@ -45,7 +46,7 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
|
||||
align-self: ${({ mobile }) => mobile && 'flex-end'};
|
||||
|
||||
max-width: 420px;
|
||||
${({ maxHeight }) =>
|
||||
@@ -58,11 +59,11 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, rede
|
||||
css`
|
||||
min-height: ${minHeight}vh;
|
||||
`}
|
||||
display: flex;
|
||||
display: ${({ scrollOverlay }) => (scrollOverlay ? 'inline-table' : 'flex')};
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
${({ theme, redesignFlag }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
width: 65vw;
|
||||
margin: 0;
|
||||
margin: ${redesignFlag ? 'auto' : '0'};
|
||||
`}
|
||||
${({ theme, mobile }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
width: 85vw;
|
||||
@@ -87,6 +88,7 @@ interface ModalProps {
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
redesignFlag?: boolean
|
||||
scrollOverlay?: boolean
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
@@ -97,6 +99,7 @@ export default function Modal({
|
||||
initialFocusRef,
|
||||
children,
|
||||
redesignFlag,
|
||||
scrollOverlay,
|
||||
}: ModalProps) {
|
||||
const fadeTransition = useTransition(isOpen, {
|
||||
config: { duration: 200 },
|
||||
@@ -129,6 +132,7 @@ export default function Modal({
|
||||
initialFocusRef={initialFocusRef}
|
||||
unstable_lockFocusAcrossFrames={false}
|
||||
redesignFlag={redesignFlag}
|
||||
scrollOverlay={scrollOverlay}
|
||||
>
|
||||
<StyledDialogContent
|
||||
{...(isMobile
|
||||
@@ -142,6 +146,7 @@ export default function Modal({
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
redesignFlag={redesignFlag}
|
||||
scrollOverlay={scrollOverlay}
|
||||
>
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
|
||||
|
||||
@@ -143,7 +143,7 @@ export const MenuDropdown = () => {
|
||||
<BarChartIcon width={24} height={24} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>View token analytics</Trans>
|
||||
<Trans>View more analytics</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
</Column>
|
||||
|
||||
@@ -103,8 +103,10 @@ export const suggestionImage = sprinkles({
|
||||
export const suggestionPrimaryContainer = style([
|
||||
sprinkles({
|
||||
alignItems: 'flex-start',
|
||||
width: 'full',
|
||||
}),
|
||||
{
|
||||
width: '90%',
|
||||
},
|
||||
])
|
||||
|
||||
export const suggestionSecondaryContainer = sprinkles({
|
||||
|
||||
@@ -106,7 +106,7 @@ interface SearchBarDropdownProps {
|
||||
|
||||
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, isLoading }: SearchBarDropdownProps) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
|
||||
const searchHistory = useSearchHistory((state: { history: (FungibleToken | GenieCollection)[] }) => state.history)
|
||||
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
|
||||
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
|
||||
const { pathname } = useLocation()
|
||||
const isNFTPage = pathname.includes('/nfts')
|
||||
@@ -146,9 +146,11 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, i
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
useEffect(() => {
|
||||
trendingTokenResults?.forEach(updateSearchHistory)
|
||||
}, [trendingTokenResults, updateSearchHistory])
|
||||
|
||||
const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4
|
||||
|
||||
const trendingTokens = useMemo(
|
||||
() =>
|
||||
trendingTokenResults
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import clsx from 'clsx'
|
||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { getTokenDetailsURL } from 'graphql/data/util'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { Box } from 'nft/components/Box'
|
||||
@@ -10,8 +15,8 @@ import { ethNumberStandardFormatter } from 'nft/utils/currency'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
import { VerifiedIcon } from '../../nft/components/icons'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
interface CollectionRowProps {
|
||||
@@ -83,7 +88,6 @@ export const CollectionRow = ({
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{collection.name}</Box>
|
||||
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
|
||||
</Column>
|
||||
@@ -100,6 +104,15 @@ export const CollectionRow = ({
|
||||
)
|
||||
}
|
||||
|
||||
function useBridgedAddress(token: FungibleToken): [string | undefined, number | undefined, string | undefined] {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const bridgedAddress = connectedChainId ? token.extensions?.bridgeInfo?.[connectedChainId]?.tokenAddress : undefined
|
||||
if (bridgedAddress && connectedChainId) {
|
||||
return [bridgedAddress, connectedChainId, getChainInfo(connectedChainId)?.circleLogoUrl]
|
||||
}
|
||||
return [undefined, undefined, undefined]
|
||||
}
|
||||
|
||||
interface TokenRowProps {
|
||||
token: FungibleToken
|
||||
isHovered: boolean
|
||||
@@ -123,7 +136,8 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceE
|
||||
traceEvent()
|
||||
}, [addToSearchHistory, toggleOpen, token, traceEvent])
|
||||
|
||||
const tokenDetailsPath = getTokenDetailsURL(token.address, undefined, token.chainId)
|
||||
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
|
||||
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
|
||||
// Close the modal on escape
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
@@ -150,21 +164,24 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceE
|
||||
>
|
||||
<Row style={{ width: '65%' }}>
|
||||
{!brokenImage && token.logoURI ? (
|
||||
<Box
|
||||
as="img"
|
||||
src={token.logoURI.includes('ipfs://') ? uriToHttp(token.logoURI)[0] : token.logoURI}
|
||||
alt={token.name}
|
||||
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
|
||||
onError={() => setBrokenImage(true)}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
<LogoContainer>
|
||||
<Box
|
||||
as="img"
|
||||
src={token.logoURI.includes('ipfs://') ? uriToHttp(token.logoURI)[0] : token.logoURI}
|
||||
alt={token.name}
|
||||
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
|
||||
onError={() => setBrokenImage(true)}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
||||
</LogoContainer>
|
||||
) : (
|
||||
<Box className={styles.imageHolder} />
|
||||
)}
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{token.name}</Box>
|
||||
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />}
|
||||
<TokenSafetyIcon warning={checkWarning(token.address)} />
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>{token.symbol}</Box>
|
||||
</Column>
|
||||
@@ -173,7 +190,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, traceE
|
||||
<Column className={styles.suggestionSecondaryContainer}>
|
||||
{token.priceUsd && (
|
||||
<Row gap="4">
|
||||
<Box className={styles.primaryText}>{ethNumberStandardFormatter(token.priceUsd, true)}</Box>
|
||||
<Box className={styles.primaryText}>{formatDollar({ num: token.priceUsd, isPrice: true })}</Box>
|
||||
</Row>
|
||||
)}
|
||||
{token.price24hChange && (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import { useGlobalChainName } from 'graphql/data/util'
|
||||
import { chainIdToBackendName } from 'graphql/data/util'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { UniIcon } from 'nft/components/icons'
|
||||
@@ -37,7 +38,8 @@ const MenuItem = ({ href, id, isActive, children }: MenuItemProps) => {
|
||||
const PageTabs = () => {
|
||||
const { pathname } = useLocation()
|
||||
const nftFlag = useNftFlag()
|
||||
const chainName = useGlobalChainName()
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const chainName = chainIdToBackendName(connectedChainId)
|
||||
|
||||
const isPoolActive =
|
||||
pathname.startsWith('/pool') ||
|
||||
|
||||
@@ -36,7 +36,7 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textSecondary : theme.deprecated_text4)};
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.textTertiary : theme.deprecated_text4)};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { animated } from 'react-spring'
|
||||
@@ -8,7 +7,6 @@ import styled, { useTheme } from 'styled-components/macro'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { PopupContent } from '../../state/application/reducer'
|
||||
import FailedNetworkSwitchPopup from './FailedNetworkSwitchPopup'
|
||||
import TransactionPopup from './TransactionPopup'
|
||||
|
||||
const StyledClose = styled(X)`
|
||||
position: absolute;
|
||||
@@ -58,7 +56,6 @@ export default function PopupItem({
|
||||
popKey: string
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
const navbarFlag = useNavBarFlag()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
useEffect(() => {
|
||||
if (removeAfterMs === null) return undefined
|
||||
@@ -80,22 +77,15 @@ export default function PopupItem({
|
||||
})
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash },
|
||||
} = content
|
||||
if (navbarFlag === NavBarVariant.Enabled) return null
|
||||
|
||||
popupContent = <TransactionPopup hash={hash} />
|
||||
} else if ('failedSwitchNetwork' in content) {
|
||||
if ('failedSwitchNetwork' in content) {
|
||||
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
|
||||
}
|
||||
|
||||
return (
|
||||
return popupContent ? (
|
||||
<Popup>
|
||||
<StyledClose color={theme.deprecated_text2} onClick={removeThisPopup} />
|
||||
{popupContent}
|
||||
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
|
||||
</Popup>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { HelpCircle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Tooltip from '../Tooltip'
|
||||
|
||||
const QuestionWrapper = styled.div<{ redesignFlag: boolean }>`
|
||||
const QuestionWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -18,8 +17,7 @@ const QuestionWrapper = styled.div<{ redesignFlag: boolean }>`
|
||||
cursor: default;
|
||||
border-radius: 36px;
|
||||
font-size: 12px;
|
||||
border-radius: ${({ redesignFlag }) => redesignFlag && '12px'};
|
||||
color: ${({ theme, redesignFlag }) => !redesignFlag && theme.deprecated_text2};
|
||||
border-radius: 12px;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
@@ -27,12 +25,12 @@ const QuestionWrapper = styled.div<{ redesignFlag: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
const QuestionMark = styled.span<{ redesignFlag?: boolean }>`
|
||||
const QuestionMark = styled.span`
|
||||
font-size: 14px;
|
||||
margin-left: ${({ redesignFlag }) => redesignFlag && '8px'};
|
||||
align-items: ${({ redesignFlag }) => redesignFlag && 'center'};
|
||||
color: ${({ theme, redesignFlag }) => redesignFlag && theme.textSecondary};
|
||||
margin-top: ${({ redesignFlag }) => redesignFlag && '2.5px'};
|
||||
margin-left: 8px;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-top: 2.5px;
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text }: { text: ReactNode; size?: number }) {
|
||||
@@ -40,14 +38,12 @@ export default function QuestionHelper({ text }: { text: ReactNode; size?: numbe
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
return (
|
||||
<span style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>
|
||||
<Tooltip text={text} show={show}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close} redesignFlag={redesignFlagEnabled}>
|
||||
<QuestionMark redesignFlag={redesignFlagEnabled}>
|
||||
{redesignFlagEnabled ? <HelpCircle size={16}></HelpCircle> : '?'}
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<QuestionMark>
|
||||
<HelpCircle size={16} />
|
||||
</QuestionMark>
|
||||
</QuestionWrapper>
|
||||
</Tooltip>
|
||||
|
||||
@@ -7,7 +7,7 @@ exports[`renders multi route 1`] = `
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background-color: #EDEEF2;
|
||||
background-color: #E8ECFB;
|
||||
border: unset;
|
||||
border-radius: 0.5rem;
|
||||
color: #000;
|
||||
@@ -110,11 +110,11 @@ exports[`renders multi route 1`] = `
|
||||
}
|
||||
|
||||
.c6 path {
|
||||
stroke: #888D9B;
|
||||
stroke: #99A1BD;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
background-color: #EDEEF2;
|
||||
background-color: #E8ECFB;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
font-size: 12px;
|
||||
@@ -129,9 +129,9 @@ exports[`renders multi route 1`] = `
|
||||
}
|
||||
|
||||
.c9 {
|
||||
background-color: #CED0D9;
|
||||
background-color: #C9D0E7;
|
||||
border-radius: 4px;
|
||||
color: #565A69;
|
||||
color: #5E6887;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
z-index: 1021;
|
||||
@@ -245,7 +245,7 @@ exports[`renders single route 1`] = `
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background-color: #EDEEF2;
|
||||
background-color: #E8ECFB;
|
||||
border: unset;
|
||||
border-radius: 0.5rem;
|
||||
color: #000;
|
||||
@@ -348,11 +348,11 @@ exports[`renders single route 1`] = `
|
||||
}
|
||||
|
||||
.c6 path {
|
||||
stroke: #888D9B;
|
||||
stroke: #99A1BD;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
background-color: #EDEEF2;
|
||||
background-color: #E8ECFB;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
font-size: 12px;
|
||||
@@ -367,9 +367,9 @@ exports[`renders single route 1`] = `
|
||||
}
|
||||
|
||||
.c9 {
|
||||
background-color: #CED0D9;
|
||||
background-color: #C9D0E7;
|
||||
border-radius: 4px;
|
||||
color: #565A69;
|
||||
color: #5E6887;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
z-index: 1021;
|
||||
|
||||
@@ -2,8 +2,26 @@
|
||||
|
||||
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
|
||||
<DocumentFragment>
|
||||
.c7 {
|
||||
color: #6E727D;
|
||||
.c9 {
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
margin-left: 4px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
@@ -55,7 +73,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
.c10 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
@@ -72,7 +90,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
background-color: #EDEEF2;
|
||||
background-color: #C9D0E714;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
@@ -111,9 +129,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<svg
|
||||
class="c8"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7 css-1j6a53a"
|
||||
class="c9 css-1j6a53a"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
@@ -122,7 +172,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c8"
|
||||
class="c0 c1 c10"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
@@ -150,9 +200,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
>
|
||||
USD//C
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<svg
|
||||
class="c8"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7 css-1j6a53a"
|
||||
class="c9 css-1j6a53a"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
@@ -161,7 +243,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c8"
|
||||
class="c0 c1 c10"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
@@ -189,9 +271,41 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<svg
|
||||
class="c8"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7 css-1j6a53a"
|
||||
class="c9 css-1j6a53a"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
@@ -200,7 +314,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c8"
|
||||
class="c0 c1 c10"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
@@ -212,12 +326,74 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
|
||||
exports[`renders loading rows when isLoading is true 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
.c0 {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
-webkit-animation: fAQEyV 1.5s infinite;
|
||||
animation: fAQEyV 1.5s infinite;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient( to left,#F5F6FC 25%,#E8ECFB 50%,#F5F6FC 75% );
|
||||
background-size: 400%;
|
||||
border-radius: 12px;
|
||||
height: 2.4em;
|
||||
will-change: background-position;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
grid-column-gap: 0.5em;
|
||||
grid-template-columns: repeat(12,1fr);
|
||||
max-width: 960px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 1) {
|
||||
grid-column: 1 / 8;
|
||||
height: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 2) {
|
||||
grid-column: 12;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 3) {
|
||||
grid-column: 1 / 4;
|
||||
height: 0.75em;
|
||||
}
|
||||
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 0px; width: 100%;"
|
||||
/>
|
||||
style="height: 560px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
@@ -6,7 +5,6 @@ import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
|
||||
import { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
|
||||
import { XOctagon } from 'react-feather'
|
||||
import { Check } from 'react-feather'
|
||||
@@ -16,10 +14,8 @@ import styled from 'styled-components/macro'
|
||||
|
||||
import { useIsUserAddedToken } from '../../../hooks/Tokens'
|
||||
import { useCurrencyBalance } from '../../../state/connection/hooks'
|
||||
import { useCombinedActiveList } from '../../../state/lists/hooks'
|
||||
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
|
||||
import { ThemedText } from '../../../theme'
|
||||
import { isTokenOnList } from '../../../utils'
|
||||
import Column, { AutoColumn } from '../../Column'
|
||||
import CurrencyLogo from '../../CurrencyLogo'
|
||||
import Loader from '../../Loader'
|
||||
@@ -129,13 +125,10 @@ export function CurrencyRow({
|
||||
}) {
|
||||
const { account } = useWeb3React()
|
||||
const key = currencyKey(currency)
|
||||
const selectedTokenList = useCombinedActiveList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenList, currency.isToken ? currency : undefined)
|
||||
const customAdded = useIsUserAddedToken(currency)
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
const warning = currency.isNative ? null : checkWarning(currency.address)
|
||||
const redesignFlagEnabled = useRedesignFlag() === RedesignVariant.Enabled
|
||||
const tokenSafetyFlagEnabled = useTokenSafetyFlag() === TokenSafetyVariant.Enabled
|
||||
const isBlockedToken = !!warning && !warning.canProceed
|
||||
const blockedTokenOpacity = '0.6'
|
||||
|
||||
@@ -168,15 +161,11 @@ export function CurrencyRow({
|
||||
<AutoColumn style={{ opacity: isBlockedToken ? blockedTokenOpacity : '1' }}>
|
||||
<Row>
|
||||
<CurrencyName title={currency.name}>{currency.name}</CurrencyName>
|
||||
{tokenSafetyFlagEnabled && <TokenSafetyIcon warning={warning} />}
|
||||
<TokenSafetyIcon warning={warning} />
|
||||
{isBlockedToken && <BlockedTokenIcon />}
|
||||
</Row>
|
||||
<ThemedText.DeprecatedDarkGray ml="0px" fontSize={'12px'} fontWeight={300}>
|
||||
{!currency.isNative && !isOnSelectedList && customAdded ? (
|
||||
<Trans>{currency.symbol} • Added by user</Trans>
|
||||
) : (
|
||||
currency.symbol
|
||||
)}
|
||||
{currency.symbol}
|
||||
</ThemedText.DeprecatedDarkGray>
|
||||
</AutoColumn>
|
||||
<Column>
|
||||
@@ -227,6 +216,14 @@ export const formatAnalyticsEventProperties = (
|
||||
: { search_token_address_input: isAddressSearch }),
|
||||
})
|
||||
|
||||
const LoadingRow = () => (
|
||||
<LoadingRows>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</LoadingRows>
|
||||
)
|
||||
|
||||
export default function CurrencyList({
|
||||
height,
|
||||
currencies,
|
||||
@@ -272,13 +269,7 @@ export default function CurrencyList({
|
||||
const token = currency?.wrapped
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingRows>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</LoadingRows>
|
||||
)
|
||||
return LoadingRow()
|
||||
} else if (currency) {
|
||||
return (
|
||||
<CurrencyRow
|
||||
@@ -303,7 +294,11 @@ export default function CurrencyList({
|
||||
return currencyKey(currency)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
return isLoading ? (
|
||||
<FixedSizeList height={height} ref={fixedListRef as any} width="100%" itemData={[]} itemCount={10} itemSize={56}>
|
||||
{LoadingRow}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
|
||||
@@ -13,18 +13,17 @@ import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
|
||||
import { tokenComparator, useSortTokensByQuery } from 'lib/hooks/useTokenList/sorting'
|
||||
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Edit } from 'react-feather'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { useAllTokenBalances } from 'state/connection/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useAllTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
|
||||
import { ButtonText, CloseIcon, IconWrapper, ThemedText } from '../../theme'
|
||||
import { useActiveTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
|
||||
import { CloseIcon, ThemedText } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import CommonBases from './CommonBases'
|
||||
import { CurrencyRow, formatAnalyticsEventProperties } from './CurrencyList'
|
||||
import CurrencyList from './CurrencyList'
|
||||
@@ -37,16 +36,6 @@ const ContentWrapper = styled(Column)<{ redesignFlag?: boolean }>`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const Footer = styled.div`
|
||||
width: 100%;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg1};
|
||||
border-top: 1px solid ${({ theme }) => theme.deprecated_bg2};
|
||||
`
|
||||
|
||||
interface CurrencySearchProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
@@ -56,7 +45,6 @@ interface CurrencySearchProps {
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
showManageView: () => void
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
@@ -68,7 +56,6 @@ export function CurrencySearch({
|
||||
disableNonToken,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
showManageView,
|
||||
}: CurrencySearchProps) {
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
@@ -84,7 +71,8 @@ export function CurrencySearch({
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const debouncedQuery = useDebounce(searchQuery, 200)
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
// Only display 'imported' tokens when the search filter has input
|
||||
const defaultTokens = useActiveTokens(debouncedQuery.length > 0)
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(debouncedQuery)
|
||||
@@ -104,32 +92,30 @@ export function CurrencySearch({
|
||||
}, [isAddressSearch])
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
return Object.values(allTokens).filter(getTokenFilter(debouncedQuery))
|
||||
}, [allTokens, debouncedQuery])
|
||||
return Object.values(defaultTokens).filter(getTokenFilter(debouncedQuery))
|
||||
}, [defaultTokens, debouncedQuery])
|
||||
|
||||
const [balances, balancesIsLoading] = useAllTokenBalances()
|
||||
const [balances, balancesAreLoading] = useAllTokenBalances()
|
||||
const sortedTokens: Token[] = useMemo(
|
||||
() => (!balancesIsLoading ? [...filteredTokens].sort(tokenComparator.bind(null, balances)) : []),
|
||||
[balances, filteredTokens, balancesIsLoading]
|
||||
() => (!balancesAreLoading ? [...filteredTokens].sort(tokenComparator.bind(null, balances)) : []),
|
||||
[balances, filteredTokens, balancesAreLoading]
|
||||
)
|
||||
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
|
||||
|
||||
const filteredSortedTokens = useSortTokensByQuery(debouncedQuery, sortedTokens)
|
||||
|
||||
const native = useNativeCurrency()
|
||||
const wrapped = native.wrapped
|
||||
|
||||
const filteredSortedTokensWithETH: Currency[] = useMemo(() => {
|
||||
// Use Celo ERC20 Implementation and exclude the native asset
|
||||
if (!native) {
|
||||
return filteredSortedTokens
|
||||
}
|
||||
|
||||
const searchCurrencies: Currency[] = useMemo(() => {
|
||||
const s = debouncedQuery.toLowerCase().trim()
|
||||
if (native.symbol?.toLowerCase()?.indexOf(s) !== -1) {
|
||||
// Always bump the native token to the top of the list.
|
||||
return [native, ...filteredSortedTokens.filter((t) => !t.equals(native))]
|
||||
}
|
||||
return filteredSortedTokens
|
||||
}, [debouncedQuery, native, filteredSortedTokens])
|
||||
|
||||
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
|
||||
const natives = (disableNonToken || native.equals(wrapped) ? [wrapped] : [native, wrapped]).filter(
|
||||
(n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1
|
||||
)
|
||||
return [...natives, ...tokens]
|
||||
}, [debouncedQuery, filteredSortedTokens, wrapped, disableNonToken, native])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency, hasWarning?: boolean) => {
|
||||
@@ -159,17 +145,17 @@ export function CurrencySearch({
|
||||
const s = debouncedQuery.toLowerCase().trim()
|
||||
if (s === native?.symbol?.toLowerCase()) {
|
||||
handleCurrencySelect(native)
|
||||
} else if (filteredSortedTokensWithETH.length > 0) {
|
||||
} else if (searchCurrencies.length > 0) {
|
||||
if (
|
||||
filteredSortedTokensWithETH[0].symbol?.toLowerCase() === debouncedQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokensWithETH.length === 1
|
||||
searchCurrencies[0].symbol?.toLowerCase() === debouncedQuery.trim().toLowerCase() ||
|
||||
searchCurrencies.length === 1
|
||||
) {
|
||||
handleCurrencySelect(filteredSortedTokensWithETH[0])
|
||||
handleCurrencySelect(searchCurrencies[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[debouncedQuery, native, filteredSortedTokensWithETH, handleCurrencySelect]
|
||||
[debouncedQuery, native, searchCurrencies, handleCurrencySelect]
|
||||
)
|
||||
|
||||
// menu ui
|
||||
@@ -241,20 +227,20 @@ export function CurrencySearch({
|
||||
)}
|
||||
/>
|
||||
</Column>
|
||||
) : filteredSortedTokens?.length > 0 || filteredInactiveTokens?.length > 0 ? (
|
||||
) : searchCurrencies?.length > 0 || filteredInactiveTokens?.length > 0 || isLoading ? (
|
||||
<div style={{ flex: '1' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
currencies={disableNonToken ? filteredSortedTokens : filteredSortedTokensWithETH}
|
||||
currencies={searchCurrencies}
|
||||
otherListTokens={filteredInactiveTokens}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={selectedCurrency}
|
||||
fixedListRef={fixedList}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
isLoading={balancesIsLoading && !tokenLoaderTimerElapsed}
|
||||
isLoading={isLoading}
|
||||
searchQuery={searchQuery}
|
||||
isAddressSearch={isAddressSearch}
|
||||
/>
|
||||
@@ -268,26 +254,6 @@ export function CurrencySearch({
|
||||
</ThemedText.DeprecatedMain>
|
||||
</Column>
|
||||
)}
|
||||
{!redesignFlagEnabled && (
|
||||
<Footer>
|
||||
<Row justify="center">
|
||||
<ButtonText
|
||||
onClick={showManageView}
|
||||
color={theme.deprecated_primary1}
|
||||
className="list-token-manage-button"
|
||||
>
|
||||
<RowFixed>
|
||||
<IconWrapper size="16px" marginRight="6px" stroke={theme.deprecated_primaryText1}>
|
||||
<Edit />
|
||||
</IconWrapper>
|
||||
<ThemedText.DeprecatedMain color={theme.deprecated_primaryText1}>
|
||||
<Trans>Manage Token Lists</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</RowFixed>
|
||||
</ButtonText>
|
||||
</Row>
|
||||
</Footer>
|
||||
)}
|
||||
</Trace>
|
||||
</ContentWrapper>
|
||||
)
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import TokenSafety from 'components/TokenSafety'
|
||||
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import { useUserAddedTokens } from 'state/user/hooks'
|
||||
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { useWindowSize } from '../../hooks/useWindowSize'
|
||||
import Modal from '../Modal'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import { ImportList } from './ImportList'
|
||||
import { ImportToken } from './ImportToken'
|
||||
import Manage from './Manage'
|
||||
|
||||
interface CurrencySearchModalProps {
|
||||
isOpen: boolean
|
||||
@@ -28,9 +21,7 @@ interface CurrencySearchModalProps {
|
||||
|
||||
export enum CurrencyModalView {
|
||||
search,
|
||||
manage,
|
||||
importToken,
|
||||
importList,
|
||||
tokenSafety,
|
||||
}
|
||||
|
||||
@@ -44,7 +35,7 @@ export default memo(function CurrencySearchModal({
|
||||
showCurrencyAmount = true,
|
||||
disableNonToken = false,
|
||||
}: CurrencySearchModalProps) {
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.search)
|
||||
const lastOpen = useLast(isOpen)
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
|
||||
@@ -59,44 +50,20 @@ export default memo(function CurrencySearchModal({
|
||||
setModalView(CurrencyModalView.tokenSafety)
|
||||
}
|
||||
|
||||
const tokenSafetyFlag = useTokenSafetyFlag()
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency, hasWarning?: boolean) => {
|
||||
if (
|
||||
tokenSafetyFlag === TokenSafetyVariant.Enabled &&
|
||||
hasWarning &&
|
||||
currency.isToken &&
|
||||
!userAddedTokens.find((token) => token.equals(currency))
|
||||
) {
|
||||
if (hasWarning && currency.isToken && !userAddedTokens.find((token) => token.equals(currency))) {
|
||||
showTokenSafetySpeedbump(currency)
|
||||
} else {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
[onDismiss, onCurrencySelect, tokenSafetyFlag, userAddedTokens]
|
||||
[onDismiss, onCurrencySelect, userAddedTokens]
|
||||
)
|
||||
|
||||
// for token import view
|
||||
const prevView = usePrevious(modalView)
|
||||
|
||||
// used for import token flow
|
||||
const [importToken, setImportToken] = useState<Token | undefined>()
|
||||
|
||||
// used for import list
|
||||
const [importList, setImportList] = useState<TokenList | undefined>()
|
||||
const [listURL, setListUrl] = useState<string | undefined>()
|
||||
|
||||
// used for token safety
|
||||
const [warningToken, setWarningToken] = useState<Token | undefined>()
|
||||
|
||||
const showManageView = useCallback(() => setModalView(CurrencyModalView.manage), [setModalView])
|
||||
const handleBackImport = useCallback(
|
||||
() => setModalView(prevView && prevView !== CurrencyModalView.importToken ? prevView : CurrencyModalView.search),
|
||||
[setModalView, prevView]
|
||||
)
|
||||
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
// change min height if not searching
|
||||
let modalHeight: number | undefined = 80
|
||||
@@ -117,13 +84,12 @@ export default memo(function CurrencySearchModal({
|
||||
showCommonBases={showCommonBases}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
disableNonToken={disableNonToken}
|
||||
showManageView={showManageView}
|
||||
/>
|
||||
)
|
||||
break
|
||||
case CurrencyModalView.tokenSafety:
|
||||
modalHeight = undefined
|
||||
if (tokenSafetyFlag === TokenSafetyVariant.Enabled && warningToken) {
|
||||
if (warningToken) {
|
||||
content = (
|
||||
<TokenSafety
|
||||
tokenAddress={warningToken.address}
|
||||
@@ -134,40 +100,6 @@ export default memo(function CurrencySearchModal({
|
||||
)
|
||||
}
|
||||
break
|
||||
case CurrencyModalView.importToken:
|
||||
if (importToken) {
|
||||
modalHeight = undefined
|
||||
if (tokenSafetyFlag === TokenSafetyVariant.Enabled) {
|
||||
showTokenSafetySpeedbump(importToken)
|
||||
}
|
||||
content = (
|
||||
<ImportToken
|
||||
tokens={[importToken]}
|
||||
onDismiss={onDismiss}
|
||||
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
|
||||
onBack={handleBackImport}
|
||||
handleCurrencySelect={handleCurrencySelect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
break
|
||||
case CurrencyModalView.importList:
|
||||
modalHeight = 40
|
||||
if (importList && listURL) {
|
||||
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
|
||||
}
|
||||
break
|
||||
case CurrencyModalView.manage:
|
||||
content = (
|
||||
<Manage
|
||||
onDismiss={onDismiss}
|
||||
setModalView={setModalView}
|
||||
setImportToken={setImportToken}
|
||||
setImportList={setImportList}
|
||||
setListUrl={setListUrl}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import ListLogo from 'components/ListLogo'
|
||||
import { AutoRow, RowBetween, RowFixed } from 'components/Row'
|
||||
import { SectionBreak } from 'components/swap/styleds'
|
||||
import { useFetchListCallback } from 'hooks/useFetchListCallback'
|
||||
import { transparentize } from 'polished'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { AlertTriangle, ArrowLeft } from 'react-feather'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { enableList, removeList } from 'state/lists/actions'
|
||||
import { useAllLists } from 'state/lists/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { CloseIcon, ThemedText } from 'theme'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { Checkbox, PaddedColumn, TextDot } from './styleds'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
interface ImportProps {
|
||||
listURL: string
|
||||
list: TokenList
|
||||
onDismiss: () => void
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
}
|
||||
|
||||
export function ImportList({ listURL, list, setModalView, onDismiss }: ImportProps) {
|
||||
const theme = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// user must accept
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
|
||||
const lists = useAllLists()
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
// monitor is list is loading
|
||||
const adding = Boolean(lists[listURL]?.loadingRequestId)
|
||||
const [addError, setAddError] = useState<string | null>(null)
|
||||
|
||||
const handleAddList = useCallback(() => {
|
||||
if (adding) return
|
||||
setAddError(null)
|
||||
fetchList(listURL)
|
||||
.then(() => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listURL,
|
||||
})
|
||||
|
||||
// turn list on
|
||||
dispatch(enableList(listURL))
|
||||
// go back to lists
|
||||
setModalView(CurrencyModalView.manage)
|
||||
})
|
||||
.catch((error) => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listURL,
|
||||
})
|
||||
setAddError(error.message)
|
||||
dispatch(removeList(listURL))
|
||||
})
|
||||
}, [adding, dispatch, fetchList, listURL, setModalView])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
|
||||
<RowBetween>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.manage)} />
|
||||
<ThemedText.DeprecatedMediumHeader>
|
||||
<Trans>Import List</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<SectionBreak />
|
||||
<PaddedColumn gap="md">
|
||||
<AutoColumn gap="md">
|
||||
<Card backgroundColor={theme.deprecated_bg2} padding="12px 20px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
{list.logoURI && <ListLogo logoURI={list.logoURI} size="40px" />}
|
||||
<AutoColumn gap="sm" style={{ marginLeft: '20px' }}>
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBody fontWeight={600} mr="6px">
|
||||
{list.name}
|
||||
</ThemedText.DeprecatedBody>
|
||||
<TextDot />
|
||||
<ThemedText.DeprecatedMain fontSize={'16px'} ml="6px">
|
||||
<Trans>{list.tokens.length} tokens</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</RowFixed>
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listURL}`}>
|
||||
<ThemedText.DeprecatedMain fontSize={'12px'} color={theme.deprecated_blue1}>
|
||||
{listURL}
|
||||
</ThemedText.DeprecatedMain>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</Card>
|
||||
<Card style={{ backgroundColor: transparentize(0.8, theme.deprecated_red1) }}>
|
||||
<AutoColumn justify="center" style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<AlertTriangle stroke={theme.deprecated_red1} size={32} />
|
||||
<ThemedText.DeprecatedBody fontWeight={500} fontSize={20} color={theme.deprecated_red1}>
|
||||
<Trans>Import at your own risk</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn style={{ textAlign: 'center', gap: '16px', marginBottom: '12px' }}>
|
||||
<ThemedText.DeprecatedBody fontWeight={500} color={theme.deprecated_red1}>
|
||||
<Trans>
|
||||
By adding this list you are implicitly trusting that the data is correct. Anyone can create a list,
|
||||
including creating fake versions of existing lists and lists that claim to represent projects that do
|
||||
not have one.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedBody fontWeight={600} color={theme.deprecated_red1}>
|
||||
<Trans>If you purchase a token from this list, you may not be able to sell it back.</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
<AutoRow justify="center" style={{ cursor: 'pointer' }} onClick={() => setConfirmed(!confirmed)}>
|
||||
<Checkbox
|
||||
name="confirmed"
|
||||
type="checkbox"
|
||||
checked={confirmed}
|
||||
onChange={() => setConfirmed(!confirmed)}
|
||||
/>
|
||||
<ThemedText.DeprecatedBody ml="10px" fontSize="16px" color={theme.deprecated_red1} fontWeight={500}>
|
||||
<Trans>I understand</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
|
||||
<ButtonPrimary
|
||||
disabled={!confirmed}
|
||||
altDisabledStyle={true}
|
||||
$borderRadius="20px"
|
||||
padding="10px 1rem"
|
||||
onClick={handleAddList}
|
||||
>
|
||||
<Trans>Import</Trans>
|
||||
</ButtonPrimary>
|
||||
{addError ? (
|
||||
<ThemedText.DeprecatedError title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</ThemedText.DeprecatedError>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
{/* </Card> */}
|
||||
</PaddedColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CloseIcon } from 'theme'
|
||||
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { ManageLists } from './ManageLists'
|
||||
import ManageTokens from './ManageTokens'
|
||||
import { PaddedColumn, Separator } from './styleds'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
`
|
||||
|
||||
const ToggleWrapper = styled(RowBetween)`
|
||||
background-color: ${({ theme }) => theme.deprecated_bg3};
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
`
|
||||
|
||||
const ToggleOption = styled.div<{ active?: boolean }>`
|
||||
width: 48%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
background-color: ${({ theme, active }) => (active ? theme.deprecated_bg1 : theme.deprecated_bg3)};
|
||||
color: ${({ theme, active }) => (active ? theme.deprecated_text1 : theme.deprecated_text2)};
|
||||
user-select: none;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Manage({
|
||||
onDismiss,
|
||||
setModalView,
|
||||
setImportList,
|
||||
setImportToken,
|
||||
setListUrl,
|
||||
}: {
|
||||
onDismiss: () => void
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportToken: (token: Token) => void
|
||||
setImportList: (list: TokenList) => void
|
||||
setListUrl: (url: string) => void
|
||||
}) {
|
||||
// toggle between tokens and lists
|
||||
const [showLists, setShowLists] = useState(true)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn>
|
||||
<RowBetween>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={() => setModalView(CurrencyModalView.search)} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
<Trans>Manage</Trans>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<Separator />
|
||||
<PaddedColumn style={{ paddingBottom: 0 }}>
|
||||
<ToggleWrapper>
|
||||
<ToggleOption onClick={() => setShowLists(!showLists)} active={showLists}>
|
||||
<Trans>Lists</Trans>
|
||||
</ToggleOption>
|
||||
<ToggleOption onClick={() => setShowLists(!showLists)} active={!showLists}>
|
||||
<Trans>Tokens</Trans>
|
||||
</ToggleOption>
|
||||
</ToggleWrapper>
|
||||
</PaddedColumn>
|
||||
{showLists ? (
|
||||
<ManageLists setModalView={setModalView} setImportList={setImportList} setListUrl={setListUrl} />
|
||||
) : (
|
||||
<ManageTokens setModalView={setModalView} setImportToken={setImportToken} />
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import Card from 'components/Card'
|
||||
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
|
||||
import { useListColor } from 'hooks/useColor'
|
||||
import parseENSAddress from 'lib/utils/parseENSAddress'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { ChangeEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CheckCircle, Settings } from 'react-feather'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { acceptListUpdate, disableList, enableList, removeList } from '../../state/lists/actions'
|
||||
import { useActiveListUrls, useAllLists, useIsListActive } from '../../state/lists/hooks'
|
||||
import { ExternalLink, IconWrapper, LinkStyledButton, ThemedText } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { ButtonEmpty, ButtonPrimary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import Toggle from '../Toggle'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
const Wrapper = styled(Column)`
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
`
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.deprecated_bg2};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.deprecated_text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledTitleText = styled.div<{ active: boolean }>`
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: ${({ theme, active }) => (active ? theme.deprecated_white : theme.deprecated_text2)};
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled(ThemedText.DeprecatedMain)<{ active: boolean }>`
|
||||
font-size: 12px;
|
||||
color: ${({ theme, active }) => (active ? theme.deprecated_white : theme.deprecated_text2)};
|
||||
`
|
||||
|
||||
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean; hasActiveTokens: boolean }>`
|
||||
background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.deprecated_bg2)};
|
||||
opacity: ${({ hasActiveTokens }) => (hasActiveTokens ? 1 : 0.4)};
|
||||
transition: 200ms;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
function listUrlRowHTMLId(listUrl: string) {
|
||||
return `list-row-${listUrl.replace(/\./g, '-')}`
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
const { chainId } = useWeb3React()
|
||||
const listsByUrl = useAppSelector((state) => state.lists.byUrl)
|
||||
const dispatch = useAppDispatch()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const activeTokensOnThisChain = useMemo(() => {
|
||||
if (!list || !chainId) {
|
||||
return 0
|
||||
}
|
||||
return list.tokens.reduce((acc, cur) => (cur.chainId === chainId ? acc + 1 : acc), 0)
|
||||
}, [chainId, list])
|
||||
|
||||
const theme = useTheme()
|
||||
const listColor = useListColor(list?.logoURI)
|
||||
const isActive = useIsListActive(listUrl)
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }],
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl,
|
||||
})
|
||||
if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleEnableList = useCallback(() => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Enable List',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(enableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleDisableList = useCallback(() => {
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Disable List',
|
||||
label: listUrl,
|
||||
})
|
||||
dispatch(disableList(listUrl))
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<RowWrapper
|
||||
active={isActive}
|
||||
hasActiveTokens={activeTokensOnThisChain > 0}
|
||||
bgColor={listColor}
|
||||
key={listUrl}
|
||||
id={listUrlRowHTMLId(listUrl)}
|
||||
>
|
||||
{list.logoURI ? (
|
||||
<ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<StyledTitleText active={isActive}>{list.name}</StyledTitleText>
|
||||
</Row>
|
||||
<RowFixed mt="4px">
|
||||
<StyledListUrlText active={isActive} mr="6px">
|
||||
<Trans>{activeTokensOnThisChain} tokens</Trans>
|
||||
</StyledListUrlText>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0">
|
||||
<Settings stroke={isActive ? theme.deprecated_bg1 : theme.deprecated_text1} size={12} />
|
||||
</ButtonEmpty>
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>
|
||||
<Trans>View list</Trans>
|
||||
</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
<Trans>Remove list</Trans>
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>
|
||||
<Trans>Update list</Trans>
|
||||
</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</RowFixed>
|
||||
</Column>
|
||||
<Toggle
|
||||
isActive={isActive}
|
||||
bgColor={listColor}
|
||||
toggle={() => {
|
||||
isActive ? handleDisableList() : handleEnableList()
|
||||
}}
|
||||
/>
|
||||
</RowWrapper>
|
||||
)
|
||||
})
|
||||
|
||||
const ListContainer = styled.div`
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
export function ManageLists({
|
||||
setModalView,
|
||||
setImportList,
|
||||
setListUrl,
|
||||
}: {
|
||||
setModalView: (view: CurrencyModalView) => void
|
||||
setImportList: (list: TokenList) => void
|
||||
setListUrl: (url: string) => void
|
||||
}) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const lists = useAllLists()
|
||||
|
||||
const tokenCountByListName = useMemo<Record<string, number>>(
|
||||
() =>
|
||||
Object.values(lists).reduce((acc, { current: list }) => {
|
||||
if (!list) {
|
||||
return acc
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[list.name]: list.tokens.reduce((count: number, token) => (token.chainId === chainId ? count + 1 : count), 0),
|
||||
}
|
||||
}, {}),
|
||||
[chainId, lists]
|
||||
)
|
||||
|
||||
// sort by active but only if not visible
|
||||
const activeListUrls = useActiveListUrls()
|
||||
|
||||
const handleInput = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setListUrlInput(e.target.value)
|
||||
}, [])
|
||||
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter((listUrl) => {
|
||||
// only show loaded lists, hide unsupported lists
|
||||
return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl))
|
||||
})
|
||||
.sort((listUrlA, listUrlB) => {
|
||||
const { current: listA } = lists[listUrlA]
|
||||
const { current: listB } = lists[listUrlB]
|
||||
|
||||
// first filter on active lists
|
||||
if (activeListUrls?.includes(listUrlA) && !activeListUrls?.includes(listUrlB)) {
|
||||
return -1
|
||||
}
|
||||
if (!activeListUrls?.includes(listUrlA) && activeListUrls?.includes(listUrlB)) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (listA && listB) {
|
||||
if (tokenCountByListName[listA.name] > tokenCountByListName[listB.name]) {
|
||||
return -1
|
||||
}
|
||||
if (tokenCountByListName[listA.name] < tokenCountByListName[listB.name]) {
|
||||
return 1
|
||||
}
|
||||
return listA.name.toLowerCase() < listB.name.toLowerCase()
|
||||
? -1
|
||||
: listA.name.toLowerCase() === listB.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (listA) return -1
|
||||
if (listB) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists, activeListUrls, tokenCountByListName])
|
||||
|
||||
// temporary fetched list for import flow
|
||||
const [tempList, setTempList] = useState<TokenList>()
|
||||
const [addError, setAddError] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchTempList() {
|
||||
fetchList(listUrlInput, false)
|
||||
.then((list) => setTempList(list))
|
||||
.catch(() => setAddError(t`Error importing list`))
|
||||
}
|
||||
// if valid url, fetch details for card
|
||||
if (validUrl) {
|
||||
fetchTempList()
|
||||
} else {
|
||||
setTempList(undefined)
|
||||
listUrlInput !== '' && setAddError(t`Enter valid list location`)
|
||||
}
|
||||
|
||||
// reset error
|
||||
if (listUrlInput === '') {
|
||||
setAddError(undefined)
|
||||
}
|
||||
}, [fetchList, listUrlInput, validUrl])
|
||||
|
||||
// check if list is already imported
|
||||
const isImported = Object.keys(lists).includes(listUrlInput)
|
||||
|
||||
// set list values and have parent modal switch to import list view
|
||||
const handleImport = useCallback(() => {
|
||||
if (!tempList) return
|
||||
setImportList(tempList)
|
||||
setModalView(CurrencyModalView.importList)
|
||||
setListUrl(listUrlInput)
|
||||
}, [listUrlInput, setImportList, setListUrl, setModalView, tempList])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<PaddedColumn gap="14px">
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder={t`https:// or ipfs:// or ENS name`}
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<ThemedText.DeprecatedError title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</ThemedText.DeprecatedError>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
{tempList && (
|
||||
<PaddedColumn style={{ paddingTop: 0 }}>
|
||||
<Card backgroundColor={theme.deprecated_bg2} padding="12px 20px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
{tempList.logoURI && <ListLogo logoURI={tempList.logoURI} size="40px" />}
|
||||
<AutoColumn gap="4px" style={{ marginLeft: '20px' }}>
|
||||
<ThemedText.DeprecatedBody fontWeight={600}>{tempList.name}</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedMain fontSize={'12px'}>
|
||||
<Trans>{tempList.tokens.length} tokens</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
{isImported ? (
|
||||
<RowFixed>
|
||||
<IconWrapper stroke={theme.deprecated_text2} size="16px" marginRight={'10px'}>
|
||||
<CheckCircle />
|
||||
</IconWrapper>
|
||||
<ThemedText.DeprecatedBody color={theme.deprecated_text2}>
|
||||
<Trans>Loaded</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</RowFixed>
|
||||
) : (
|
||||
<ButtonPrimary
|
||||
style={{ fontSize: '14px' }}
|
||||
padding="6px 8px"
|
||||
width="fit-content"
|
||||
onClick={handleImport}
|
||||
>
|
||||
<Trans>Import</Trans>
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</PaddedColumn>
|
||||
)}
|
||||
<Separator />
|
||||
<ListContainer>
|
||||
<AutoColumn gap="md">
|
||||
{sortedLists.map((listUrl) => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} />
|
||||
))}
|
||||
</AutoColumn>
|
||||
</ListContainer>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -11,10 +11,10 @@ exports[`ResizableTextArea renders correctly 1`] = `
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
resize: none;
|
||||
background-color: #F7F8FA;
|
||||
background-color: #F5F6FC;
|
||||
-webkit-transition: color 300ms step-start;
|
||||
transition: color 300ms step-start;
|
||||
color: #000000;
|
||||
color: #0E111A;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
@@ -34,19 +34,19 @@ exports[`ResizableTextArea renders correctly 1`] = `
|
||||
}
|
||||
|
||||
.c0::-webkit-input-placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c0::-moz-placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c0:-ms-input-placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c0::placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
<textarea
|
||||
@@ -74,10 +74,10 @@ exports[`TextInput renders correctly 1`] = `
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
background-color: #F7F8FA;
|
||||
background-color: #F5F6FC;
|
||||
-webkit-transition: color 300ms step-start;
|
||||
transition: color 300ms step-start;
|
||||
color: #000000;
|
||||
color: #0E111A;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
@@ -96,19 +96,19 @@ exports[`TextInput renders correctly 1`] = `
|
||||
}
|
||||
|
||||
.c0::-webkit-input-placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c0::-moz-placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c0:-ms-input-placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
.c0::placeholder {
|
||||
color: #C3C5CB;
|
||||
color: #99A1BD;
|
||||
}
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactComponent as Verified } from 'assets/svg/verified.svg'
|
||||
import { Warning } from 'constants/tokenSafety'
|
||||
import { Warning, WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const VerifiedContainer = styled.div`
|
||||
@@ -8,16 +8,17 @@ const VerifiedContainer = styled.div`
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export const VerifiedIcon = styled(Verified)<{ size?: string }>`
|
||||
export const WarningIcon = styled(AlertTriangle)<{ size?: string }>`
|
||||
width: ${({ size }) => size ?? '1em'};
|
||||
height: ${({ size }) => size ?? '1em'};
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
|
||||
export default function TokenSafetyIcon({ warning }: { warning: Warning | null }) {
|
||||
if (warning) return null
|
||||
if (warning?.level !== WARNING_LEVEL.UNKNOWN) return null
|
||||
return (
|
||||
<VerifiedContainer>
|
||||
<VerifiedIcon />
|
||||
<WarningIcon />
|
||||
</VerifiedContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Color } from 'theme/styled'
|
||||
|
||||
const Label = styled.div<{ color: Color }>`
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 20px 16px;
|
||||
background-color: ${({ color }) => color + '1F'};
|
||||
border-radius: 16px;
|
||||
color: ${({ color }) => color};
|
||||
@@ -31,9 +31,15 @@ const Title = styled(Text)`
|
||||
const DetailsRow = styled.div`
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const StyledLink = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 700;
|
||||
`
|
||||
|
||||
type TokenWarningMessageProps = {
|
||||
warning: Warning
|
||||
tokenAddress: string
|
||||
@@ -56,9 +62,9 @@ export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarn
|
||||
{description}
|
||||
{Boolean(description) && ' '}
|
||||
{tokenAddress && (
|
||||
<ExternalLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
<StyledLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</StyledLink>
|
||||
)}
|
||||
</DetailsRow>
|
||||
</Label>
|
||||
|
||||
@@ -244,6 +244,11 @@ export default function TokenSafety({
|
||||
}
|
||||
|
||||
const { heading, description } = getWarningCopy(displayWarning, plural)
|
||||
const learnMoreUrl = (
|
||||
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
<Trans>Learn more</Trans>
|
||||
</StyledExternalLink>
|
||||
)
|
||||
|
||||
return (
|
||||
displayWarning && (
|
||||
@@ -255,13 +260,9 @@ export default function TokenSafety({
|
||||
<ShortColumn>
|
||||
<SafetyLabel warning={displayWarning} />
|
||||
</ShortColumn>
|
||||
<ShortColumn>{heading && <InfoText fontSize="20px">{heading}</InfoText>}</ShortColumn>
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
{description}{' '}
|
||||
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
<Trans>Learn more</Trans>
|
||||
</StyledExternalLink>
|
||||
{heading} {description} {learnMoreUrl}
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<LinkColumn>{urls}</LinkColumn>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import { darken } from 'polished'
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { textFadeIn } from 'theme/animations'
|
||||
|
||||
import Resource from './Resource'
|
||||
@@ -24,8 +25,8 @@ const TokenDescriptionContainer = styled.div`
|
||||
const TruncateDescriptionButton = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
padding-top: 14px;
|
||||
font-size: 0.85em;
|
||||
padding-top: 0.5em;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
@@ -52,9 +53,8 @@ export const AboutContainer = styled.div`
|
||||
padding: 24px 0px;
|
||||
${textFadeIn}
|
||||
`
|
||||
export const AboutHeader = styled.span`
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
export const AboutHeader = styled(ThemedText.MediumHeader)`
|
||||
font-size: 28px !important;
|
||||
`
|
||||
|
||||
export const ResourcesContainer = styled.div`
|
||||
@@ -90,13 +90,17 @@ export function AboutSection({ address, description, homepageUrl, twitterName }:
|
||||
{tokenDescription}
|
||||
{shouldTruncate && (
|
||||
<TruncateDescriptionButton onClick={() => setIsDescriptionTruncated(!isDescriptionTruncated)}>
|
||||
{isDescriptionTruncated ? <Trans>Read more</Trans> : <Trans>Hide</Trans>}
|
||||
{isDescriptionTruncated ? <Trans>Show more</Trans> : <Trans>Hide</Trans>}
|
||||
</TruncateDescriptionButton>
|
||||
)}
|
||||
</TokenDescriptionContainer>
|
||||
<br />
|
||||
<ThemedText.SubHeaderSmall>
|
||||
<Trans>Links</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ResourcesContainer>
|
||||
<Resource name={'Etherscan'} link={`https://etherscan.io/address/${address}`} />
|
||||
<Resource name={'Protocol info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
|
||||
<Resource name={'More analytics'} link={`https://info.uniswap.org/#/tokens/${address}`} />
|
||||
{homepageUrl && <Resource name={'Website'} link={homepageUrl} />}
|
||||
{twitterName && <Resource name={'Twitter'} link={`https://twitter.com/${twitterName}`} />}
|
||||
</ResourcesContainer>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CopyContractAddress } from 'theme'
|
||||
import { CopyContractAddress, ThemedText } from 'theme'
|
||||
|
||||
export const ContractAddressSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-size: 0.9em;
|
||||
gap: 4px;
|
||||
padding: 36px 0px;
|
||||
padding: 4px 0px;
|
||||
`
|
||||
|
||||
const ContractAddress = styled.button`
|
||||
@@ -21,13 +20,14 @@ const ContractAddress = styled.button`
|
||||
border: none;
|
||||
min-height: 38px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export default function AddressSection({ address }: { address: string }) {
|
||||
return (
|
||||
<ContractAddressSection>
|
||||
<Trans>Contract address</Trans>
|
||||
<ThemedText.SubHeaderSmall>
|
||||
<Trans>Contract address</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ContractAddress>
|
||||
<CopyContractAddress address={address} />
|
||||
</ContractAddress>
|
||||
|
||||
@@ -1,83 +1,140 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { formatToDecimal } from 'analytics/utils'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { validateUrlChainParam } from 'graphql/data/util'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { StyledInternalLink } from 'theme'
|
||||
import { currencyAmountToPreciseFloat, formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
const BalancesCard = styled.div`
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: ${({ theme }) => theme.shallowShadow};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
border-radius: 16px;
|
||||
`
|
||||
const ErrorState = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
`
|
||||
const ErrorText = styled.span`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
height: fit-content;
|
||||
line-height: 16px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
|
||||
// 768 hardcoded to match NFT-redesign navbar breakpoints
|
||||
// src/nft/css/sprinkles.css.ts
|
||||
// change to match theme breakpoints when this navbar is updated
|
||||
@media screen and (min-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
const TotalBalanceSection = styled.div`
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
`
|
||||
const TotalBalance = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
font-size: 20px;
|
||||
justify-content: space-between;
|
||||
line-height: 28px;
|
||||
margin-top: 12px;
|
||||
align-items: center;
|
||||
`
|
||||
const TotalBalanceItem = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
export default function BalanceSummary({
|
||||
address,
|
||||
balance,
|
||||
balanceUsd,
|
||||
}: {
|
||||
address: string
|
||||
balance?: number
|
||||
balanceUsd?: number
|
||||
}) {
|
||||
const token = useToken(address)
|
||||
const { loading, error } = useNetworkTokenBalances({ address })
|
||||
const BalanceRowLink = styled(StyledInternalLink)`
|
||||
color: unset;
|
||||
`
|
||||
|
||||
function BalanceRow({ currency, formattedBalance, usdValue, href }: BalanceRowData) {
|
||||
const content = (
|
||||
<TotalBalance key={currency.wrapped.address}>
|
||||
<TotalBalanceItem>
|
||||
<CurrencyLogo currency={currency} />
|
||||
{formattedBalance} {currency?.symbol}
|
||||
</TotalBalanceItem>
|
||||
<TotalBalanceItem>{formatDollar({ num: usdValue === 0 ? undefined : usdValue, isPrice: true })}</TotalBalanceItem>
|
||||
</TotalBalance>
|
||||
)
|
||||
if (href) {
|
||||
return <BalanceRowLink to={href}>{content}</BalanceRowLink>
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
interface BalanceRowData {
|
||||
currency: Currency
|
||||
formattedBalance: number
|
||||
usdValue: number | undefined
|
||||
href?: string
|
||||
}
|
||||
export interface BalanceSummaryProps {
|
||||
tokenAmount: CurrencyAmount<Token> | undefined
|
||||
nativeCurrencyAmount: CurrencyAmount<Currency> | undefined
|
||||
isNative: boolean
|
||||
}
|
||||
|
||||
export default function BalanceSummary({ tokenAmount, nativeCurrencyAmount, isNative }: BalanceSummaryProps) {
|
||||
const balanceUsdValue = useStablecoinValue(tokenAmount)
|
||||
const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)
|
||||
|
||||
const { chainName } = useParams<{ chainName?: string }>()
|
||||
const pageChainName = validateUrlChainParam(chainName).toLowerCase()
|
||||
|
||||
const tokenIsWrappedNative =
|
||||
tokenAmount &&
|
||||
nativeCurrencyAmount &&
|
||||
tokenAmount.currency.address.toLowerCase() === nativeCurrencyAmount.currency.wrapped.address.toLowerCase()
|
||||
|
||||
if (
|
||||
(!tokenAmount && !nativeCurrencyAmount) ||
|
||||
(!tokenAmount && !tokenIsWrappedNative && !isNative) ||
|
||||
(!isNative && !tokenIsWrappedNative && tokenAmount?.equalTo(0)) ||
|
||||
(isNative && tokenAmount?.equalTo(0) && nativeCurrencyAmount?.equalTo(0))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const showNative = tokenIsWrappedNative || isNative
|
||||
|
||||
const currencies = []
|
||||
|
||||
if (tokenAmount) {
|
||||
const tokenData: BalanceRowData = {
|
||||
currency: tokenAmount.currency,
|
||||
formattedBalance: formatToDecimal(tokenAmount, Math.min(tokenAmount.currency.decimals, 2)),
|
||||
usdValue: balanceUsdValue ? currencyAmountToPreciseFloat(balanceUsdValue) : undefined,
|
||||
}
|
||||
if (isNative) {
|
||||
tokenData.href = `/tokens/${pageChainName}/${tokenAmount.currency.address}`
|
||||
}
|
||||
currencies.push(tokenData)
|
||||
}
|
||||
if (showNative && nativeCurrencyAmount) {
|
||||
const nativeData: BalanceRowData = {
|
||||
currency: nativeCurrencyAmount.currency,
|
||||
formattedBalance: formatToDecimal(nativeCurrencyAmount, Math.min(nativeCurrencyAmount.currency.decimals, 2)),
|
||||
usdValue: nativeBalanceUsdValue ? currencyAmountToPreciseFloat(nativeBalanceUsdValue) : undefined,
|
||||
}
|
||||
if (isNative) {
|
||||
currencies.unshift(nativeData)
|
||||
} else {
|
||||
nativeData.href = `/tokens/${pageChainName}/NATIVE`
|
||||
currencies.push(nativeData)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading || (!error && !balance && !balanceUsd)) return null
|
||||
return (
|
||||
<BalancesCard>
|
||||
{error ? (
|
||||
<ErrorState>
|
||||
<AlertTriangle size={24} />
|
||||
<ErrorText>
|
||||
<Trans>There was an error loading your {token?.symbol} balance</Trans>
|
||||
</ErrorText>
|
||||
</ErrorState>
|
||||
) : (
|
||||
<>
|
||||
<TotalBalanceSection>
|
||||
Your balance
|
||||
<TotalBalance>
|
||||
<TotalBalanceItem>{`${balance} ${token?.symbol}`}</TotalBalanceItem>
|
||||
<TotalBalanceItem>{`$${balanceUsd}`}</TotalBalanceItem>
|
||||
</TotalBalance>
|
||||
</TotalBalanceSection>
|
||||
</>
|
||||
)}
|
||||
<TotalBalanceSection>
|
||||
<Trans>Your balance</Trans>
|
||||
{currencies.map((props, i) => (
|
||||
<BalanceRow {...props} key={props.currency.wrapped.address + i} />
|
||||
))}
|
||||
</TotalBalanceSection>
|
||||
</BalancesCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { NativeCurrency, Token } from '@uniswap/sdk-core'
|
||||
import { Currency, NativeCurrency, Token } from '@uniswap/sdk-core'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { SingleTokenData, useTokenPricesCached } from 'graphql/data/Token'
|
||||
import { PriceDurations, PricePoint, SingleTokenData } from 'graphql/data/Token'
|
||||
import { TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||
import useCurrencyLogoURIs, { getTokenLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import { useMemo } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { textFadeIn } from 'theme/animations'
|
||||
import { isAddress } from 'utils'
|
||||
|
||||
import { useIsFavorited, useToggleFavorite } from '../state'
|
||||
import { ClickFavorited, FavoriteIcon, L2NetworkLogo, LogoContainer } from '../TokenTable/TokenRow'
|
||||
import { filterTimeAtom } from '../state'
|
||||
import { L2NetworkLogo, LogoContainer } from '../TokenTable/TokenRow'
|
||||
import PriceChart from './PriceChart'
|
||||
import ShareButton from './ShareButton'
|
||||
|
||||
@@ -59,58 +57,74 @@ export function useTokenLogoURI(
|
||||
token: NonNullable<SingleTokenData> | NonNullable<TopToken>,
|
||||
nativeCurrency?: Token | NativeCurrency
|
||||
) {
|
||||
const checksummedAddress = isAddress(token.address)
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
|
||||
return (
|
||||
useCurrencyLogoURIs(nativeCurrency)[0] ??
|
||||
(checksummedAddress && getTokenLogoURI(checksummedAddress, chainId)) ??
|
||||
token.project?.logoUrl
|
||||
)
|
||||
return [
|
||||
...useCurrencyLogoURIs(nativeCurrency),
|
||||
...useCurrencyLogoURIs({ ...token, chainId }),
|
||||
token.project?.logoUrl,
|
||||
][0]
|
||||
}
|
||||
|
||||
export default function ChartSection({
|
||||
token,
|
||||
currency,
|
||||
nativeCurrency,
|
||||
prices,
|
||||
}: {
|
||||
token: NonNullable<SingleTokenData>
|
||||
currency?: Currency | null
|
||||
nativeCurrency?: Token | NativeCurrency
|
||||
prices: PriceDurations
|
||||
}) {
|
||||
const isFavorited = useIsFavorited(token.address)
|
||||
const toggleFavorite = useToggleFavorite(token.address)
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
|
||||
const L2Icon = getChainInfo(chainId).circleLogoUrl
|
||||
const warning = checkWarning(token.address ?? '')
|
||||
|
||||
const { prices } = useTokenPricesCached(token)
|
||||
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
|
||||
const logoSrc = useTokenLogoURI(token, nativeCurrency)
|
||||
|
||||
// Backend doesn't always return latest price point for every duration.
|
||||
// Thus we need to manually determine latest price point available, and
|
||||
// append it to the prices list for every duration.
|
||||
useMemo(() => {
|
||||
let latestPricePoint: PricePoint = { value: 0, timestamp: 0 }
|
||||
let latestPricePointTimePeriod: TimePeriod
|
||||
Object.keys(prices).forEach((key) => {
|
||||
const latestPricePointForTimePeriod = prices[key as unknown as TimePeriod]?.slice(-1)[0]
|
||||
if (latestPricePointForTimePeriod && latestPricePointForTimePeriod.timestamp > latestPricePoint.timestamp) {
|
||||
latestPricePoint = latestPricePointForTimePeriod
|
||||
latestPricePointTimePeriod = key as unknown as TimePeriod
|
||||
}
|
||||
})
|
||||
Object.keys(prices).forEach((key) => {
|
||||
if ((key as unknown as TimePeriod) !== latestPricePointTimePeriod) {
|
||||
prices[key as unknown as TimePeriod]?.push(latestPricePoint)
|
||||
}
|
||||
})
|
||||
}, [prices])
|
||||
|
||||
return (
|
||||
<ChartHeader>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<LogoContainer>
|
||||
<CurrencyLogo src={logoSrc} size={'32px'} symbol={nativeCurrency?.symbol ?? token.symbol} />
|
||||
<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>
|
||||
{!warning && <VerifiedIcon size="20px" />}
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
{token.name && token.symbol && token.address && (
|
||||
<ShareButton tokenName={token.name} tokenSymbol={token.symbol} tokenAddress={token.address} />
|
||||
)}
|
||||
{useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled && (
|
||||
<ClickFavorited onClick={toggleFavorite}>
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
)}
|
||||
{token.name && token.symbol && token.address && <ShareButton token={token} isNative={!!nativeCurrency} />}
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ParentSize>
|
||||
{({ width, height }) => prices && <PriceChart prices={prices} width={width} height={height} />}
|
||||
{({ width, height }) => prices && <PriceChart prices={prices[timePeriod]} width={width} height={height} />}
|
||||
</ParentSize>
|
||||
</ChartContainer>
|
||||
</ChartHeader>
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { useState } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { LoadingBubble } from '../loading'
|
||||
|
||||
const PLACEHOLDER_NAV_FOOTER_HEIGHT = '56px'
|
||||
const BalanceFooter = styled.div`
|
||||
height: fit-content;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
padding: 12px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: ${PLACEHOLDER_NAV_FOOTER_HEIGHT};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
`
|
||||
const BalanceValue = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`
|
||||
const BalanceTotal = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const BalanceInfo = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
`
|
||||
const FakeFooterNavBar = styled.div`
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
background-color: ${({ theme }) => theme.backgroundBackdrop};
|
||||
height: ${PLACEHOLDER_NAV_FOOTER_HEIGHT};
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
padding: 20px 8px;
|
||||
font-size: 10px;
|
||||
`
|
||||
const FiatValue = styled.span`
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
|
||||
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
|
||||
line-height: 16px;
|
||||
}
|
||||
`
|
||||
const NetworkBalancesSection = styled.div`
|
||||
height: fit-content;
|
||||
border-top: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 0px 8px 0px;
|
||||
margin-top: 16px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const NetworkBalancesLabel = styled.span`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
const SwapButton = styled.button`
|
||||
background-color: ${({ theme }) => theme.accentAction};
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
padding: 12px 16px;
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
`
|
||||
const TotalBalancesSection = styled.div`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
const ViewAll = styled.span`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
`
|
||||
const ErrorState = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 8px;
|
||||
`
|
||||
const LoadingState = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
`
|
||||
const TopBalanceLoadBubble = styled(LoadingBubble)`
|
||||
height: 12px;
|
||||
width: 172px;
|
||||
`
|
||||
const BottomBalanceLoadBubble = styled(LoadingBubble)`
|
||||
height: 16px;
|
||||
width: 188px;
|
||||
`
|
||||
const ErrorText = styled.span`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
|
||||
export default function FooterBalanceSummary({
|
||||
address,
|
||||
networkBalances,
|
||||
balance,
|
||||
balanceUsd,
|
||||
}: {
|
||||
address: string
|
||||
networkBalances: (JSX.Element | null)[] | null
|
||||
balance?: number
|
||||
balanceUsd?: number
|
||||
}) {
|
||||
const tokenSymbol = useToken(address)?.symbol
|
||||
const [showMultipleBalances, setShowMultipleBalances] = useState(false)
|
||||
const multipleBalances = false // for testing purposes
|
||||
const networkNameIfOneBalance = 'Ethereum' // for testing purposes
|
||||
const { loading, error } = useNetworkTokenBalances({ address })
|
||||
return (
|
||||
<BalanceFooter>
|
||||
<TotalBalancesSection>
|
||||
{loading ? (
|
||||
<LoadingState>
|
||||
<TopBalanceLoadBubble></TopBalanceLoadBubble>
|
||||
<BottomBalanceLoadBubble></BottomBalanceLoadBubble>
|
||||
</LoadingState>
|
||||
) : error ? (
|
||||
<ErrorState>
|
||||
<AlertTriangle size={17} />
|
||||
<ErrorText>
|
||||
<Trans>There was an error fetching your balance</Trans>
|
||||
</ErrorText>
|
||||
</ErrorState>
|
||||
) : (
|
||||
!!balance &&
|
||||
!!balanceUsd && (
|
||||
<BalanceInfo>
|
||||
{multipleBalances ? 'Balance on all networks' : `Your balance on ${networkNameIfOneBalance}`}
|
||||
<BalanceTotal>
|
||||
<BalanceValue>
|
||||
{balance} {tokenSymbol}
|
||||
</BalanceValue>
|
||||
<FiatValue>{`$${balanceUsd}`}</FiatValue>
|
||||
</BalanceTotal>
|
||||
{multipleBalances && (
|
||||
<ViewAll onClick={() => setShowMultipleBalances(!showMultipleBalances)}>
|
||||
<Trans>{showMultipleBalances ? 'Hide' : 'View'} all balances</Trans>
|
||||
</ViewAll>
|
||||
)}
|
||||
</BalanceInfo>
|
||||
)
|
||||
)}
|
||||
<Link to={`/swap?outputCurrency=${address}`}>
|
||||
<SwapButton>
|
||||
<Trans>Swap</Trans>
|
||||
</SwapButton>
|
||||
</Link>
|
||||
</TotalBalancesSection>
|
||||
{showMultipleBalances && (
|
||||
<NetworkBalancesSection>
|
||||
<NetworkBalancesLabel>
|
||||
<Trans>Your balances by network</Trans>
|
||||
</NetworkBalancesLabel>
|
||||
{networkBalances}
|
||||
</NetworkBalancesSection>
|
||||
)}
|
||||
<FakeFooterNavBar>**leaving space for updated nav footer**</FakeFooterNavBar>
|
||||
</BalanceFooter>
|
||||
)
|
||||
}
|
||||
40
src/components/Tokens/TokenDetails/InfoTip.tsx
Normal file
40
src/components/Tokens/TokenDetails/InfoTip.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { Info } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const InfoTipContainer = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
cursor: help;
|
||||
`
|
||||
|
||||
const InfoTipBody = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
`
|
||||
|
||||
const InfoTipWrapper = styled.div`
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function InfoTip({ text }: { text: ReactNode; size?: number }) {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
return (
|
||||
<InfoTipWrapper>
|
||||
<Tooltip text={<InfoTipBody>{text}</InfoTipBody>} show={show} placement="right">
|
||||
<InfoTipContainer onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Info size={14} />
|
||||
</InfoTipContainer>
|
||||
</Tooltip>
|
||||
</InfoTipWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Footer, LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
|
||||
import { WidgetSkeleton } from 'components/Widget'
|
||||
import { LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { LoadingBubble } from '../loading'
|
||||
@@ -154,8 +155,9 @@ export function LoadingTokenDetails() {
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
<LoadingTokenDetail />
|
||||
<RightPanel />
|
||||
<Footer />
|
||||
<RightPanel>
|
||||
<WidgetSkeleton />
|
||||
</RightPanel>
|
||||
</TokenDetailsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatToDecimal } from 'analytics/utils'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import styled from 'styled-components/macro'
|
||||
import { StyledInternalLink } from 'theme'
|
||||
import { currencyAmountToPreciseFloat, formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
import { BalanceSummaryProps } from './BalanceSummary'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
bottom: 56px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
height: fit-content;
|
||||
justify-content: space-between;
|
||||
left: 0;
|
||||
line-height: 20px;
|
||||
padding: 12px 16px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||
bottom: 0px;
|
||||
}
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const BalanceValue = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`
|
||||
const BalanceTotal = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
`
|
||||
const BalanceInfo = styled.div`
|
||||
display: flex;
|
||||
flex: 10 1 auto;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
`
|
||||
const FiatValue = styled.span`
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.sm}px) {
|
||||
line-height: 24px;
|
||||
}
|
||||
`
|
||||
const SwapButton = styled(StyledInternalLink)`
|
||||
background-color: ${({ theme }) => theme.accentAction};
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
padding: 12px 16px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
height: 44px;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
max-width: 100vw;
|
||||
`
|
||||
|
||||
export default function MobileBalanceSummaryFooter({
|
||||
tokenAmount,
|
||||
nativeCurrencyAmount,
|
||||
isNative,
|
||||
tokenAddress,
|
||||
}: BalanceSummaryProps & { tokenAddress: string }) {
|
||||
const balanceUsdValue = useStablecoinValue(tokenAmount)
|
||||
const nativeBalanceUsdValue = useStablecoinValue(nativeCurrencyAmount)
|
||||
|
||||
const formattedBalance = tokenAmount
|
||||
? formatToDecimal(tokenAmount, Math.min(tokenAmount.currency.decimals, 2))
|
||||
: undefined
|
||||
|
||||
const balanceUsd = balanceUsdValue ? currencyAmountToPreciseFloat(balanceUsdValue) : undefined
|
||||
|
||||
const formattedNativeBalance = nativeCurrencyAmount
|
||||
? formatToDecimal(nativeCurrencyAmount, Math.min(nativeCurrencyAmount.currency.decimals, 2))
|
||||
: undefined
|
||||
const nativeBalanceUsd = nativeBalanceUsdValue ? currencyAmountToPreciseFloat(nativeBalanceUsdValue) : undefined
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{Boolean(formattedBalance !== undefined && !isNative && tokenAmount?.greaterThan(0)) && (
|
||||
<BalanceInfo>
|
||||
<Trans>Your {tokenAmount?.currency?.symbol} balance</Trans>
|
||||
<BalanceTotal>
|
||||
<BalanceValue>
|
||||
{formattedBalance} {tokenAmount?.currency?.symbol}
|
||||
</BalanceValue>
|
||||
<FiatValue>{formatDollar({ num: balanceUsd, isPrice: true })}</FiatValue>
|
||||
</BalanceTotal>
|
||||
</BalanceInfo>
|
||||
)}
|
||||
{Boolean(isNative && nativeCurrencyAmount?.greaterThan(0)) && (
|
||||
<BalanceInfo>
|
||||
<Trans>Your {nativeCurrencyAmount?.currency?.symbol} balance</Trans>
|
||||
<BalanceTotal>
|
||||
<BalanceValue>
|
||||
{formattedNativeBalance} {nativeCurrencyAmount?.currency?.symbol}
|
||||
</BalanceValue>
|
||||
<FiatValue>{formatDollar({ num: nativeBalanceUsd, isPrice: true })}</FiatValue>
|
||||
</BalanceTotal>
|
||||
</BalanceInfo>
|
||||
)}
|
||||
<SwapButton to={`/swap?outputCurrency=${tokenAddress}`}>
|
||||
<Trans>Swap</Trans>
|
||||
</SwapButton>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
const Balance = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
`
|
||||
const BalanceItem = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
const BalanceRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
const Logo = styled.img`
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-right: 8px;
|
||||
`
|
||||
const Network = styled.span<{ color: string }>`
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: ${({ color }) => color};
|
||||
`
|
||||
const NetworkBalanceContainer = styled.div`
|
||||
display: flex;
|
||||
padding-top: 16px;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function NetworkBalance({
|
||||
logoUrl,
|
||||
balance,
|
||||
tokenSymbol,
|
||||
fiatValue,
|
||||
label,
|
||||
networkColor,
|
||||
}: {
|
||||
logoUrl: string
|
||||
balance: string
|
||||
tokenSymbol: string
|
||||
fiatValue: string | number
|
||||
label: string
|
||||
networkColor: string | undefined
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<NetworkBalanceContainer>
|
||||
<Logo src={logoUrl} />
|
||||
<Balance>
|
||||
<BalanceRow>
|
||||
<BalanceItem>
|
||||
{balance} {tokenSymbol}
|
||||
</BalanceItem>
|
||||
<BalanceItem>${fiatValue}</BalanceItem>
|
||||
</BalanceRow>
|
||||
<Network color={networkColor ?? theme.textPrimary}>{label}</Network>
|
||||
</Balance>
|
||||
</NetworkBalanceContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { AxisBottom, TickFormatter } from '@visx/axis'
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
@@ -5,23 +6,13 @@ 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,
|
||||
timeTicks,
|
||||
} from 'd3'
|
||||
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
|
||||
import { PricePoint } from 'graphql/data/Token'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import {
|
||||
dayHourFormatter,
|
||||
@@ -29,9 +20,9 @@ import {
|
||||
monthDayFormatter,
|
||||
monthTickFormatter,
|
||||
monthYearDayFormatter,
|
||||
monthYearFormatter,
|
||||
weekFormatter,
|
||||
} from 'utils/formatChartTimes'
|
||||
import { formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
|
||||
@@ -120,6 +111,7 @@ 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;
|
||||
@@ -221,12 +213,6 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
monthYearDayFormatter(locale),
|
||||
timeMonth.range(startDateWithOffset, endDateWithOffset, 2).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.ALL:
|
||||
return [
|
||||
monthYearFormatter(locale),
|
||||
monthYearDayFormatter(locale),
|
||||
timeTicks(startDateWithOffset, endDateWithOffset, 6).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,100 +250,110 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
setDisplayPrice(endingPrice)
|
||||
}, [setCrosshair, setDisplayPrice, endingPrice])
|
||||
|
||||
// TODO: Display no data available error
|
||||
if (!prices) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [tickFormatter, crosshairDateFormatter, ticks] = tickFormat(timePeriod, locale)
|
||||
const delta = calculateDelta(startingPrice.value, displayPrice.value)
|
||||
const formattedDelta = formatDelta(delta)
|
||||
const arrow = getDeltaArrow(delta)
|
||||
const crosshairEdgeMax = width * 0.85
|
||||
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
|
||||
const hasData = prices && prices.length > 0
|
||||
|
||||
/* Default curve doesn't look good for the HOUR/ALL chart */
|
||||
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : timePeriod === TimePeriod.HOUR ? 1 : 0.9
|
||||
/*
|
||||
* Default curve doesn't look good for the HOUR chart.
|
||||
* Higher values make the curve more rigid, lower values smooth the curve but make it less "sticky" to real data points,
|
||||
* making it unacceptable for shorter durations / smaller variances.
|
||||
*/
|
||||
const curveTension = timePeriod === TimePeriod.HOUR ? 1 : 0.9
|
||||
|
||||
const getX = useMemo(() => (p: PricePoint) => timeScale(p.timestamp), [timeScale])
|
||||
const getY = useMemo(() => (p: PricePoint) => rdScale(p.value), [rdScale])
|
||||
const curve = useMemo(() => curveCardinal.tension(curveTension), [curveTension])
|
||||
return (
|
||||
<>
|
||||
<ChartHeader>
|
||||
<TokenPrice>${displayPrice.value < 0.000001 ? '<0.000001' : displayPrice.value.toFixed(6)}</TokenPrice>
|
||||
<TokenPrice>{formatDollar({ num: displayPrice.value, isPrice: true })}</TokenPrice>
|
||||
<DeltaContainer>
|
||||
{formattedDelta}
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<AnimatedInLineChart
|
||||
data={prices}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
marginTop={margin.top}
|
||||
curve={curveCardinal.tension(curveTension)}
|
||||
strokeWidth={2}
|
||||
width={width}
|
||||
height={graphHeight}
|
||||
>
|
||||
{crosshair !== null ? (
|
||||
<g>
|
||||
<AxisBottom
|
||||
scale={timeScale}
|
||||
stroke={theme.backgroundOutline}
|
||||
tickFormat={tickFormatter}
|
||||
tickStroke={theme.backgroundOutline}
|
||||
tickLength={4}
|
||||
hideTicks={true}
|
||||
tickTransform={'translate(0 -5)'}
|
||||
tickValues={ticks}
|
||||
top={graphHeight - 1}
|
||||
tickLabelProps={() => ({
|
||||
fill: theme.textSecondary,
|
||||
fontSize: 12,
|
||||
textAnchor: 'middle',
|
||||
transform: 'translate(0 -24)',
|
||||
})}
|
||||
/>
|
||||
<text
|
||||
x={crosshair + (crosshairAtEdge ? -4 : 4)}
|
||||
y={margin.crosshair + 10}
|
||||
textAnchor={crosshairAtEdge ? 'end' : 'start'}
|
||||
fontSize={12}
|
||||
fill={theme.textSecondary}
|
||||
>
|
||||
{crosshairDateFormatter(displayPrice.timestamp)}
|
||||
</text>
|
||||
<Line
|
||||
from={{ x: crosshair, y: margin.crosshair }}
|
||||
to={{ x: crosshair, y: graphHeight }}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={1}
|
||||
pointerEvents="none"
|
||||
strokeDasharray="4,4"
|
||||
/>
|
||||
<GlyphCircle
|
||||
left={crosshair}
|
||||
top={rdScale(displayPrice.value) + margin.top}
|
||||
size={50}
|
||||
fill={theme.accentActive}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</g>
|
||||
) : (
|
||||
<AxisBottom scale={timeScale} stroke={theme.backgroundOutline} top={graphHeight - 1} hideTicks />
|
||||
)}
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
{!hasData ? (
|
||||
<MissingPriceChart
|
||||
width={width}
|
||||
height={graphHeight}
|
||||
fill={'transparent'}
|
||||
onTouchStart={handleHover}
|
||||
onTouchMove={handleHover}
|
||||
onMouseMove={handleHover}
|
||||
onMouseLeave={resetDisplay}
|
||||
message={prices && prices.length === 0 ? <NoV3DataMessage /> : <MissingDataMessage />}
|
||||
/>
|
||||
</AnimatedInLineChart>
|
||||
) : (
|
||||
<svg width={width} height={graphHeight}>
|
||||
<AnimatedInLineChart
|
||||
data={prices}
|
||||
getX={getX}
|
||||
getY={getY}
|
||||
marginTop={margin.top}
|
||||
curve={curve}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
{crosshair !== null ? (
|
||||
<g>
|
||||
<AxisBottom
|
||||
scale={timeScale}
|
||||
stroke={theme.backgroundOutline}
|
||||
tickFormat={tickFormatter}
|
||||
tickStroke={theme.backgroundOutline}
|
||||
tickLength={4}
|
||||
hideTicks={true}
|
||||
tickTransform={'translate(0 -5)'}
|
||||
tickValues={ticks}
|
||||
top={graphHeight - 1}
|
||||
tickLabelProps={() => ({
|
||||
fill: theme.textSecondary,
|
||||
fontSize: 12,
|
||||
textAnchor: 'middle',
|
||||
transform: 'translate(0 -24)',
|
||||
})}
|
||||
/>
|
||||
<text
|
||||
x={crosshair + (crosshairAtEdge ? -4 : 4)}
|
||||
y={margin.crosshair + 10}
|
||||
textAnchor={crosshairAtEdge ? 'end' : 'start'}
|
||||
fontSize={12}
|
||||
fill={theme.textSecondary}
|
||||
>
|
||||
{crosshairDateFormatter(displayPrice.timestamp)}
|
||||
</text>
|
||||
<Line
|
||||
from={{ x: crosshair, y: margin.crosshair }}
|
||||
to={{ x: crosshair, y: graphHeight }}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={1}
|
||||
pointerEvents="none"
|
||||
strokeDasharray="4,4"
|
||||
/>
|
||||
<GlyphCircle
|
||||
left={crosshair}
|
||||
top={rdScale(displayPrice.value) + margin.top}
|
||||
size={50}
|
||||
fill={theme.accentAction}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
</g>
|
||||
) : (
|
||||
<AxisBottom scale={timeScale} stroke={theme.backgroundOutline} top={graphHeight - 1} hideTicks />
|
||||
)}
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={width}
|
||||
height={graphHeight}
|
||||
fill={'transparent'}
|
||||
onTouchStart={handleHover}
|
||||
onTouchMove={handleHover}
|
||||
onMouseMove={handleHover}
|
||||
onMouseLeave={resetDisplay}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<TimeOptionsWrapper>
|
||||
<TimeOptionsContainer>
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
@@ -377,4 +373,44 @@ export function PriceChart({ width, height, prices }: PriceChartProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const StyledMissingChart = styled.svg`
|
||||
text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
|
||||
const chartBottomPadding = 15
|
||||
|
||||
const NoV3DataMessage = () => (
|
||||
<Trans>This token doesn't have chart data because it hasn't been traded on Uniswap v3</Trans>
|
||||
)
|
||||
const MissingDataMessage = () => <Trans>Missing chart data</Trans>
|
||||
|
||||
function MissingPriceChart({ width, height, message }: { width: number; height: number; message: ReactNode }) {
|
||||
const theme = useTheme()
|
||||
const midPoint = height / 2 + 45
|
||||
return (
|
||||
<StyledMissingChart width={width} height={height}>
|
||||
<path
|
||||
d={`M 0 ${midPoint} Q 104 ${midPoint - 70}, 208 ${midPoint} T 416 ${midPoint}
|
||||
M 416 ${midPoint} Q 520 ${midPoint - 70}, 624 ${midPoint} T 832 ${midPoint}`}
|
||||
stroke={theme.backgroundOutline}
|
||||
fill="transparent"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<TrendingUp stroke={theme.textTertiary} x={0} size={12} y={height - chartBottomPadding - 10} />
|
||||
<text y={height - chartBottomPadding} x="20" fill={theme.textTertiary}>
|
||||
{message || <Trans>Missing chart data</Trans>}
|
||||
</text>
|
||||
<path
|
||||
d={`M 0 ${height - 1}, ${width} ${height - 1}`}
|
||||
stroke={theme.backgroundOutline}
|
||||
fill="transparent"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</StyledMissingChart>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceChart
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { SingleTokenData } from 'graphql/data/Token'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useRef } from 'react'
|
||||
import { Twitter } from 'react-feather'
|
||||
@@ -62,9 +64,8 @@ const ShareAction = styled.div`
|
||||
`
|
||||
|
||||
interface TokenInfo {
|
||||
tokenName: string
|
||||
tokenSymbol: string
|
||||
tokenAddress: string
|
||||
token: NonNullable<SingleTokenData>
|
||||
isNative: boolean
|
||||
}
|
||||
|
||||
export default function ShareButton(tokenInfo: TokenInfo) {
|
||||
@@ -75,11 +76,12 @@ export default function ShareButton(tokenInfo: TokenInfo) {
|
||||
useOnClickOutside(node, open ? toggleShare : undefined)
|
||||
const positionX = (window.screen.width - TWITTER_WIDTH) / 2
|
||||
const positionY = (window.screen.height - TWITTER_HEIGHT) / 2
|
||||
const tokenAddress = tokenInfo.isNative ? NATIVE_CHAIN_ID : tokenInfo.token.address
|
||||
|
||||
const shareTweet = () => {
|
||||
toggleShare()
|
||||
window.open(
|
||||
`https://twitter.com/intent/tweet?text=Check%20out%20${tokenInfo.tokenName}%20(${tokenInfo.tokenSymbol})%20https://app.uniswap.org/%23/tokens/${tokenInfo.tokenAddress}%20via%20@uniswap`,
|
||||
`https://twitter.com/intent/tweet?text=Check%20out%20${tokenInfo.token.name}%20(${tokenInfo.token.symbol})%20https://app.uniswap.org/%23/tokens/${tokenInfo.token.chain}/${tokenAddress}%20via%20@uniswap`,
|
||||
'newwindow',
|
||||
`left=${positionX}, top=${positionY}, width=${TWITTER_WIDTH}, height=${TWITTER_HEIGHT}`
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { textFadeIn } from 'theme/animations'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
import { formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
import { TokenSortMethod } from '../state'
|
||||
import { HEADER_DESCRIPTIONS } from '../TokenTable/TokenRow'
|
||||
import InfoTip from './InfoTip'
|
||||
|
||||
export const StatWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -17,13 +22,21 @@ export const StatWrapper = styled.div`
|
||||
export const TokenStatsSection = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
${textFadeIn}
|
||||
`
|
||||
export const StatPair = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
|
||||
const Header = styled(ThemedText.MediumHeader)`
|
||||
font-size: 28px !important;
|
||||
`
|
||||
const StatTitle = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
`
|
||||
const StatPrice = styled.span`
|
||||
font-size: 28px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
@@ -31,14 +44,32 @@ const StatPrice = styled.span`
|
||||
const NoData = styled.div`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
const Wrapper = styled.div`
|
||||
gap: 16px;
|
||||
${textFadeIn}
|
||||
`
|
||||
|
||||
type NumericStat = number | undefined | null
|
||||
|
||||
function Stat({ value, title }: { value: NumericStat; title: ReactNode }) {
|
||||
function Stat({
|
||||
value,
|
||||
title,
|
||||
description,
|
||||
isPrice = false,
|
||||
}: {
|
||||
value: NumericStat
|
||||
title: ReactNode
|
||||
description?: ReactNode
|
||||
isPrice?: boolean
|
||||
}) {
|
||||
return (
|
||||
<StatWrapper>
|
||||
{title}
|
||||
<StatPrice>{value ? formatDollarAmount(value) : '-'}</StatPrice>
|
||||
<StatTitle>
|
||||
{title}
|
||||
{description && <InfoTip text={description}></InfoTip>}
|
||||
</StatTitle>
|
||||
|
||||
<StatPrice>{formatDollar({ num: value, isPrice })}</StatPrice>
|
||||
</StatWrapper>
|
||||
)
|
||||
}
|
||||
@@ -53,16 +84,33 @@ export default function StatsSection(props: StatsSectionProps) {
|
||||
const { priceLow52W, priceHigh52W, TVL, volume24H } = props
|
||||
if (TVL || volume24H || priceLow52W || priceHigh52W) {
|
||||
return (
|
||||
<TokenStatsSection>
|
||||
<StatPair>
|
||||
<Stat value={TVL} title={<Trans>Total Value Locked</Trans>} />
|
||||
<Stat value={volume24H} title={<Trans>24H volume</Trans>} />
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} />
|
||||
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} />
|
||||
</StatPair>
|
||||
</TokenStatsSection>
|
||||
<Wrapper>
|
||||
<Header>
|
||||
<Trans>Stats</Trans>
|
||||
</Header>
|
||||
<TokenStatsSection>
|
||||
<StatPair>
|
||||
<Stat
|
||||
value={TVL}
|
||||
description={HEADER_DESCRIPTIONS[TokenSortMethod.TOTAL_VALUE_LOCKED]}
|
||||
title={<Trans>TVL</Trans>}
|
||||
/>
|
||||
<Stat
|
||||
value={volume24H}
|
||||
description={
|
||||
<Trans>
|
||||
24H volume is the amount of the asset that has been traded on Uniswap v3 during the past 24 hours.
|
||||
</Trans>
|
||||
}
|
||||
title={<Trans>24H volume</Trans>}
|
||||
/>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} isPrice={true} />
|
||||
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} isPrice={true} />
|
||||
</StatPair>
|
||||
</TokenStatsSection>
|
||||
</Wrapper>
|
||||
)
|
||||
} else {
|
||||
return <NoData>No stats available</NoData>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Heart } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { showFavoritesAtom } from '../state'
|
||||
import FilterOption from './FilterOption'
|
||||
|
||||
const FavoriteButtonContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
const FavoriteText = styled.span`
|
||||
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function FavoriteButton() {
|
||||
const theme = useTheme()
|
||||
const [showFavorites, setShowFavorites] = useAtom(showFavoritesAtom)
|
||||
return (
|
||||
<FilterOption onClick={() => setShowFavorites(!showFavorites)} active={showFavorites} highlight>
|
||||
<FavoriteButtonContent>
|
||||
<Heart size={20} color={showFavorites ? theme.accentActive : theme.textPrimary} />
|
||||
<FavoriteText>
|
||||
<Trans>Favorites</Trans>
|
||||
</FavoriteText>
|
||||
</FavoriteButtonContent>
|
||||
</FilterOption>
|
||||
)
|
||||
}
|
||||
@@ -64,7 +64,6 @@ const StyledMenuContent = styled.div`
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
`
|
||||
@@ -85,6 +84,9 @@ const CheckContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: flex-end;
|
||||
`
|
||||
const NetworkFilterOption = styled(FilterOption)`
|
||||
width: 156px;
|
||||
`
|
||||
|
||||
export default function NetworkFilter() {
|
||||
const theme = useTheme()
|
||||
@@ -97,14 +99,14 @@ export default function NetworkFilter() {
|
||||
const { chainName } = useParams<{ chainName?: string }>()
|
||||
const currentChainName = validateUrlChainParam(chainName)
|
||||
|
||||
const { label, circleLogoUrl, logoUrl } = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[currentChainName])
|
||||
const chainInfo = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[currentChainName])
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<FilterOption onClick={toggleMenu} aria-label={`networkFilter`} active={open}>
|
||||
<NetworkFilterOption onClick={toggleMenu} aria-label={`networkFilter`} active={open}>
|
||||
<StyledMenuContent>
|
||||
<NetworkLabel>
|
||||
<Logo src={circleLogoUrl ?? logoUrl} /> {label}
|
||||
<Logo src={chainInfo?.logoUrl} /> {chainInfo?.label}
|
||||
</NetworkLabel>
|
||||
<Chevron open={open}>
|
||||
{open ? (
|
||||
@@ -114,11 +116,12 @@ export default function NetworkFilter() {
|
||||
)}
|
||||
</Chevron>
|
||||
</StyledMenuContent>
|
||||
</FilterOption>
|
||||
</NetworkFilterOption>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
{BACKEND_CHAIN_NAMES.map((network) => {
|
||||
const chainInfo = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[network])
|
||||
if (!chainInfo) return null
|
||||
return (
|
||||
<InternalLinkMenuItem
|
||||
key={network}
|
||||
@@ -128,7 +131,7 @@ export default function NetworkFilter() {
|
||||
}}
|
||||
>
|
||||
<NetworkLabel>
|
||||
<Logo src={chainInfo.circleLogoUrl ?? chainInfo.logoUrl} />
|
||||
<Logo src={chainInfo.logoUrl} />
|
||||
{chainInfo.label}
|
||||
</NetworkLabel>
|
||||
{network === currentChainName && (
|
||||
|
||||
@@ -23,10 +23,10 @@ const SearchInput = styled.input`
|
||||
background-position: 12px center;
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border: 1.5px solid ${({ theme }) => theme.backgroundOutline};
|
||||
height: 100%;
|
||||
width: min(200px, 100%);
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
padding-left: 40px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
|
||||
@@ -17,16 +17,14 @@ export const DISPLAYS: Record<TimePeriod, string> = {
|
||||
[TimePeriod.WEEK]: '1W',
|
||||
[TimePeriod.MONTH]: '1M',
|
||||
[TimePeriod.YEAR]: '1Y',
|
||||
[TimePeriod.ALL]: 'All',
|
||||
}
|
||||
|
||||
export const ORDERED_TIMES = [
|
||||
export const ORDERED_TIMES: TimePeriod[] = [
|
||||
TimePeriod.HOUR,
|
||||
TimePeriod.DAY,
|
||||
TimePeriod.WEEK,
|
||||
TimePeriod.MONTH,
|
||||
TimePeriod.YEAR,
|
||||
TimePeriod.ALL,
|
||||
]
|
||||
|
||||
const InternalMenuItem = styled.div`
|
||||
|
||||
@@ -5,17 +5,16 @@ import { EventName } from 'analytics/constants'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL, TimePeriod } from 'graphql/data/util'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
import { CSSProperties, ReactNode } from 'react'
|
||||
import { ArrowDown, ArrowUp, Heart } from 'react-feather'
|
||||
import { ArrowDown, ArrowUp } from 'react-feather'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
import { ClickableStyle } from 'theme'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
import { formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
import {
|
||||
LARGE_MEDIA_BREAKPOINT,
|
||||
@@ -29,13 +28,12 @@ import {
|
||||
filterTimeAtom,
|
||||
sortAscendingAtom,
|
||||
sortMethodAtom,
|
||||
useIsFavorited,
|
||||
TokenSortMethod,
|
||||
useSetSortMethod,
|
||||
useToggleFavorite,
|
||||
} from '../state'
|
||||
import { useTokenLogoURI } from '../TokenDetails/ChartSection'
|
||||
import InfoTip from '../TokenDetails/InfoTip'
|
||||
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
|
||||
import { DISPLAYS } from './TimeSelector'
|
||||
|
||||
const Cell = styled.div`
|
||||
display: flex;
|
||||
@@ -46,19 +44,19 @@ const StyledTokenRow = styled.div<{
|
||||
first?: boolean
|
||||
last?: boolean
|
||||
loading?: boolean
|
||||
favoriteTokensEnabled?: boolean
|
||||
}>`
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
font-size: 15px;
|
||||
grid-template-columns: ${({ favoriteTokensEnabled }) =>
|
||||
favoriteTokensEnabled ? '1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr' : '1fr 7fr 4fr 4fr 4fr 4fr 5fr'};
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
grid-template-columns: 1fr 7fr 4fr 4fr 4fr 4fr 5fr;
|
||||
line-height: 24px;
|
||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||
min-width: 390px;
|
||||
padding-top: ${({ first }) => (first ? '4px' : '0px')};
|
||||
padding-bottom: ${({ last }) => (last ? '4px' : '0px')};
|
||||
${({ first, last }) => css`
|
||||
height: ${first || last ? '72px' : '64px'};
|
||||
padding-top: ${first ? '8px' : '0px'};
|
||||
padding-bottom: ${last ? '8px' : '0px'};
|
||||
`}
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
transition: ${({
|
||||
@@ -107,23 +105,6 @@ const StyledTokenRow = styled.div<{
|
||||
}
|
||||
}
|
||||
`
|
||||
export const ClickFavorited = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 60%;
|
||||
}
|
||||
`
|
||||
|
||||
export const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
|
||||
${ClickableStyle}
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
|
||||
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
|
||||
`
|
||||
|
||||
const ClickableContent = styled.div`
|
||||
display: flex;
|
||||
@@ -136,21 +117,12 @@ const ClickableName = styled(ClickableContent)`
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
`
|
||||
const FavoriteCell = styled(Cell)`
|
||||
min-width: 40px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
fill: none;
|
||||
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const StyledHeaderRow = styled(StyledTokenRow)`
|
||||
border-bottom: 1px solid;
|
||||
border-color: ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
line-height: 16px;
|
||||
padding: 0px 12px;
|
||||
@@ -169,7 +141,7 @@ const StyledHeaderRow = styled(StyledTokenRow)`
|
||||
const ListNumberCell = styled(Cell)<{ header: boolean }>`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
min-width: 32px;
|
||||
height: ${({ header }) => (header ? '48px' : '60px')};
|
||||
font-size: 14px;
|
||||
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
@@ -179,14 +151,13 @@ const DataCell = styled(Cell)<{ sortable: boolean }>`
|
||||
justify-content: flex-end;
|
||||
min-width: 80px;
|
||||
user-select: ${({ sortable }) => (sortable ? 'none' : 'unset')};
|
||||
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => css`background-color ${duration.medium} ${timing.ease}`};
|
||||
`
|
||||
const MarketCapCell = styled(DataCell)`
|
||||
const TvlCell = styled(DataCell)`
|
||||
padding-right: 8px;
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
@@ -195,7 +166,7 @@ const MarketCapCell = styled(DataCell)`
|
||||
const NameCell = styled(Cell)`
|
||||
justify-content: flex-start;
|
||||
padding: 0px 8px;
|
||||
min-width: 200px;
|
||||
min-width: 240px;
|
||||
gap: 8px;
|
||||
`
|
||||
const PriceCell = styled(DataCell)`
|
||||
@@ -227,17 +198,18 @@ const PriceInfoCell = styled(Cell)`
|
||||
align-items: flex-end;
|
||||
}
|
||||
`
|
||||
const SortArrowCell = styled(Cell)`
|
||||
padding-right: 2px;
|
||||
`
|
||||
const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
|
||||
align-items: center;
|
||||
${ClickableStyle}
|
||||
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'unset')};
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
${ClickableStyle}
|
||||
}
|
||||
`
|
||||
const SparkLineCell = styled(Cell)`
|
||||
padding: 0px 24px;
|
||||
@@ -307,7 +279,7 @@ const IconLoadingBubble = styled(LoadingBubble)`
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
`
|
||||
const SparkLineLoadingBubble = styled(LongLoadingBubble)`
|
||||
export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
|
||||
height: 4px;
|
||||
`
|
||||
|
||||
@@ -316,7 +288,7 @@ export const L2NetworkLogo = styled.div<{ networkUrl?: string; size?: string }>`
|
||||
width: ${({ size }) => size ?? '12px'};
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
bottom: 0;
|
||||
background: url(${({ networkUrl }) => networkUrl});
|
||||
background-repeat: no-repeat;
|
||||
background-size: ${({ size }) => (size ? `${size} ${size}` : '12px 12px')};
|
||||
@@ -328,100 +300,85 @@ export const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
/* formatting for volume with timeframe header display */
|
||||
function getHeaderDisplay(method: string, timeframe: TimePeriod): string {
|
||||
if (method === TokenSortMethod.VOLUME || method === TokenSortMethod.PERCENT_CHANGE)
|
||||
return `${DISPLAYS[timeframe]} ${method}`
|
||||
return method
|
||||
export const HEADER_DESCRIPTIONS: Record<TokenSortMethod, ReactNode | undefined> = {
|
||||
[TokenSortMethod.PRICE]: undefined,
|
||||
[TokenSortMethod.PERCENT_CHANGE]: undefined,
|
||||
[TokenSortMethod.TOTAL_VALUE_LOCKED]: (
|
||||
<Trans>Total value locked (TVL) is the amount of the asset that’s currently in a Uniswap v3 liquidity pool.</Trans>
|
||||
),
|
||||
[TokenSortMethod.VOLUME]: (
|
||||
<Trans>Volume is the amount of the asset that has been traded on Uniswap v3 during the selected time frame.</Trans>
|
||||
),
|
||||
}
|
||||
|
||||
/* Get singular header cell for header row */
|
||||
function HeaderCell({
|
||||
category,
|
||||
sortable,
|
||||
}: {
|
||||
category: TokenSortMethod // TODO: change this to make it work for trans
|
||||
sortable: boolean
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
const handleSortCategory = useSetSortMethod(category)
|
||||
const sortMethod = useAtomValue(sortMethodAtom)
|
||||
const timeframe = useAtomValue(filterTimeAtom)
|
||||
|
||||
if (sortMethod === category) {
|
||||
return (
|
||||
<HeaderCellWrapper onClick={handleSortCategory}>
|
||||
<SortArrowCell>
|
||||
const description = HEADER_DESCRIPTIONS[category]
|
||||
|
||||
return (
|
||||
<HeaderCellWrapper onClick={handleSortCategory}>
|
||||
{sortMethod === category && (
|
||||
<>
|
||||
{sortAscending ? (
|
||||
<ArrowUp size={14} color={theme.accentActive} />
|
||||
<ArrowUp size={20} strokeWidth={1.8} color={theme.accentActive} />
|
||||
) : (
|
||||
<ArrowDown size={14} color={theme.accentActive} />
|
||||
<ArrowDown size={20} strokeWidth={1.8} color={theme.accentActive} />
|
||||
)}
|
||||
</SortArrowCell>
|
||||
{getHeaderDisplay(category, timeframe)}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
if (sortable) {
|
||||
return (
|
||||
<HeaderCellWrapper onClick={handleSortCategory}>
|
||||
<SortArrowCell>
|
||||
<ArrowUp size={14} visibility="hidden" />
|
||||
</SortArrowCell>
|
||||
{getHeaderDisplay(category, timeframe)}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
return <HeaderCellWrapper>{getHeaderDisplay(category, timeframe)}</HeaderCellWrapper>
|
||||
</>
|
||||
)}
|
||||
{category}
|
||||
{description && <InfoTip text={description}></InfoTip>}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
/* Token Row: skeleton row component */
|
||||
export function TokenRow({
|
||||
favorited,
|
||||
header,
|
||||
listNumber,
|
||||
tokenInfo,
|
||||
price,
|
||||
percentChange,
|
||||
marketCap,
|
||||
tvl,
|
||||
volume,
|
||||
sparkLine,
|
||||
...rest
|
||||
}: {
|
||||
favorited: ReactNode
|
||||
first?: boolean
|
||||
header: boolean
|
||||
listNumber: ReactNode
|
||||
loading?: boolean
|
||||
marketCap: ReactNode
|
||||
tvl: ReactNode
|
||||
price: ReactNode
|
||||
percentChange: ReactNode
|
||||
sparkLine: ReactNode
|
||||
sparkLine?: ReactNode
|
||||
tokenInfo: ReactNode
|
||||
volume: ReactNode
|
||||
last?: boolean
|
||||
style?: CSSProperties
|
||||
}) {
|
||||
const favoriteTokensEnabled = useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled
|
||||
const rowCells = (
|
||||
<>
|
||||
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
|
||||
<NameCell>{tokenInfo}</NameCell>
|
||||
<PriceCell sortable={header}>{price}</PriceCell>
|
||||
<PercentChangeCell sortable={header}>{percentChange}</PercentChangeCell>
|
||||
<MarketCapCell sortable={header}>{marketCap}</MarketCapCell>
|
||||
<TvlCell sortable={header}>{tvl}</TvlCell>
|
||||
<VolumeCell sortable={header}>{volume}</VolumeCell>
|
||||
<SparkLineCell>{sparkLine}</SparkLineCell>
|
||||
{favoriteTokensEnabled && <FavoriteCell>{favorited}</FavoriteCell>}
|
||||
</>
|
||||
)
|
||||
if (header) return <StyledHeaderRow favoriteTokensEnabled={favoriteTokensEnabled}>{rowCells}</StyledHeaderRow>
|
||||
return (
|
||||
<StyledTokenRow favoriteTokensEnabled={favoriteTokensEnabled} {...rest}>
|
||||
{rowCells}
|
||||
</StyledTokenRow>
|
||||
)
|
||||
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
|
||||
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
|
||||
}
|
||||
|
||||
/* Header Row: top header row component for table */
|
||||
@@ -429,23 +386,21 @@ export function HeaderRow() {
|
||||
return (
|
||||
<TokenRow
|
||||
header={true}
|
||||
favorited={null}
|
||||
listNumber="#"
|
||||
tokenInfo={<Trans>Token Name</Trans>}
|
||||
price={<HeaderCell category={TokenSortMethod.PRICE} sortable />}
|
||||
percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} sortable />}
|
||||
marketCap={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} sortable />}
|
||||
volume={<HeaderCell category={TokenSortMethod.VOLUME} sortable />}
|
||||
tokenInfo={<Trans>Token name</Trans>}
|
||||
price={<HeaderCell category={TokenSortMethod.PRICE} />}
|
||||
percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} />}
|
||||
tvl={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} />}
|
||||
volume={<HeaderCell category={TokenSortMethod.VOLUME} />}
|
||||
sparkLine={null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/* Loading State: row component with loading bubbles */
|
||||
export function LoadingRow() {
|
||||
export function LoadingRow(props: { first?: boolean; last?: boolean }) {
|
||||
return (
|
||||
<TokenRow
|
||||
favorited={null}
|
||||
header={false}
|
||||
listNumber={<SmallLoadingBubble />}
|
||||
loading
|
||||
@@ -457,9 +412,10 @@ export function LoadingRow() {
|
||||
}
|
||||
price={<MediumLoadingBubble />}
|
||||
percentChange={<LoadingBubble />}
|
||||
marketCap={<LoadingBubble />}
|
||||
tvl={<LoadingBubble />}
|
||||
volume={<LoadingBubble />}
|
||||
sparkLine={<SparkLineLoadingBubble />}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -468,6 +424,7 @@ interface LoadedRowProps {
|
||||
tokenListIndex: number
|
||||
tokenListLength: number
|
||||
token: NonNullable<TopToken>
|
||||
sparklineMap: SparklineMap
|
||||
}
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
@@ -476,24 +433,24 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
const tokenAddress = token.address
|
||||
const tokenName = token.name
|
||||
const tokenSymbol = token.symbol
|
||||
const isFavorited = useIsFavorited(tokenAddress)
|
||||
const toggleFavorite = useToggleFavorite(tokenAddress)
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
|
||||
const filterNetwork = lowercaseChainName.toUpperCase()
|
||||
const L2Icon = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[filterNetwork]).circleLogoUrl
|
||||
const L2Icon = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[filterNetwork])?.circleLogoUrl
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
const delta = token.market?.pricePercentChange?.value
|
||||
const arrow = getDeltaArrow(delta)
|
||||
const formattedDelta = formatDelta(delta)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
const rank = sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: filterNetwork,
|
||||
token_address: tokenAddress,
|
||||
token_symbol: tokenSymbol,
|
||||
token_list_index: tokenListIndex,
|
||||
token_list_rank: rank,
|
||||
token_list_length: tokenListLength,
|
||||
time_frame: timePeriod,
|
||||
search_token_address_input: filterString,
|
||||
@@ -508,17 +465,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
>
|
||||
<TokenRow
|
||||
header={false}
|
||||
favorited={
|
||||
<ClickFavorited
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
toggleFavorite()
|
||||
}}
|
||||
>
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
}
|
||||
listNumber={sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1}
|
||||
listNumber={rank}
|
||||
tokenInfo={
|
||||
<ClickableName>
|
||||
<LogoContainer>
|
||||
@@ -534,7 +481,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
price={
|
||||
<ClickableContent>
|
||||
<PriceInfoCell>
|
||||
{token.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'}
|
||||
{formatDollar({ num: token.market?.price?.value, isPrice: true, lessPreciseStablecoinValues: true })}
|
||||
<PercentChangeInfoCell>
|
||||
{formattedDelta}
|
||||
{arrow}
|
||||
@@ -548,28 +495,23 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
{arrow}
|
||||
</ClickableContent>
|
||||
}
|
||||
marketCap={
|
||||
<ClickableContent>
|
||||
{token.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'}
|
||||
</ClickableContent>
|
||||
}
|
||||
volume={
|
||||
<ClickableContent>
|
||||
{token.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'}
|
||||
</ClickableContent>
|
||||
}
|
||||
tvl={<ClickableContent>{formatDollar({ num: token.market?.totalValueLocked?.value })}</ClickableContent>}
|
||||
volume={<ClickableContent>{formatDollar({ num: token.market?.volume?.value })}</ClickableContent>}
|
||||
sparkLine={
|
||||
<SparkLine>
|
||||
<ParentSize>
|
||||
{({ width, height }) => (
|
||||
<SparklineChart
|
||||
width={width}
|
||||
height={height}
|
||||
tokenData={token}
|
||||
pricePercentChange={token.market?.pricePercentChange?.value}
|
||||
timePeriod={timePeriod}
|
||||
/>
|
||||
)}
|
||||
{({ width, height }) =>
|
||||
props.sparklineMap && (
|
||||
<SparklineChart
|
||||
width={width}
|
||||
height={height}
|
||||
tokenData={token}
|
||||
pricePercentChange={token.market?.pricePercentChange?.value}
|
||||
timePeriod={timePeriod}
|
||||
sparklineMap={props.sparklineMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ParentSize>
|
||||
</SparkLine>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { showFavoritesAtom } from 'components/Tokens/state'
|
||||
import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens'
|
||||
import { validateUrlChainParam } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode, useCallback, useRef } from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -11,8 +9,6 @@ import styled from 'styled-components/macro'
|
||||
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { HeaderRow, LoadedRow, LoadingRow } from './TokenRow'
|
||||
|
||||
const LOADING_ROWS_COUNT = 3
|
||||
|
||||
const GridContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -22,13 +18,16 @@ const GridContainer = styled.div`
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
|
||||
const TokenDataContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`
|
||||
@@ -55,85 +54,66 @@ function NoTokensState({ message }: { message: ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
const LoadingMoreRows = Array(LOADING_ROWS_COUNT).fill(<LoadingRow />)
|
||||
const LoadingRows = (rowCount?: number) => Array(rowCount ?? PAGE_SIZE).fill(<LoadingRow />)
|
||||
const LoadingRows = ({ rowCount }: { rowCount: number }) => (
|
||||
<>
|
||||
{Array(rowCount)
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
return <LoadingRow key={index} first={index === 0} last={index === rowCount - 1} />
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
|
||||
export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenDataContainer>{LoadingRows(rowCount)}</TokenDataContainer>
|
||||
<TokenDataContainer>
|
||||
<LoadingRows rowCount={rowCount} />
|
||||
</TokenDataContainer>
|
||||
</GridContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenTable() {
|
||||
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
|
||||
|
||||
export default function TokenTable({ setRowCount }: { setRowCount: (c: number) => void }) {
|
||||
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
|
||||
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
|
||||
const { error, loading, tokens, hasMore, loadMoreTokens, maxFetchable } = useTopTokens(chainName)
|
||||
const showMoreLoadingRows = Boolean(loading && hasMore)
|
||||
|
||||
const observer = useRef<IntersectionObserver>()
|
||||
const lastTokenRef = useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
if (loading) return
|
||||
if (observer.current) observer.current.disconnect()
|
||||
observer.current = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && hasMore) {
|
||||
loadMoreTokens()
|
||||
}
|
||||
})
|
||||
if (node) observer.current.observe(node)
|
||||
},
|
||||
[loading, hasMore, loadMoreTokens]
|
||||
)
|
||||
const { tokens, sparklines } = useTopTokens(chainName)
|
||||
setRowCount(tokens?.length ?? PAGE_SIZE)
|
||||
|
||||
/* loading and error state */
|
||||
if (loading && (!tokens || tokens?.length === 0)) {
|
||||
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||
if (!tokens) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
<>
|
||||
<AlertTriangle size={16} />
|
||||
<Trans>An error occurred loading tokens. Please try again.</Trans>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else if (tokens?.length === 0) {
|
||||
return <NoTokensState message={<Trans>No tokens found</Trans>} />
|
||||
} else {
|
||||
if (error || !tokens) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
<>
|
||||
<AlertTriangle size={16} />
|
||||
<Trans>An error occured loading tokens. Please try again.</Trans>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else if (tokens?.length === 0) {
|
||||
return showFavorites ? (
|
||||
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
|
||||
) : (
|
||||
<NoTokensState message={<Trans>No tokens found</Trans>} />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenDataContainer>
|
||||
{tokens.map(
|
||||
(token, index) =>
|
||||
token && (
|
||||
<LoadedRow
|
||||
key={token?.address}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={maxFetchable}
|
||||
token={token}
|
||||
ref={index + 1 === tokens.length ? lastTokenRef : undefined}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{showMoreLoadingRows && LoadingMoreRows}
|
||||
</TokenDataContainer>
|
||||
</GridContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenDataContainer>
|
||||
{tokens.map(
|
||||
(token, index) =>
|
||||
token && (
|
||||
<LoadedRow
|
||||
key={token?.address}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={tokens.length}
|
||||
token={token}
|
||||
sparklineMap={sparklines}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</TokenDataContainer>
|
||||
</GridContainer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
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'
|
||||
@@ -8,26 +13,32 @@ import { Z_INDEX } from 'theme/zIndex'
|
||||
import tokensPromoDark from '../../assets/images/tokensPromoDark.png'
|
||||
import tokensPromoLight from '../../assets/images/tokensPromoLight.png'
|
||||
|
||||
const PopupContainer = styled.div<{ show: boolean }>`
|
||||
position: fixed;
|
||||
display: ${({ show }) => (show ? 'flex' : 'none')};
|
||||
flex-direction: column;
|
||||
padding: 12px 16px 12px 20px;
|
||||
gap: 8px;
|
||||
bottom: 48px;
|
||||
right: 16px;
|
||||
width: 320px;
|
||||
height: 88px;
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
background-color: ${({ theme }) => (theme.darkMode ? theme.backgroundScrim : opacify(60, '#FDF0F8'))};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
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: {
|
||||
@@ -36,46 +47,54 @@ const PopupContainer = styled.div<{ show: boolean }>`
|
||||
}) => `${duration.slow} opacity ${timing.in}`};
|
||||
`
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
const HeaderText = styled(Link)`
|
||||
const HeaderText = styled.span`
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const Description = styled(Link)`
|
||||
|
||||
const Description = styled.span`
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
width: 75%;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
width: max(212px, calc(100% - 36px));
|
||||
`
|
||||
|
||||
export default function TokensBanner() {
|
||||
const theme = useTheme()
|
||||
const [showTokensPromoBanner, setShowTokensPromoBanner] = useShowTokensPromoBanner()
|
||||
|
||||
const closeBanner = () => {
|
||||
setShowTokensPromoBanner(false)
|
||||
}
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const chainName = chainIdToBackendName(connectedChainId).toLowerCase()
|
||||
|
||||
return (
|
||||
<PopupContainer show={showTokensPromoBanner}>
|
||||
<Header>
|
||||
<HeaderText to={'/tokens'} onClick={closeBanner}>
|
||||
Explore Top Tokens
|
||||
</HeaderText>
|
||||
<X size={20} color={theme.textSecondary} onClick={closeBanner} style={{ cursor: 'pointer' }} />
|
||||
</Header>
|
||||
<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 to={'/tokens'} onClick={closeBanner}>
|
||||
Check out the new explore tab to discover and learn more
|
||||
</Description>
|
||||
</PopupContainer>
|
||||
<Description>
|
||||
<Trans>Sort and filter assets across networks on the new Tokens page.</Trans>
|
||||
</Description>
|
||||
</PopupContainer>
|
||||
</TraceEvent>
|
||||
</BackgroundColor>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,20 @@
|
||||
import { TokenSortMethod } from 'graphql/data/TopTokens'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { atomWithReset } from 'jotai/utils'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export enum TokenSortMethod {
|
||||
PRICE = 'Price',
|
||||
PERCENT_CHANGE = 'Change',
|
||||
TOTAL_VALUE_LOCKED = 'TVL',
|
||||
VOLUME = 'Volume',
|
||||
}
|
||||
|
||||
export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
|
||||
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
|
||||
export const filterStringAtom = atomWithReset<string>('')
|
||||
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY)
|
||||
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED)
|
||||
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.VOLUME)
|
||||
export const sortAscendingAtom = atom<boolean>(false)
|
||||
|
||||
/* for favoriting tokens */
|
||||
export function useToggleFavorite(tokenAddress: string | undefined | null) {
|
||||
const [favoriteTokens, updateFavoriteTokens] = useAtom(favoritesAtom)
|
||||
|
||||
return useCallback(() => {
|
||||
if (!tokenAddress) return
|
||||
let updatedFavoriteTokens
|
||||
if (favoriteTokens.includes(tokenAddress.toLocaleLowerCase())) {
|
||||
updatedFavoriteTokens = favoriteTokens.filter((address: string) => {
|
||||
return address !== tokenAddress.toLocaleLowerCase()
|
||||
})
|
||||
} else {
|
||||
updatedFavoriteTokens = [...favoriteTokens, tokenAddress.toLocaleLowerCase()]
|
||||
}
|
||||
updateFavoriteTokens(updatedFavoriteTokens)
|
||||
}, [favoriteTokens, tokenAddress, updateFavoriteTokens])
|
||||
}
|
||||
|
||||
/* keep track of sort category for token table */
|
||||
export function useSetSortMethod(newSortMethod: TokenSortMethod) {
|
||||
const [sortMethod, setSortMethod] = useAtom(sortMethodAtom)
|
||||
@@ -43,12 +29,3 @@ export function useSetSortMethod(newSortMethod: TokenSortMethod) {
|
||||
}
|
||||
}, [sortMethod, setSortMethod, setSortAscending, sortAscending, newSortMethod])
|
||||
}
|
||||
|
||||
export function useIsFavorited(tokenAddress: string | null | undefined) {
|
||||
const favoritedTokens = useAtomValue<string[]>(favoritesAtom)
|
||||
|
||||
return useMemo(
|
||||
() => (tokenAddress ? favoritedTokens.includes(tokenAddress.toLocaleLowerCase()) : false),
|
||||
[favoritedTokens, tokenAddress]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,12 @@ 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 { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
|
||||
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'
|
||||
|
||||
const Cart = lazy(() => import('nft/components/profile/modal/ListingTag'))
|
||||
const Bag = lazy(() => import('nft/components/bag/Bag'))
|
||||
const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal'))
|
||||
|
||||
@@ -28,9 +26,7 @@ export default function TopLevelModals() {
|
||||
<>
|
||||
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
|
||||
<ConnectedAccountBlocked account={account} isOpen={open} />
|
||||
{useTokensFlag() === TokensVariant.Enabled &&
|
||||
(location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />}
|
||||
<Cart />
|
||||
{(location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />}
|
||||
<Bag />
|
||||
{useNftFlag() === NftVariant.Enabled && <TransactionCompleteModal />}
|
||||
</>
|
||||
|
||||
@@ -454,7 +454,7 @@ export default function TransactionConfirmationModal({
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} redesignFlag={redesignFlagEnabled}>
|
||||
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90} redesignFlag={redesignFlagEnabled}>
|
||||
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
|
||||
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
|
||||
) : attemptingTxn ? (
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { L2_CHAIN_IDS } from 'constants/chains'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import ms from 'ms.macro'
|
||||
import { darken } from 'polished'
|
||||
import { useState } from 'react'
|
||||
@@ -42,9 +41,9 @@ const FancyButton = styled.button`
|
||||
}
|
||||
`
|
||||
|
||||
const Option = styled(FancyButton)<{ active: boolean; redesignFlag: boolean }>`
|
||||
const Option = styled(FancyButton)<{ active: boolean }>`
|
||||
margin-right: 8px;
|
||||
border-radius: ${({ redesignFlag }) => redesignFlag && '12px'};
|
||||
border-radius: 12px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -52,10 +51,10 @@ const Option = styled(FancyButton)<{ active: boolean; redesignFlag: boolean }>`
|
||||
color: ${({ active, theme }) => (active ? theme.deprecated_white : theme.deprecated_text1)};
|
||||
`
|
||||
|
||||
const Input = styled.input<{ redesignFlag: boolean }>`
|
||||
const Input = styled.input`
|
||||
background: ${({ theme }) => theme.deprecated_bg1};
|
||||
font-size: 16px;
|
||||
border-radius: ${({ redesignFlag }) => redesignFlag && '12px'};
|
||||
border-radius: 12px;
|
||||
width: auto;
|
||||
outline: none;
|
||||
&::-webkit-outer-spin-button,
|
||||
@@ -66,15 +65,15 @@ const Input = styled.input<{ redesignFlag: boolean }>`
|
||||
text-align: right;
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme, redesignFlag }) => redesignFlag && theme.textTertiary};
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
|
||||
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean; redesignFlag: boolean }>`
|
||||
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>`
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
padding: 0 0.75rem;
|
||||
border-radius: ${({ redesignFlag }) => redesignFlag && '12px'};
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
border: ${({ theme, active, warning }) =>
|
||||
active
|
||||
@@ -109,8 +108,6 @@ const THREE_DAYS_IN_SECONDS = ms`3 days` / 1000
|
||||
export default function TransactionSettings({ placeholderSlippage }: TransactionSettingsProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
const [userSlippageTolerance, setUserSlippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
@@ -185,7 +182,6 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<Option
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
onClick={() => {
|
||||
parseSlippageInput('')
|
||||
}}
|
||||
@@ -193,12 +189,7 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
|
||||
>
|
||||
<Trans>Auto</Trans>
|
||||
</Option>
|
||||
<OptionCustom
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
active={userSlippageTolerance !== 'auto'}
|
||||
warning={!!slippageError}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<OptionCustom active={userSlippageTolerance !== 'auto'} warning={!!slippageError} tabIndex={-1}>
|
||||
<RowBetween>
|
||||
{tooLow || tooHigh ? (
|
||||
<SlippageEmojiContainer>
|
||||
@@ -208,7 +199,6 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
|
||||
</SlippageEmojiContainer>
|
||||
) : null}
|
||||
<Input
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
placeholder={placeholderSlippage.toFixed(2)}
|
||||
value={
|
||||
slippageInput.length > 0
|
||||
@@ -258,14 +248,8 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
|
||||
/>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<OptionCustom
|
||||
style={{ width: '80px' }}
|
||||
warning={!!deadlineError}
|
||||
tabIndex={-1}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
<OptionCustom style={{ width: '80px' }} warning={!!deadlineError} tabIndex={-1}>
|
||||
<Input
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
placeholder={(DEFAULT_DEADLINE_FROM_NOW / 60).toString()}
|
||||
value={
|
||||
deadlineInput.length > 0
|
||||
|
||||
@@ -149,14 +149,15 @@ const AuthenticatedHeader = () => {
|
||||
</FlexContainer>
|
||||
</StatusWrapper>
|
||||
<IconContainer>
|
||||
<IconButton onClick={copy} Icon={Copy} text={isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>} />
|
||||
<IconButton href={`${explorer}address/${account}`} Icon={ExternalLink} text={<Trans>Explore</Trans>} />
|
||||
<IconButton
|
||||
dataTestId="wallet-disconnect"
|
||||
onClick={disconnect}
|
||||
Icon={Power}
|
||||
text={<Trans>Disconnect</Trans>}
|
||||
/>
|
||||
<IconButton onClick={copy} Icon={Copy}>
|
||||
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
||||
</IconButton>
|
||||
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLink}>
|
||||
<Trans>Explore</Trans>
|
||||
</IconButton>
|
||||
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
|
||||
<Trans>Disconnect</Trans>
|
||||
</IconButton>
|
||||
</IconContainer>
|
||||
</HeaderWrapper>
|
||||
<Column>
|
||||
|
||||
@@ -58,31 +58,28 @@ const IconWrapper = styled.span`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`
|
||||
|
||||
interface IconButtonProps {
|
||||
text: React.ReactNode
|
||||
interface BaseProps {
|
||||
Icon: Icon
|
||||
onClick?: () => void
|
||||
href?: string
|
||||
dataTestId?: string
|
||||
}
|
||||
interface IconLinkProps extends React.ComponentPropsWithoutRef<'a'>, BaseProps {}
|
||||
interface IconButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseProps {}
|
||||
|
||||
const IconBlock = (props: React.ComponentPropsWithoutRef<'a' | 'button'>) => {
|
||||
if ('href' in props) {
|
||||
return <IconBlockLink {...props} />
|
||||
}
|
||||
// ignoring 'button' 'type' conflict between React and styled-components
|
||||
// @ts-ignore
|
||||
return <IconBlockButton {...props} />
|
||||
}
|
||||
|
||||
const IconButton = ({ Icon, onClick, text, href, dataTestId }: IconButtonProps) => {
|
||||
return href ? (
|
||||
<IconBlockLink data-testId={dataTestId} href={href} target="_blank">
|
||||
<IconWrapper>
|
||||
<Icon strokeWidth={1.5} size={16} />
|
||||
<IconHoverText>{text}</IconHoverText>
|
||||
</IconWrapper>
|
||||
</IconBlockLink>
|
||||
) : (
|
||||
<IconBlockButton data-testId={dataTestId} onClick={onClick}>
|
||||
<IconWrapper>
|
||||
<Icon strokeWidth={1.5} size={16} />
|
||||
<IconHoverText>{text}</IconHoverText>
|
||||
</IconWrapper>
|
||||
</IconBlockButton>
|
||||
)
|
||||
}
|
||||
const IconButton = ({ Icon, children, ...rest }: IconButtonProps | IconLinkProps) => (
|
||||
<IconBlock {...rest}>
|
||||
<IconWrapper>
|
||||
<Icon strokeWidth={1.5} size={16} />
|
||||
<IconHoverText>{children}</IconHoverText>
|
||||
</IconWrapper>
|
||||
</IconBlock>
|
||||
)
|
||||
|
||||
export default IconButton
|
||||
|
||||
@@ -6,6 +6,7 @@ const Menu = styled.div`
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
overflow: auto;
|
||||
max-height: 450px;
|
||||
|
||||
// Firefox scrollbar styling
|
||||
scrollbar-width: thin;
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import React from 'react'
|
||||
import { Check } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
|
||||
const InfoCard = styled.button<{ isActive?: boolean; redesignFlag?: boolean }>`
|
||||
background-color: ${({ theme, isActive, redesignFlag }) =>
|
||||
redesignFlag ? theme.backgroundInteractive : isActive ? theme.deprecated_bg3 : theme.deprecated_bg2};
|
||||
const InfoCard = styled.button<{ isActive?: boolean }>`
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
padding: 1rem;
|
||||
outline: none;
|
||||
border: 1px solid;
|
||||
border-radius: 12px;
|
||||
width: 100% !important;
|
||||
&:focus {
|
||||
box-shadow: ${({ theme, redesignFlag }) => !redesignFlag && `0 0 0 1px ${theme.deprecated_primary1}`};
|
||||
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.hoverState};
|
||||
background-color: ${({ theme }) => theme.hoverState};
|
||||
}
|
||||
border-color: ${({ theme, isActive, redesignFlag }) =>
|
||||
redesignFlag ? (isActive ? theme.accentActive : 'transparent') : isActive ? 'transparent' : theme.deprecated_bg3};
|
||||
border-color: ${({ theme, isActive }) => (isActive ? theme.accentActive : 'transparent')};
|
||||
`
|
||||
|
||||
const CheckIcon = styled(Check)`
|
||||
@@ -53,48 +49,24 @@ const OptionCardLeft = styled.div`
|
||||
const OptionCardClickable = styled(OptionCard as any)<{
|
||||
active?: boolean
|
||||
clickable?: boolean
|
||||
redesignFlag?: boolean
|
||||
}>`
|
||||
margin-top: 0;
|
||||
border: ${({ active, theme }) => active && `1px solid ${theme.accentActive}`};
|
||||
&:hover {
|
||||
cursor: ${({ clickable }) => clickable && 'pointer'};
|
||||
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.hoverState};
|
||||
border: ${({ clickable, redesignFlag, theme }) =>
|
||||
clickable && !redesignFlag && `1px solid ${theme.deprecated_primary1}`};
|
||||
background-color: ${({ theme }) => theme.hoverState};
|
||||
}
|
||||
opacity: ${({ disabled }) => (disabled ? '0.5' : '1')};
|
||||
`
|
||||
|
||||
const GreenCircle = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:first-child {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
margin-right: 8px;
|
||||
background-color: ${({ theme }) => theme.deprecated_green1};
|
||||
border-radius: 50%;
|
||||
}
|
||||
`
|
||||
|
||||
const CircleWrapper = styled.div`
|
||||
color: ${({ theme }) => theme.deprecated_green1};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const HeaderText = styled.div<{ redesignFlag?: boolean }>`
|
||||
const HeaderText = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) =>
|
||||
props.color === 'blue' ? ({ theme }) => theme.deprecated_primary1 : ({ theme }) => theme.deprecated_text1};
|
||||
font-size: ${({ redesignFlag }) => (redesignFlag ? '16px' : '1rem')};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const SubHeader = styled.div`
|
||||
@@ -103,20 +75,6 @@ const SubHeader = styled.div`
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
const IconWrapperDeprecated = styled.div<{ size?: number | null }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
& > img,
|
||||
span {
|
||||
height: ${({ size }) => (size ? size + 'px' : '24px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '24px')};
|
||||
}
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number | null }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
@@ -155,9 +113,6 @@ export default function Option({
|
||||
isActive?: boolean
|
||||
id: string
|
||||
}) {
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
const content = (
|
||||
<TraceEvent
|
||||
events={[Event.onClick]}
|
||||
@@ -165,53 +120,24 @@ export default function Option({
|
||||
properties={{ wallet_type: header }}
|
||||
element={ElementName.WALLET_TYPE_OPTION}
|
||||
>
|
||||
{redesignFlagEnabled ? (
|
||||
<OptionCardClickable
|
||||
id={id}
|
||||
onClick={onClick}
|
||||
clickable={clickable && !isActive}
|
||||
active={isActive}
|
||||
redesignFlag={true}
|
||||
data-testid="wallet-modal-option"
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color} redesignFlag={true}>
|
||||
<IconWrapper size={size}>
|
||||
<img src={icon} alt={'Icon'} />
|
||||
</IconWrapper>
|
||||
{header}
|
||||
</HeaderText>
|
||||
{subheader && <SubHeader>{subheader}</SubHeader>}
|
||||
</OptionCardLeft>
|
||||
{isActive && <CheckIcon />}
|
||||
</OptionCardClickable>
|
||||
) : (
|
||||
<OptionCardClickable
|
||||
id={id}
|
||||
onClick={onClick}
|
||||
clickable={clickable && !isActive}
|
||||
active={isActive}
|
||||
redesignFlag={false}
|
||||
data-testid="wallet-modal-option"
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color} redesignFlag={false}>
|
||||
{isActive && (
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
<div />
|
||||
</GreenCircle>
|
||||
</CircleWrapper>
|
||||
)}
|
||||
{header}
|
||||
</HeaderText>
|
||||
{subheader && <SubHeader>{subheader}</SubHeader>}
|
||||
</OptionCardLeft>
|
||||
<IconWrapperDeprecated size={size}>
|
||||
<img src={icon} alt={'Icon'} />
|
||||
</IconWrapperDeprecated>
|
||||
</OptionCardClickable>
|
||||
)}
|
||||
<OptionCardClickable
|
||||
id={id}
|
||||
onClick={onClick}
|
||||
clickable={clickable && !isActive}
|
||||
active={isActive}
|
||||
data-testid="wallet-modal-option"
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color}>
|
||||
<IconWrapper size={size}>
|
||||
<img src={icon} alt={'Icon'} />
|
||||
</IconWrapper>
|
||||
{header}
|
||||
</HeaderText>
|
||||
{subheader && <SubHeader>{subheader}</SubHeader>}
|
||||
</OptionCardLeft>
|
||||
{isActive && <CheckIcon />}
|
||||
</OptionCardClickable>
|
||||
</TraceEvent>
|
||||
)
|
||||
if (link) {
|
||||
|
||||
@@ -116,9 +116,9 @@ export default function PendingView({
|
||||
<ThemedText.MediumHeader>
|
||||
<Trans>Waiting to connect</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
<ThemedText.SubHeader style={{ paddingTop: '8px' }}>
|
||||
<ThemedText.BodyPrimary style={{ paddingTop: '8px' }}>
|
||||
<Trans>Confirm this connection in your wallet</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</ThemedText.BodyPrimary>
|
||||
</WaitingToConnectSection>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,9 @@ import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'ana
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { networkConnection } from 'connection'
|
||||
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsInjected, getIsMetaMask } from 'connection/utils'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
@@ -46,30 +47,30 @@ const CloseColor = styled(Close)`
|
||||
}
|
||||
`
|
||||
|
||||
const Wrapper = styled.div<{ redesignFlag?: boolean }>`
|
||||
const Wrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
background-color: ${({ redesignFlag, theme }) => redesignFlag && theme.backgroundSurface};
|
||||
outline: ${({ theme, redesignFlag }) => redesignFlag && `1px solid ${theme.backgroundOutline}`};
|
||||
box-shadow: ${({ redesignFlag, theme }) => redesignFlag && theme.deepShadow};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
outline: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const HeaderRow = styled.div<{ redesignFlag?: boolean }>`
|
||||
const HeaderRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
padding: 1rem 1rem;
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
size: ${({ redesignFlag }) => redesignFlag && '16px'};
|
||||
font-weight: 600;
|
||||
size: 16px;
|
||||
color: ${(props) => (props.color === 'blue' ? ({ theme }) => theme.deprecated_primary1 : 'inherit')};
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
padding: 1rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const ContentWrapper = styled.div<{ redesignFlag?: boolean }>`
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundSurface : theme.deprecated_bg0)};
|
||||
border: ${({ theme, redesignFlag }) => redesignFlag && `1px solid ${theme.backgroundOutline}`};
|
||||
const ContentWrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
@@ -154,8 +155,7 @@ export default function WalletModal({
|
||||
|
||||
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
|
||||
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
const nftFlagEnabled = useNftFlag() === NftVariant.Enabled
|
||||
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
|
||||
const [lastActiveWalletAddress, setLastActiveWalletAddress] = useState<string | undefined>(account)
|
||||
|
||||
@@ -190,6 +190,13 @@ export default function WalletModal({
|
||||
}
|
||||
}, [pendingConnector, walletView])
|
||||
|
||||
// Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection.
|
||||
useEffect(() => {
|
||||
if (chainId && connector !== networkConnection.connector) {
|
||||
networkConnection.connector.activate(chainId)
|
||||
}
|
||||
}, [chainId, connector])
|
||||
|
||||
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
|
||||
useEffect(() => {
|
||||
if (account && account !== lastActiveWalletAddress) {
|
||||
@@ -299,7 +306,7 @@ export default function WalletModal({
|
||||
)
|
||||
} else {
|
||||
headerRow = (
|
||||
<HeaderRow redesignFlag={redesignFlagEnabled}>
|
||||
<HeaderRow>
|
||||
<HoverText>
|
||||
<Trans>Connect a wallet</Trans>
|
||||
</HoverText>
|
||||
@@ -307,15 +314,15 @@ export default function WalletModal({
|
||||
)
|
||||
}
|
||||
|
||||
function getTermsOfService(redesignFlagEnabled: boolean) {
|
||||
return redesignFlagEnabled ? (
|
||||
function getTermsOfService(nftFlagEnabled: boolean, walletView: string) {
|
||||
if (nftFlagEnabled && walletView === WALLET_VIEWS.PENDING) return null
|
||||
return nftFlagEnabled ? (
|
||||
<AutoRow style={{ flexWrap: 'nowrap', padding: '4px 16px' }}>
|
||||
<ThemedText.BodySecondary fontSize={12}>
|
||||
<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 acknowledge
|
||||
that you have read and understand the Uniswap{' '}
|
||||
<ExternalLink href="https://uniswap.org/disclaimer/">Protocol Disclaimer</ExternalLink>.
|
||||
<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>
|
||||
</ThemedText.BodySecondary>
|
||||
</AutoRow>
|
||||
@@ -357,7 +364,7 @@ export default function WalletModal({
|
||||
/>
|
||||
)}
|
||||
{walletView !== WALLET_VIEWS.PENDING && <OptionGrid data-testid="option-grid">{getOptions()}</OptionGrid>}
|
||||
{!pendingError && getTermsOfService(redesignFlagEnabled)}
|
||||
{!pendingError && getTermsOfService(nftFlagEnabled, walletView)}
|
||||
</AutoColumn>
|
||||
</ContentWrapper>
|
||||
</UpperSection>
|
||||
@@ -365,16 +372,8 @@ export default function WalletModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={walletModalOpen}
|
||||
onDismiss={toggleWalletModal}
|
||||
minHeight={false}
|
||||
maxHeight={90}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
<Wrapper data-testid="wallet-modal" redesignFlag={redesignFlagEnabled}>
|
||||
{getModalContent()}
|
||||
</Wrapper>
|
||||
<Modal isOpen={walletModalOpen} onDismiss={toggleWalletModal} minHeight={false} maxHeight={90}>
|
||||
<Wrapper data-testid="wallet-modal">{getModalContent()}</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import WalletDropdown from 'components/WalletDropdown'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
@@ -13,10 +11,9 @@ import { useMemo, useRef } from 'react'
|
||||
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||
import {
|
||||
useCloseModal,
|
||||
useModalIsOpen,
|
||||
@@ -36,6 +33,23 @@ import WalletModal from '../WalletModal'
|
||||
// https://stackoverflow.com/a/31617326
|
||||
const FULL_BORDER_RADIUS = 9999
|
||||
|
||||
const ChevronWrapper = styled.button`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 10px 16px 10px 4px;
|
||||
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.accentActionSoft};
|
||||
}
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
border: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Web3StatusGeneric = styled(ButtonSecondary)`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
width: 100%;
|
||||
@@ -62,14 +76,13 @@ const Web3StatusError = styled(Web3StatusGeneric)`
|
||||
}
|
||||
`
|
||||
|
||||
const Web3StatusConnectButton = styled.button<{ faded?: boolean }>`
|
||||
const Web3StatusConnectWrapper = styled.div<{ faded?: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
border-radius: ${FULL_BORDER_RADIUS}px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
padding: 0;
|
||||
height: 40px;
|
||||
|
||||
:hover,
|
||||
@@ -79,33 +92,6 @@ const Web3StatusConnectButton = styled.button<{ faded?: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
const Web3StatusConnect = styled(Web3StatusGeneric)<{ faded?: boolean }>`
|
||||
background-color: ${({ theme }) => theme.deprecated_primary4};
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.deprecated_primaryText1};
|
||||
font-weight: 500;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => darken(0.05, theme.deprecated_primary4)};
|
||||
color: ${({ theme }) => theme.deprecated_primaryText1};
|
||||
}
|
||||
|
||||
${({ faded }) =>
|
||||
faded &&
|
||||
css`
|
||||
background-color: ${({ theme }) => theme.deprecated_primary5};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_primary5};
|
||||
color: ${({ theme }) => theme.deprecated_primaryText1};
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => darken(0.05, theme.deprecated_primary4)};
|
||||
color: ${({ theme }) => darken(0.05, theme.deprecated_primaryText1)};
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>`
|
||||
background-color: ${({ pending, theme }) => (pending ? theme.deprecated_primary1 : theme.deprecated_bg1)};
|
||||
border: 1px solid ${({ pending, theme }) => (pending ? theme.deprecated_primary1 : theme.deprecated_bg1)};
|
||||
@@ -146,34 +132,37 @@ function newTransactionsFirst(a: TransactionDetails, b: TransactionDetails) {
|
||||
return b.addedTime - a.addedTime
|
||||
}
|
||||
|
||||
function Sock() {
|
||||
return (
|
||||
<span role="img" aria-label={t`has socks emoji`} style={{ marginTop: -4, marginBottom: -4 }}>
|
||||
🧦
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const VerticalDivider = styled.div`
|
||||
height: 20px;
|
||||
margin: 0px 4px;
|
||||
margin: 0px;
|
||||
width: 1px;
|
||||
background-color: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
|
||||
const StyledConnect = styled.div`
|
||||
const StyledConnectButton = styled.button`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-top-left-radius: ${FULL_BORDER_RADIUS}px;
|
||||
border-bottom-left-radius: ${FULL_BORDER_RADIUS}px;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
padding: 10px 8px 10px 12px;
|
||||
|
||||
&:hover {
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast} color ${timing.in}`};
|
||||
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
border: none;
|
||||
}
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.accentActionSoft};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast} color ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -190,11 +179,10 @@ function Web3StatusInner() {
|
||||
inputError: swapInputError,
|
||||
} = useDerivedSwapInfo()
|
||||
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
|
||||
const navbarFlagEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
const theme = useTheme()
|
||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const walletIsOpen = useIsOpen()
|
||||
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
|
||||
@@ -208,8 +196,7 @@ function Web3StatusInner() {
|
||||
const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash)
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
const hasSocks = useHasSocks()
|
||||
const toggleWallet = navbarFlagEnabled ? toggleWalletDropdown : toggleWalletModal
|
||||
const toggleWallet = toggleWalletDropdown
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
@@ -229,7 +216,7 @@ function Web3StatusInner() {
|
||||
}
|
||||
return (
|
||||
<Web3StatusConnected data-testid="web3-status-connected" onClick={toggleWallet} pending={hasPendingTransactions}>
|
||||
{navbarFlagEnabled && !hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
|
||||
{!hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>
|
||||
@@ -239,18 +226,10 @@ function Web3StatusInner() {
|
||||
</RowBetween>
|
||||
) : (
|
||||
<>
|
||||
{hasSocks && !navbarFlagEnabled ? <Sock /> : null}
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
{navbarFlagEnabled ? (
|
||||
walletIsOpen ? (
|
||||
<ChevronUp {...chevronProps} />
|
||||
) : (
|
||||
<ChevronDown {...chevronProps} />
|
||||
)
|
||||
) : null}
|
||||
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
|
||||
</>
|
||||
)}
|
||||
{!navbarFlagEnabled && !hasPendingTransactions && <StatusIcon connectionType={connectionType} />}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else {
|
||||
@@ -258,7 +237,6 @@ function Web3StatusInner() {
|
||||
...CHEVRON_PROPS,
|
||||
color: theme.accentAction,
|
||||
'data-testid': 'navbar-wallet-dropdown',
|
||||
onClick: toggleWalletDropdown,
|
||||
}
|
||||
return (
|
||||
<TraceEvent
|
||||
@@ -267,33 +245,20 @@ function Web3StatusInner() {
|
||||
properties={{ received_swap_quote: validSwapQuote }}
|
||||
element={ElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
{navbarFlagEnabled ? (
|
||||
<Web3StatusConnectButton faded={!account}>
|
||||
<StyledConnect data-testid="navbar-connect-wallet" onClick={toggleWalletModal}>
|
||||
<Trans>Connect</Trans>
|
||||
</StyledConnect>
|
||||
<VerticalDivider />
|
||||
<Web3StatusConnectWrapper faded={!account}>
|
||||
<StyledConnectButton data-testid="navbar-connect-wallet" onClick={toggleWalletModal}>
|
||||
<Trans>Connect</Trans>
|
||||
</StyledConnectButton>
|
||||
<VerticalDivider />
|
||||
<ChevronWrapper onClick={toggleWalletDropdown}>
|
||||
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
|
||||
</Web3StatusConnectButton>
|
||||
) : (
|
||||
<Web3StatusConnect onClick={toggleWallet} faded={!account}>
|
||||
<Text>
|
||||
<Trans>Connect Wallet</Trans>
|
||||
</Text>
|
||||
</Web3StatusConnect>
|
||||
)}
|
||||
</ChevronWrapper>
|
||||
</Web3StatusConnectWrapper>
|
||||
</TraceEvent>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const useIsOpen = () => {
|
||||
const walletDropdownOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
const navbarFlag = useNavBarFlag()
|
||||
|
||||
return useMemo(() => navbarFlag === NavBarVariant.Enabled && walletDropdownOpen, [navbarFlag, walletDropdownOpen])
|
||||
}
|
||||
|
||||
export default function Web3Status() {
|
||||
const { ENSName } = useWeb3React()
|
||||
|
||||
@@ -301,7 +266,7 @@ export default function Web3Status() {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const walletRef = useRef<HTMLDivElement>(null)
|
||||
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
|
||||
const isOpen = useIsOpen()
|
||||
const isOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
|
||||
useOnClickOutside(ref, isOpen ? closeModal : undefined, [walletRef])
|
||||
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import { Currency, OnReviewSwapClick, SwapWidget } from '@uniswap/widgets'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import {
|
||||
AddEthereumChainParameter,
|
||||
EMPTY_TOKEN_LIST,
|
||||
OnReviewSwapClick,
|
||||
SwapWidget,
|
||||
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 {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatSwapQuoteReceivedEventProperties,
|
||||
formatToDecimal,
|
||||
getDurationFromDateMilliseconds,
|
||||
getPriceUpdateBasisPoints,
|
||||
getTokenAddress,
|
||||
} from 'analytics/utils'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import { DARK_THEME, LIGHT_THEME } from 'theme/widget'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { useSyncWidgetInputs } from './inputs'
|
||||
import { useSyncWidgetSettings } from './settings'
|
||||
@@ -20,32 +41,130 @@ export interface WidgetProps {
|
||||
|
||||
export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps) {
|
||||
const locale = useActiveLocale()
|
||||
const darkMode = useIsDarkMode()
|
||||
const theme = useMemo(() => (darkMode ? DARK_THEME : LIGHT_THEME), [darkMode])
|
||||
const { provider } = useWeb3React()
|
||||
const theme = useIsDarkMode() ? DARK_THEME : LIGHT_THEME
|
||||
const { connector, provider } = useWeb3React()
|
||||
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs(defaultToken)
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const trace = useTrace({ section: SectionName.WIDGET })
|
||||
|
||||
const [initialQuoteDate, setInitialQuoteDate] = useState<Date>()
|
||||
|
||||
const onInitialSwapQuote = useCallback(
|
||||
(trade: Trade<Currency, Currency, TradeType>) => {
|
||||
setInitialQuoteDate(new Date())
|
||||
const eventProperties = {
|
||||
// TODO(1416): Include undefined values.
|
||||
...formatSwapQuoteReceivedEventProperties(
|
||||
trade,
|
||||
/* gasUseEstimateUSD= */ undefined,
|
||||
/* fetchingSwapQuoteStartTime= */ undefined
|
||||
),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.SWAP_QUOTE_RECEIVED, eventProperties)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
|
||||
const onApproveToken = useCallback(() => {
|
||||
const input = inputs.value.INPUT
|
||||
if (!input) return
|
||||
const eventProperties = {
|
||||
chain_id: input.chainId,
|
||||
token_symbol: input.symbol,
|
||||
token_address: getTokenAddress(input),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
}, [inputs.value.INPUT, trace])
|
||||
|
||||
const onExpandSwapDetails = useCallback(() => {
|
||||
sendAnalyticsEvent(EventName.SWAP_DETAILS_EXPANDED, { ...trace })
|
||||
}, [trace])
|
||||
|
||||
const onSwapPriceUpdateAck = useCallback(
|
||||
(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,
|
||||
token_in_symbol: update.inputAmount.currency.symbol,
|
||||
token_out_symbol: update.outputAmount.currency.symbol,
|
||||
price_update_basis_points: getPriceUpdateBasisPoints(stale.executionPrice, update.executionPrice),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED, eventProperties)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
|
||||
const onSubmitSwapClick = useCallback(
|
||||
(trade: Trade<Currency, Currency, TradeType>) => {
|
||||
const eventProperties = {
|
||||
// TODO(1416): Include undefined values.
|
||||
estimated_network_fee_usd: undefined,
|
||||
transaction_deadline_seconds: undefined,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
token_in_amount_usd: undefined,
|
||||
token_out_amount_usd: undefined,
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: undefined,
|
||||
is_auto_router_api: undefined,
|
||||
is_auto_slippage: undefined,
|
||||
chain_id: trade.inputAmount.currency.chainId,
|
||||
duration_from_first_quote_to_swap_submission_milliseconds: getDurationFromDateMilliseconds(initialQuoteDate),
|
||||
swap_quote_block_number: undefined,
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.SWAP_SUBMITTED_BUTTON_CLICKED, eventProperties)
|
||||
},
|
||||
[initialQuoteDate, trace]
|
||||
)
|
||||
const onSwitchChain = useCallback(
|
||||
// TODO: Widget should not break if this rejects - upstream the catch to ignore it.
|
||||
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
|
||||
[connector]
|
||||
)
|
||||
|
||||
if (!inputs.value.INPUT && !inputs.value.OUTPUT) {
|
||||
return <WidgetSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SwapWidget
|
||||
disableBranding
|
||||
hideConnectionUI
|
||||
// jsonRpcUrlMap is excluded - network providers are always passed directly
|
||||
routerUrl={WIDGET_ROUTER_URL}
|
||||
width={WIDGET_WIDTH}
|
||||
locale={locale}
|
||||
theme={theme}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
// defaultChainId is excluded - it is always inferred from the passed provider
|
||||
provider={provider}
|
||||
onSwitchChain={onSwitchChain}
|
||||
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
|
||||
{...inputs}
|
||||
{...settings}
|
||||
{...transactions}
|
||||
onExpandSwapDetails={onExpandSwapDetails}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
onSubmitSwapClick={onSubmitSwapClick}
|
||||
onSwapApprove={onApproveToken}
|
||||
onInitialSwapQuote={onInitialSwapQuote}
|
||||
onSwapPriceUpdateAck={onSwapPriceUpdateAck}
|
||||
/>
|
||||
{tokenSelector}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function WidgetSkeleton() {
|
||||
return <SwapWidgetSkeleton theme={useIsDarkMode() ? DARK_THEME : LIGHT_THEME} width={WIDGET_WIDTH} />
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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'
|
||||
|
||||
@@ -9,18 +12,28 @@ const EMPTY_AMOUNT = ''
|
||||
* Treats the Widget as a controlled component, using the app's own token selector for selection.
|
||||
*/
|
||||
export function useSyncWidgetInputs(defaultToken?: Currency) {
|
||||
const trace = useTrace({ section: SectionName.WIDGET })
|
||||
|
||||
const [type, setType] = useState(TradeType.EXACT_INPUT)
|
||||
const [amount, setAmount] = useState(EMPTY_AMOUNT)
|
||||
const onAmountChange = useCallback((field: Field, amount: string) => {
|
||||
setType(toTradeType(field))
|
||||
setAmount(amount)
|
||||
}, [])
|
||||
const onAmountChange = useCallback(
|
||||
(field: Field, amount: string, origin?: 'max') => {
|
||||
if (origin === 'max') {
|
||||
sendAnalyticsEvent(EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED, { ...trace })
|
||||
}
|
||||
setType(toTradeType(field))
|
||||
setAmount(amount)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
|
||||
const [tokens, setTokens] = useState<{ [Field.INPUT]?: Currency; [Field.OUTPUT]?: Currency }>({
|
||||
[Field.OUTPUT]: defaultToken,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Avoid overwriting tokens if none are specified, so that a loading token does not cause layout flashing.
|
||||
if (!defaultToken) return
|
||||
setTokens({
|
||||
[Field.OUTPUT]: defaultToken,
|
||||
})
|
||||
@@ -28,12 +41,13 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
|
||||
}, [defaultToken])
|
||||
|
||||
const onSwitchTokens = useCallback(() => {
|
||||
sendAnalyticsEvent(EventName.SWAP_TOKENS_REVERSED, { ...trace })
|
||||
setType((type) => invertTradeType(type))
|
||||
setTokens((tokens) => ({
|
||||
[Field.INPUT]: tokens[Field.OUTPUT],
|
||||
[Field.OUTPUT]: tokens[Field.INPUT],
|
||||
}))
|
||||
}, [])
|
||||
}, [trace])
|
||||
|
||||
const [selectingField, setSelectingField] = useState<Field>()
|
||||
const otherField = useMemo(() => (selectingField === Field.INPUT ? Field.OUTPUT : Field.INPUT), [selectingField])
|
||||
@@ -51,7 +65,7 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
|
||||
setType(TradeType.EXACT_INPUT)
|
||||
setTokens(() => {
|
||||
return {
|
||||
[otherField]: token === otherToken ? selectingToken : otherToken,
|
||||
[otherField]: otherToken?.equals(token) ? selectingToken : otherToken,
|
||||
[selectingField]: token,
|
||||
}
|
||||
})
|
||||
@@ -68,7 +82,7 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
|
||||
/>
|
||||
)
|
||||
|
||||
const value: SwapController = useMemo(() => ({ type, amount, ...tokens }), [amount, tokens, type])
|
||||
const value: SwapController['value'] = useMemo(() => ({ type, amount, ...tokens }), [amount, tokens, type])
|
||||
const valueHandlers: SwapEventHandlers = useMemo(
|
||||
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),
|
||||
[onAmountChange, onSwitchTokens, onTokenSelectorClick]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { Slippage, SwapEventHandlers, SwapSettingsController } from '@uniswap/widgets'
|
||||
import { Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
|
||||
@@ -44,7 +44,7 @@ export function useSyncWidgetSettings() {
|
||||
setAppSlippage('auto')
|
||||
}, [setAppSlippage, setAppTtl])
|
||||
|
||||
const settings: SwapSettingsController = useMemo(() => {
|
||||
const settings: SwapController['settings'] = useMemo(() => {
|
||||
const auto = appSlippage === 'auto'
|
||||
return { slippage: { auto, max: widgetSlippage }, transactionTtl: widgetTtl }
|
||||
}, [widgetSlippage, widgetTtl, appSlippage])
|
||||
|
||||
@@ -6,6 +6,12 @@ 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 { useCallback, useMemo } from 'react'
|
||||
import { useTransactionAdder } from 'state/transactions/hooks'
|
||||
import {
|
||||
@@ -18,6 +24,8 @@ import { currencyId } from 'utils/currencyId'
|
||||
|
||||
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
|
||||
export function useSyncWidgetTransactions() {
|
||||
const trace = useTrace({ section: SectionName.WIDGET })
|
||||
|
||||
const { chainId } = useWeb3React()
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
@@ -28,8 +36,23 @@ export function useSyncWidgetTransactions() {
|
||||
if (!type || !response) {
|
||||
return
|
||||
} else if (type === WidgetTransactionType.WRAP || type === WidgetTransactionType.UNWRAP) {
|
||||
const { amount } = transaction.info
|
||||
const { type, amount: transactionAmount } = transaction.info
|
||||
|
||||
const eventProperties = {
|
||||
// get this info from widget handlers
|
||||
token_in_address: getTokenAddress(transactionAmount.currency),
|
||||
token_out_address: getTokenAddress(transactionAmount.currency.wrapped),
|
||||
token_in_symbol: transactionAmount.currency.symbol,
|
||||
token_out_symbol: transactionAmount.currency.wrapped.symbol,
|
||||
chain_id: transactionAmount.currency.chainId,
|
||||
amount: transactionAmount
|
||||
? formatToDecimal(transactionAmount, transactionAmount?.currency.decimals)
|
||||
: undefined,
|
||||
type: type === WidgetTransactionType.WRAP ? WrapType.WRAP : WrapType.UNWRAP,
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.WRAP_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
const { amount } = transaction.info
|
||||
addTransaction(response, {
|
||||
type: AppTransactionType.WRAP,
|
||||
unwrapped: type === WidgetTransactionType.UNWRAP,
|
||||
@@ -38,6 +61,15 @@ export function useSyncWidgetTransactions() {
|
||||
} as WrapTransactionInfo)
|
||||
} else if (type === WidgetTransactionType.SWAP) {
|
||||
const { slippageTolerance, trade, tradeType } = transaction.info
|
||||
|
||||
const eventProperties = {
|
||||
...formatSwapSignedAnalyticsEventProperties({
|
||||
trade,
|
||||
txHash: transaction.receipt?.transactionHash ?? '',
|
||||
}),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.SWAP_SIGNED, eventProperties)
|
||||
const baseTxInfo = {
|
||||
type: AppTransactionType.SWAP,
|
||||
tradeType,
|
||||
@@ -61,7 +93,7 @@ export function useSyncWidgetTransactions() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[addTransaction, chainId]
|
||||
[addTransaction, chainId, trace]
|
||||
)
|
||||
|
||||
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit }), [onTxSubmit])
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import Card from 'components/Card'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
@@ -55,8 +54,6 @@ export function AdvancedSwapDetails({
|
||||
const theme = useTheme()
|
||||
const { chainId } = useWeb3React()
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
const { expectedOutputAmount, priceImpact } = useMemo(() => {
|
||||
return {
|
||||
@@ -109,7 +106,7 @@ export function AdvancedSwapDetails({
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
<Separator redesignFlag={redesignFlagEnabled} />
|
||||
<Separator />
|
||||
<RowBetween>
|
||||
<RowFixed style={{ marginRight: '20px' }}>
|
||||
<MouseoverTooltip
|
||||
|
||||
@@ -5,10 +5,9 @@ import { sendAnalyticsEvent } from 'analytics'
|
||||
import { ModalName } from 'analytics/constants'
|
||||
import { EventName } from 'analytics/constants'
|
||||
import { Trace } from 'analytics/Trace'
|
||||
import { formatPercentInBasisPointsNumber, formatToDecimal, getTokenAddress } from 'analytics/utils'
|
||||
import { formatSwapSignedAnalyticsEventProperties } from 'analytics/utils'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import TransactionConfirmationModal, {
|
||||
@@ -18,27 +17,6 @@ import TransactionConfirmationModal, {
|
||||
import SwapModalFooter from './SwapModalFooter'
|
||||
import SwapModalHeader from './SwapModalHeader'
|
||||
|
||||
const formatAnalyticsEventProperties = ({
|
||||
trade,
|
||||
txHash,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
txHash: string
|
||||
}) => ({
|
||||
transaction_hash: txHash,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
})
|
||||
|
||||
export default function ConfirmSwapModal({
|
||||
trade,
|
||||
originalTrade,
|
||||
@@ -149,7 +127,7 @@ export default function ConfirmSwapModal({
|
||||
|
||||
useEffect(() => {
|
||||
if (!attemptingTxn && isOpen && txHash && trade && txHash !== lastTxnHashLogged) {
|
||||
sendAnalyticsEvent(EventName.SWAP_SIGNED, formatAnalyticsEventProperties({ trade, txHash }))
|
||||
sendAnalyticsEvent(EventName.SWAP_SIGNED, formatSwapSignedAnalyticsEventProperties({ trade, txHash }))
|
||||
setLastTxnHashLogged(txHash)
|
||||
}
|
||||
}, [attemptingTxn, isOpen, txHash, trade, lastTxnHashLogged])
|
||||
|
||||
@@ -10,7 +10,6 @@ import { LoadingOpacityContainer } from 'components/Loader/styled'
|
||||
import Row, { RowBetween, RowFixed } from 'components/Row'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown, Info } from 'react-feather'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
@@ -26,6 +25,10 @@ import TradePrice from './TradePrice'
|
||||
const Wrapper = styled(Row)`
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
border-radius: inherit;
|
||||
padding: 8px 12px;
|
||||
margin-top: 0;
|
||||
min-height: 32px;
|
||||
`
|
||||
|
||||
const StyledInfoIcon = styled(Info)`
|
||||
@@ -35,21 +38,15 @@ const StyledInfoIcon = styled(Info)`
|
||||
color: ${({ theme }) => theme.deprecated_text3};
|
||||
`
|
||||
|
||||
const StyledCard = styled(OutlineCard)<{ redesignFlag: boolean }>`
|
||||
const StyledCard = styled(OutlineCard)`
|
||||
padding: 12px;
|
||||
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundOutline : theme.deprecated_bg3)};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
|
||||
const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean; redesignFlag: boolean }>`
|
||||
padding: ${({ redesignFlag }) => (redesignFlag ? '8px 0px 0px 0px' : '4px 8px')};
|
||||
background-color: ${({ open, theme, redesignFlag }) =>
|
||||
open && !redesignFlag ? theme.deprecated_bg1 : 'transparent'};
|
||||
const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>`
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundOutline : 'transparent')};
|
||||
margin-top: ${({ redesignFlag }) => redesignFlag && '8px'};
|
||||
cursor: ${({ disabled }) => (disabled ? 'initial' : 'pointer')};
|
||||
min-height: 40px;
|
||||
border-radius: ${({ redesignFlag }) => !redesignFlag && '12px'};
|
||||
`
|
||||
|
||||
const RotatingArrow = styled(ChevronDown)<{ open?: boolean }>`
|
||||
@@ -129,11 +126,9 @@ export default function SwapDetailsDropdown({
|
||||
const theme = useTheme()
|
||||
const { chainId } = useWeb3React()
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
return (
|
||||
<Wrapper style={{ marginTop: '8px' }}>
|
||||
<Wrapper style={{ marginTop: '0' }}>
|
||||
<AutoColumn gap={'8px'} style={{ width: '100%', marginBottom: '-8px' }}>
|
||||
<TraceEvent
|
||||
events={[Event.onClick]}
|
||||
@@ -141,12 +136,7 @@ export default function SwapDetailsDropdown({
|
||||
element={ElementName.SWAP_DETAILS_DROPDOWN}
|
||||
shouldLogImpression={!showDetails}
|
||||
>
|
||||
<StyledHeaderRow
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
disabled={!trade}
|
||||
open={showDetails}
|
||||
>
|
||||
<StyledHeaderRow onClick={() => setShowDetails(!showDetails)} disabled={!trade} open={showDetails}>
|
||||
<RowFixed style={{ position: 'relative' }}>
|
||||
{loading || syncing ? (
|
||||
<StyledPolling>
|
||||
@@ -213,7 +203,7 @@ export default function SwapDetailsDropdown({
|
||||
<AnimatedDropdown open={showDetails}>
|
||||
<AutoColumn gap={'8px'} style={{ padding: '0', paddingBottom: '8px' }}>
|
||||
{trade ? (
|
||||
<StyledCard redesignFlag={redesignFlagEnabled}>
|
||||
<StyledCard>
|
||||
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
|
||||
</StyledCard>
|
||||
) : null}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Price } from '@uniswap/sdk-core'
|
||||
import { sendAnalyticsEvent } from 'analytics'
|
||||
import { EventName, SWAP_PRICE_UPDATE_USER_RESPONSE } from 'analytics/constants'
|
||||
import { formatPercentInBasisPointsNumber } from 'analytics/utils'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { getPriceUpdateBasisPoints } from 'analytics/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertTriangle, ArrowDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -25,11 +23,11 @@ import TradePrice from '../swap/TradePrice'
|
||||
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
|
||||
import { SwapShowAcceptChanges, TruncatedText } from './styleds'
|
||||
|
||||
const ArrowWrapper = styled.div<{ redesignFlag: boolean }>`
|
||||
const ArrowWrapper = styled.div`
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
height: ${({ redesignFlag }) => (redesignFlag ? '40px' : '32px')};
|
||||
width: ${({ redesignFlag }) => (redesignFlag ? '40px' : '32px')};
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -18px;
|
||||
@@ -37,9 +35,9 @@ const ArrowWrapper = styled.div<{ redesignFlag: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundSurface : theme.deprecated_bg1)};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 4px solid;
|
||||
border-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundModule : theme.deprecated_bg0)};
|
||||
border-color: ${({ theme }) => theme.backgroundModule};
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
@@ -58,15 +56,6 @@ const formatAnalyticsEventProperties = (
|
||||
price_update_basis_points: priceUpdate,
|
||||
})
|
||||
|
||||
const getPriceUpdateBasisPoints = (
|
||||
prevPrice: Price<Currency, Currency>,
|
||||
newPrice: Price<Currency, Currency>
|
||||
): number => {
|
||||
const changeFraction = newPrice.subtract(prevPrice).divide(prevPrice)
|
||||
const changePercentage = new Percent(changeFraction.numerator, changeFraction.denominator)
|
||||
return formatPercentInBasisPointsNumber(changePercentage)
|
||||
}
|
||||
|
||||
export default function SwapModalHeader({
|
||||
trade,
|
||||
shouldLogModalCloseEvent,
|
||||
@@ -85,8 +74,6 @@ export default function SwapModalHeader({
|
||||
onAcceptChanges: () => void
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
const [lastExecutionPrice, setLastExecutionPrice] = useState(trade.executionPrice)
|
||||
@@ -137,8 +124,8 @@ export default function SwapModalHeader({
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
<ArrowWrapper redesignFlag={redesignFlagEnabled}>
|
||||
<ArrowDown size="16" color={redesignFlagEnabled ? theme.textPrimary : theme.deprecated_text2} />
|
||||
<ArrowWrapper>
|
||||
<ArrowDown size="16" color={theme.textPrimary} />
|
||||
</ArrowWrapper>
|
||||
<LightCard padding="0.75rem 1rem" style={{ marginBottom: '0.25rem' }}>
|
||||
<AutoColumn gap={'8px'}>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { LoadingRows } from 'components/Loader/styled'
|
||||
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
|
||||
import { AutoRow, RowBetween } from 'components/Row'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
|
||||
import { memo, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
@@ -23,12 +22,10 @@ import { Separator, ThemedText } from 'theme'
|
||||
|
||||
import { AutoRouterLabel, AutoRouterLogo } from './RouterLabel'
|
||||
|
||||
const Wrapper = styled(AutoColumn)<{ darkMode?: boolean; fixedOpen?: boolean; redesignFlag: boolean }>`
|
||||
const Wrapper = styled(AutoColumn)<{ darkMode?: boolean; fixedOpen?: boolean }>`
|
||||
padding: ${({ fixedOpen }) => (fixedOpen ? '12px' : '12px 8px 12px 12px')};
|
||||
border-radius: 16px;
|
||||
border: 1px solid
|
||||
${({ theme, fixedOpen, redesignFlag }) =>
|
||||
fixedOpen ? 'transparent' : redesignFlag ? theme.backgroundOutline : theme.deprecated_bg2};
|
||||
border: 1px solid ${({ theme, fixedOpen }) => (fixedOpen ? 'transparent' : theme.backgroundOutline)};
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
@@ -56,8 +53,6 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
|
||||
const routes = getTokenPath(trade)
|
||||
const [open, setOpen] = useState(false)
|
||||
const { chainId } = useWeb3React()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
const [darkMode] = useDarkModeManager()
|
||||
|
||||
@@ -68,7 +63,7 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen} redesignFlag={redesignFlagEnabled}>
|
||||
<Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen}>
|
||||
<TraceEvent
|
||||
events={[Event.onClick]}
|
||||
name={EventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { formatDollar, formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers'
|
||||
|
||||
interface TradePriceProps {
|
||||
price: Price<Currency, Currency>
|
||||
@@ -33,15 +34,12 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
|
||||
const theme = useTheme()
|
||||
|
||||
const usdcPrice = useStablecoinPrice(showInverted ? price.baseCurrency : price.quoteCurrency)
|
||||
/*
|
||||
* calculate needed amount of decimal prices, for prices between 0.95-1.05 use 4 decimal places
|
||||
*/
|
||||
const p = Number(usdcPrice?.toFixed())
|
||||
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
|
||||
|
||||
let formattedPrice: string
|
||||
try {
|
||||
formattedPrice = showInverted ? price.toSignificant(4) : price.invert()?.toSignificant(4)
|
||||
formattedPrice = showInverted
|
||||
? formatTransactionAmount(priceToPreciseFloat(price))
|
||||
: formatTransactionAmount(priceToPreciseFloat(price.invert()))
|
||||
} catch (error) {
|
||||
formattedPrice = '0'
|
||||
}
|
||||
@@ -65,7 +63,7 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
|
||||
</Text>{' '}
|
||||
{usdcPrice && (
|
||||
<ThemedText.DeprecatedDarkGray>
|
||||
<Trans>(${usdcPrice.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })})</Trans>
|
||||
<Trans>({formatDollar({ num: priceToPreciseFloat(usdcPrice) })})</Trans>
|
||||
</ThemedText.DeprecatedDarkGray>
|
||||
)}
|
||||
</StyledPriceContainer>
|
||||
|
||||
@@ -8,46 +8,42 @@ import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export const PageWrapper = styled.div<{ redesignFlag: boolean; navBarFlag: boolean }>`
|
||||
padding: ${({ navBarFlag }) => (navBarFlag ? '68px 8px 0px' : '0px 8px')};
|
||||
export const PageWrapper = styled.div`
|
||||
padding: 68px 8px 0px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
padding-top: ${({ navBarFlag }) => (navBarFlag ? '48px' : '0px')};
|
||||
padding-top: 48px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
padding-top: ${({ navBarFlag }) => (navBarFlag ? '20px' : '0px')};
|
||||
padding-top: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
// Mostly copied from `AppBody` but it was getting too hard to maintain backwards compatibility.
|
||||
export const SwapWrapper = styled.main<{ margin?: string; maxWidth?: string; redesignFlag: boolean }>`
|
||||
export const SwapWrapper = styled.main<{ margin?: string; maxWidth?: string }>`
|
||||
position: relative;
|
||||
background: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundSurface : theme.deprecated_bg0)};
|
||||
border-radius: ${({ redesignFlag }) => (redesignFlag ? '16px' : '24px')};
|
||||
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundOutline : 'transparent')};
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
padding: 8px;
|
||||
z-index: ${Z_INDEX.deprecated_content};
|
||||
box-shadow: ${({ redesignFlag }) =>
|
||||
!redesignFlag &&
|
||||
'0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 24px 32px rgba(0, 0, 0, 0.01)'};
|
||||
`
|
||||
|
||||
export const ArrowWrapper = styled.div<{ clickable: boolean; redesignFlag: boolean }>`
|
||||
padding: 4px;
|
||||
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||
border-radius: 12px;
|
||||
height: ${({ redesignFlag }) => (redesignFlag ? '40px' : '32px')};
|
||||
width: ${({ redesignFlag }) => (redesignFlag ? '40px' : '32px')};
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
margin-top: ${({ redesignFlag }) => (redesignFlag ? '-18px' : '-14px')};
|
||||
margin-bottom: ${({ redesignFlag }) => (redesignFlag ? '-18px' : '-14px')};
|
||||
left: calc(50% - 16px);
|
||||
/* transform: rotate(90deg); */
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundInteractive : theme.deprecated_bg1)};
|
||||
margin-top: -18px;
|
||||
margin-bottom: -18px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
border: 4px solid;
|
||||
border-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.backgroundSurface : theme.deprecated_bg0)};
|
||||
border-color: ${({ theme }) => theme.backgroundSurface};
|
||||
|
||||
z-index: 2;
|
||||
${({ clickable }) =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TokenInfo } from '@uniswap/token-lists'
|
||||
import store from '../state'
|
||||
import { UNI_EXTENDED_LIST, UNI_LIST, UNSUPPORTED_LIST_URLS } from './lists'
|
||||
import brokenTokenList from './tokenLists/broken.tokenlist.json'
|
||||
import { NATIVE_CHAIN_ID } from './tokens'
|
||||
|
||||
export enum TOKEN_LIST_TYPES {
|
||||
UNI_DEFAULT = 1,
|
||||
@@ -47,6 +48,9 @@ class TokenSafetyLookupTable {
|
||||
if (!this.dict) {
|
||||
this.dict = this.createMap()
|
||||
}
|
||||
if (address === NATIVE_CHAIN_ID.toLowerCase()) {
|
||||
return TOKEN_LIST_TYPES.UNI_DEFAULT
|
||||
}
|
||||
return this.dict[address] ?? TOKEN_LIST_TYPES.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import celoCircleLogoUrl from 'assets/images/celoCircle.png'
|
||||
import ethereumLogoUrl from 'assets/images/ethereum-logo.png'
|
||||
import optimismCircleLogoUrl from 'assets/images/optimismCircle.png'
|
||||
import polygonCircleLogoUrl from 'assets/images/polygonCircle.png'
|
||||
import { default as arbitrumCircleLogoUrl, default as arbitrumLogoUrl } from 'assets/svg/arbitrum_logo.svg'
|
||||
import celoLogo from 'assets/svg/celo_logo.svg'
|
||||
@@ -114,7 +113,8 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/optimism/',
|
||||
label: 'Optimism',
|
||||
logoUrl: optimismLogoUrl,
|
||||
circleLogoUrl: optimismCircleLogoUrl,
|
||||
// Optimism perfers same icon for both
|
||||
circleLogoUrl: optimismLogoUrl,
|
||||
statusPage: 'https://optimism.io/status',
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ',
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const UNI_LIST = 'https://tokens.uniswap.org'
|
||||
export const UNI_EXTENDED_LIST = 'https://extendedtokens.uniswap.org/'
|
||||
const UNI_UNSUPPORTED_LISTS = 'https://unsupportedtokens.uniswap.org/'
|
||||
const UNI_UNSUPPORTED_LIST = 'https://unsupportedtokens.uniswap.org/'
|
||||
const AAVE_LIST = 'tokenlist.aave.eth'
|
||||
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
|
||||
const CMC_ALL_LIST = 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json'
|
||||
@@ -16,12 +16,11 @@ export const OPTIMISM_LIST = 'https://static.optimism.io/optimism.tokenlist.json
|
||||
export const ARBITRUM_LIST = 'https://bridge.arbitrum.io/token-list-42161.json'
|
||||
export const CELO_LIST = 'https://celo-org.github.io/celo-token-list/celo.tokenlist.json'
|
||||
|
||||
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LISTS]
|
||||
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LIST]
|
||||
|
||||
// this is the default list of lists that are exposed to users
|
||||
// lower index == higher priority for token import
|
||||
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
|
||||
UNI_LIST,
|
||||
// default lists to be 'active' aka searched across
|
||||
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST]
|
||||
export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
|
||||
UNI_EXTENDED_LIST,
|
||||
COMPOUND_LIST,
|
||||
AAVE_LIST,
|
||||
@@ -37,10 +36,11 @@ const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
|
||||
CELO_LIST,
|
||||
]
|
||||
|
||||
// this is the default list of lists that are exposed to users
|
||||
// lower index == higher priority for token import
|
||||
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [...DEFAULT_ACTIVE_LIST_URLS, ...DEFAULT_INACTIVE_LIST_URLS]
|
||||
|
||||
export const DEFAULT_LIST_OF_LISTS: string[] = [
|
||||
...DEFAULT_LIST_OF_LISTS_TO_DISPLAY,
|
||||
...UNSUPPORTED_LIST_URLS, // need to load dynamic unsupported tokens as well
|
||||
]
|
||||
|
||||
// default lists to be 'active' aka searched across
|
||||
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST, GEMINI_LIST]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
|
||||
import { ZERO_ADDRESS } from './misc'
|
||||
import { NATIVE_CHAIN_ID } from './tokens'
|
||||
import WarningCache, { TOKEN_LIST_TYPES } from './TokenSafetyLookupTable'
|
||||
|
||||
export const TOKEN_SAFETY_ARTICLE = 'https://support.uniswap.org/hc/en-us/articles/8723118437133'
|
||||
@@ -14,17 +16,36 @@ export function getWarningCopy(warning: Warning | null, plural = false) {
|
||||
let heading = null,
|
||||
description = null
|
||||
if (warning) {
|
||||
if (warning.canProceed) {
|
||||
heading = <Plural value={plural ? 2 : 1} _1="This token isn't verified." other="These tokens aren't verified." />
|
||||
description = <Trans>Please do your own research before trading.</Trans>
|
||||
} else {
|
||||
description = (
|
||||
<Plural
|
||||
value={plural ? 2 : 1}
|
||||
_1="You can't trade this token using the Uniswap App."
|
||||
other="You can't trade these tokens using the Uniswap App."
|
||||
/>
|
||||
)
|
||||
switch (warning.level) {
|
||||
case WARNING_LEVEL.MEDIUM:
|
||||
heading = (
|
||||
<Plural
|
||||
value={plural ? 2 : 1}
|
||||
_1="This token isn't traded on leading U.S. centralized exchanges."
|
||||
other="These tokens aren't traded on leading U.S. centralized exchanges."
|
||||
/>
|
||||
)
|
||||
description = <Trans>Always conduct your own research before trading.</Trans>
|
||||
break
|
||||
case WARNING_LEVEL.UNKNOWN:
|
||||
heading = (
|
||||
<Plural
|
||||
value={plural ? 2 : 1}
|
||||
_1="This token isn't traded on leading U.S. centralized exchanges or frequently swapped on Uniswap."
|
||||
other="These tokens aren't traded on leading U.S. centralized exchanges or frequently swapped on Uniswap."
|
||||
/>
|
||||
)
|
||||
description = <Trans>Always conduct your own research before trading.</Trans>
|
||||
break
|
||||
case WARNING_LEVEL.BLOCKED:
|
||||
description = (
|
||||
<Plural
|
||||
value={plural ? 2 : 1}
|
||||
_1="You can't trade this token using the Uniswap App."
|
||||
other="You can't trade these tokens using the Uniswap App."
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
return { heading, description }
|
||||
@@ -57,6 +78,9 @@ const BlockedWarning: Warning = {
|
||||
}
|
||||
|
||||
export function checkWarning(tokenAddress: string) {
|
||||
if (tokenAddress === NATIVE_CHAIN_ID || tokenAddress === ZERO_ADDRESS) {
|
||||
return null
|
||||
}
|
||||
switch (WarningCache.checkToken(tokenAddress.toLowerCase())) {
|
||||
case TOKEN_LIST_TYPES.UNI_DEFAULT:
|
||||
return null
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user