Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a257e0ca8 | ||
|
|
82646b77dd | ||
|
|
1992c5de06 | ||
|
|
0208ccd7d2 | ||
|
|
12df4b3981 | ||
|
|
3eaeb65b07 | ||
|
|
6df2f3677e | ||
|
|
80edf5a0d6 | ||
|
|
96f6929127 | ||
|
|
4ec95d0927 | ||
|
|
fba6cc9e02 | ||
|
|
bc2f68565b | ||
|
|
f232643d8e | ||
|
|
527270e33f | ||
|
|
18cd5ec9d9 | ||
|
|
5ddb565805 | ||
|
|
f0b4b92b88 | ||
|
|
4d82f9fb3a | ||
|
|
654b26dc54 | ||
|
|
c0753ae52f | ||
|
|
16bb9470ae | ||
|
|
6dcfca24cb | ||
|
|
9cac9f8299 | ||
|
|
5def0dd166 | ||
|
|
7229637c4c | ||
|
|
f26b09537d | ||
|
|
8f922b665a | ||
|
|
134b1d708f | ||
|
|
e9bddcb670 | ||
|
|
19e45fd119 | ||
|
|
ae4135fa49 | ||
|
|
89e438bcc5 | ||
|
|
92af2167ee | ||
|
|
db6084d717 | ||
|
|
927d35d59e | ||
|
|
b4e981b2fd | ||
|
|
967a698178 | ||
|
|
7818426b53 | ||
|
|
93e0054f10 | ||
|
|
661d2b6a33 | ||
|
|
c560b94366 | ||
|
|
93a4f00287 | ||
|
|
48833f27e3 | ||
|
|
35a03e2681 | ||
|
|
ac0badfb1d | ||
|
|
149b18f02e | ||
|
|
52a43f3db0 | ||
|
|
0a2a46d506 | ||
|
|
a7c1bd4391 | ||
|
|
13221e6935 | ||
|
|
26fc3caa55 | ||
|
|
6072bb1be0 | ||
|
|
302af21a22 | ||
|
|
b61a2d4111 | ||
|
|
9be26788a2 | ||
|
|
ed393de481 | ||
|
|
cf5c393d97 | ||
|
|
68d81a0040 | ||
|
|
53caa51ac3 | ||
|
|
409ba72f9f | ||
|
|
9d9b3dca78 | ||
|
|
a11c7e9573 | ||
|
|
31bbcae1ed | ||
|
|
a1f6c7270e | ||
|
|
8471d9b46f | ||
|
|
5fc4d98faa | ||
|
|
8d9ddf36a2 | ||
|
|
6cfd5fa475 | ||
|
|
f2c5a7c09c | ||
|
|
fb52770953 | ||
|
|
94aa8ae2c9 | ||
|
|
6cb0824a0b | ||
|
|
777887b25d | ||
|
|
d15d5d85f5 | ||
|
|
43218d5655 | ||
|
|
a534ba41ed | ||
|
|
4715115743 | ||
|
|
3389d01213 | ||
|
|
d9a0aa3ff0 | ||
|
|
e9e5d2e43e | ||
|
|
d58dc14bd5 | ||
|
|
909e18cb23 | ||
|
|
a9ab5717de | ||
|
|
94544de74b | ||
|
|
96f24d5a9b | ||
|
|
8e59a352c0 | ||
|
|
3b765b4f05 | ||
|
|
9f4a1f48a5 | ||
|
|
de9533399a | ||
|
|
a02afd50b5 | ||
|
|
1f7ba5ae9f | ||
|
|
3686803c17 | ||
|
|
6f147c1ff3 | ||
|
|
049a09a346 | ||
|
|
4b9a885a34 | ||
|
|
e3918d039f | ||
|
|
9719af66e5 | ||
|
|
14b02eda0f | ||
|
|
60bc2a1660 | ||
|
|
ef3407f299 | ||
|
|
f312a148d0 | ||
|
|
cf5bb5740d | ||
|
|
5f280ffd0e | ||
|
|
97075acb91 | ||
|
|
6089d38daf | ||
|
|
3c6e067e90 | ||
|
|
fd1ee61daf | ||
|
|
de71f07b65 | ||
|
|
59f9c6c2d8 | ||
|
|
3efcd3b23a | ||
|
|
726640787d | ||
|
|
889cdf6b66 | ||
|
|
400666cd0b | ||
|
|
7f4fe6cc9b | ||
|
|
dce891ddbd | ||
|
|
bc9bb39a8f | ||
|
|
0cc6879638 | ||
|
|
a5534803a1 | ||
|
|
a06f885724 | ||
|
|
de7cfc93e6 | ||
|
|
aa6c469042 | ||
|
|
dc478ce37e | ||
|
|
3f3f16c366 | ||
|
|
8e84a53e57 | ||
|
|
e88a50d309 | ||
|
|
102d99afa2 | ||
|
|
73e4202497 | ||
|
|
6e228185b4 | ||
|
|
bb92a9ee36 | ||
|
|
4936ec5cfc | ||
|
|
bb1db9048a | ||
|
|
20dbb2a9f7 | ||
|
|
fd4430fe69 | ||
|
|
167cc695c9 | ||
|
|
e175bff7f4 | ||
|
|
4442933eac | ||
|
|
8447b30327 | ||
|
|
22ce55ec46 | ||
|
|
b25da9de2d | ||
|
|
ae1fb4367f | ||
|
|
1410edda32 | ||
|
|
3bc7f015ee | ||
|
|
802714377c | ||
|
|
352daf959e | ||
|
|
92c21c2811 | ||
|
|
e70723aaf3 | ||
|
|
1802f50163 | ||
|
|
2aa1b18d14 | ||
|
|
a286e5b114 | ||
|
|
62361647e0 | ||
|
|
deee278439 | ||
|
|
6340deb201 | ||
|
|
28b154ebe8 | ||
|
|
3bde2165f4 | ||
|
|
78c8fd2359 | ||
|
|
c378752910 | ||
|
|
0bf7b92013 | ||
|
|
283479f76e | ||
|
|
d3c30e2f6b | ||
|
|
32d226f78e | ||
|
|
96744505c0 | ||
|
|
97236033d4 | ||
|
|
86e62dc4b9 | ||
|
|
e584a5fa36 | ||
|
|
332ef6e6c8 | ||
|
|
8cbd111e65 | ||
|
|
55ffcbd465 | ||
|
|
404775e86d | ||
|
|
0ae9fe28a2 | ||
|
|
89c0caae43 | ||
|
|
c8086e3c76 | ||
|
|
1c2842e5a0 | ||
|
|
a2c6d3f475 | ||
|
|
841ea7f8a1 | ||
|
|
804692b114 | ||
|
|
6282298d13 | ||
|
|
7a5b855097 | ||
|
|
c9908748cf | ||
|
|
79b77deee1 | ||
|
|
a554af6670 | ||
|
|
1843f214b1 | ||
|
|
3e0788092e | ||
|
|
d14c49df0d | ||
|
|
c098ad1ffe | ||
|
|
48114ef51d | ||
|
|
cb7132ee17 | ||
|
|
0fa4859a09 | ||
|
|
f8bb5046f0 | ||
|
|
7d1589d1df | ||
|
|
26b603cc2e | ||
|
|
ece68a0ec7 | ||
|
|
fd212477ce | ||
|
|
a16d2387cc | ||
|
|
cae56ec385 | ||
|
|
d16b3473e0 | ||
|
|
f66f249dba | ||
|
|
08afd888d0 | ||
|
|
b427be2673 | ||
|
|
f753a5e325 | ||
|
|
46d9d8e3df | ||
|
|
680d3a3f26 | ||
|
|
e4c625ee71 |
7
.env
@@ -1,7 +1,12 @@
|
|||||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
||||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||||
REACT_APP_AWS_API_REGION="us-east-2"
|
REACT_APP_AWS_API_REGION="us-east-2"
|
||||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
||||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
||||||
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
||||||
|
REACT_APP_SENTRY_ENABLED=false
|
||||||
ESLINT_NO_DEV_ERRORS=true
|
ESLINT_NO_DEV_ERRORS=true
|
||||||
|
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||||
|
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||||
|
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
|
||||||
|
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
||||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||||
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
||||||
|
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||||
|
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||||
|
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink?platform=web"
|
||||||
|
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||||
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
|
||||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||||
|
REACT_APP_SENTRY_ENABLED=false
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
*.config.ts
|
*.config.ts
|
||||||
*.d.ts
|
*.d.ts
|
||||||
|
/src/graphql/data/__generated__/types-and-hooks.ts
|
||||||
|
/src/graphql/thegraph/__generated__/types-and-hooks.ts
|
||||||
|
/src/schema/schema.graphql
|
||||||
|
|||||||
7
.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
require('@uniswap/eslint-config/load')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extends: '@uniswap/eslint-config/react',
|
||||||
|
}
|
||||||
109
.eslintrc.json
@@ -1,109 +0,0 @@
|
|||||||
{
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2020,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
// Allows for the parsing of JSX
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
},
|
|
||||||
"import/parsers": {
|
|
||||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
|
||||||
},
|
|
||||||
"import/resolver": {
|
|
||||||
"typescript": {
|
|
||||||
"alwaysTryTypes": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ignorePatterns": [
|
|
||||||
"src/types/v3",
|
|
||||||
"src/abis/types",
|
|
||||||
"src/locales/**/*.js",
|
|
||||||
"src/locales/**/en-US.po",
|
|
||||||
"node_modules",
|
|
||||||
"coverage",
|
|
||||||
"build",
|
|
||||||
"dist",
|
|
||||||
".DS_Store",
|
|
||||||
".env.local",
|
|
||||||
".env.development.local",
|
|
||||||
".env.test.local",
|
|
||||||
".env.production.local",
|
|
||||||
".idea/",
|
|
||||||
".vscode/",
|
|
||||||
"package-lock.json",
|
|
||||||
"yarn.lock"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"prettier/@typescript-eslint",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"plugin:import/typescript"
|
|
||||||
],
|
|
||||||
"plugins": ["import", "simple-import-sort", "unused-imports"],
|
|
||||||
"rules": {
|
|
||||||
"import/no-unused-modules": [2, { "unusedExports": true }],
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
|
||||||
"simple-import-sort/imports": "error",
|
|
||||||
"simple-import-sort/exports": "error",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/ban-ts-ignore": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react/jsx-curly-brace-presence": ["error", { "props": "never", "children": "never" }],
|
|
||||||
"object-shorthand": ["error", "always"],
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"paths": [
|
|
||||||
{
|
|
||||||
"name": "ethers",
|
|
||||||
"message": "Please import from '@ethersproject/module' directly to support tree-shaking."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "styled-components",
|
|
||||||
"message": "Please import from styled-components/macro."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@lingui/macro",
|
|
||||||
"importNames": ["t"],
|
|
||||||
"message": "Please use <Trans> instead of t."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"group": ["**/dist"],
|
|
||||||
"message": "Do not import from dist/ - this is an implementation detail, and breaks tree-shaking."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": ["!styled-components/macro"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"paths": [
|
|
||||||
{
|
|
||||||
"name": "@ethersproject/providers",
|
|
||||||
"message": "Please only use Providers instantiated in constants/providers to improve traceability.",
|
|
||||||
"allowTypeImports": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
.github/workflows/release.yaml
vendored
@@ -113,6 +113,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload source maps to Sentry
|
- name: Upload source maps to Sentry
|
||||||
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
||||||
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
@@ -120,4 +121,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
environment: production
|
environment: production
|
||||||
sourcemaps: './build/static/js'
|
sourcemaps: './build/static/js'
|
||||||
url_prefix: '/static/js'
|
url_prefix: '~/static/js'
|
||||||
|
|||||||
27
.github/workflows/revert.yaml
vendored
@@ -1,27 +0,0 @@
|
|||||||
name: Revert
|
|
||||||
on:
|
|
||||||
# manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: ./.github/actions/setup
|
|
||||||
- run: yarn prepare
|
|
||||||
- run: yarn build
|
|
||||||
|
|
||||||
- name: Setup node@16 (required by Cloudflare Pages)
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
|
|
||||||
- name: Update Cloudflare Pages deployment
|
|
||||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
|
||||||
with:
|
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
|
||||||
directory: build
|
|
||||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
5
.github/workflows/test.yml
vendored
@@ -30,6 +30,11 @@ jobs:
|
|||||||
- uses: ./.github/actions/setup
|
- uses: ./.github/actions/setup
|
||||||
- run: yarn prepare
|
- run: yarn prepare
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
|
- uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
fail_ci_if_error: false
|
||||||
|
verbose: true
|
||||||
|
|
||||||
cypress-build:
|
cypress-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -9,7 +9,6 @@
|
|||||||
/src/locales/**/pseudo.po
|
/src/locales/**/pseudo.po
|
||||||
|
|
||||||
# generated graphql types
|
# generated graphql types
|
||||||
__generated__/
|
|
||||||
schema.graphql
|
schema.graphql
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/src/schema/schema.graphql
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
7
.vscode/settings.json
vendored
@@ -5,15 +5,12 @@
|
|||||||
"editor.formatOnSaveMode": "file",
|
"editor.formatOnSaveMode": "file",
|
||||||
"editor.tabCompletion": "on",
|
"editor.tabCompletion": "on",
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": false,
|
||||||
"editor.inlineSuggest.enabled": true,
|
"editor.inlineSuggest.enabled": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": true
|
||||||
},
|
},
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"eslint.enable": true,
|
"eslint.enable": true,
|
||||||
"eslint.debug": true,
|
"eslint.debug": true
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
README.md
@@ -1,5 +1,7 @@
|
|||||||
# Uniswap Labs Interface
|
# Uniswap Labs Interface
|
||||||
|
|
||||||
|
[](https://codecov.io/gh/Uniswap/interface)
|
||||||
|
|
||||||
[](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
|
[](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
|
||||||
[](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
|
[](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
|
||||||
[](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
|
[](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
|
||||||
@@ -40,10 +42,10 @@ For steps on local deployment, development, and code contribution, please see [C
|
|||||||
|
|
||||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||||
|
|
||||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
|
||||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
- View V2 liquidity: <https://app.uniswap.org/#/pool/v2>
|
||||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
|
||||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
|
||||||
|
|
||||||
## Accessing Uniswap V1
|
## Accessing Uniswap V1
|
||||||
|
|
||||||
|
|||||||
25
apollo-codegen.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
import type { CodegenConfig } from '@graphql-codegen/cli'
|
||||||
|
|
||||||
|
// Generates TS objects from the schemas returned by graphql queries
|
||||||
|
// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project
|
||||||
|
const config: CodegenConfig = {
|
||||||
|
overwrite: true,
|
||||||
|
schema: './src/graphql/data/schema.graphql',
|
||||||
|
documents: ['./src/graphql/data/**', '!./src/graphql/data/__generated__/**', '!**/thegraph/**'],
|
||||||
|
generates: {
|
||||||
|
'src/graphql/data/__generated__/types-and-hooks.ts': {
|
||||||
|
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
|
||||||
|
config: {
|
||||||
|
withHooks: true,
|
||||||
|
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
|
||||||
|
maybeValue: 'T',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used in package.json when generating apollo schemas however the linter stills flags this as unused
|
||||||
|
// eslint-disable-next-line import/no-unused-modules
|
||||||
|
export default config
|
||||||
25
apollo-codegen_thegraph.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
import type { CodegenConfig } from '@graphql-codegen/cli'
|
||||||
|
|
||||||
|
// Generates TS objects from the schemas returned by graphql queries
|
||||||
|
// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project
|
||||||
|
const config: CodegenConfig = {
|
||||||
|
overwrite: true,
|
||||||
|
schema: './src/graphql/thegraph/schema.graphql',
|
||||||
|
documents: ['!./src/graphql/data/**', '!./src/graphql/thegraph/__generated__/**', './src/graphql/thegraph/**'],
|
||||||
|
generates: {
|
||||||
|
'src/graphql/thegraph/__generated__/types-and-hooks.ts': {
|
||||||
|
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
|
||||||
|
config: {
|
||||||
|
withHooks: true,
|
||||||
|
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
|
||||||
|
maybeValue: 'T',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used in package.json when generating apollo schemas however the linter stills flags this as unused
|
||||||
|
// eslint-disable-next-line import/no-unused-modules
|
||||||
|
export default config
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development'
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ module.exports = {
|
|||||||
babel: {
|
babel: {
|
||||||
plugins: ['@vanilla-extract/babel-plugin'],
|
plugins: ['@vanilla-extract/babel-plugin'],
|
||||||
},
|
},
|
||||||
|
jest: {
|
||||||
|
configure(jestConfig) {
|
||||||
|
return Object.assign({}, jestConfig, {
|
||||||
|
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||||
|
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
webpack: {
|
webpack: {
|
||||||
plugins: [
|
plugins: [
|
||||||
new VanillaExtractPlugin(),
|
new VanillaExtractPlugin(),
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
describe('Landing Page', () => {
|
describe('Landing Page', () => {
|
||||||
beforeEach(() => cy.visit('/'))
|
it('shows landing page when no selectedWallet', () => {
|
||||||
it('loads swap page', () => {
|
cy.visit('/', { noWallet: true })
|
||||||
cy.get('#swap-page')
|
cy.get(getTestSelector('landing-page'))
|
||||||
cy.screenshot()
|
cy.screenshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('redirects to swap page when selectedWallet is INJECTED', () => {
|
||||||
|
cy.visit('/', { selectedWallet: 'INJECTED' })
|
||||||
|
cy.get('#swap-page')
|
||||||
|
cy.url().should('include', '/swap')
|
||||||
|
cy.screenshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => {
|
||||||
|
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' })
|
||||||
|
cy.get(getTestSelector('landing-page'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows landing page when the unicorn icon in nav is selected', () => {
|
||||||
|
cy.get(getTestSelector('uniswap-logo')).click()
|
||||||
|
cy.get(getTestSelector('landing-page'))
|
||||||
|
})
|
||||||
|
|
||||||
it('allows navigation to pool', () => {
|
it('allows navigation to pool', () => {
|
||||||
cy.get('#pool-nav-link').click()
|
cy.get('#pool-nav-link').click()
|
||||||
cy.url().should('include', '/pool')
|
cy.url().should('include', '/pool')
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { getTestSelector } from '../utils'
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||||
|
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||||
|
|
||||||
describe('Testing nfts', () => {
|
describe('Testing nfts', () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should load nft leaderboard', () => {
|
it('should load nft leaderboard', () => {
|
||||||
@@ -15,26 +17,21 @@ describe('Testing nfts', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should load pudgy penguin collection page', () => {
|
it('should load pudgy penguin collection page', () => {
|
||||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||||
cy.get(getTestSelector('nft-collection-asset')).should('exist')
|
cy.get(getTestSelector('nft-collection-asset')).should('exist')
|
||||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
||||||
cy.get(getTestSelector('nft-filter')).first().click()
|
cy.get(getTestSelector('nft-filter')).first().click()
|
||||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('exist')
|
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should be able to open bag and open sweep', () => {
|
|
||||||
cy.get(getTestSelector('nft-sweep-button')).first().click()
|
|
||||||
cy.get(getTestSelector('nft-empty-bag')).should('exist')
|
|
||||||
cy.get(getTestSelector('nft-sweep-slider')).should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be able to navigate to activity', () => {
|
it('should be able to navigate to activity', () => {
|
||||||
|
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||||
cy.get(getTestSelector('nft-activity')).first().click()
|
cy.get(getTestSelector('nft-activity')).first().click()
|
||||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should go to the details page', () => {
|
it('should go to the details page', () => {
|
||||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||||
cy.get(getTestSelector('nft-filter')).first().click()
|
cy.get(getTestSelector('nft-filter')).first().click()
|
||||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||||
cy.get(getTestSelector('nft-details-link')).first().click()
|
cy.get(getTestSelector('nft-details-link')).first().click()
|
||||||
@@ -45,20 +42,11 @@ describe('Testing nfts', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should toggle buy now on details page', () => {
|
it('should toggle buy now on details page', () => {
|
||||||
|
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
|
||||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||||
cy.get(getTestSelector('nft-details-description')).click()
|
cy.get(getTestSelector('nft-details-description')).click()
|
||||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||||
cy.get(getTestSelector('nft-details-toggle-bag')).eq(1).click()
|
cy.get(getTestSelector('nft-details-toggle-bag')).eq(1).click()
|
||||||
cy.get(getTestSelector('nft-bag')).should('exist')
|
cy.get(getTestSelector('nft-bag')).should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should go view my nfts', () => {
|
|
||||||
cy.get(getTestSelector('web3-status-connected')).click()
|
|
||||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
|
||||||
cy.get(getTestSelector('nft-explore-nfts-button')).should('exist')
|
|
||||||
cy.get(getTestSelector('nft-no-nfts-selected')).should('exist')
|
|
||||||
cy.get(getTestSelector('nft-bag-close-icon')).click()
|
|
||||||
cy.get(getTestSelector('nft-explore-nfts-button')).click()
|
|
||||||
cy.get(getTestSelector('nft-welcome-modal')).should('exist')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
describe('Pool', () => {
|
describe('Pool', () => {
|
||||||
beforeEach(() => cy.visit('/pool'))
|
beforeEach(() => cy.visit('/pool'))
|
||||||
|
|
||||||
it('add liquidity links to /add/ETH', () => {
|
it('add liquidity links to /add/ETH', () => {
|
||||||
|
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
|
||||||
cy.get('#join-pool-button').click()
|
cy.get('#join-pool-button').click()
|
||||||
cy.url().should('contain', '/add/ETH')
|
cy.url().should('contain', '/add/ETH')
|
||||||
})
|
})
|
||||||
|
|||||||
92
cypress/e2e/token-details.test.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
|
describe('Token details', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Uniswap token should have all information populated', () => {
|
||||||
|
// Uniswap token
|
||||||
|
cy.visit('/tokens/ethereum/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||||
|
|
||||||
|
// Price chart should be filled in
|
||||||
|
cy.get('[data-cy="chart-header"]').should('include.text', '$')
|
||||||
|
cy.get('[data-cy="price-chart"]').should('exist')
|
||||||
|
|
||||||
|
// Stats should have: TVL, 24H Volume, 52W low, 52W high
|
||||||
|
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||||
|
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||||
|
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||||
|
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||||
|
cy.get('[data-cy="52w-low"]').should('include.text', '$')
|
||||||
|
cy.get('[data-cy="52w-high"]').should('include.text', '$')
|
||||||
|
})
|
||||||
|
|
||||||
|
// About section should have description of token
|
||||||
|
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||||
|
cy.contains('UNI is the governance token for Uniswap').should('exist')
|
||||||
|
|
||||||
|
// Links section should link out to Etherscan, More analytics, Website, Twitter
|
||||||
|
cy.get('[data-cy="resources-container"]').within(() => {
|
||||||
|
cy.contains('Etherscan')
|
||||||
|
.should('have.attr', 'href')
|
||||||
|
.and('include', 'etherscan.io/address/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||||
|
cy.contains('More analytics')
|
||||||
|
.should('have.attr', 'href')
|
||||||
|
.and('include', 'info.uniswap.org/#/tokens/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||||
|
cy.contains('Website').should('have.attr', 'href').and('include', 'uniswap.org')
|
||||||
|
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/Uniswap')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Contract address should be displayed
|
||||||
|
cy.contains('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984').should('exist')
|
||||||
|
|
||||||
|
// Swap widget should have this token pre-selected as the “destination” token
|
||||||
|
cy.get(getTestSelector('token-select')).should('include.text', 'UNI')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('token with warning and low trading volume should have all information populated', () => {
|
||||||
|
// Shiba predator token, low trading volume and also has warning modal
|
||||||
|
cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||||
|
|
||||||
|
// Should have missing price chart when price unavailable (expected for this token)
|
||||||
|
if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) {
|
||||||
|
cy.get('[data-cy="missing-chart"]').should('exist')
|
||||||
|
}
|
||||||
|
// Stats should have: TVL, 24H Volume, 52W low, 52W high
|
||||||
|
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||||
|
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||||
|
cy.get('[data-cy="tvl"]').should('exist')
|
||||||
|
cy.get('[data-cy="volume-24h"]').should('exist')
|
||||||
|
cy.get('[data-cy="52w-low"]').should('exist')
|
||||||
|
cy.get('[data-cy="52w-high"]').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
// About section should have description of token
|
||||||
|
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||||
|
cy.contains('QOM is the Shiba Predator').should('exist')
|
||||||
|
|
||||||
|
// Links section should link out to Etherscan, More analytics, Website, Twitter
|
||||||
|
cy.get('[data-cy="resources-container"]').within(() => {
|
||||||
|
cy.contains('Etherscan')
|
||||||
|
.should('have.attr', 'href')
|
||||||
|
.and('include', 'etherscan.io/address/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||||
|
cy.contains('More analytics')
|
||||||
|
.should('have.attr', 'href')
|
||||||
|
.and('include', 'info.uniswap.org/#/tokens/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||||
|
cy.contains('Website').should('have.attr', 'href').and('include', 'qom')
|
||||||
|
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/ShibaPredator1')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Contract address should be displayed
|
||||||
|
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
|
||||||
|
|
||||||
|
// Swap widget should have this token pre-selected as the “destination” token
|
||||||
|
cy.get(getTestSelector('token-select')).should('include.text', 'QOM')
|
||||||
|
|
||||||
|
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
|
||||||
|
cy.get('[data-cy="token-safety-message"]')
|
||||||
|
.should('include.text', 'Warning')
|
||||||
|
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
|
||||||
|
})
|
||||||
|
})
|
||||||
83
cypress/e2e/token-explore-filter.test.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
describe.skip('Token explore filter', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter correctly by uni search term', () => {
|
||||||
|
cy.visit('/tokens')
|
||||||
|
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||||
|
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||||
|
const filteredByUni = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('uni'))
|
||||||
|
cy.wrap(filteredByUni).as('filteredByUni')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||||
|
.clear()
|
||||||
|
.type('uni')
|
||||||
|
.type('{enter}')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||||
|
cy.get('@filteredByUni').then((filteredByUni) => {
|
||||||
|
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||||
|
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||||
|
expect(tokenNames.length).to.equal(filteredByUni.length)
|
||||||
|
tokenNames.forEach((tokenName) => {
|
||||||
|
expect(filteredByUni).to.include(tokenName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter correctly by dao search term', () => {
|
||||||
|
cy.visit('/tokens')
|
||||||
|
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||||
|
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||||
|
const filteredByDao = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('dao'))
|
||||||
|
cy.wrap(filteredByDao).as('filteredByDao')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||||
|
.clear()
|
||||||
|
.type('dao')
|
||||||
|
.type('{enter}')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||||
|
cy.get('@filteredByDao').then((filteredByDao) => {
|
||||||
|
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||||
|
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||||
|
expect(tokenNames.length).to.equal(filteredByDao.length)
|
||||||
|
tokenNames.forEach((tokenName) => {
|
||||||
|
expect(filteredByDao).to.include(tokenName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter correctly by ax search term', () => {
|
||||||
|
cy.visit('/tokens')
|
||||||
|
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||||
|
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||||
|
const filteredByAx = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('ax'))
|
||||||
|
cy.wrap(filteredByAx).as('filteredByAx')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||||
|
.clear()
|
||||||
|
.type('ax')
|
||||||
|
.type('{enter}')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||||
|
cy.get('@filteredByAx').then((filteredByAx) => {
|
||||||
|
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||||
|
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||||
|
expect(tokenNames.length).to.equal(filteredByAx.length)
|
||||||
|
tokenNames.forEach((tokenName) => {
|
||||||
|
expect(filteredByAx).to.include(tokenName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
74
cypress/e2e/token-explore.test.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { getTestSelector, getTestSelectorStartsWith } from '../utils'
|
||||||
|
|
||||||
|
describe('Token explore', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load token leaderboard', () => {
|
||||||
|
cy.visit('/tokens/ethereum')
|
||||||
|
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.eq', 100)
|
||||||
|
// check sorted svg icon is present in volume cell, since tokens are sorted by volume by default
|
||||||
|
cy.get(getTestSelector('header-row')).find(getTestSelector('volume-cell')).find('svg').should('exist')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('volume-cell')).should('include.text', '$')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('price-cell')).should('include.text', '$')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('tvl-cell')).should('include.text', '$')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH'))
|
||||||
|
.find(getTestSelector('percent-change-cell'))
|
||||||
|
.should('include.text', '%')
|
||||||
|
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).click()
|
||||||
|
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).find('svg').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update when time window toggled', () => {
|
||||||
|
cy.visit('/tokens/ethereum')
|
||||||
|
cy.get(getTestSelector('time-selector')).should('contain', '1D')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH'))
|
||||||
|
.find(getTestSelector('volume-cell'))
|
||||||
|
.then(function ($elem) {
|
||||||
|
cy.wrap($elem.text()).as('dailyEthVol')
|
||||||
|
})
|
||||||
|
cy.get(getTestSelector('time-selector')).click()
|
||||||
|
cy.get(getTestSelector('1Y')).click()
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH'))
|
||||||
|
.find(getTestSelector('volume-cell'))
|
||||||
|
.then(function ($elem) {
|
||||||
|
cy.wrap($elem.text()).as('yearlyEthVol')
|
||||||
|
})
|
||||||
|
expect(cy.get('@dailyEthVol')).to.not.equal(cy.get('@yearlyEthVol'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate to token detail page when row clicked', () => {
|
||||||
|
cy.visit('/tokens/ethereum')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH')).click()
|
||||||
|
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||||
|
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||||
|
cy.get(getTestSelector('token-info-container')).should('exist')
|
||||||
|
cy.get(getTestSelector('chart-container')).should('exist')
|
||||||
|
cy.contains('Ethereum is a smart contract platform that enables developers to build tokens').should('exist')
|
||||||
|
cy.contains('Etherscan').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update when global network changed', () => {
|
||||||
|
cy.visit('/tokens/ethereum')
|
||||||
|
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Ethereum')
|
||||||
|
cy.get(getTestSelector('token-table-row-ETH')).should('exist')
|
||||||
|
|
||||||
|
// note: cannot switch global chain via UI because we cannot approve the network switch
|
||||||
|
// in metamask modal using plain cypress. this is a workaround.
|
||||||
|
cy.visit('/tokens/polygon')
|
||||||
|
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Polygon')
|
||||||
|
cy.get(getTestSelector('token-table-row-MATIC')).should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update when token explore table network changed', () => {
|
||||||
|
cy.visit('/tokens/ethereum')
|
||||||
|
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||||
|
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
|
||||||
|
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||||
|
cy.reload()
|
||||||
|
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||||
|
cy.get(getTestSelector('chain-selector')).last().should('contain', 'Ethereum')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { getTestSelector, getTestSelectorStartsWith } from '../utils'
|
|
||||||
|
|
||||||
describe('Testing tokens on uniswap page', () => {
|
|
||||||
before(() => {
|
|
||||||
cy.visit('/')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should load token leaderboard', () => {
|
|
||||||
cy.visit('/tokens/ethereum')
|
|
||||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.gte', 25)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should load go to ethereum token and return to token list page', () => {
|
|
||||||
cy.visit('/tokens/ethereum')
|
|
||||||
cy.get(getTestSelector('token-table-row-Ether')).click()
|
|
||||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
|
||||||
cy.get(getTestSelector('token-details-return-button')).click()
|
|
||||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.gte', 25)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
64
cypress/e2e/universal-search.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { getTestSelector } from '../utils'
|
||||||
|
|
||||||
|
describe('Universal search bar', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('[data-cy="magnifying-icon"]')
|
||||||
|
.parent()
|
||||||
|
.then(($navIcon) => {
|
||||||
|
$navIcon.click()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should yield clickable result for regular token or nft collection search term', () => {
|
||||||
|
// Search for uni token by name.
|
||||||
|
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
||||||
|
cy.get('[data-cy="searchbar-token-row-UNI"]')
|
||||||
|
.should('contain.text', 'Uniswap')
|
||||||
|
.and('contain.text', 'UNI')
|
||||||
|
.and('contain.text', '$')
|
||||||
|
.and('contain.text', '%')
|
||||||
|
cy.get('[data-cy="searchbar-token-row-UNI"]').click()
|
||||||
|
|
||||||
|
cy.get('div').contains('Uniswap').should('exist')
|
||||||
|
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
|
||||||
|
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||||
|
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||||
|
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||||
|
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||||
|
cy.get('[data-cy="52w-low"]').should('include.text', '$')
|
||||||
|
cy.get('[data-cy="52w-high"]').should('include.text', '$')
|
||||||
|
})
|
||||||
|
|
||||||
|
// About section should have description of token.
|
||||||
|
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||||
|
cy.contains('UNI is the governance token for Uniswap').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show recent tokens and popular tokens with empty search term', () => {
|
||||||
|
cy.get('[data-cy="magnifying-icon"]')
|
||||||
|
.parent()
|
||||||
|
.then(($navIcon) => {
|
||||||
|
$navIcon.click()
|
||||||
|
})
|
||||||
|
// Recently searched UNI token should exist.
|
||||||
|
cy.get('[data-cy="search-bar-input"]').last().clear()
|
||||||
|
cy.get('[data-cy="searchbar-dropdown"]')
|
||||||
|
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
|
||||||
|
.find('[data-cy="searchbar-token-row-UNI"]')
|
||||||
|
.should('exist')
|
||||||
|
|
||||||
|
// Most popular 3 tokens should be shown.
|
||||||
|
cy.get('[data-cy="searchbar-dropdown"]')
|
||||||
|
.contains('[data-cy="searchbar-dropdown"]', 'Popular tokens')
|
||||||
|
.find('[data-cy^="searchbar-token-row"]')
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('should show blocked badge when blocked token is searched for', () => {
|
||||||
|
// Search for mTSLA, which is a blocked token.
|
||||||
|
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
||||||
|
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -8,7 +8,8 @@ describe(
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
it('loads swap page', () => {
|
it('loads swap page', () => {
|
||||||
// We *must* wait in order to space out the retry attempts.
|
// TODO: We *must* wait in order to space out the retry attempts. Find a better way to do this.
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
cy.wait(ONE_MINUTE)
|
cy.wait(ONE_MINUTE)
|
||||||
.visit('/', {
|
.visit('/', {
|
||||||
retryOnStatusCodeFailure: true,
|
retryOnStatusCodeFailure: true,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ declare global {
|
|||||||
interface VisitOptions {
|
interface VisitOptions {
|
||||||
serviceWorker?: true
|
serviceWorker?: true
|
||||||
featureFlags?: Array<FeatureFlag>
|
featureFlags?: Array<FeatureFlag>
|
||||||
|
selectedWallet?: string
|
||||||
|
noWallet?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,12 @@ Cypress.Commands.overwrite(
|
|||||||
onBeforeLoad(win) {
|
onBeforeLoad(win) {
|
||||||
options?.onBeforeLoad?.(win)
|
options?.onBeforeLoad?.(win)
|
||||||
win.localStorage.clear()
|
win.localStorage.clear()
|
||||||
win.localStorage.setItem('redux_localstorage_simple_user', '{"selectedWallet":"INJECTED"}')
|
|
||||||
|
const userState = {
|
||||||
|
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined,
|
||||||
|
fiatOnrampDismissed: true,
|
||||||
|
}
|
||||||
|
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState))
|
||||||
|
|
||||||
if (options?.featureFlags) {
|
if (options?.featureFlags) {
|
||||||
const featureFlags = options.featureFlags.reduce(
|
const featureFlags = options.featureFlags.reduce(
|
||||||
@@ -78,8 +85,7 @@ beforeEach(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.on('uncaught:exception', (_err, _runnable) => {
|
Cypress.on('uncaught:exception', () => {
|
||||||
// returning false here prevents Cypress from
|
// returning false here prevents Cypress from failing the test
|
||||||
// failing the test
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Utility to match GraphQL mutation based on the query name
|
// Utility to match GraphQL mutation based on the query name
|
||||||
export const hasQuery = (req: any, queryName: string) => {
|
export const hasQuery = (req: any, queryName: string) => {
|
||||||
const { body } = req
|
const { body } = req
|
||||||
return body.hasOwnProperty('query') && body.query.includes(queryName)
|
return Object.prototype.hasOwnProperty.call(body, 'query') && body.query.includes(queryName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias query if queryName matches
|
// Alias query if queryName matches
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable */
|
/* eslint-env node */
|
||||||
|
|
||||||
require('dotenv').config({ path: '.env.production' })
|
require('dotenv').config({ path: '.env.production' })
|
||||||
|
|
||||||
const { exec } = require('child_process')
|
const { exec } = require('child_process')
|
||||||
const dataConfig = require('./relay.config')
|
const dataConfig = require('./graphql.config')
|
||||||
const thegraphConfig = require('./relay_thegraph.config')
|
const thegraphConfig = require('./graphql_thegraph.config')
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
function fetchSchema(url, outputFile) {
|
function fetchSchema(url, outputFile) {
|
||||||
exec(
|
exec(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
src: './src',
|
src: './src',
|
||||||
language: 'typescript',
|
language: 'typescript',
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
/* eslint-env node */
|
||||||
const defaultConfig = require('./relay.config')
|
|
||||||
|
const defaultConfig = require('./graphql.config')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
src: defaultConfig.src,
|
src: defaultConfig.src,
|
||||||
50
package.json
@@ -8,10 +8,10 @@
|
|||||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
||||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||||
"relay": "relay-compiler relay.config.js",
|
|
||||||
"relay-thegraph": "relay-compiler relay_thegraph.config.js",
|
|
||||||
"graphql:fetch": "node fetch-schema.js",
|
"graphql:fetch": "node fetch-schema.js",
|
||||||
"graphql:generate": "yarn relay && yarn relay-thegraph",
|
"graphql:generate:data": "graphql-codegen --config apollo-codegen.ts",
|
||||||
|
"graphql:generate:thegraph": "graphql-codegen --config apollo-codegen_thegraph.ts",
|
||||||
|
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
|
||||||
"prei18n:extract": "node prei18n-extract.js",
|
"prei18n:extract": "node prei18n-extract.js",
|
||||||
"i18n:extract": "lingui extract --locale en-US",
|
"i18n:extract": "lingui extract --locale en-US",
|
||||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||||
@@ -90,39 +90,36 @@
|
|||||||
"@types/ua-parser-js": "^0.7.35",
|
"@types/ua-parser-js": "^0.7.35",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@types/wcag-contrast": "^3.0.0",
|
"@types/wcag-contrast": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4",
|
"@uniswap/eslint-config": "^1.1.1",
|
||||||
"@typescript-eslint/parser": "^4",
|
|
||||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||||
"babel-plugin-relay": "^14.1.0",
|
|
||||||
"cypress": "^10.3.1",
|
"cypress": "^10.3.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"eslint": "^7.11.0",
|
"eslint": "^7.11.0",
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"eslint-import-resolver-typescript": "^3.5.2",
|
|
||||||
"eslint-plugin-better-styled-components": "^1.1.2",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"eslint-plugin-prettier": "^3.1.3",
|
|
||||||
"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",
|
"jest-styled-components": "^7.0.8",
|
||||||
"ms.macro": "^2.0.0",
|
"ms.macro": "^2.0.0",
|
||||||
"patch-package": "^6.4.7",
|
"patch-package": "^6.4.7",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^4.0.3",
|
||||||
"relay-compiler": "^14.1.0",
|
|
||||||
"serve": "^11.3.2",
|
"serve": "^11.3.2",
|
||||||
|
"ts-transform-graphql-tag": "^0.2.1",
|
||||||
"typechain": "^5.0.0",
|
"typechain": "^5.0.0",
|
||||||
"typescript": "^4.4.3",
|
"typescript": "^4.4.3",
|
||||||
"yarn-deduplicate": "^6.0.0"
|
"yarn-deduplicate": "^6.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apollo/client": "^3.7.2",
|
||||||
"@coinbase/wallet-sdk": "^3.3.0",
|
"@coinbase/wallet-sdk": "^3.3.0",
|
||||||
"@fontsource/ibm-plex-mono": "^4.5.1",
|
"@fontsource/ibm-plex-mono": "^4.5.1",
|
||||||
"@fontsource/inter": "^4.5.1",
|
"@fontsource/inter": "^4.5.1",
|
||||||
|
"@graphql-codegen/cli": "^2.15.0",
|
||||||
|
"@graphql-codegen/client-preset": "^1.2.1",
|
||||||
|
"@graphql-codegen/typescript": "^2.8.3",
|
||||||
|
"@graphql-codegen/typescript-operations": "^2.5.8",
|
||||||
|
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||||
|
"@graphql-codegen/typescript-resolvers": "^2.7.8",
|
||||||
"@lingui/core": "^3.14.0",
|
"@lingui/core": "^3.14.0",
|
||||||
"@lingui/macro": "^3.14.0",
|
"@lingui/macro": "^3.14.0",
|
||||||
"@lingui/react": "^3.14.0",
|
"@lingui/react": "^3.14.0",
|
||||||
@@ -134,12 +131,11 @@
|
|||||||
"@reach/portal": "^0.10.3",
|
"@reach/portal": "^0.10.3",
|
||||||
"@react-hook/window-scroll": "^1.3.0",
|
"@react-hook/window-scroll": "^1.3.0",
|
||||||
"@reduxjs/toolkit": "^1.6.1",
|
"@reduxjs/toolkit": "^1.6.1",
|
||||||
"@sentry/react": "7.20.1",
|
"@sentry/react": "^7.29.0",
|
||||||
"@types/react-relay": "^13.0.2",
|
|
||||||
"@types/react-window-infinite-loader": "^1.0.6",
|
"@types/react-window-infinite-loader": "^1.0.6",
|
||||||
"@uniswap/analytics": "1.2.0",
|
"@uniswap/analytics": "1.2.0",
|
||||||
"@uniswap/analytics-events": "1.3.1",
|
"@uniswap/analytics-events": "^2.1.0",
|
||||||
"@uniswap/conedison": "^1.1.0",
|
"@uniswap/conedison": "^1.3.0",
|
||||||
"@uniswap/governance": "^1.0.2",
|
"@uniswap/governance": "^1.0.2",
|
||||||
"@uniswap/liquidity-staker": "^1.0.2",
|
"@uniswap/liquidity-staker": "^1.0.2",
|
||||||
"@uniswap/merkle-distributor": "1.0.1",
|
"@uniswap/merkle-distributor": "1.0.1",
|
||||||
@@ -149,14 +145,14 @@
|
|||||||
"@uniswap/sdk-core": "^3.0.1",
|
"@uniswap/sdk-core": "^3.0.1",
|
||||||
"@uniswap/smart-order-router": "^2.10.0",
|
"@uniswap/smart-order-router": "^2.10.0",
|
||||||
"@uniswap/token-lists": "^1.0.0-beta.30",
|
"@uniswap/token-lists": "^1.0.0-beta.30",
|
||||||
"@uniswap/universal-router-sdk": "1.3.0",
|
"@uniswap/universal-router-sdk": "1.3.4",
|
||||||
"@uniswap/v2-core": "1.0.0",
|
"@uniswap/v2-core": "1.0.0",
|
||||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||||
"@uniswap/v2-sdk": "^3.0.1",
|
"@uniswap/v2-sdk": "^3.0.1",
|
||||||
"@uniswap/v3-core": "1.0.0",
|
"@uniswap/v3-core": "1.0.0",
|
||||||
"@uniswap/v3-periphery": "^1.1.1",
|
"@uniswap/v3-periphery": "^1.1.1",
|
||||||
"@uniswap/v3-sdk": "^3.9.0",
|
"@uniswap/v3-sdk": "^3.9.0",
|
||||||
"@uniswap/widgets": "2.22.11",
|
"@uniswap/widgets": "^2.27.0",
|
||||||
"@vanilla-extract/css": "^1.7.2",
|
"@vanilla-extract/css": "^1.7.2",
|
||||||
"@vanilla-extract/css-utils": "^0.1.2",
|
"@vanilla-extract/css-utils": "^0.1.2",
|
||||||
"@vanilla-extract/dynamic": "^2.0.2",
|
"@vanilla-extract/dynamic": "^2.0.2",
|
||||||
@@ -169,16 +165,16 @@
|
|||||||
"@visx/responsive": "^2.10.0",
|
"@visx/responsive": "^2.10.0",
|
||||||
"@visx/shape": "^2.11.1",
|
"@visx/shape": "^2.11.1",
|
||||||
"@walletconnect/ethereum-provider": "^1.8.0",
|
"@walletconnect/ethereum-provider": "^1.8.0",
|
||||||
"@web3-react/coinbase-wallet": "8.0.34-beta.0",
|
"@web3-react/coinbase-wallet": "8.0.35-beta.0",
|
||||||
"@web3-react/core": "8.0.35-beta.0",
|
"@web3-react/core": "8.0.35-beta.0",
|
||||||
"@web3-react/eip1193": "8.0.26-beta.0",
|
"@web3-react/eip1193": "8.0.27-beta.0",
|
||||||
"@web3-react/empty": "8.0.20-beta.0",
|
"@web3-react/empty": "8.0.20-beta.0",
|
||||||
"@web3-react/gnosis-safe": "8.0.7-beta.0",
|
"@web3-react/gnosis-safe": "8.0.7-beta.0",
|
||||||
"@web3-react/metamask": "8.0.28-beta.0",
|
"@web3-react/metamask": "8.0.30-beta.0",
|
||||||
"@web3-react/network": "8.0.27-beta.0",
|
"@web3-react/network": "8.0.27-beta.0",
|
||||||
"@web3-react/types": "8.0.20-beta.0",
|
"@web3-react/types": "8.0.20-beta.0",
|
||||||
"@web3-react/url": "8.0.25-beta.0",
|
"@web3-react/url": "8.0.25-beta.0",
|
||||||
"@web3-react/walletconnect": "8.0.36-beta.0",
|
"@web3-react/walletconnect": "8.0.37-beta.0",
|
||||||
"array.prototype.flat": "^1.2.4",
|
"array.prototype.flat": "^1.2.4",
|
||||||
"array.prototype.flatmap": "^1.2.4",
|
"array.prototype.flatmap": "^1.2.4",
|
||||||
"cids": "^1.0.0",
|
"cids": "^1.0.0",
|
||||||
@@ -215,8 +211,6 @@
|
|||||||
"react-popper": "^2.2.3",
|
"react-popper": "^2.2.3",
|
||||||
"react-query": "^3.39.1",
|
"react-query": "^3.39.1",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-relay": "^14.1.0",
|
|
||||||
"react-relay-network-modern": "^6.2.1",
|
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-spring": "^9.5.5",
|
"react-spring": "^9.5.5",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
/* eslint-env node */
|
||||||
|
|
||||||
const { exec } = require('child_process')
|
const { exec } = require('child_process')
|
||||||
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
|
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,8 @@
|
|||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||||
#background-radial-gradient {
|
#background-radial-gradient {
|
||||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -97,13 +97,13 @@
|
|||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html {
|
html {
|
||||||
background-color: #212429;
|
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
html {
|
html {
|
||||||
background-color: #f7f8fa;
|
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
41
src/abis/permit2.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "allowance",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint160",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint160"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint48",
|
||||||
|
"name": "expiration",
|
||||||
|
"type": "uint48"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint48",
|
||||||
|
"name": "nonce",
|
||||||
|
"type": "uint48"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]
|
||||||
21
src/assets/svg/fiat_mask.svg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
207
src/components/About/AboutFooter.tsx
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import { TraceEvent } from '@uniswap/analytics'
|
||||||
|
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||||
|
import { useIsDarkMode } from 'state/user/hooks'
|
||||||
|
import styled from 'styled-components/macro'
|
||||||
|
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
|
||||||
|
|
||||||
|
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
|
||||||
|
import darkUnicornImgSrc from './images/unicornEmbossDark.png'
|
||||||
|
import lightUnicornImgSrc from './images/unicornEmbossLight.png'
|
||||||
|
|
||||||
|
const Footer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 48px;
|
||||||
|
max-width: 1440px;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LogoSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LogoSectionLeft = styled(LogoSection)`
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LogoSectionBottom = styled(LogoSection)`
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledLogo = styled.img`
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SocialLinks = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 20px 0 0 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SocialLink = styled.a`
|
||||||
|
display: flex;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FooterLinks = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LinkGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 200px;
|
||||||
|
margin: 20px 0 0 0;
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const LinkGroupTitle = styled.span`
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExternalTextLink = styled(ExternalLink)`
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: ${({ theme }) => theme.textSecondary};
|
||||||
|
`
|
||||||
|
|
||||||
|
const TextLink = styled(StyledRouterLink)`
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: ${({ theme }) => theme.textSecondary};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Copyright = styled.span`
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 1rem 0 0 0;
|
||||||
|
color: ${({ theme }) => theme.textTertiary};
|
||||||
|
`
|
||||||
|
|
||||||
|
const LogoSectionContent = () => {
|
||||||
|
const isDarkMode = useIsDarkMode()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledLogo src={isDarkMode ? darkUnicornImgSrc : lightUnicornImgSrc} alt="Uniswap Logo" />
|
||||||
|
<SocialLinks>
|
||||||
|
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
|
||||||
|
<DiscordIcon size={32} />
|
||||||
|
</SocialLink>
|
||||||
|
<TraceEvent
|
||||||
|
events={[BrowserEvent.onClick]}
|
||||||
|
name={SharedEventName.ELEMENT_CLICKED}
|
||||||
|
element={InterfaceElementName.TWITTER_LINK}
|
||||||
|
>
|
||||||
|
<SocialLink href="https://twitter.com/uniswap" target="_blank" rel="noopener noreferrer">
|
||||||
|
<TwitterIcon size={32} />
|
||||||
|
</SocialLink>
|
||||||
|
</TraceEvent>
|
||||||
|
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
|
||||||
|
<GithubIcon size={32} />
|
||||||
|
</SocialLink>
|
||||||
|
</SocialLinks>
|
||||||
|
<Copyright>© {new Date().getFullYear()} Uniswap Labs</Copyright>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AboutFooter = () => {
|
||||||
|
return (
|
||||||
|
<Footer>
|
||||||
|
<LogoSectionLeft>
|
||||||
|
<LogoSectionContent />
|
||||||
|
</LogoSectionLeft>
|
||||||
|
|
||||||
|
<FooterLinks>
|
||||||
|
<LinkGroup>
|
||||||
|
<LinkGroupTitle>App</LinkGroupTitle>
|
||||||
|
<TextLink to="/swap">Swap</TextLink>
|
||||||
|
<TextLink to="/tokens">Tokens</TextLink>
|
||||||
|
<TextLink to="/nfts">NFTs</TextLink>
|
||||||
|
<TextLink to="/pool">Pools</TextLink>
|
||||||
|
</LinkGroup>
|
||||||
|
<LinkGroup>
|
||||||
|
<LinkGroupTitle>Protocol</LinkGroupTitle>
|
||||||
|
<ExternalTextLink href="https://uniswap.org/community">Community</ExternalTextLink>
|
||||||
|
<ExternalTextLink href="https://uniswap.org/governance">Governance</ExternalTextLink>
|
||||||
|
<ExternalTextLink href="https://uniswap.org/developers">Developers</ExternalTextLink>
|
||||||
|
</LinkGroup>
|
||||||
|
<LinkGroup>
|
||||||
|
<LinkGroupTitle>Company</LinkGroupTitle>
|
||||||
|
<TraceEvent
|
||||||
|
events={[BrowserEvent.onClick]}
|
||||||
|
name={SharedEventName.ELEMENT_CLICKED}
|
||||||
|
element={InterfaceElementName.CAREERS_LINK}
|
||||||
|
>
|
||||||
|
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs">Careers</ExternalTextLink>
|
||||||
|
</TraceEvent>
|
||||||
|
<TraceEvent
|
||||||
|
events={[BrowserEvent.onClick]}
|
||||||
|
name={SharedEventName.ELEMENT_CLICKED}
|
||||||
|
element={InterfaceElementName.BLOG_LINK}
|
||||||
|
>
|
||||||
|
<ExternalTextLink href="https://uniswap.org/blog">Blog</ExternalTextLink>
|
||||||
|
</TraceEvent>
|
||||||
|
</LinkGroup>
|
||||||
|
<LinkGroup>
|
||||||
|
<LinkGroupTitle>Get Help</LinkGroupTitle>
|
||||||
|
<TraceEvent
|
||||||
|
events={[BrowserEvent.onClick]}
|
||||||
|
name={SharedEventName.ELEMENT_CLICKED}
|
||||||
|
element={InterfaceElementName.SUPPORT_LINK}
|
||||||
|
>
|
||||||
|
<ExternalTextLink
|
||||||
|
href="https://support.uniswap.org/hc/en-us/requests/new"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Contact Us
|
||||||
|
</ExternalTextLink>
|
||||||
|
</TraceEvent>
|
||||||
|
<TraceEvent
|
||||||
|
events={[BrowserEvent.onClick]}
|
||||||
|
name={SharedEventName.ELEMENT_CLICKED}
|
||||||
|
element={InterfaceElementName.SUPPORT_LINK}
|
||||||
|
>
|
||||||
|
<ExternalTextLink href="https://support.uniswap.org/hc/en-us">Help Center</ExternalTextLink>
|
||||||
|
</TraceEvent>
|
||||||
|
</LinkGroup>
|
||||||
|
</FooterLinks>
|
||||||
|
|
||||||
|
<LogoSectionBottom>
|
||||||
|
<LogoSectionContent />
|
||||||
|
</LogoSectionBottom>
|
||||||
|
</Footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
150
src/components/About/Card.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { TraceEvent } from '@uniswap/analytics'
|
||||||
|
import { BrowserEvent, SharedEventName } from '@uniswap/analytics-events'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { useIsDarkMode } from 'state/user/hooks'
|
||||||
|
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||||
|
import { BREAKPOINTS } from 'theme'
|
||||||
|
|
||||||
|
export enum CardType {
|
||||||
|
Primary = 'Primary',
|
||||||
|
Secondary = 'Secondary',
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string; type: CardType }>`
|
||||||
|
display: flex;
|
||||||
|
background: ${({ isDarkMode, backgroundImgSrc, type, theme }) =>
|
||||||
|
isDarkMode
|
||||||
|
? `${type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
|
||||||
|
backgroundImgSrc ? ` url(${backgroundImgSrc})` : ''
|
||||||
|
}`
|
||||||
|
: `${type === CardType.Primary ? 'white' : theme.backgroundModule} url(${backgroundImgSrc})`};
|
||||||
|
background-size: auto 100%;
|
||||||
|
background-position: right;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-origin: border-box;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-decoration: none;
|
||||||
|
color: ${({ theme }) => theme.textPrimary};
|
||||||
|
padding: 24px;
|
||||||
|
height: 212px;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid ${({ theme, type }) => (type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
|
||||||
|
box-shadow: 0px 10px 24px 0px rgba(51, 53, 72, 0.04);
|
||||||
|
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
|
||||||
|
}
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||||
|
height: ${({ backgroundImgSrc }) => (backgroundImgSrc ? 360 : 260)}px;
|
||||||
|
}
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TitleRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CardTitle = styled.div`
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getCardDescriptionColor = (type: CardType, theme: DefaultTheme) => {
|
||||||
|
switch (type) {
|
||||||
|
case CardType.Secondary:
|
||||||
|
return theme.textSecondary
|
||||||
|
default:
|
||||||
|
return theme.textPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardDescription = styled.div<{ type: CardType }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: ${({ theme, type }) => getCardDescriptionColor(type, theme)};
|
||||||
|
padding: 0 40px 0 0;
|
||||||
|
max-width: 480px;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const CardCTA = styled(CardDescription)`
|
||||||
|
color: ${({ theme }) => theme.accentAction};
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 24px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Card = ({
|
||||||
|
type = CardType.Primary,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
cta,
|
||||||
|
to,
|
||||||
|
external,
|
||||||
|
backgroundImgSrc,
|
||||||
|
icon,
|
||||||
|
elementName,
|
||||||
|
}: {
|
||||||
|
type?: CardType
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
cta?: string
|
||||||
|
to: string
|
||||||
|
external?: boolean
|
||||||
|
backgroundImgSrc?: string
|
||||||
|
icon?: React.ReactNode
|
||||||
|
elementName?: string
|
||||||
|
}) => {
|
||||||
|
const isDarkMode = useIsDarkMode()
|
||||||
|
return (
|
||||||
|
<TraceEvent events={[BrowserEvent.onClick]} name={SharedEventName.ELEMENT_CLICKED} element={elementName}>
|
||||||
|
<StyledCard
|
||||||
|
type={type}
|
||||||
|
as={external ? 'a' : Link}
|
||||||
|
to={external ? undefined : to}
|
||||||
|
href={external ? to : undefined}
|
||||||
|
target={external ? '_blank' : undefined}
|
||||||
|
rel={external ? 'noopenener noreferrer' : undefined}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
backgroundImgSrc={backgroundImgSrc}
|
||||||
|
>
|
||||||
|
<TitleRow>
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
{icon}
|
||||||
|
</TitleRow>
|
||||||
|
<CardDescription type={type}>
|
||||||
|
{description}
|
||||||
|
<CardCTA type={type}>{cta}</CardCTA>
|
||||||
|
</CardDescription>
|
||||||
|
</StyledCard>
|
||||||
|
</TraceEvent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card
|
||||||
26
src/components/About/Icons.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import styled from 'styled-components/macro'
|
||||||
|
|
||||||
|
import { ReactComponent as DiscordI } from './images/discord.svg'
|
||||||
|
import { ReactComponent as GithubI } from './images/github.svg'
|
||||||
|
import { ReactComponent as TwitterI } from './images/twitter-safe.svg'
|
||||||
|
|
||||||
|
export const DiscordIcon = styled(DiscordI)<{ size?: number; fill?: string }>`
|
||||||
|
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||||
|
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||||
|
fill: ${({ fill }) => fill ?? '#98A1C0'};
|
||||||
|
opacity: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const TwitterIcon = styled(TwitterI)<{ size?: number; fill?: string }>`
|
||||||
|
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||||
|
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||||
|
fill: ${({ fill }) => fill ?? '#98A1C0'};
|
||||||
|
opacity: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GithubIcon = styled(GithubI)<{ size?: number; fill?: string }>`
|
||||||
|
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||||
|
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||||
|
fill: ${({ fill }) => fill ?? '#98A1C0'};
|
||||||
|
opacity: 1;
|
||||||
|
`
|
||||||
106
src/components/About/ProtocolBanner.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { ButtonEmpty } from 'components/Button'
|
||||||
|
import { useIsDarkMode } from 'state/user/hooks'
|
||||||
|
import styled from 'styled-components/macro'
|
||||||
|
import { BREAKPOINTS } from 'theme'
|
||||||
|
|
||||||
|
import meshSrc from './images/Mesh.png'
|
||||||
|
|
||||||
|
const DARK_MODE_GRADIENT = 'radial-gradient(101.8% 4091.31% at 0% 0%, #4673FA 0%, #9646FA 100%)'
|
||||||
|
|
||||||
|
const Banner = styled.div<{ isDarkMode: boolean }>`
|
||||||
|
height: 340px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 32px;
|
||||||
|
max-width: 1440px;
|
||||||
|
margin: 80px 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 32px 48px;
|
||||||
|
|
||||||
|
box-shadow: 0px 10px 24px rgba(51, 53, 72, 0.04);
|
||||||
|
|
||||||
|
background: ${({ isDarkMode }) =>
|
||||||
|
isDarkMode
|
||||||
|
? `url(${meshSrc}), ${DARK_MODE_GRADIENT}`
|
||||||
|
: `url(${meshSrc}), linear-gradient(93.06deg, #FF00C7 2.66%, #FF9FFB 98.99%);`};
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||||
|
height: 140px;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TextContainer = styled.div`
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const HeaderText = styled.div`
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 36px;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DescriptionText = styled.div`
|
||||||
|
margin: 10px 10px 0 0;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const BannerButtonContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const BannerButton = styled(ButtonEmpty)`
|
||||||
|
color: white;
|
||||||
|
border: 1px solid white;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProtocolBanner = () => {
|
||||||
|
const isDarkMode = useIsDarkMode()
|
||||||
|
return (
|
||||||
|
<Banner isDarkMode={isDarkMode}>
|
||||||
|
<TextContainer>
|
||||||
|
<HeaderText>Powered by the Uniswap Protocol</HeaderText>
|
||||||
|
<DescriptionText>
|
||||||
|
The leading decentralized crypto trading protocol, governed by a global community.
|
||||||
|
</DescriptionText>
|
||||||
|
</TextContainer>
|
||||||
|
<BannerButtonContainer>
|
||||||
|
<BannerButton width="200px" as="a" href="https://uniswap.org" rel="noopener noreferrer" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</BannerButton>
|
||||||
|
</BannerButtonContainer>
|
||||||
|
</Banner>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolBanner
|
||||||
71
src/components/About/constants.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||||
|
import { DollarSign, Terminal } from 'react-feather'
|
||||||
|
import styled from 'styled-components/macro'
|
||||||
|
import { lightTheme } from 'theme/colors'
|
||||||
|
|
||||||
|
import darkArrowImgSrc from './images/aboutArrowDark.png'
|
||||||
|
import lightArrowImgSrc from './images/aboutArrowLight.png'
|
||||||
|
import darkDollarImgSrc from './images/aboutDollarDark.png'
|
||||||
|
import darkTerminalImgSrc from './images/aboutTerminalDark.png'
|
||||||
|
import nftCardImgSrc from './images/nftCard.png'
|
||||||
|
import swapCardImgSrc from './images/swapCard.png'
|
||||||
|
|
||||||
|
export const MAIN_CARDS = [
|
||||||
|
{
|
||||||
|
to: '/swap',
|
||||||
|
title: 'Swap tokens',
|
||||||
|
description: 'Buy, sell, and explore tokens on Ethereum, Polygon, Optimism, and more.',
|
||||||
|
cta: 'Trade Tokens',
|
||||||
|
darkBackgroundImgSrc: swapCardImgSrc,
|
||||||
|
lightBackgroundImgSrc: swapCardImgSrc,
|
||||||
|
elementName: InterfaceElementName.ABOUT_PAGE_SWAP_CARD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/nfts',
|
||||||
|
title: 'Trade NFTs',
|
||||||
|
description: 'Buy and sell NFTs across marketplaces to find more listings at better prices.',
|
||||||
|
cta: 'Explore NFTs',
|
||||||
|
darkBackgroundImgSrc: nftCardImgSrc,
|
||||||
|
lightBackgroundImgSrc: nftCardImgSrc,
|
||||||
|
elementName: InterfaceElementName.ABOUT_PAGE_NFTS_CARD,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const StyledCardLogo = styled.img`
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 48px;
|
||||||
|
max-width: 48px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MORE_CARDS = [
|
||||||
|
{
|
||||||
|
to: 'https://support.uniswap.org/hc/en-us/articles/11306574799117-How-to-use-Moon-Pay-on-the-Uniswap-web-app-',
|
||||||
|
external: true,
|
||||||
|
title: 'Buy crypto',
|
||||||
|
description: 'Buy crypto with your credit card or bank account at the best rates.',
|
||||||
|
lightIcon: <DollarSign color={lightTheme.textTertiary} size={48} />,
|
||||||
|
darkIcon: <StyledCardLogo src={darkDollarImgSrc} alt="Earn" />,
|
||||||
|
cta: 'Buy now',
|
||||||
|
elementName: InterfaceElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/pool',
|
||||||
|
title: 'Earn',
|
||||||
|
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
|
||||||
|
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,
|
||||||
|
darkIcon: <StyledCardLogo src={darkArrowImgSrc} alt="Analytics" />,
|
||||||
|
cta: 'Provide liquidity',
|
||||||
|
elementName: InterfaceElementName.ABOUT_PAGE_EARN_CARD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: 'https://docs.uniswap.org',
|
||||||
|
external: true,
|
||||||
|
title: 'Build dApps',
|
||||||
|
description: 'Build apps and tools on the largest DeFi protocol on Ethereum.',
|
||||||
|
lightIcon: <Terminal color={lightTheme.textTertiary} size={48} />,
|
||||||
|
darkIcon: <StyledCardLogo src={darkTerminalImgSrc} alt="Developers" />,
|
||||||
|
cta: 'Developer docs',
|
||||||
|
elementName: InterfaceElementName.ABOUT_PAGE_DEV_DOCS_CARD,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 379 KiB |
BIN
src/components/About/images/Mesh.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/components/About/images/aboutArrowDark.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/components/About/images/aboutArrowLight.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
src/components/About/images/aboutDollarDark.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/components/About/images/aboutTerminalDark.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
3
src/components/About/images/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 71 55" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/components/About/images/github.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||||
|
After Width: | Height: | Size: 822 B |
BIN
src/components/About/images/nftCard.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
src/components/About/images/swapCard.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
3
src/components/About/images/twitter-safe.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 32 32" role="img" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M31.2746 5.92398C30.7719 6.14694 30.2551 6.33512 29.727 6.4879C30.3522 5.7808 30.8289 4.9488 31.1199 4.03835C31.1851 3.83427 31.1175 3.61089 30.9498 3.47742C30.7822 3.34385 30.5495 3.32785 30.365 3.43716C29.2434 4.10235 28.0334 4.58039 26.7647 4.85993C25.4866 3.6111 23.7508 2.90039 21.9563 2.90039C18.1684 2.90039 15.0867 5.98199 15.0867 9.76975C15.0867 10.0681 15.1056 10.3647 15.143 10.6573C10.4426 10.2446 6.07276 7.9343 3.07198 4.25337C2.96504 4.12217 2.80029 4.05146 2.63162 4.06498C2.46285 4.0782 2.31121 4.17337 2.22595 4.31964C1.61733 5.36398 1.29557 6.5584 1.29557 7.77368C1.29557 9.4289 1.88654 10.9994 2.93046 12.2265C2.61304 12.1166 2.30502 11.9792 2.01103 11.816C1.8532 11.7282 1.66058 11.7295 1.50378 11.8194C1.34687 11.9093 1.2485 12.0747 1.24437 12.2554C1.24365 12.2859 1.24365 12.3163 1.24365 12.3472C1.24365 14.8179 2.5734 17.0423 4.60644 18.2547C4.43178 18.2373 4.25722 18.212 4.0838 18.1788C3.90502 18.1447 3.72117 18.2073 3.6006 18.3437C3.47983 18.4799 3.43988 18.6699 3.49552 18.8433C4.24804 21.1927 6.18548 22.9208 8.52767 23.4477C6.58507 24.6644 4.36355 25.3017 2.03147 25.3017C1.54486 25.3017 1.05547 25.2731 0.5765 25.2165C0.338565 25.1882 0.111055 25.3287 0.0300229 25.5549C-0.0510093 25.7813 0.0348745 26.0337 0.2373 26.1634C3.23322 28.0844 6.69738 29.0997 10.2551 29.0997C17.249 29.0997 21.6242 25.8016 24.063 23.0349C27.104 19.585 28.8481 15.0186 28.8481 10.5067C28.8481 10.3182 28.8452 10.1278 28.8394 9.93812C30.0392 9.03417 31.0722 7.94018 31.9128 6.68279C32.0404 6.49182 32.0266 6.23943 31.8787 6.06364C31.731 5.88774 31.4848 5.83087 31.2746 5.92398Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/components/About/images/unicornEmbossDark.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/components/About/images/unicornEmbossLight.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -14,18 +14,15 @@ import {
|
|||||||
CollectFeesTransactionInfo,
|
CollectFeesTransactionInfo,
|
||||||
CreateV3PoolTransactionInfo,
|
CreateV3PoolTransactionInfo,
|
||||||
DelegateTransactionInfo,
|
DelegateTransactionInfo,
|
||||||
DepositLiquidityStakingTransactionInfo,
|
|
||||||
ExactInputSwapTransactionInfo,
|
ExactInputSwapTransactionInfo,
|
||||||
ExactOutputSwapTransactionInfo,
|
ExactOutputSwapTransactionInfo,
|
||||||
ExecuteTransactionInfo,
|
ExecuteTransactionInfo,
|
||||||
MigrateV2LiquidityToV3TransactionInfo,
|
MigrateV2LiquidityToV3TransactionInfo,
|
||||||
QueueTransactionInfo,
|
QueueTransactionInfo,
|
||||||
RemoveLiquidityV3TransactionInfo,
|
RemoveLiquidityV3TransactionInfo,
|
||||||
SubmitProposalTransactionInfo,
|
|
||||||
TransactionInfo,
|
TransactionInfo,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
VoteTransactionInfo,
|
VoteTransactionInfo,
|
||||||
WithdrawLiquidityStakingTransactionInfo,
|
|
||||||
WrapTransactionInfo,
|
WrapTransactionInfo,
|
||||||
} from '../../state/transactions/types'
|
} from '../../state/transactions/types'
|
||||||
|
|
||||||
@@ -83,7 +80,7 @@ function ClaimSummary({ info: { recipient, uniAmountRaw } }: { info: ClaimTransa
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubmitProposalTransactionSummary(_: { info: SubmitProposalTransactionInfo }) {
|
function SubmitProposalTransactionSummary() {
|
||||||
return <Trans>Submit new proposal</Trans>
|
return <Trans>Submit new proposal</Trans>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,13 +172,13 @@ function WrapSummary({ info: { chainId, currencyAmountRaw, unwrapped } }: { info
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function DepositLiquidityStakingSummary(_: { info: DepositLiquidityStakingTransactionInfo }) {
|
function DepositLiquidityStakingSummary() {
|
||||||
// not worth rendering the tokens since you can should no longer deposit liquidity in the staking contracts
|
// not worth rendering the tokens since you can should no longer deposit liquidity in the staking contracts
|
||||||
// todo: deprecate and delete the code paths that allow this, show user more information
|
// todo: deprecate and delete the code paths that allow this, show user more information
|
||||||
return <Trans>Deposit liquidity</Trans>
|
return <Trans>Deposit liquidity</Trans>
|
||||||
}
|
}
|
||||||
|
|
||||||
function WithdrawLiquidityStakingSummary(_: { info: WithdrawLiquidityStakingTransactionInfo }) {
|
function WithdrawLiquidityStakingSummary() {
|
||||||
return <Trans>Withdraw deposited liquidity</Trans>
|
return <Trans>Withdraw deposited liquidity</Trans>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,10 +316,10 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
|
|||||||
return <ClaimSummary info={info} />
|
return <ClaimSummary info={info} />
|
||||||
|
|
||||||
case TransactionType.DEPOSIT_LIQUIDITY_STAKING:
|
case TransactionType.DEPOSIT_LIQUIDITY_STAKING:
|
||||||
return <DepositLiquidityStakingSummary info={info} />
|
return <DepositLiquidityStakingSummary />
|
||||||
|
|
||||||
case TransactionType.WITHDRAW_LIQUIDITY_STAKING:
|
case TransactionType.WITHDRAW_LIQUIDITY_STAKING:
|
||||||
return <WithdrawLiquidityStakingSummary info={info} />
|
return <WithdrawLiquidityStakingSummary />
|
||||||
|
|
||||||
case TransactionType.SWAP:
|
case TransactionType.SWAP:
|
||||||
return <SwapSummary info={info} />
|
return <SwapSummary info={info} />
|
||||||
@@ -358,6 +355,6 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
|
|||||||
return <ExecuteSummary info={info} />
|
return <ExecuteSummary info={info} />
|
||||||
|
|
||||||
case TransactionType.SUBMIT_PROPOSAL:
|
case TransactionType.SUBMIT_PROPOSAL:
|
||||||
return <SubmitProposalTransactionSummary info={info} />
|
return <SubmitProposalTransactionSummary />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMask } from 'connection/utils'
|
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMaskWallet } from 'connection/utils'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||||
import { useAppDispatch } from 'state/hooks'
|
import { useAppDispatch } from 'state/hooks'
|
||||||
@@ -210,14 +210,14 @@ export default function AccountDetails({
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const isMetaMask = getIsMetaMask()
|
const hasMetaMaskExtension = getIsMetaMaskWallet()
|
||||||
const isCoinbaseWallet = getIsCoinbaseWallet()
|
const hasCoinbaseExtension = getIsCoinbaseWallet()
|
||||||
const isInjectedMobileBrowser = (isMetaMask || isCoinbaseWallet) && isMobile
|
const isInjectedMobileBrowser = (hasMetaMaskExtension || hasCoinbaseExtension) && isMobile
|
||||||
|
|
||||||
function formatConnectorName() {
|
function formatConnectorName() {
|
||||||
return (
|
return (
|
||||||
<WalletName>
|
<WalletName>
|
||||||
<Trans>Connected with</Trans> {getConnectionName(connectionType, isMetaMask)}
|
<Trans>Connected with</Trans> {getConnectionName(connectionType, hasMetaMaskExtension)}
|
||||||
</WalletName>
|
</WalletName>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -246,7 +246,7 @@ export default function AccountDetails({
|
|||||||
<WalletAction
|
<WalletAction
|
||||||
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
|
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const walletType = getConnectionName(getConnection(connector).type, getIsMetaMask())
|
const walletType = getConnectionName(getConnection(connector).type)
|
||||||
if (connector.deactivate) {
|
if (connector.deactivate) {
|
||||||
connector.deactivate()
|
connector.deactivate()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -39,26 +39,32 @@ const getCurrency = ({ info, chainId }: { info: TransactionInfo; chainId: number
|
|||||||
switch (info.type) {
|
switch (info.type) {
|
||||||
case TransactionType.ADD_LIQUIDITY_V3_POOL:
|
case TransactionType.ADD_LIQUIDITY_V3_POOL:
|
||||||
case TransactionType.REMOVE_LIQUIDITY_V3:
|
case TransactionType.REMOVE_LIQUIDITY_V3:
|
||||||
case TransactionType.CREATE_V3_POOL:
|
case TransactionType.CREATE_V3_POOL: {
|
||||||
const { baseCurrencyId, quoteCurrencyId } = info
|
const { baseCurrencyId, quoteCurrencyId } = info
|
||||||
return { currencyId0: baseCurrencyId, currencyId1: quoteCurrencyId }
|
return { currencyId0: baseCurrencyId, currencyId1: quoteCurrencyId }
|
||||||
case TransactionType.SWAP:
|
}
|
||||||
|
case TransactionType.SWAP: {
|
||||||
const { inputCurrencyId, outputCurrencyId } = info
|
const { inputCurrencyId, outputCurrencyId } = info
|
||||||
return { currencyId0: inputCurrencyId, currencyId1: outputCurrencyId }
|
return { currencyId0: inputCurrencyId, currencyId1: outputCurrencyId }
|
||||||
case TransactionType.WRAP:
|
}
|
||||||
|
case TransactionType.WRAP: {
|
||||||
const { unwrapped } = info
|
const { unwrapped } = info
|
||||||
const native = info.chainId ? nativeOnChain(info.chainId) : undefined
|
const native = info.chainId ? nativeOnChain(info.chainId) : undefined
|
||||||
const base = 'ETH'
|
const base = 'ETH'
|
||||||
const wrappedCurrency = native?.wrapped.address ?? 'WETH'
|
const wrappedCurrency = native?.wrapped.address ?? 'WETH'
|
||||||
return { currencyId0: unwrapped ? wrappedCurrency : base, currencyId1: unwrapped ? base : wrappedCurrency }
|
return { currencyId0: unwrapped ? wrappedCurrency : base, currencyId1: unwrapped ? base : wrappedCurrency }
|
||||||
case TransactionType.COLLECT_FEES:
|
}
|
||||||
|
case TransactionType.COLLECT_FEES: {
|
||||||
const { currencyId0, currencyId1 } = info
|
const { currencyId0, currencyId1 } = info
|
||||||
return { currencyId0, currencyId1 }
|
return { currencyId0, currencyId1 }
|
||||||
case TransactionType.APPROVAL:
|
}
|
||||||
|
case TransactionType.APPROVAL: {
|
||||||
return { currencyId0: info.tokenAddress, currencyId1: undefined }
|
return { currencyId0: info.tokenAddress, currencyId1: undefined }
|
||||||
case TransactionType.CLAIM:
|
}
|
||||||
|
case TransactionType.CLAIM: {
|
||||||
const uniAddress = chainId ? UNI_ADDRESS[chainId] : undefined
|
const uniAddress = chainId ? UNI_ADDRESS[chainId] : undefined
|
||||||
return { currencyId0: uniAddress, currencyId1: undefined }
|
return { currencyId0: uniAddress, currencyId1: undefined }
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return { currencyId0: undefined, currencyId1: undefined }
|
return { currencyId0: undefined, currencyId1: undefined }
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/components/Button/LoadingButtonSpinner.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { SpinnerSVG } from 'theme'
|
||||||
|
|
||||||
|
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
|
||||||
|
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
opacity="0.1"
|
||||||
|
d="M18.8334 10.0003C18.8334 14.6027 15.1025 18.3337 10.5001 18.3337C5.89771 18.3337 2.16675 14.6027 2.16675 10.0003C2.16675 5.39795 5.89771 1.66699 10.5001 1.66699C15.1025 1.66699 18.8334 5.39795 18.8334 10.0003ZM4.66675 10.0003C4.66675 13.222 7.27842 15.8337 10.5001 15.8337C13.7217 15.8337 16.3334 13.222 16.3334 10.0003C16.3334 6.77867 13.7217 4.16699 10.5001 4.16699C7.27842 4.16699 4.66675 6.77867 4.66675 10.0003Z"
|
||||||
|
/>
|
||||||
|
<path d="M17.5834 10.0003C18.2738 10.0003 18.843 9.4376 18.7398 8.755C18.6392 8.0891 18.458 7.43633 18.1991 6.8113C17.7803 5.80025 17.1665 4.88159 16.3926 4.10777C15.6188 3.33395 14.7002 2.72012 13.6891 2.30133C13.0641 2.04243 12.4113 1.86121 11.7454 1.76057C11.0628 1.6574 10.5001 2.22664 10.5001 2.91699C10.5001 3.60735 11.066 4.15361 11.7405 4.30041C12.0789 4.37406 12.4109 4.47786 12.7324 4.61103C13.4401 4.90418 14.0832 5.33386 14.6249 5.87554C15.1665 6.41721 15.5962 7.06027 15.8894 7.76801C16.0225 8.08949 16.1264 8.42147 16.2 8.75986C16.3468 9.43443 16.8931 10.0003 17.5834 10.0003Z" />
|
||||||
|
</SpinnerSVG>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ButtonLoadingSpinner
|
||||||
@@ -5,16 +5,31 @@ import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
|
|||||||
|
|
||||||
import { RowBetween } from '../Row'
|
import { RowBetween } from '../Row'
|
||||||
|
|
||||||
|
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
|
||||||
|
|
||||||
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
|
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
|
||||||
|
|
||||||
export const BaseButton = styled(RebassButton)<
|
const ButtonOverlay = styled.div`
|
||||||
{
|
background-color: transparent;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
transition: 150ms ease background-color;
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
type BaseButtonProps = {
|
||||||
padding?: string
|
padding?: string
|
||||||
width?: string
|
width?: string
|
||||||
$borderRadius?: string
|
$borderRadius?: string
|
||||||
altDisabledStyle?: boolean
|
altDisabledStyle?: boolean
|
||||||
} & ButtonProps
|
} & ButtonProps
|
||||||
>`
|
|
||||||
|
export const BaseButton = styled(RebassButton)<BaseButtonProps>`
|
||||||
padding: ${({ padding }) => padding ?? '16px'};
|
padding: ${({ padding }) => padding ?? '16px'};
|
||||||
width: ${({ width }) => width ?? '100%'};
|
width: ${({ width }) => width ?? '100%'};
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -82,11 +97,12 @@ export const ButtonPrimary = styled(BaseButton)`
|
|||||||
export const SmallButtonPrimary = styled(ButtonPrimary)`
|
export const SmallButtonPrimary = styled(ButtonPrimary)`
|
||||||
width: auto;
|
width: auto;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 10px 16px;
|
padding: ${({ padding }) => padding ?? '8px 12px'};
|
||||||
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ButtonLight = styled(BaseButton)`
|
const BaseButtonLight = styled(BaseButton)`
|
||||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||||
color: ${({ theme }) => theme.accentAction};
|
color: ${({ theme }) => theme.accentAction};
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -103,6 +119,19 @@ export const ButtonLight = styled(BaseButton)`
|
|||||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
||||||
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
${ButtonOverlay} {
|
||||||
|
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:active {
|
||||||
|
${ButtonOverlay} {
|
||||||
|
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:disabled {
|
:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
:hover {
|
:hover {
|
||||||
@@ -369,18 +398,6 @@ export function ButtonRadioChecked({ active = false, children, ...rest }: { acti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonOverlay = styled.div`
|
|
||||||
background-color: transparent;
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 16px;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
transition: 150ms ease background-color;
|
|
||||||
width: 100%;
|
|
||||||
`
|
|
||||||
export enum ButtonSize {
|
export enum ButtonSize {
|
||||||
small,
|
small,
|
||||||
medium,
|
medium,
|
||||||
@@ -395,7 +412,7 @@ export enum ButtonEmphasis {
|
|||||||
warning,
|
warning,
|
||||||
destructive,
|
destructive,
|
||||||
}
|
}
|
||||||
interface BaseButtonProps {
|
interface BaseThemeButtonProps {
|
||||||
size: ButtonSize
|
size: ButtonSize
|
||||||
emphasis: ButtonEmphasis
|
emphasis: ButtonEmphasis
|
||||||
}
|
}
|
||||||
@@ -474,7 +491,7 @@ function pickThemeButtonTextColor({ theme, emphasis }: { theme: DefaultTheme; em
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseThemeButton = styled.button<BaseButtonProps>`
|
const BaseThemeButton = styled.button<BaseThemeButtonProps>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${pickThemeButtonBackgroundColor};
|
background-color: ${pickThemeButtonBackgroundColor};
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -491,16 +508,13 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
|||||||
padding: ${pickThemeButtonPadding};
|
padding: ${pickThemeButtonPadding};
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: 150ms ease opacity;
|
transition: 150ms ease opacity;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
:active {
|
:active {
|
||||||
${ButtonOverlay} {
|
${ButtonOverlay} {
|
||||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:disabled {
|
|
||||||
cursor: default;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
:focus {
|
:focus {
|
||||||
${ButtonOverlay} {
|
${ButtonOverlay} {
|
||||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||||
@@ -511,9 +525,20 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
|||||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
:disabled:active,
|
||||||
|
:disabled:focus,
|
||||||
|
:disabled:hover {
|
||||||
|
${ButtonOverlay} {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseButtonProps {}
|
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseThemeButtonProps {}
|
||||||
|
|
||||||
export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
||||||
return (
|
return (
|
||||||
@@ -523,3 +548,12 @@ export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
|||||||
</BaseThemeButton>
|
</BaseThemeButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ButtonLight = ({ children, ...rest }: BaseButtonProps) => {
|
||||||
|
return (
|
||||||
|
<BaseButtonLight {...rest}>
|
||||||
|
<ButtonOverlay />
|
||||||
|
{children}
|
||||||
|
</BaseButtonLight>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
|
|||||||
import { curveCardinal, scaleLinear } from 'd3'
|
import { curveCardinal, scaleLinear } from 'd3'
|
||||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||||
import { PricePoint } from 'graphql/data/util'
|
import { PricePoint } from 'graphql/data/util'
|
||||||
import { TimePeriod } from 'graphql/data/util'
|
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import styled, { useTheme } from 'styled-components/macro'
|
import styled, { useTheme } from 'styled-components/macro'
|
||||||
|
|
||||||
@@ -21,18 +20,10 @@ interface SparklineChartProps {
|
|||||||
height: number
|
height: number
|
||||||
tokenData: TopToken
|
tokenData: TopToken
|
||||||
pricePercentChange: number | undefined | null
|
pricePercentChange: number | undefined | null
|
||||||
timePeriod: TimePeriod
|
|
||||||
sparklineMap: SparklineMap
|
sparklineMap: SparklineMap
|
||||||
}
|
}
|
||||||
|
|
||||||
function _SparklineChart({
|
function _SparklineChart({ width, height, tokenData, pricePercentChange, sparklineMap }: SparklineChartProps) {
|
||||||
width,
|
|
||||||
height,
|
|
||||||
tokenData,
|
|
||||||
pricePercentChange,
|
|
||||||
timePeriod,
|
|
||||||
sparklineMap,
|
|
||||||
}: SparklineChartProps) {
|
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
// for sparkline
|
// for sparkline
|
||||||
const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null
|
const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { TraceEvent } from '@uniswap/analytics'
|
import { TraceEvent } from '@uniswap/analytics'
|
||||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||||
import { Pair } from '@uniswap/v2-sdk'
|
import { Pair } from '@uniswap/v2-sdk'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
@@ -332,8 +332,8 @@ export default function SwapCurrencyInputPanel({
|
|||||||
{showMaxButton && selectedCurrencyBalance ? (
|
{showMaxButton && selectedCurrencyBalance ? (
|
||||||
<TraceEvent
|
<TraceEvent
|
||||||
events={[BrowserEvent.onClick]}
|
events={[BrowserEvent.onClick]}
|
||||||
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
name={SwapEventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||||
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
element={InterfaceElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||||
>
|
>
|
||||||
<StyledBalanceMax onClick={onMax}>
|
<StyledBalanceMax onClick={onMax}>
|
||||||
<Trans>Max</Trans>
|
<Trans>Max</Trans>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { TraceEvent } from '@uniswap/analytics'
|
import { TraceEvent } from '@uniswap/analytics'
|
||||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||||
import { Pair } from '@uniswap/v2-sdk'
|
import { Pair } from '@uniswap/v2-sdk'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
@@ -315,8 +315,8 @@ export default function CurrencyInputPanel({
|
|||||||
{showMaxButton && selectedCurrencyBalance ? (
|
{showMaxButton && selectedCurrencyBalance ? (
|
||||||
<TraceEvent
|
<TraceEvent
|
||||||
events={[BrowserEvent.onClick]}
|
events={[BrowserEvent.onClick]}
|
||||||
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
name={SwapEventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||||
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
element={InterfaceElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||||
>
|
>
|
||||||
<StyledBalanceMax onClick={onMax}>
|
<StyledBalanceMax onClick={onMax}>
|
||||||
<Trans>MAX</Trans>
|
<Trans>MAX</Trans>
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ interface DoubleCurrencyLogoProps {
|
|||||||
currency1?: Currency
|
currency1?: Currency
|
||||||
}
|
}
|
||||||
|
|
||||||
const HigherLogo = styled(CurrencyLogo)`
|
const HigherLogoWrapper = styled.div`
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
`
|
`
|
||||||
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
|
const CoveredLogoWapper = styled.div<{ sizeraw: number }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important;
|
left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important;
|
||||||
`
|
`
|
||||||
@@ -33,8 +33,16 @@ export default function DoubleCurrencyLogo({
|
|||||||
}: DoubleCurrencyLogoProps) {
|
}: DoubleCurrencyLogoProps) {
|
||||||
return (
|
return (
|
||||||
<Wrapper sizeraw={size} margin={margin}>
|
<Wrapper sizeraw={size} margin={margin}>
|
||||||
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
|
{currency0 && (
|
||||||
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
|
<HigherLogoWrapper>
|
||||||
|
<CurrencyLogo hideL2Icon currency={currency0} size={size.toString() + 'px'} />
|
||||||
|
</HigherLogoWrapper>
|
||||||
|
)}
|
||||||
|
{currency1 && (
|
||||||
|
<CoveredLogoWapper sizeraw={size}>
|
||||||
|
<CurrencyLogo hideL2Icon currency={currency1} size={size.toString() + 'px'} />
|
||||||
|
</CoveredLogoWapper>
|
||||||
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useIsMobile } from 'nft/hooks'
|
|||||||
import React, { PropsWithChildren, useState } from 'react'
|
import React, { PropsWithChildren, useState } from 'react'
|
||||||
import { Copy } from 'react-feather'
|
import { Copy } from 'react-feather'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
|
import { isSentryEnabled } from 'utils/env'
|
||||||
|
|
||||||
import { CopyToClipboard, ExternalLink, ThemedText } from '../../theme'
|
import { CopyToClipboard, ExternalLink, ThemedText } from '../../theme'
|
||||||
import { Column } from '../Column'
|
import { Column } from '../Column'
|
||||||
@@ -85,6 +86,7 @@ const CodeTitle = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -92,49 +94,87 @@ const Fallback = ({ error, eventId }: { error: Error; eventId: string | null })
|
|||||||
const [isExpanded, setExpanded] = useState(false)
|
const [isExpanded, setExpanded] = useState(false)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const errorId = eventId || 'unknown-error'
|
|
||||||
|
|
||||||
// @todo: ThemedText components should be responsive by default
|
// @todo: ThemedText components should be responsive by default
|
||||||
const [Title, Description] = isMobile
|
const [Title, Description] = isMobile
|
||||||
? [ThemedText.HeadlineSmall, ThemedText.BodySmall]
|
? [ThemedText.HeadlineSmall, ThemedText.BodySmall]
|
||||||
: [ThemedText.HeadlineLarge, ThemedText.BodySecondary]
|
: [ThemedText.HeadlineLarge, ThemedText.BodySecondary]
|
||||||
|
|
||||||
return (
|
const showErrorId = isSentryEnabled() && eventId
|
||||||
<FallbackWrapper>
|
|
||||||
<BodyWrapper>
|
const showMoreButton = (
|
||||||
<Column gap="xl">
|
|
||||||
<Column gap="sm">
|
|
||||||
<Title textAlign="center">
|
|
||||||
<Trans>Something went wrong</Trans>
|
|
||||||
</Title>
|
|
||||||
<Description textAlign="center" color="textSecondary">
|
|
||||||
<Trans>
|
|
||||||
Sorry, an error occured while processing your request. If you request support, be sure to provide your
|
|
||||||
error ID.
|
|
||||||
</Trans>
|
|
||||||
</Description>
|
|
||||||
</Column>
|
|
||||||
<CodeBlockWrapper>
|
|
||||||
<CodeTitle>
|
|
||||||
<ThemedText.SubHeader fontWeight={500}>Error ID: {errorId}</ThemedText.SubHeader>
|
|
||||||
<CopyToClipboard toCopy={errorId}>
|
|
||||||
<CopyIcon />
|
|
||||||
</CopyToClipboard>
|
|
||||||
</CodeTitle>
|
|
||||||
<Separator />
|
|
||||||
{isExpanded && (
|
|
||||||
<>
|
|
||||||
<Code>{error.stack}</Code>
|
|
||||||
<Separator />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<ShowMoreButton onClick={() => setExpanded((s) => !s)}>
|
<ShowMoreButton onClick={() => setExpanded((s) => !s)}>
|
||||||
<ThemedText.Link color="textSecondary">
|
<ThemedText.Link color="textSecondary">
|
||||||
<Trans>{isExpanded ? 'Show less' : 'Show more'}</Trans>
|
<Trans>{isExpanded ? 'Show less' : 'Show more'}</Trans>
|
||||||
</ThemedText.Link>
|
</ThemedText.Link>
|
||||||
<ShowMoreIcon $isExpanded={isExpanded} secondaryWidth="20" secondaryHeight="20" />
|
<ShowMoreIcon $isExpanded={isExpanded} secondaryWidth="20" secondaryHeight="20" />
|
||||||
</ShowMoreButton>
|
</ShowMoreButton>
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorDetails = error.stack || error.message
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FallbackWrapper>
|
||||||
|
<BodyWrapper>
|
||||||
|
<Column gap="xl">
|
||||||
|
{showErrorId ? (
|
||||||
|
<>
|
||||||
|
<Column gap="sm">
|
||||||
|
<Title textAlign="center">
|
||||||
|
<Trans>Something went wrong</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description textAlign="center" color="textSecondary">
|
||||||
|
<Trans>
|
||||||
|
Sorry, an error occured while processing your request. If you request support, be sure to provide
|
||||||
|
your error ID.
|
||||||
|
</Trans>
|
||||||
|
</Description>
|
||||||
|
</Column>
|
||||||
|
<CodeBlockWrapper>
|
||||||
|
<CodeTitle>
|
||||||
|
<ThemedText.SubHeader fontWeight={500}>
|
||||||
|
<Trans>Error ID: {eventId}</Trans>
|
||||||
|
</ThemedText.SubHeader>
|
||||||
|
<CopyToClipboard toCopy={eventId}>
|
||||||
|
<CopyIcon />
|
||||||
|
</CopyToClipboard>
|
||||||
|
</CodeTitle>
|
||||||
|
<Separator />
|
||||||
|
{isExpanded && (
|
||||||
|
<>
|
||||||
|
<Code>{errorDetails}</Code>
|
||||||
|
<Separator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showMoreButton}
|
||||||
</CodeBlockWrapper>
|
</CodeBlockWrapper>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Column gap="sm">
|
||||||
|
<Title textAlign="center">
|
||||||
|
<Trans>Something went wrong</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description textAlign="center" color="textSecondary">
|
||||||
|
<Trans>
|
||||||
|
Sorry, an error occured while processing your request. If you request support, be sure to copy the
|
||||||
|
details of this error.
|
||||||
|
</Trans>
|
||||||
|
</Description>
|
||||||
|
</Column>
|
||||||
|
<CodeBlockWrapper>
|
||||||
|
<CodeTitle>
|
||||||
|
<ThemedText.SubHeader fontWeight={500}>Error details</ThemedText.SubHeader>
|
||||||
|
<CopyToClipboard toCopy={errorDetails}>
|
||||||
|
<CopyIcon />
|
||||||
|
</CopyToClipboard>
|
||||||
|
</CodeTitle>
|
||||||
|
<Separator />
|
||||||
|
<Code>{errorDetails.split('\n').slice(0, isExpanded ? undefined : 4)}</Code>
|
||||||
|
<Separator />
|
||||||
|
{showMoreButton}
|
||||||
|
</CodeBlockWrapper>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<StretchedRow>
|
<StretchedRow>
|
||||||
<SmallButtonPrimary onClick={() => window.location.reload()}>
|
<SmallButtonPrimary onClick={() => window.location.reload()}>
|
||||||
<Trans>Reload the app</Trans>
|
<Trans>Reload the app</Trans>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||||
|
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||||
|
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
|
||||||
|
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||||
|
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||||
|
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||||
@@ -163,7 +168,7 @@ function FeatureFlagGroup({ name, children }: PropsWithChildren<{ name: string }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeatureFlagOption({ variant, featureFlag, value, label }: FeatureFlagProps) {
|
function FeatureFlagOption({ variant, featureFlag, label }: FeatureFlagProps) {
|
||||||
const updateFlag = useUpdateFlag()
|
const updateFlag = useUpdateFlag()
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = useState(0)
|
||||||
const featureFlags = useAtomValue(featureFlagSettings)
|
const featureFlags = useAtomValue(featureFlagSettings)
|
||||||
@@ -208,6 +213,36 @@ export default function FeatureFlagModal() {
|
|||||||
featureFlag={FeatureFlag.permit2}
|
featureFlag={FeatureFlag.permit2}
|
||||||
label="Permit 2 / Universal Router"
|
label="Permit 2 / Universal Router"
|
||||||
/>
|
/>
|
||||||
|
<FeatureFlagOption
|
||||||
|
variant={BaseVariant}
|
||||||
|
value={useFiatOnrampFlag()}
|
||||||
|
featureFlag={FeatureFlag.fiatOnramp}
|
||||||
|
label="Fiat on-ramp"
|
||||||
|
/>
|
||||||
|
<FeatureFlagOption
|
||||||
|
variant={NftListV2Variant}
|
||||||
|
value={useNftListV2Flag()}
|
||||||
|
featureFlag={FeatureFlag.nftListV2}
|
||||||
|
label="NFT Listing Page v2"
|
||||||
|
/>
|
||||||
|
<FeatureFlagOption
|
||||||
|
variant={PayWithAnyTokenVariant}
|
||||||
|
value={usePayWithAnyTokenFlag()}
|
||||||
|
featureFlag={FeatureFlag.payWithAnyToken}
|
||||||
|
label="Pay With Any Token"
|
||||||
|
/>
|
||||||
|
<FeatureFlagOption
|
||||||
|
variant={SwapWidgetVariant}
|
||||||
|
value={useSwapWidgetFlag()}
|
||||||
|
featureFlag={FeatureFlag.swapWidget}
|
||||||
|
label="Swap Widget"
|
||||||
|
/>
|
||||||
|
<FeatureFlagOption
|
||||||
|
variant={GqlRoutingVariant}
|
||||||
|
value={useGqlRoutingFlag()}
|
||||||
|
featureFlag={FeatureFlag.gqlRouting}
|
||||||
|
label="GraphQL NFT Routing"
|
||||||
|
/>
|
||||||
<FeatureFlagGroup name="Debug">
|
<FeatureFlagGroup name="Debug">
|
||||||
<FeatureFlagOption
|
<FeatureFlagOption
|
||||||
variant={TraceJsonRpcVariant}
|
variant={TraceJsonRpcVariant}
|
||||||
|
|||||||
152
src/components/FiatOnrampAnnouncement/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||||
|
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||||
|
import { useWeb3React } from '@web3-react/core'
|
||||||
|
import fiatMaskUrl from 'assets/svg/fiat_mask.svg'
|
||||||
|
import { BaseVariant } from 'featureFlags'
|
||||||
|
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { X } from 'react-feather'
|
||||||
|
import { useToggleWalletDropdown } from 'state/application/hooks'
|
||||||
|
import { useAppSelector } from 'state/hooks'
|
||||||
|
import { useFiatOnrampAck } from 'state/user/hooks'
|
||||||
|
import styled from 'styled-components/macro'
|
||||||
|
import { ThemedText } from 'theme'
|
||||||
|
import { isMobile } from 'utils/userAgent'
|
||||||
|
|
||||||
|
const Arrow = styled.div`
|
||||||
|
top: -4px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
width: 16px;
|
||||||
|
|
||||||
|
::before {
|
||||||
|
background: hsl(315.75, 93%, 83%);
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
content: '';
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const ArrowWrapper = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 90%;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
min-height: 92px;
|
||||||
|
|
||||||
|
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
|
||||||
|
right: 36px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const CloseIcon = styled(X)`
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
`
|
||||||
|
const Wrapper = styled.button`
|
||||||
|
background: radial-gradient(105% 250% at 100% 5%, hsla(318, 95%, 85%) 1%, hsla(331, 80%, 75%, 0.1) 84%),
|
||||||
|
linear-gradient(180deg, hsla(296, 92%, 67%, 0.5) 0%, hsla(313, 96%, 60%, 0.5) 130%);
|
||||||
|
background-color: hsla(297, 93%, 68%, 1);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
text-align: start;
|
||||||
|
max-width: 320px;
|
||||||
|
min-height: 92px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:before {
|
||||||
|
background-image: url(${fiatMaskUrl});
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
right: -154px; // roughly width of fiat mask image
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Header = styled(ThemedText.SubHeader)`
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
const Body = styled(ThemedText.BodySmall)`
|
||||||
|
color: white;
|
||||||
|
margin: 0 12px 12px 12px !important;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ANNOUNCEMENT_RENDERED = 'FiatOnrampAnnouncement-rendered'
|
||||||
|
const ANNOUNCEMENT_DISMISSED = 'FiatOnrampAnnouncement-dismissed'
|
||||||
|
|
||||||
|
const MAX_RENDER_COUNT = 3
|
||||||
|
export function FiatOnrampAnnouncement() {
|
||||||
|
const { account } = useWeb3React()
|
||||||
|
const [acks, acknowledge] = useFiatOnrampAck()
|
||||||
|
const [localClose, setLocalClose] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionStorage.getItem(ANNOUNCEMENT_RENDERED)) {
|
||||||
|
acknowledge({ renderCount: acks?.renderCount + 1 })
|
||||||
|
sessionStorage.setItem(ANNOUNCEMENT_RENDERED, 'true')
|
||||||
|
}
|
||||||
|
}, [acknowledge, acks])
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setLocalClose(true)
|
||||||
|
localStorage.setItem(ANNOUNCEMENT_DISMISSED, 'true')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_BANNER_CLICKED)
|
||||||
|
toggleWalletDropdown()
|
||||||
|
acknowledge({ user: true })
|
||||||
|
}, [acknowledge, toggleWalletDropdown])
|
||||||
|
|
||||||
|
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||||
|
const openModal = useAppSelector((state) => state.application.openModal)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!account ||
|
||||||
|
acks?.user ||
|
||||||
|
fiatOnrampFlag === BaseVariant.Control ||
|
||||||
|
localStorage.getItem(ANNOUNCEMENT_DISMISSED) ||
|
||||||
|
acks?.renderCount >= MAX_RENDER_COUNT ||
|
||||||
|
isMobile ||
|
||||||
|
openModal !== null ||
|
||||||
|
localClose
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ArrowWrapper>
|
||||||
|
<Arrow />
|
||||||
|
<CloseIcon onClick={handleClose} data-testid="FiatOnrampAnnouncement-close" />
|
||||||
|
<Wrapper onClick={handleClick}>
|
||||||
|
<Header>
|
||||||
|
<Trans>Buy crypto</Trans>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
<Trans>Get tokens at the best prices in web3 on Uniswap, powered by Moonpay.</Trans>
|
||||||
|
</Body>
|
||||||
|
</Wrapper>
|
||||||
|
</ArrowWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
143
src/components/FiatOnrampModal/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { useWeb3React } from '@web3-react/core'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
|
||||||
|
import { ApplicationModal } from 'state/application/reducer'
|
||||||
|
import styled, { useTheme } from 'styled-components/macro'
|
||||||
|
import { CustomLightSpinner, ThemedText } from 'theme'
|
||||||
|
|
||||||
|
import Circle from '../../assets/images/blue-loader.svg'
|
||||||
|
import Modal from '../Modal'
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 720px;
|
||||||
|
min-width: 375px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ErrorText = styled(ThemedText.BodyPrimary)`
|
||||||
|
color: ${({ theme }) => theme.accentFailure};
|
||||||
|
margin: auto !important;
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
`
|
||||||
|
const StyledIframe = styled.iframe`
|
||||||
|
background-color: ${({ theme }) => theme.white};
|
||||||
|
border-radius: 12px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: calc(100% - 16px);
|
||||||
|
margin: 8px;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
`
|
||||||
|
const StyledSpinner = styled(CustomLightSpinner)`
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MOONPAY_SUPPORTED_CURRENCY_CODES = [
|
||||||
|
'eth',
|
||||||
|
'eth_arbitrum',
|
||||||
|
'eth_optimism',
|
||||||
|
'eth_polygon',
|
||||||
|
'weth',
|
||||||
|
'wbtc',
|
||||||
|
'matic_polygon',
|
||||||
|
'polygon',
|
||||||
|
'usdc_arbitrum',
|
||||||
|
'usdc_optimism',
|
||||||
|
'usdc_polygon',
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function FiatOnrampModal() {
|
||||||
|
const { account } = useWeb3React()
|
||||||
|
const theme = useTheme()
|
||||||
|
const closeModal = useCloseModal()
|
||||||
|
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
|
||||||
|
|
||||||
|
const [signedIframeUrl, setSignedIframeUrl] = useState<string | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const fetchSignedIframeUrl = useCallback(async () => {
|
||||||
|
if (!account) {
|
||||||
|
setError('Please connect an account before making a purchase.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const signedIframeUrlFetchEndpoint = process.env.REACT_APP_MOONPAY_LINK as string
|
||||||
|
const res = await fetch(signedIframeUrlFetchEndpoint, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
colorCode: theme.accentAction,
|
||||||
|
defaultCurrencyCode: 'eth',
|
||||||
|
redirectUrl: 'https://app.uniswap.org/#/swap',
|
||||||
|
walletAddresses: JSON.stringify(
|
||||||
|
MOONPAY_SUPPORTED_CURRENCY_CODES.reduce(
|
||||||
|
(acc, currencyCode) => ({
|
||||||
|
...acc,
|
||||||
|
[currencyCode]: account,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const { url } = await res.json()
|
||||||
|
setSignedIframeUrl(url)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('there was an error fetching the link', e)
|
||||||
|
setError(e.toString())
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [account, theme.accentAction])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSignedIframeUrl()
|
||||||
|
}, [fetchSignedIframeUrl])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} maxHeight={720}>
|
||||||
|
<Wrapper data-testid="fiat-onramp-modal">
|
||||||
|
{error ? (
|
||||||
|
<>
|
||||||
|
<ThemedText.MediumHeader>
|
||||||
|
<Trans>Moonpay Fiat On-ramp iframe</Trans>
|
||||||
|
</ThemedText.MediumHeader>
|
||||||
|
<ErrorText>
|
||||||
|
<Trans>something went wrong!</Trans>
|
||||||
|
<br />
|
||||||
|
{error}
|
||||||
|
</ErrorText>
|
||||||
|
</>
|
||||||
|
) : loading ? (
|
||||||
|
<StyledSpinner src={Circle} alt="loading spinner" size="90px" />
|
||||||
|
) : (
|
||||||
|
<StyledIframe src={signedIframeUrl ?? ''} frameBorder="0" title="fiat-onramp-iframe" />
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getChainInfo } from 'constants/chainInfo'
|
||||||
import { SupportedChainId } from 'constants/chains'
|
import { SupportedChainId } from 'constants/chains'
|
||||||
import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -29,10 +30,28 @@ export type AssetLogoBaseProps = {
|
|||||||
backupImg?: string | null
|
backupImg?: string | null
|
||||||
size?: string
|
size?: string
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
|
hideL2Icon?: boolean
|
||||||
}
|
}
|
||||||
type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number }
|
type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number }
|
||||||
|
|
||||||
// TODO(cartcrom): add prop to optionally render an L2Icon w/ the logo
|
const LogoContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
`
|
||||||
|
|
||||||
|
const L2NetworkLogo = styled.div<{ networkUrl?: string; parentSize: string }>`
|
||||||
|
--size: ${({ parentSize }) => `calc(${parentSize} / 2)`};
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
background: url(${({ networkUrl }) => networkUrl});
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: ${({ parentSize }) => `calc(${parentSize} / 2) calc(${parentSize} / 2)`};
|
||||||
|
display: ${({ networkUrl }) => !networkUrl && 'none'};
|
||||||
|
`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
|
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
|
||||||
*/
|
*/
|
||||||
@@ -44,25 +63,27 @@ export default function AssetLogo({
|
|||||||
backupImg,
|
backupImg,
|
||||||
size = '24px',
|
size = '24px',
|
||||||
style,
|
style,
|
||||||
...rest
|
hideL2Icon = false,
|
||||||
}: AssetLogoProps) {
|
}: AssetLogoProps) {
|
||||||
const imageProps = {
|
const imageProps = {
|
||||||
alt: `${symbol ?? 'token'} logo`,
|
alt: `${symbol ?? 'token'} logo`,
|
||||||
size,
|
size,
|
||||||
style,
|
|
||||||
...rest,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
|
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
|
||||||
|
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
|
||||||
|
|
||||||
if (src) {
|
|
||||||
return <LogoImage {...imageProps} src={src} onError={nextSrc} />
|
|
||||||
} else {
|
|
||||||
return (
|
return (
|
||||||
|
<LogoContainer style={style}>
|
||||||
|
{src ? (
|
||||||
|
<LogoImage {...imageProps} src={src} onError={nextSrc} />
|
||||||
|
) : (
|
||||||
<MissingImageLogo size={size}>
|
<MissingImageLogo size={size}>
|
||||||
{/* use only first 3 characters of Symbol for design reasons */}
|
{/* use only first 3 characters of Symbol for design reasons */}
|
||||||
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
||||||
</MissingImageLogo>
|
</MissingImageLogo>
|
||||||
|
)}
|
||||||
|
{!hideL2Icon && <L2NetworkLogo networkUrl={L2Icon} parentSize={size} />}
|
||||||
|
</LogoContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function CurrencyLogo(
|
|||||||
address={props.currency?.wrapped.address}
|
address={props.currency?.wrapped.address}
|
||||||
symbol={props.symbol ?? props.currency?.symbol}
|
symbol={props.symbol ?? props.currency?.symbol}
|
||||||
backupImg={(props.currency as TokenInfo)?.logoURI}
|
backupImg={(props.currency as TokenInfo)?.logoURI}
|
||||||
|
hideL2Icon={props.hideL2Icon ?? true}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||||
|
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
|
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||||
import { TokenQueryData } from 'graphql/data/Token'
|
import { TokenQueryData } from 'graphql/data/Token'
|
||||||
import { TopToken } from 'graphql/data/TopTokens'
|
import { TopToken } from 'graphql/data/TopTokens'
|
||||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||||
@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
|
|||||||
|
|
||||||
export default function QueryTokenLogo(
|
export default function QueryTokenLogo(
|
||||||
props: AssetLogoBaseProps & {
|
props: AssetLogoBaseProps & {
|
||||||
token?: TopToken | TokenQueryData
|
token?: TopToken | TokenQueryData | SearchToken
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
|
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AssetLogo
|
<AssetLogo
|
||||||
isNative={props.token?.address === NATIVE_CHAIN_ID}
|
isNative={props.token?.standard === TokenStandard.Native || props.token?.address === NATIVE_CHAIN_ID}
|
||||||
chainId={chainId}
|
chainId={chainId}
|
||||||
address={props.token?.address}
|
address={props.token?.address}
|
||||||
symbol={props.token?.symbol}
|
symbol={props.token?.symbol}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
|
|||||||
|
|
||||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||||
|
|
||||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>`
|
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
|
||||||
&[data-reach-dialog-overlay] {
|
&[data-reach-dialog-overlay] {
|
||||||
z-index: ${Z_INDEX.modalBackdrop};
|
z-index: ${Z_INDEX.modalBackdrop};
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -17,7 +17,10 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
|
|||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
|
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
background-color: ${({ theme }) => theme.backgroundScrim};
|
background-color: ${({ theme }) => theme.backgroundScrim};
|
||||||
@@ -27,7 +30,6 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
|
|||||||
type StyledDialogProps = {
|
type StyledDialogProps = {
|
||||||
$minHeight?: number | false
|
$minHeight?: number | false
|
||||||
$maxHeight?: number
|
$maxHeight?: number
|
||||||
$isBottomSheet?: boolean
|
|
||||||
$scrollOverlay?: boolean
|
$scrollOverlay?: boolean
|
||||||
$hideBorder?: boolean
|
$hideBorder?: boolean
|
||||||
$maxWidth: number
|
$maxWidth: number
|
||||||
@@ -40,14 +42,12 @@ const StyledDialogContent = styled(AnimatedDialogContent)<StyledDialogProps>`
|
|||||||
&[data-reach-dialog-content] {
|
&[data-reach-dialog-content] {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||||
border: ${({ theme, $hideBorder }) => !$hideBorder && `1px solid ${theme.deprecated_bg1}`};
|
border: ${({ theme, $hideBorder }) => !$hideBorder && `1px solid ${theme.backgroundOutline}`};
|
||||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
width: 50vw;
|
width: 50vw;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
align-self: ${({ $isBottomSheet }) => $isBottomSheet && 'flex-end'};
|
|
||||||
max-width: ${({ $maxWidth }) => $maxWidth}px;
|
max-width: ${({ $maxWidth }) => $maxWidth}px;
|
||||||
${({ $maxHeight }) =>
|
${({ $maxHeight }) =>
|
||||||
$maxHeight &&
|
$maxHeight &&
|
||||||
@@ -61,22 +61,17 @@ const StyledDialogContent = styled(AnimatedDialogContent)<StyledDialogProps>`
|
|||||||
`}
|
`}
|
||||||
display: ${({ $scrollOverlay }) => ($scrollOverlay ? 'inline-table' : 'flex')};
|
display: ${({ $scrollOverlay }) => ($scrollOverlay ? 'inline-table' : 'flex')};
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
|
||||||
|
@media screen and (max-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||||
width: 65vw;
|
width: 65vw;
|
||||||
margin: auto;
|
}
|
||||||
`}
|
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
|
||||||
${({ theme, $isBottomSheet }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
margin: 0;
|
||||||
width: 85vw;
|
|
||||||
${
|
|
||||||
$isBottomSheet &&
|
|
||||||
css`
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
`
|
|
||||||
}
|
}
|
||||||
`}
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -89,9 +84,8 @@ interface ModalProps {
|
|||||||
maxWidth?: number
|
maxWidth?: number
|
||||||
initialFocusRef?: React.RefObject<any>
|
initialFocusRef?: React.RefObject<any>
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
scrollOverlay?: boolean
|
$scrollOverlay?: boolean
|
||||||
hideBorder?: boolean
|
hideBorder?: boolean
|
||||||
isBottomSheet?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({
|
export default function Modal({
|
||||||
@@ -103,8 +97,7 @@ export default function Modal({
|
|||||||
initialFocusRef,
|
initialFocusRef,
|
||||||
children,
|
children,
|
||||||
onSwipe = onDismiss,
|
onSwipe = onDismiss,
|
||||||
scrollOverlay,
|
$scrollOverlay,
|
||||||
isBottomSheet = isMobile,
|
|
||||||
hideBorder = false,
|
hideBorder = false,
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const fadeTransition = useTransition(isOpen, {
|
const fadeTransition = useTransition(isOpen, {
|
||||||
@@ -136,7 +129,7 @@ export default function Modal({
|
|||||||
onDismiss={onDismiss}
|
onDismiss={onDismiss}
|
||||||
initialFocusRef={initialFocusRef}
|
initialFocusRef={initialFocusRef}
|
||||||
unstable_lockFocusAcrossFrames={false}
|
unstable_lockFocusAcrossFrames={false}
|
||||||
scrollOverlay={scrollOverlay}
|
$scrollOverlay={$scrollOverlay}
|
||||||
>
|
>
|
||||||
<StyledDialogContent
|
<StyledDialogContent
|
||||||
{...(isMobile
|
{...(isMobile
|
||||||
@@ -148,8 +141,7 @@ export default function Modal({
|
|||||||
aria-label="dialog"
|
aria-label="dialog"
|
||||||
$minHeight={minHeight}
|
$minHeight={minHeight}
|
||||||
$maxHeight={maxHeight}
|
$maxHeight={maxHeight}
|
||||||
$isBottomSheet={isBottomSheet}
|
$scrollOverlay={$scrollOverlay}
|
||||||
$scrollOverlay={scrollOverlay}
|
|
||||||
$hideBorder={hideBorder}
|
$hideBorder={hideBorder}
|
||||||
$maxWidth={maxWidth}
|
$maxWidth={maxWidth}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
|
|||||||
gap="8"
|
gap="8"
|
||||||
className={styles.ChainSelector}
|
className={styles.ChainSelector}
|
||||||
background={isOpen ? 'accentActiveSoft' : 'none'}
|
background={isOpen ? 'accentActiveSoft' : 'none'}
|
||||||
|
data-testid="chain-selector"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
{!isSupported ? (
|
{!isSupported ? (
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default function ChainSelectorRow({
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onClick={() => onSelectChain(targetChain)}>
|
<Container onClick={() => onSelectChain(targetChain)} data-testid={`chain-selector-option-${label.toLowerCase()}`}>
|
||||||
<Logo src={logoUrl} alt={label} />
|
<Logo src={logoUrl} alt={label} />
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
{isPending && <ApproveText>Approve in wallet</ApproveText>}
|
{isPending && <ApproveText>Approve in wallet</ApproveText>}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { body, bodySmall } from 'nft/css/common.css'
|
|||||||
import { themeVars } from 'nft/css/sprinkles.css'
|
import { themeVars } from 'nft/css/sprinkles.css'
|
||||||
import { ReactNode, useReducer, useRef } from 'react'
|
import { ReactNode, useReducer, useRef } from 'react'
|
||||||
import { NavLink, NavLinkProps } from 'react-router-dom'
|
import { NavLink, NavLinkProps } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components/macro'
|
||||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||||
|
|
||||||
import { useToggleModal } from '../../state/application/hooks'
|
import { useToggleModal } from '../../state/application/hooks'
|
||||||
@@ -50,8 +51,13 @@ const PrimaryMenuRow = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
|
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
|
||||||
return <Box className={`${styles.PrimaryText} ${body}`}>{children}</Box>
|
return <StyledBox className={`${styles.PrimaryText} ${body}`}>{children}</StyledBox>
|
||||||
}
|
}
|
||||||
|
|
||||||
PrimaryMenuRow.Text = PrimaryMenuRowText
|
PrimaryMenuRow.Text = PrimaryMenuRowText
|
||||||
@@ -115,7 +121,6 @@ export const MenuDropdown = () => {
|
|||||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||||
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||||
|
|
||||||
@@ -161,6 +166,9 @@ export const MenuDropdown = () => {
|
|||||||
<SecondaryLinkedText href="https://docs.uniswap.org/">
|
<SecondaryLinkedText href="https://docs.uniswap.org/">
|
||||||
<Trans>Documentation</Trans> ↗
|
<Trans>Documentation</Trans> ↗
|
||||||
</SecondaryLinkedText>
|
</SecondaryLinkedText>
|
||||||
|
<SecondaryLinkedText href="https://uniswap.canny.io/feature-requests">
|
||||||
|
<Trans>Feedback</Trans> ↗
|
||||||
|
</SecondaryLinkedText>
|
||||||
<SecondaryLinkedText
|
<SecondaryLinkedText
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toggleOpen()
|
toggleOpen()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const baseNavDropdown = style([
|
|||||||
borderWidth: '1px',
|
borderWidth: '1px',
|
||||||
paddingBottom: '8',
|
paddingBottom: '8',
|
||||||
paddingTop: '8',
|
paddingTop: '8',
|
||||||
zIndex: '2',
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
boxShadow: '0px 4px 12px 0px #00000026',
|
boxShadow: '0px 4px 12px 0px #00000026',
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { Box, BoxProps } from 'nft/components/Box'
|
import { Box, BoxProps } from 'nft/components/Box'
|
||||||
import { useIsMobile } from 'nft/hooks'
|
import { useIsMobile } from 'nft/hooks'
|
||||||
import { ForwardedRef, forwardRef } from 'react'
|
import { ForwardedRef, forwardRef } from 'react'
|
||||||
|
import { Z_INDEX } from 'theme/zIndex'
|
||||||
|
|
||||||
import * as styles from './NavDropdown.css'
|
import * as styles from './NavDropdown.css'
|
||||||
|
|
||||||
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
|
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
return <Box ref={ref} className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown} {...props} />
|
return (
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
style={{ zIndex: Z_INDEX.modal }}
|
||||||
|
className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
NavDropdown.displayName = 'NavDropdown'
|
NavDropdown.displayName = 'NavDropdown'
|
||||||
|
|||||||
102
src/components/NavBar/RecentlySearchedAssets.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { SupportedChainId } from 'constants/chains'
|
||||||
|
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||||
|
import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
|
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||||
|
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||||
|
import { GenieCollection } from 'nft/types'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
||||||
|
|
||||||
|
type RecentlySearchedAsset = {
|
||||||
|
isNft?: boolean
|
||||||
|
address: string
|
||||||
|
chain: Chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary measure used until backend supports addressing by "NATIVE"
|
||||||
|
const NATIVE_QUERY_ADDRESS_INPUT = null as unknown as string
|
||||||
|
function getQueryAddress(chain: Chain) {
|
||||||
|
return getNativeTokenDBAddress(chain) ?? NATIVE_QUERY_ADDRESS_INPUT
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentlySearchedAssetsAtom = atomWithStorage<RecentlySearchedAsset[]>('recentlySearchedAssets', [])
|
||||||
|
|
||||||
|
export function useAddRecentlySearchedAsset() {
|
||||||
|
const [searchHistory, updateSearchHistory] = useAtom(recentlySearchedAssetsAtom)
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(asset: RecentlySearchedAsset) => {
|
||||||
|
// Removes the new asset if it was already in the array
|
||||||
|
const newHistory = searchHistory.filter(
|
||||||
|
(oldAsset) => !(oldAsset.address === asset.address && oldAsset.chain === asset.chain)
|
||||||
|
)
|
||||||
|
newHistory.unshift(asset)
|
||||||
|
updateSearchHistory(newHistory)
|
||||||
|
},
|
||||||
|
[searchHistory, updateSearchHistory]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRecentlySearchedAssets() {
|
||||||
|
const history = useAtomValue(recentlySearchedAssetsAtom)
|
||||||
|
const shortenedHistory = useMemo(() => history.slice(0, 4), [history])
|
||||||
|
|
||||||
|
const { data: queryData, loading } = useRecentlySearchedAssetsQuery({
|
||||||
|
variables: {
|
||||||
|
collectionAddresses: shortenedHistory.filter((asset) => asset.isNft).map((asset) => asset.address),
|
||||||
|
contracts: shortenedHistory
|
||||||
|
.filter((asset) => !asset.isNft)
|
||||||
|
.map((token) => ({
|
||||||
|
address: token.address === NATIVE_CHAIN_ID ? getQueryAddress(token.chain) : token.address,
|
||||||
|
chain: token.chain,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
if (shortenedHistory.length === 0) return []
|
||||||
|
else if (!queryData) return undefined
|
||||||
|
// Collects both tokens and collections in a map, so they can later be returned in original order
|
||||||
|
const resultsMap: { [key: string]: GenieCollection | SearchToken } = {}
|
||||||
|
|
||||||
|
const queryCollections = queryData?.nftCollections?.edges.map((edge) => edge.node as NonNullable<NftCollection>)
|
||||||
|
const collections = queryCollections?.map(
|
||||||
|
(queryCollection): GenieCollection => {
|
||||||
|
return {
|
||||||
|
address: queryCollection.nftContracts?.[0]?.address ?? '',
|
||||||
|
isVerified: queryCollection?.isVerified,
|
||||||
|
name: queryCollection?.name,
|
||||||
|
stats: {
|
||||||
|
floor_price: queryCollection?.markets?.[0]?.floorPrice?.value,
|
||||||
|
total_supply: queryCollection?.numAssets,
|
||||||
|
},
|
||||||
|
imageUrl: queryCollection?.image?.url ?? '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[queryCollections]
|
||||||
|
)
|
||||||
|
collections?.forEach((collection) => (resultsMap[collection.address] = collection))
|
||||||
|
queryData.tokens?.filter(Boolean).forEach((token) => {
|
||||||
|
resultsMap[token.address ?? `NATIVE-${token.chain}`] = token
|
||||||
|
})
|
||||||
|
|
||||||
|
const data: (SearchToken | GenieCollection)[] = []
|
||||||
|
shortenedHistory.forEach((asset) => {
|
||||||
|
if (asset.address === 'NATIVE') {
|
||||||
|
// Handles special case where wMATIC data needs to be used for MATIC
|
||||||
|
const native = nativeOnChain(CHAIN_NAME_TO_CHAIN_ID[asset.chain] ?? SupportedChainId.MAINNET)
|
||||||
|
const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}`
|
||||||
|
const result = resultsMap[queryAddress]
|
||||||
|
if (result) data.push({ ...result, address: 'NATIVE', ...native })
|
||||||
|
} else {
|
||||||
|
const result = resultsMap[asset.address]
|
||||||
|
if (result) data.push(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}, [queryData, shortenedHistory])
|
||||||
|
|
||||||
|
return { data, loading }
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { t, Trans } from '@lingui/macro'
|
import { t, Trans } from '@lingui/macro'
|
||||||
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
|
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
|
||||||
import { BrowserEvent, ElementName, EventName, SectionName } from '@uniswap/analytics-events'
|
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
|
||||||
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { useSearchTokens } from 'graphql/data/SearchTokens'
|
||||||
import useDebounce from 'hooks/useDebounce'
|
import useDebounce from 'hooks/useDebounce'
|
||||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||||
@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex'
|
|||||||
import { magicalGradientOnHover } from 'nft/css/common.css'
|
import { magicalGradientOnHover } from 'nft/css/common.css'
|
||||||
import { useIsMobile, useIsTablet } from 'nft/hooks'
|
import { useIsMobile, useIsTablet } from 'nft/hooks'
|
||||||
import { fetchSearchCollections } from 'nft/queries'
|
import { fetchSearchCollections } from 'nft/queries'
|
||||||
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
|
|
||||||
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
@@ -64,16 +65,8 @@ export const SearchBar = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
|
const { chainId } = useWeb3React()
|
||||||
['searchTokens', debouncedSearchValue],
|
const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
|
||||||
() => fetchSearchTokens(debouncedSearchValue),
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
enabled: !!debouncedSearchValue.length,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const isNFTPage = useIsNftPage()
|
const isNFTPage = useIsNftPage()
|
||||||
|
|
||||||
@@ -109,7 +102,7 @@ export const SearchBar = () => {
|
|||||||
|
|
||||||
const isMobileOrTablet = isMobile || isTablet
|
const isMobileOrTablet = isMobile || isTablet
|
||||||
|
|
||||||
const trace = useTrace({ section: SectionName.NAVBAR_SEARCH })
|
const trace = useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH })
|
||||||
|
|
||||||
const navbarSearchEventProperties = {
|
const navbarSearchEventProperties = {
|
||||||
navbar_search_input_text: debouncedSearchValue,
|
navbar_search_input_text: debouncedSearchValue,
|
||||||
@@ -146,8 +139,9 @@ export const SearchBar = () => {
|
|||||||
}, [handleKeyPress, inputRef])
|
}, [handleKeyPress, inputRef])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Trace section={SectionName.NAVBAR_SEARCH}>
|
<Trace section={InterfaceSectionName.NAVBAR_SEARCH}>
|
||||||
<Box
|
<Box
|
||||||
|
data-cy="search-bar"
|
||||||
position={{ sm: 'fixed', md: 'absolute', xl: 'relative' }}
|
position={{ sm: 'fixed', md: 'absolute', xl: 'relative' }}
|
||||||
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
|
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
|
||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
@@ -178,8 +172,8 @@ export const SearchBar = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<TraceEvent
|
<TraceEvent
|
||||||
events={[BrowserEvent.onFocus]}
|
events={[BrowserEvent.onFocus]}
|
||||||
name={EventName.NAVBAR_SEARCH_SELECTED}
|
name={InterfaceEventName.NAVBAR_SEARCH_SELECTED}
|
||||||
element={ElementName.NAVBAR_SEARCH_INPUT}
|
element={InterfaceElementName.NAVBAR_SEARCH_INPUT}
|
||||||
properties={{ ...trace }}
|
properties={{ ...trace }}
|
||||||
>
|
>
|
||||||
<Trans
|
<Trans
|
||||||
@@ -187,12 +181,15 @@ export const SearchBar = () => {
|
|||||||
render={({ translation }) => (
|
render={({ translation }) => (
|
||||||
<Box
|
<Box
|
||||||
as="input"
|
as="input"
|
||||||
|
data-cy="search-bar-input"
|
||||||
placeholder={translation as string}
|
placeholder={translation as string}
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
!isOpen && toggleOpen()
|
!isOpen && toggleOpen()
|
||||||
setSearchValue(event.target.value)
|
setSearchValue(event.target.value)
|
||||||
}}
|
}}
|
||||||
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
|
onBlur={() =>
|
||||||
|
sendAnalyticsEvent(InterfaceEventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)
|
||||||
|
}
|
||||||
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
|
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { useTrace } from '@uniswap/analytics'
|
import { useTrace } from '@uniswap/analytics'
|
||||||
import { NavBarSearchTypes, SectionName } from '@uniswap/analytics-events'
|
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
|
||||||
|
import { useWeb3React } from '@web3-react/core'
|
||||||
|
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
|
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||||
|
import useTrendingTokens from 'graphql/data/TrendingTokens'
|
||||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||||
import { Box } from 'nft/components/Box'
|
import { Box } from 'nft/components/Box'
|
||||||
import { Column, Row } from 'nft/components/Flex'
|
import { Column, Row } from 'nft/components/Flex'
|
||||||
import { subheadSmall } from 'nft/css/common.css'
|
import { subheadSmall } from 'nft/css/common.css'
|
||||||
import { useSearchHistory } from 'nft/hooks'
|
|
||||||
import { fetchTrendingCollections } from 'nft/queries'
|
import { fetchTrendingCollections } from 'nft/queries'
|
||||||
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
|
import { GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
|
||||||
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
|
|
||||||
import { formatEthPrice } from 'nft/utils/currency'
|
import { formatEthPrice } from 'nft/utils/currency'
|
||||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
|
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
|
||||||
|
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
|
||||||
import * as styles from './SearchBar.css'
|
import * as styles from './SearchBar.css'
|
||||||
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
|
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
|
||||||
|
|
||||||
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
|
function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) {
|
||||||
return (suggestion as FungibleToken).decimals === undefined
|
return (suggestion as SearchToken).decimals === undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchBarDropdownSectionProps {
|
interface SearchBarDropdownSectionProps {
|
||||||
toggleOpen: () => void
|
toggleOpen: () => void
|
||||||
suggestions: (GenieCollection | FungibleToken)[]
|
suggestions: (GenieCollection | SearchToken)[]
|
||||||
header: JSX.Element
|
header: JSX.Element
|
||||||
headerIcon?: JSX.Element
|
headerIcon?: JSX.Element
|
||||||
hoveredIndex: number | undefined
|
hoveredIndex: number | undefined
|
||||||
@@ -46,7 +49,7 @@ const SearchBarDropdownSection = ({
|
|||||||
eventProperties,
|
eventProperties,
|
||||||
}: SearchBarDropdownSectionProps) => {
|
}: SearchBarDropdownSectionProps) => {
|
||||||
return (
|
return (
|
||||||
<Column gap="12">
|
<Column gap="12" data-cy="searchbar-dropdown">
|
||||||
<Row paddingX="16" paddingY="4" gap="8" color="gray300" className={subheadSmall} style={{ lineHeight: '20px' }}>
|
<Row paddingX="16" paddingY="4" gap="8" color="gray300" className={subheadSmall} style={{ lineHeight: '20px' }}>
|
||||||
{headerIcon ? headerIcon : null}
|
{headerIcon ? headerIcon : null}
|
||||||
<Box>{header}</Box>
|
<Box>{header}</Box>
|
||||||
@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({
|
|||||||
) : (
|
) : (
|
||||||
<TokenRow
|
<TokenRow
|
||||||
key={suggestion.address}
|
key={suggestion.address}
|
||||||
token={suggestion as FungibleToken}
|
token={suggestion as SearchToken}
|
||||||
isHovered={hoveredIndex === index + startingIndex}
|
isHovered={hoveredIndex === index + startingIndex}
|
||||||
setHoveredIndex={setHoveredIndex}
|
setHoveredIndex={setHoveredIndex}
|
||||||
toggleOpen={toggleOpen}
|
toggleOpen={toggleOpen}
|
||||||
@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isKnownToken(token: SearchToken) {
|
||||||
|
return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning
|
||||||
|
}
|
||||||
|
|
||||||
interface SearchBarDropdownProps {
|
interface SearchBarDropdownProps {
|
||||||
toggleOpen: () => void
|
toggleOpen: () => void
|
||||||
tokens: FungibleToken[]
|
tokens: SearchToken[]
|
||||||
collections: GenieCollection[]
|
collections: GenieCollection[]
|
||||||
queryText: string
|
queryText: string
|
||||||
hasInput: boolean
|
hasInput: boolean
|
||||||
@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
}: SearchBarDropdownProps) => {
|
}: SearchBarDropdownProps) => {
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
|
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
|
||||||
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
|
|
||||||
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
|
const { data: searchHistory } = useRecentlySearchedAssets()
|
||||||
|
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory])
|
||||||
|
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const isNFTPage = useIsNftPage()
|
const isNFTPage = useIsNftPage()
|
||||||
const isTokenPage = pathname.includes('/tokens')
|
const isTokenPage = pathname.includes('/tokens')
|
||||||
@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({
|
|||||||
[isNFTPage, trendingCollectionResults]
|
[isNFTPage, trendingCollectionResults]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
|
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
|
||||||
['trendingTokens'],
|
|
||||||
() => fetchTrendingTokens(4),
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
|
||||||
trendingTokenResults?.forEach(updateSearchHistory)
|
|
||||||
}, [trendingTokenResults, updateSearchHistory])
|
|
||||||
|
|
||||||
const trendingTokensLength = isTokenPage ? 3 : 2
|
const trendingTokensLength = isTokenPage ? 3 : 2
|
||||||
const trendingTokens = useMemo(
|
const trendingTokens = useMemo(
|
||||||
() =>
|
() => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array<SearchToken>(trendingTokensLength)],
|
||||||
trendingTokenResults
|
[trendingTokenData, trendingTokensLength]
|
||||||
? trendingTokenResults.slice(0, trendingTokensLength)
|
|
||||||
: [...Array<FungibleToken>(trendingTokensLength)],
|
|
||||||
[trendingTokenResults, trendingTokensLength]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const totalSuggestions = hasInput
|
const totalSuggestions = hasInput
|
||||||
@@ -197,12 +192,11 @@ export const SearchBarDropdown = ({
|
|||||||
}, [toggleOpen, hoveredIndex, totalSuggestions])
|
}, [toggleOpen, hoveredIndex, totalSuggestions])
|
||||||
|
|
||||||
const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
|
const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
|
||||||
const hasVerifiedToken = tokens.some((token) => token.onDefaultList)
|
const hasKnownToken = tokens.some(isKnownToken)
|
||||||
const showCollectionsFirst =
|
const showCollectionsFirst =
|
||||||
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) ||
|
(isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection)
|
||||||
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
|
|
||||||
|
|
||||||
const trace = JSON.stringify(useTrace({ section: SectionName.NAVBAR_SEARCH }))
|
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const eventProperties = { total_suggestions: totalSuggestions, query_text: queryText, ...JSON.parse(trace) }
|
const eventProperties = { total_suggestions: totalSuggestions, query_text: queryText, ...JSON.parse(trace) }
|
||||||
@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({
|
|||||||
}}
|
}}
|
||||||
header={<Trans>Recent searches</Trans>}
|
header={<Trans>Recent searches</Trans>}
|
||||||
headerIcon={<ClockIcon />}
|
headerIcon={<ClockIcon />}
|
||||||
|
isLoading={!searchHistory}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isNFTPage && (
|
{!isNFTPage && (
|
||||||
@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({
|
|||||||
}}
|
}}
|
||||||
header={<Trans>Popular tokens</Trans>}
|
header={<Trans>Popular tokens</Trans>}
|
||||||
headerIcon={<TrendingArrow />}
|
headerIcon={<TrendingArrow />}
|
||||||
isLoading={trendingTokensAreLoading}
|
isLoading={!trendingTokenData}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isTokenPage && (
|
{!isTokenPage && (
|
||||||
@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({
|
|||||||
trendingCollections,
|
trendingCollections,
|
||||||
trendingCollectionsAreLoading,
|
trendingCollectionsAreLoading,
|
||||||
trendingTokens,
|
trendingTokens,
|
||||||
trendingTokensAreLoading,
|
trendingTokenData,
|
||||||
hoveredIndex,
|
hoveredIndex,
|
||||||
toggleOpen,
|
toggleOpen,
|
||||||
shortenedHistory,
|
shortenedHistory,
|
||||||
@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({
|
|||||||
queryText,
|
queryText,
|
||||||
totalSuggestions,
|
totalSuggestions,
|
||||||
trace,
|
trace,
|
||||||
|
searchHistory,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||||
import { EventName } from '@uniswap/analytics-events'
|
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||||
import { formatUSDPrice } from '@uniswap/conedison/format'
|
import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import AssetLogo from 'components/Logo/AssetLogo'
|
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
|
||||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||||
import { getChainInfo } from 'constants/chainInfo'
|
import { checkSearchTokenWarning } from 'constants/tokenSafety'
|
||||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
import { checkWarning } from 'constants/tokenSafety'
|
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||||
import { getTokenDetailsURL } from 'graphql/data/util'
|
import { getTokenDetailsURL } from 'graphql/data/util'
|
||||||
import { Box } from 'nft/components/Box'
|
import { Box } from 'nft/components/Box'
|
||||||
import { Column, Row } from 'nft/components/Flex'
|
import { Column, Row } from 'nft/components/Flex'
|
||||||
import { VerifiedIcon } from 'nft/components/icons'
|
import { VerifiedIcon } from 'nft/components/icons'
|
||||||
import { vars } from 'nft/css/sprinkles.css'
|
import { vars } from 'nft/css/sprinkles.css'
|
||||||
import { useSearchHistory } from 'nft/hooks'
|
import { GenieCollection } from 'nft/types'
|
||||||
import { FungibleToken, GenieCollection } from 'nft/types'
|
|
||||||
import { ethNumberStandardFormatter } from 'nft/utils/currency'
|
import { ethNumberStandardFormatter } from 'nft/utils/currency'
|
||||||
import { putCommas } from 'nft/utils/putCommas'
|
import { putCommas } from 'nft/utils/putCommas'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
@@ -23,11 +20,9 @@ import { Link, useNavigate } from 'react-router-dom'
|
|||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
|
|
||||||
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
||||||
|
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
|
||||||
import * as styles from './SearchBar.css'
|
import * as styles from './SearchBar.css'
|
||||||
|
|
||||||
const StyledLogoContainer = styled(LogoContainer)`
|
|
||||||
margin-right: 8px;
|
|
||||||
`
|
|
||||||
const PriceChangeContainer = styled.div`
|
const PriceChangeContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -63,16 +58,15 @@ export const CollectionRow = ({
|
|||||||
}: CollectionRowProps) => {
|
}: CollectionRowProps) => {
|
||||||
const [brokenImage, setBrokenImage] = useState(false)
|
const [brokenImage, setBrokenImage] = useState(false)
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
const addToSearchHistory = useSearchHistory(
|
|
||||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
|
||||||
)
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
addToSearchHistory(collection)
|
addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum })
|
||||||
toggleOpen()
|
toggleOpen()
|
||||||
sendAnalyticsEvent(EventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||||
}, [addToSearchHistory, collection, toggleOpen, eventProperties])
|
}, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyDownHandler = (event: KeyboardEvent) => {
|
const keyDownHandler = (event: KeyboardEvent) => {
|
||||||
@@ -130,17 +124,8 @@ 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 {
|
interface TokenRowProps {
|
||||||
token: FungibleToken
|
token: SearchToken
|
||||||
isHovered: boolean
|
isHovered: boolean
|
||||||
setHoveredIndex: (index: number | undefined) => void
|
setHoveredIndex: (index: number | undefined) => void
|
||||||
toggleOpen: () => void
|
toggleOpen: () => void
|
||||||
@@ -149,19 +134,18 @@ interface TokenRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
|
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
|
||||||
const addToSearchHistory = useSearchHistory(
|
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
|
||||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
|
||||||
)
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
addToSearchHistory(token)
|
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
|
||||||
toggleOpen()
|
address && addRecentlySearchedAsset({ address, chain: token.chain })
|
||||||
sendAnalyticsEvent(EventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
|
||||||
}, [addToSearchHistory, toggleOpen, token, eventProperties])
|
|
||||||
|
|
||||||
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
|
toggleOpen()
|
||||||
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
|
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||||
|
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
|
||||||
|
|
||||||
|
const tokenDetailsPath = getTokenDetailsURL(token)
|
||||||
// Close the modal on escape
|
// Close the modal on escape
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyDownHandler = (event: KeyboardEvent) => {
|
const keyDownHandler = (event: KeyboardEvent) => {
|
||||||
@@ -177,10 +161,11 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
|||||||
}
|
}
|
||||||
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
|
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
|
||||||
|
|
||||||
const arrow = getDeltaArrow(token.price24hChange, 18)
|
const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
data-cy={`searchbar-token-row-${token.symbol}`}
|
||||||
to={tokenDetailsPath}
|
to={tokenDetailsPath}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
|
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
|
||||||
@@ -189,37 +174,33 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
|||||||
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
|
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
|
||||||
>
|
>
|
||||||
<Row style={{ width: '65%' }}>
|
<Row style={{ width: '65%' }}>
|
||||||
<StyledLogoContainer>
|
<QueryTokenLogo
|
||||||
<AssetLogo
|
token={token}
|
||||||
isNative={token.address === NATIVE_CHAIN_ID}
|
|
||||||
address={token.address}
|
|
||||||
chainId={token.chainId}
|
|
||||||
symbol={token.symbol}
|
symbol={token.symbol}
|
||||||
size="36px"
|
size="36px"
|
||||||
backupImg={token.logoURI}
|
backupImg={token.project?.logoUrl}
|
||||||
|
style={{ paddingRight: '8px' }}
|
||||||
/>
|
/>
|
||||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
|
||||||
</StyledLogoContainer>
|
|
||||||
<Column className={styles.suggestionPrimaryContainer}>
|
<Column className={styles.suggestionPrimaryContainer}>
|
||||||
<Row gap="4" width="full">
|
<Row gap="4" width="full">
|
||||||
<Box className={styles.primaryText}>{token.name}</Box>
|
<Box className={styles.primaryText}>{token.name}</Box>
|
||||||
<TokenSafetyIcon warning={checkWarning(token.address)} />
|
<TokenSafetyIcon warning={checkSearchTokenWarning(token)} />
|
||||||
</Row>
|
</Row>
|
||||||
<Box className={styles.secondaryText}>{token.symbol}</Box>
|
<Box className={styles.secondaryText}>{token.symbol}</Box>
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Column className={styles.suggestionSecondaryContainer}>
|
<Column className={styles.suggestionSecondaryContainer}>
|
||||||
{token.priceUsd && (
|
{token.market?.price?.value && (
|
||||||
<Row gap="4">
|
<Row gap="4">
|
||||||
<Box className={styles.primaryText}>{formatUSDPrice(token.priceUsd)}</Box>
|
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{token.price24hChange && (
|
{token.market?.pricePercentChange?.value && (
|
||||||
<PriceChangeContainer>
|
<PriceChangeContainer>
|
||||||
<ArrowCell>{arrow}</ArrowCell>
|
<ArrowCell>{arrow}</ArrowCell>
|
||||||
<PriceChangeText isNegative={token.price24hChange < 0}>
|
<PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
|
||||||
{Math.abs(token.price24hChange).toFixed(2)}%
|
{Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
|
||||||
</PriceChangeText>
|
</PriceChangeText>
|
||||||
</PriceChangeContainer>
|
</PriceChangeContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import Web3Status from 'components/Web3Status'
|
import Web3Status from 'components/Web3Status'
|
||||||
|
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||||
import { chainIdToBackendName } from 'graphql/data/util'
|
import { chainIdToBackendName } from 'graphql/data/util'
|
||||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||||
import { Box } from 'nft/components/Box'
|
import { Box } from 'nft/components/Box'
|
||||||
import { Row } from 'nft/components/Flex'
|
import { Row } from 'nft/components/Flex'
|
||||||
import { UniIcon } from 'nft/components/icons'
|
import { UniIcon } from 'nft/components/icons'
|
||||||
|
import { useProfilePageState } from 'nft/hooks'
|
||||||
|
import { ProfilePageStateType } from 'nft/types'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom'
|
import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
@@ -54,8 +57,7 @@ export const PageTabs = () => {
|
|||||||
pathname.startsWith('/pool') ||
|
pathname.startsWith('/pool') ||
|
||||||
pathname.startsWith('/add') ||
|
pathname.startsWith('/add') ||
|
||||||
pathname.startsWith('/remove') ||
|
pathname.startsWith('/remove') ||
|
||||||
pathname.startsWith('/increase') ||
|
pathname.startsWith('/increase')
|
||||||
pathname.startsWith('/find')
|
|
||||||
|
|
||||||
const isNftPage = useIsNftPage()
|
const isNftPage = useIsNftPage()
|
||||||
|
|
||||||
@@ -79,6 +81,8 @@ export const PageTabs = () => {
|
|||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const isNftPage = useIsNftPage()
|
const isNftPage = useIsNftPage()
|
||||||
|
const sellPageState = useProfilePageState((state) => state.state)
|
||||||
|
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,9 +94,13 @@ const Navbar = () => {
|
|||||||
<UniIcon
|
<UniIcon
|
||||||
width="48"
|
width="48"
|
||||||
height="48"
|
height="48"
|
||||||
|
data-testid="uniswap-logo"
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/')
|
navigate({
|
||||||
|
pathname: '/',
|
||||||
|
search: '?intro=true',
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -116,7 +124,7 @@ const Navbar = () => {
|
|||||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||||
<MenuDropdown />
|
<MenuDropdown />
|
||||||
</Box>
|
</Box>
|
||||||
{isNftPage && <Bag />}
|
{isNftPage && (!isNftListV2 || sellPageState !== ProfilePageStateType.LISTING) && <Bag />}
|
||||||
{!isNftPage && (
|
{!isNftPage && (
|
||||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||||
<ChainSelector />
|
<ChainSelector />
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function AddRemoveTabs({
|
|||||||
// detect if back should redirect to v3 or v2 pool page
|
// detect if back should redirect to v3 or v2 pool page
|
||||||
const poolLink = location.pathname.includes('add/v2')
|
const poolLink = location.pathname.includes('add/v2')
|
||||||
? '/pool/v2'
|
? '/pool/v2'
|
||||||
: '/pool' + (!!positionID ? `/${positionID.toString()}` : '')
|
: '/pool' + (positionID ? `/${positionID.toString()}` : '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs>
|
<Tabs>
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { useWeb3React } from '@web3-react/core'
|
|||||||
import { RowFixed } from 'components/Row'
|
import { RowFixed } from 'components/Row'
|
||||||
import { getChainInfo } from 'constants/chainInfo'
|
import { getChainInfo } from 'constants/chainInfo'
|
||||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||||
import useGasPrice from 'hooks/useGasPrice'
|
import { useIsLandingPage } from 'hooks/useIsLandingPage'
|
||||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||||
import useMachineTimeMs from 'hooks/useMachineTime'
|
import useMachineTimeMs from 'hooks/useMachineTime'
|
||||||
import JSBI from 'jsbi'
|
|
||||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||||
import ms from 'ms.macro'
|
import ms from 'ms.macro'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
@@ -70,17 +69,6 @@ const StyledPollingDot = styled.div<{ warning: boolean }>`
|
|||||||
transition: 250ms ease background-color;
|
transition: 250ms ease background-color;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledGasDot = styled.div`
|
|
||||||
background-color: ${({ theme }) => theme.textTertiary};
|
|
||||||
border-radius: 50%;
|
|
||||||
height: 4px;
|
|
||||||
min-height: 4px;
|
|
||||||
min-width: 4px;
|
|
||||||
position: relative;
|
|
||||||
transition: 250ms ease background-color;
|
|
||||||
width: 4px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const rotate360 = keyframes`
|
const rotate360 = keyframes`
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
@@ -120,9 +108,7 @@ export default function Polling() {
|
|||||||
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
|
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
|
||||||
const blockTime = useCurrentBlockTimestamp()
|
const blockTime = useCurrentBlockTimestamp()
|
||||||
const isNftPage = useIsNftPage()
|
const isNftPage = useIsNftPage()
|
||||||
|
const isLandingPage = useIsLandingPage()
|
||||||
const ethGasPrice = useGasPrice()
|
|
||||||
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
|
|
||||||
|
|
||||||
const waitMsBeforeWarning =
|
const waitMsBeforeWarning =
|
||||||
(chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
|
(chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
|
||||||
@@ -154,32 +140,13 @@ export default function Polling() {
|
|||||||
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
|
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
|
||||||
}, [blockNumber, chainId])
|
}, [blockNumber, chainId])
|
||||||
|
|
||||||
if (isNftPage) {
|
if (isNftPage || isLandingPage) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowFixed>
|
<RowFixed>
|
||||||
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
||||||
<ExternalLink href="https://etherscan.io/gastracker">
|
|
||||||
{!!priceGwei && (
|
|
||||||
<RowFixed style={{ marginRight: '8px' }}>
|
|
||||||
<ThemedText.DeprecatedMain fontSize="11px" mr="8px">
|
|
||||||
<MouseoverTooltip
|
|
||||||
text={
|
|
||||||
<Trans>
|
|
||||||
The current fast gas amount for sending a transaction on L1. Gas fees are paid in Ethereum's
|
|
||||||
native currency Ether (ETH) and denominated in GWEI.
|
|
||||||
</Trans>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{priceGwei.toString()} <Trans>gwei</Trans>
|
|
||||||
</MouseoverTooltip>
|
|
||||||
</ThemedText.DeprecatedMain>
|
|
||||||
<StyledGasDot />
|
|
||||||
</RowFixed>
|
|
||||||
)}
|
|
||||||
</ExternalLink>
|
|
||||||
<StyledPollingBlockNumber breathe={isMounting} hovering={isHover} warning={warning}>
|
<StyledPollingBlockNumber breathe={isMounting} hovering={isHover} warning={warning}>
|
||||||
<ExternalLink href={blockExternalLinkHref}>
|
<ExternalLink href={blockExternalLinkHref}>
|
||||||
<MouseoverTooltip
|
<MouseoverTooltip
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ export default function PositionList({
|
|||||||
</ToggleLabel>
|
</ToggleLabel>
|
||||||
</ToggleWrap>
|
</ToggleWrap>
|
||||||
</MobileHeader>
|
</MobileHeader>
|
||||||
{positions.map((p) => {
|
{positions.map((p) => (
|
||||||
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />
|
<PositionListItem key={p.tokenId.toString()} {...p} />
|
||||||
})}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/components/PositionListItem/PositionListItem.test.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { BigNumber } from '@ethersproject/bignumber'
|
||||||
|
import { render, screen } from 'test-utils'
|
||||||
|
|
||||||
|
import PositionListItem from '.'
|
||||||
|
|
||||||
|
jest.mock('hooks/Tokens', () => {
|
||||||
|
const originalModule = jest.requireActual('hooks/Tokens')
|
||||||
|
const uniSDK = jest.requireActual('@uniswap/sdk-core')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
|
useToken: jest.fn(
|
||||||
|
() =>
|
||||||
|
new uniSDK.Token(
|
||||||
|
1,
|
||||||
|
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||||
|
8,
|
||||||
|
'https://www.example.com',
|
||||||
|
'example.com coin'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PositionListItem should not render when the name contains a url', () => {
|
||||||
|
const positionDetails = {
|
||||||
|
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||||
|
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
||||||
|
tokenId: BigNumber.from(436148),
|
||||||
|
fee: 100,
|
||||||
|
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||||
|
tickLower: -800,
|
||||||
|
tickUpper: 1600,
|
||||||
|
}
|
||||||
|
render(<PositionListItem {...positionDetails} />)
|
||||||
|
screen.debug()
|
||||||
|
expect(screen.queryByText('.com', { exact: false })).toBe(null)
|
||||||
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BigNumber } from '@ethersproject/bignumber'
|
||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { Percent, Price, Token } from '@uniswap/sdk-core'
|
import { Percent, Price, Token } from '@uniswap/sdk-core'
|
||||||
import { Position } from '@uniswap/v3-sdk'
|
import { Position } from '@uniswap/v3-sdk'
|
||||||
@@ -15,9 +16,9 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Bound } from 'state/mint/v3/actions'
|
import { Bound } from 'state/mint/v3/actions'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
|
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
|
||||||
import { PositionDetails } from 'types/position'
|
|
||||||
import { formatTickPrice } from 'utils/formatTickPrice'
|
import { formatTickPrice } from 'utils/formatTickPrice'
|
||||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||||
|
import { hasURL } from 'utils/urlChecks'
|
||||||
|
|
||||||
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||||
|
|
||||||
@@ -109,7 +110,13 @@ const DataText = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
interface PositionListItemProps {
|
interface PositionListItemProps {
|
||||||
positionDetails: PositionDetails
|
token0: string
|
||||||
|
token1: string
|
||||||
|
tokenId: BigNumber
|
||||||
|
fee: number
|
||||||
|
liquidity: BigNumber
|
||||||
|
tickLower: number
|
||||||
|
tickUpper: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPriceOrderingFromPositionForUI(position?: Position): {
|
export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||||
@@ -166,16 +173,15 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PositionListItem({ positionDetails }: PositionListItemProps) {
|
export default function PositionListItem({
|
||||||
const {
|
|
||||||
token0: token0Address,
|
token0: token0Address,
|
||||||
token1: token1Address,
|
token1: token1Address,
|
||||||
|
tokenId,
|
||||||
fee: feeAmount,
|
fee: feeAmount,
|
||||||
liquidity,
|
liquidity,
|
||||||
tickLower,
|
tickLower,
|
||||||
tickUpper,
|
tickUpper,
|
||||||
} = positionDetails
|
}: PositionListItemProps) {
|
||||||
|
|
||||||
const token0 = useToken(token0Address)
|
const token0 = useToken(token0Address)
|
||||||
const token1 = useToken(token1Address)
|
const token1 = useToken(token1Address)
|
||||||
|
|
||||||
@@ -203,10 +209,23 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
|
|||||||
// check if price is within range
|
// check if price is within range
|
||||||
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
|
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
|
||||||
|
|
||||||
const positionSummaryLink = '/pool/' + positionDetails.tokenId
|
const positionSummaryLink = '/pool/' + tokenId
|
||||||
|
|
||||||
const removed = liquidity?.eq(0)
|
const removed = liquidity?.eq(0)
|
||||||
|
|
||||||
|
const containsURL = useMemo(
|
||||||
|
() =>
|
||||||
|
[token0?.name, token0?.symbol, token1?.name, token1?.symbol].reduce(
|
||||||
|
(acc, testString) => acc || Boolean(testString && hasURL(testString)),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (containsURL) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkRow to={positionSummaryLink}>
|
<LinkRow to={positionSummaryLink}>
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
@@ -232,8 +251,12 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
|
|||||||
<Trans>Min: </Trans>
|
<Trans>Min: </Trans>
|
||||||
</ExtentsText>
|
</ExtentsText>
|
||||||
<Trans>
|
<Trans>
|
||||||
{formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
|
{formatTickPrice({
|
||||||
per <HoverInlineText text={currencyBase?.symbol ?? ''} />
|
price: priceLower,
|
||||||
|
atLimit: tickAtLimit,
|
||||||
|
direction: Bound.LOWER,
|
||||||
|
})}{' '}
|
||||||
|
<HoverInlineText text={currencyQuote?.symbol} /> per <HoverInlineText text={currencyBase?.symbol ?? ''} />
|
||||||
</Trans>
|
</Trans>
|
||||||
</RangeText>{' '}
|
</RangeText>{' '}
|
||||||
<HideSmall>
|
<HideSmall>
|
||||||
@@ -247,8 +270,13 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
|
|||||||
<Trans>Max:</Trans>
|
<Trans>Max:</Trans>
|
||||||
</ExtentsText>
|
</ExtentsText>
|
||||||
<Trans>
|
<Trans>
|
||||||
{formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
|
{formatTickPrice({
|
||||||
per <HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
|
price: priceUpper,
|
||||||
|
atLimit: tickAtLimit,
|
||||||
|
direction: Bound.UPPER,
|
||||||
|
})}{' '}
|
||||||
|
<HoverInlineText text={currencyQuote?.symbol} /> per{' '}
|
||||||
|
<HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
|
||||||
</Trans>
|
</Trans>
|
||||||
</RangeText>
|
</RangeText>
|
||||||
</RangeLineItem>
|
</RangeLineItem>
|
||||||
|
|||||||
@@ -125,11 +125,13 @@ export const PositionPreview = ({
|
|||||||
<ThemedText.DeprecatedMain fontSize="12px">
|
<ThemedText.DeprecatedMain fontSize="12px">
|
||||||
<Trans>Min Price</Trans>
|
<Trans>Min Price</Trans>
|
||||||
</ThemedText.DeprecatedMain>
|
</ThemedText.DeprecatedMain>
|
||||||
<ThemedText.DeprecatedMediumHeader textAlign="center">{`${formatTickPrice(
|
<ThemedText.DeprecatedMediumHeader textAlign="center">
|
||||||
priceLower,
|
{formatTickPrice({
|
||||||
ticksAtLimit,
|
price: priceLower,
|
||||||
Bound.LOWER
|
atLimit: ticksAtLimit,
|
||||||
)}`}</ThemedText.DeprecatedMediumHeader>
|
direction: Bound.LOWER,
|
||||||
|
})}
|
||||||
|
</ThemedText.DeprecatedMediumHeader>
|
||||||
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
|
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
|
||||||
<Trans>
|
<Trans>
|
||||||
{quoteCurrency.symbol} per {baseCurrency.symbol}
|
{quoteCurrency.symbol} per {baseCurrency.symbol}
|
||||||
@@ -146,11 +148,13 @@ export const PositionPreview = ({
|
|||||||
<ThemedText.DeprecatedMain fontSize="12px">
|
<ThemedText.DeprecatedMain fontSize="12px">
|
||||||
<Trans>Max Price</Trans>
|
<Trans>Max Price</Trans>
|
||||||
</ThemedText.DeprecatedMain>
|
</ThemedText.DeprecatedMain>
|
||||||
<ThemedText.DeprecatedMediumHeader textAlign="center">{`${formatTickPrice(
|
<ThemedText.DeprecatedMediumHeader textAlign="center">
|
||||||
priceUpper,
|
{formatTickPrice({
|
||||||
ticksAtLimit,
|
price: priceUpper,
|
||||||
Bound.UPPER
|
atLimit: ticksAtLimit,
|
||||||
)}`}</ThemedText.DeprecatedMediumHeader>
|
direction: Bound.UPPER,
|
||||||
|
})}
|
||||||
|
</ThemedText.DeprecatedMediumHeader>
|
||||||
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
|
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
|
||||||
<Trans>
|
<Trans>
|
||||||
{quoteCurrency.symbol} per {baseCurrency.symbol}
|
{quoteCurrency.symbol} per {baseCurrency.symbol}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import styled, { useTheme } from 'styled-components/macro'
|
|
||||||
|
|
||||||
import { ThemedText } from '../../theme'
|
|
||||||
import { AutoColumn } from '../Column'
|
|
||||||
|
|
||||||
const Wrapper = styled(AutoColumn)`
|
|
||||||
margin-right: 8px;
|
|
||||||
height: 100%;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Grouping = styled(AutoColumn)`
|
|
||||||
width: fit-content;
|
|
||||||
padding: 4px;
|
|
||||||
/* background-color: ${({ theme }) => theme.backgroundInteractive}; */
|
|
||||||
border-radius: 16px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
background-color: ${({ theme, confirmed, disabled }) =>
|
|
||||||
disabled ? theme.deprecated_bg3 : confirmed ? theme.accentSuccess : theme.accentAction};
|
|
||||||
border-radius: 50%;
|
|
||||||
color: ${({ theme, disabled }) => (disabled ? theme.textTertiary : theme.textPrimary)};
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const CircleRow = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
interface ProgressCirclesProps {
|
|
||||||
steps: boolean[]
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Based on array of steps, create a step counter of circles.
|
|
||||||
* A circle can be enabled, disabled, or confirmed. States are derived
|
|
||||||
* from previous step.
|
|
||||||
*
|
|
||||||
* An extra circle is added to represent the ability to swap, add, or remove.
|
|
||||||
* This step will never be marked as complete (because no 'txn done' state in body ui).
|
|
||||||
*
|
|
||||||
* @param steps array of booleans where true means step is complete
|
|
||||||
*/
|
|
||||||
export default function ProgressCircles({ steps, disabled = false, ...rest }: ProgressCirclesProps) {
|
|
||||||
const theme = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper justify="center" {...rest}>
|
|
||||||
<Grouping>
|
|
||||||
{steps.map((step, i) => {
|
|
||||||
return (
|
|
||||||
<CircleRow key={i}>
|
|
||||||
<Circle confirmed={step} disabled={disabled || (!steps[i - 1] && i !== 0)}>
|
|
||||||
{step ? '✓' : i + 1 + '.'}
|
|
||||||
</Circle>
|
|
||||||
<ThemedText.DeprecatedMain color={theme.deprecated_text4}>|</ThemedText.DeprecatedMain>
|
|
||||||
</CircleRow>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<Circle disabled={disabled || !steps[steps.length - 1]}>{steps.length + 1 + '.'}</Circle>
|
|
||||||
</Grouping>
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TraceEvent } from '@uniswap/analytics'
|
import { TraceEvent } from '@uniswap/analytics'
|
||||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||||
import { Currency } from '@uniswap/sdk-core'
|
import { Currency } from '@uniswap/sdk-core'
|
||||||
import { AutoColumn } from 'components/Column'
|
import { AutoColumn } from 'components/Column'
|
||||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||||
@@ -70,9 +70,9 @@ export default function CommonBases({
|
|||||||
return (
|
return (
|
||||||
<TraceEvent
|
<TraceEvent
|
||||||
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
|
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
|
||||||
name={EventName.TOKEN_SELECTED}
|
name={InterfaceEventName.TOKEN_SELECTED}
|
||||||
properties={formatAnalyticsEventProperties(currency, searchQuery, isAddressSearch)}
|
properties={formatAnalyticsEventProperties(currency, searchQuery, isAddressSearch)}
|
||||||
element={ElementName.COMMON_BASES_CURRENCY_BUTTON}
|
element={InterfaceElementName.COMMON_BASES_CURRENCY_BUTTON}
|
||||||
key={currencyId(currency)}
|
key={currencyId(currency)}
|
||||||
>
|
>
|
||||||
<BaseWrapper
|
<BaseWrapper
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
style="padding-right: 8px; padding-top: 8px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -337,6 +341,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -383,6 +388,10 @@ exports[`renders loading rows when isLoading is true 1`] = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
style="padding-right: 8px; padding-top: 8px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -411,5 +420,6 @@ exports[`renders loading rows when isLoading is true 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|||||||
20
src/components/SearchModal/CurrencyList/index.css.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { style } from '@vanilla-extract/css'
|
||||||
|
import { themeVars } from 'nft/css/sprinkles.css'
|
||||||
|
|
||||||
|
export const scrollbarStyle = style([
|
||||||
|
{
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: `${themeVars.colors.backgroundOutline} transparent`,
|
||||||
|
height: '100%',
|
||||||
|
selectors: {
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
background: 'transparent',
|
||||||
|
width: '4px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
background: `${themeVars.colors.backgroundOutline}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TraceEvent } from '@uniswap/analytics'
|
import { TraceEvent } from '@uniswap/analytics'
|
||||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||||
@@ -20,6 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
|
|||||||
import Row, { RowFixed } from '../../Row'
|
import Row, { RowFixed } from '../../Row'
|
||||||
import { MouseoverTooltip } from '../../Tooltip'
|
import { MouseoverTooltip } from '../../Tooltip'
|
||||||
import { LoadingRows, MenuItem } from '../styleds'
|
import { LoadingRows, MenuItem } from '../styleds'
|
||||||
|
import * as styles from './index.css'
|
||||||
|
|
||||||
function currencyKey(currency: Currency): string {
|
function currencyKey(currency: Currency): string {
|
||||||
return currency.isToken ? currency.address : 'ETHER'
|
return currency.isToken ? currency.address : 'ETHER'
|
||||||
@@ -131,9 +132,9 @@ export function CurrencyRow({
|
|||||||
return (
|
return (
|
||||||
<TraceEvent
|
<TraceEvent
|
||||||
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
|
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
|
||||||
name={EventName.TOKEN_SELECTED}
|
name={InterfaceEventName.TOKEN_SELECTED}
|
||||||
properties={{ is_imported_by_user: customAdded, ...eventProperties }}
|
properties={{ is_imported_by_user: customAdded, ...eventProperties }}
|
||||||
element={ElementName.TOKEN_SELECTOR_ROW}
|
element={InterfaceElementName.TOKEN_SELECTOR_ROW}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -288,12 +289,23 @@ export default function CurrencyList({
|
|||||||
return currencyKey(currency)
|
return currencyKey(currency)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return isLoading ? (
|
return (
|
||||||
<FixedSizeList height={height} ref={fixedListRef as any} width="100%" itemData={[]} itemCount={10} itemSize={56}>
|
<div style={{ paddingRight: '8px', paddingTop: '8px' }}>
|
||||||
|
{isLoading ? (
|
||||||
|
<FixedSizeList
|
||||||
|
className={styles.scrollbarStyle}
|
||||||
|
height={height}
|
||||||
|
ref={fixedListRef as any}
|
||||||
|
width="100%"
|
||||||
|
itemData={[]}
|
||||||
|
itemCount={10}
|
||||||
|
itemSize={56}
|
||||||
|
>
|
||||||
{LoadingRow}
|
{LoadingRow}
|
||||||
</FixedSizeList>
|
</FixedSizeList>
|
||||||
) : (
|
) : (
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
|
className={styles.scrollbarStyle}
|
||||||
height={height}
|
height={height}
|
||||||
ref={fixedListRef as any}
|
ref={fixedListRef as any}
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -304,5 +316,7 @@ export default function CurrencyList({
|
|||||||
>
|
>
|
||||||
{Row}
|
{Row}
|
||||||
</FixedSizeList>
|
</FixedSizeList>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { t, Trans } from '@lingui/macro'
|
import { t, Trans } from '@lingui/macro'
|
||||||
import { Trace } from '@uniswap/analytics'
|
import { Trace } from '@uniswap/analytics'
|
||||||
import { EventName, ModalName } from '@uniswap/analytics-events'
|
import { InterfaceEventName, InterfaceModalName } from '@uniswap/analytics-events'
|
||||||
import { Currency, Token } from '@uniswap/sdk-core'
|
import { Currency, Token } from '@uniswap/sdk-core'
|
||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { sendEvent } from 'components/analytics'
|
import { sendEvent } from 'components/analytics'
|
||||||
@@ -32,6 +32,7 @@ import { PaddedColumn, SearchInput, Separator } from './styleds'
|
|||||||
const ContentWrapper = styled(Column)`
|
const ContentWrapper = styled(Column)`
|
||||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
`
|
`
|
||||||
@@ -45,6 +46,7 @@ interface CurrencySearchProps {
|
|||||||
showCommonBases?: boolean
|
showCommonBases?: boolean
|
||||||
showCurrencyAmount?: boolean
|
showCurrencyAmount?: boolean
|
||||||
disableNonToken?: boolean
|
disableNonToken?: boolean
|
||||||
|
onlyShowCurrenciesWithBalance?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CurrencySearch({
|
export function CurrencySearch({
|
||||||
@@ -56,6 +58,7 @@ export function CurrencySearch({
|
|||||||
disableNonToken,
|
disableNonToken,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
onlyShowCurrenciesWithBalance,
|
||||||
}: CurrencySearchProps) {
|
}: CurrencySearchProps) {
|
||||||
const { chainId } = useWeb3React()
|
const { chainId } = useWeb3React()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@@ -92,6 +95,10 @@ export function CurrencySearch({
|
|||||||
!balancesAreLoading
|
!balancesAreLoading
|
||||||
? filteredTokens
|
? filteredTokens
|
||||||
.filter((token) => {
|
.filter((token) => {
|
||||||
|
if (onlyShowCurrenciesWithBalance) {
|
||||||
|
return balances[token.address]?.greaterThan(0)
|
||||||
|
}
|
||||||
|
|
||||||
// If there is no query, filter out unselected user-added tokens with no balance.
|
// If there is no query, filter out unselected user-added tokens with no balance.
|
||||||
if (!debouncedQuery && token instanceof UserAddedToken) {
|
if (!debouncedQuery && token instanceof UserAddedToken) {
|
||||||
if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true
|
if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true
|
||||||
@@ -101,7 +108,15 @@ export function CurrencySearch({
|
|||||||
})
|
})
|
||||||
.sort(tokenComparator.bind(null, balances))
|
.sort(tokenComparator.bind(null, balances))
|
||||||
: [],
|
: [],
|
||||||
[balances, balancesAreLoading, debouncedQuery, filteredTokens, otherSelectedCurrency, selectedCurrency]
|
[
|
||||||
|
balances,
|
||||||
|
balancesAreLoading,
|
||||||
|
debouncedQuery,
|
||||||
|
filteredTokens,
|
||||||
|
otherSelectedCurrency,
|
||||||
|
selectedCurrency,
|
||||||
|
onlyShowCurrenciesWithBalance,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
|
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
|
||||||
|
|
||||||
@@ -114,11 +129,23 @@ export function CurrencySearch({
|
|||||||
const s = debouncedQuery.toLowerCase().trim()
|
const s = debouncedQuery.toLowerCase().trim()
|
||||||
|
|
||||||
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
|
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
|
||||||
const natives = (disableNonToken || native.equals(wrapped) ? [wrapped] : [native, wrapped]).filter(
|
const shouldShowWrapped =
|
||||||
(n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1
|
!onlyShowCurrenciesWithBalance || (!balancesAreLoading && balances[wrapped.address]?.greaterThan(0))
|
||||||
)
|
const natives = (
|
||||||
|
disableNonToken || native.equals(wrapped) ? [wrapped] : shouldShowWrapped ? [native, wrapped] : [native]
|
||||||
|
).filter((n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1)
|
||||||
|
|
||||||
return [...natives, ...tokens]
|
return [...natives, ...tokens]
|
||||||
}, [debouncedQuery, filteredSortedTokens, wrapped, disableNonToken, native])
|
}, [
|
||||||
|
debouncedQuery,
|
||||||
|
filteredSortedTokens,
|
||||||
|
onlyShowCurrenciesWithBalance,
|
||||||
|
balancesAreLoading,
|
||||||
|
balances,
|
||||||
|
wrapped,
|
||||||
|
disableNonToken,
|
||||||
|
native,
|
||||||
|
])
|
||||||
|
|
||||||
const handleCurrencySelect = useCallback(
|
const handleCurrencySelect = useCallback(
|
||||||
(currency: Currency, hasWarning?: boolean) => {
|
(currency: Currency, hasWarning?: boolean) => {
|
||||||
@@ -168,7 +195,9 @@ export function CurrencySearch({
|
|||||||
|
|
||||||
// if no results on main list, show option to expand into inactive
|
// if no results on main list, show option to expand into inactive
|
||||||
const filteredInactiveTokens = useSearchInactiveTokenLists(
|
const filteredInactiveTokens = useSearchInactiveTokenLists(
|
||||||
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
|
!onlyShowCurrenciesWithBalance && (filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch))
|
||||||
|
? debouncedQuery
|
||||||
|
: undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
|
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
|
||||||
@@ -181,7 +210,11 @@ export function CurrencySearch({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
<Trace name={EventName.TOKEN_SELECTOR_OPENED} modal={ModalName.TOKEN_SELECTOR} shouldLogImpression>
|
<Trace
|
||||||
|
name={InterfaceEventName.TOKEN_SELECTOR_OPENED}
|
||||||
|
modal={InterfaceModalName.TOKEN_SELECTOR}
|
||||||
|
shouldLogImpression
|
||||||
|
>
|
||||||
<PaddedColumn gap="16px">
|
<PaddedColumn gap="16px">
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
<Text fontWeight={500} fontSize={16}>
|
<Text fontWeight={500} fontSize={16}>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface CurrencySearchModalProps {
|
|||||||
showCommonBases?: boolean
|
showCommonBases?: boolean
|
||||||
showCurrencyAmount?: boolean
|
showCurrencyAmount?: boolean
|
||||||
disableNonToken?: boolean
|
disableNonToken?: boolean
|
||||||
|
onlyShowCurrenciesWithBalance?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CurrencyModalView {
|
enum CurrencyModalView {
|
||||||
@@ -34,6 +35,7 @@ export default memo(function CurrencySearchModal({
|
|||||||
showCommonBases = false,
|
showCommonBases = false,
|
||||||
showCurrencyAmount = true,
|
showCurrencyAmount = true,
|
||||||
disableNonToken = false,
|
disableNonToken = false,
|
||||||
|
onlyShowCurrenciesWithBalance = false,
|
||||||
}: CurrencySearchModalProps) {
|
}: CurrencySearchModalProps) {
|
||||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.search)
|
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.search)
|
||||||
const lastOpen = useLast(isOpen)
|
const lastOpen = useLast(isOpen)
|
||||||
@@ -84,6 +86,7 @@ export default memo(function CurrencySearchModal({
|
|||||||
showCommonBases={showCommonBases}
|
showCommonBases={showCommonBases}
|
||||||
showCurrencyAmount={showCurrencyAmount}
|
showCurrencyAmount={showCurrencyAmount}
|
||||||
disableNonToken={disableNonToken}
|
disableNonToken={disableNonToken}
|
||||||
|
onlyShowCurrenciesWithBalance={onlyShowCurrenciesWithBalance}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string; isInit
|
|||||||
${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)}
|
${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)}
|
||||||
ease-in;
|
ease-in;
|
||||||
background: ${({ theme, bgColor, isActive }) =>
|
background: ${({ theme, bgColor, isActive }) =>
|
||||||
isActive ? bgColor ?? theme.accentAction : !!bgColor ? theme.deprecated_bg4 : theme.textTertiary};
|
isActive ? bgColor ?? theme.accentAction : bgColor ? theme.deprecated_bg4 : theme.textTertiary};
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
:hover {
|
:hover {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function TokenSafetyIcon({ warning }: { warning: Warning | null }
|
|||||||
case WARNING_LEVEL.BLOCKED:
|
case WARNING_LEVEL.BLOCKED:
|
||||||
return (
|
return (
|
||||||
<WarningContainer>
|
<WarningContainer>
|
||||||
<BlockedIcon strokeWidth={2.5} />
|
<BlockedIcon data-cy="blocked-icon" strokeWidth={2.5} />
|
||||||
</WarningContainer>
|
</WarningContainer>
|
||||||
)
|
)
|
||||||
case WARNING_LEVEL.UNKNOWN:
|
case WARNING_LEVEL.UNKNOWN:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { WARNING_LEVEL } from 'constants/tokenSafety'
|
import { WARNING_LEVEL } from 'constants/tokenSafety'
|
||||||
import { useTokenWarningColor } from 'hooks/useTokenWarningColor'
|
import { useTokenWarningColor, useTokenWarningTextColor } from 'hooks/useTokenWarningColor'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { AlertTriangle, Slash } from 'react-feather'
|
import { AlertTriangle, Slash } from 'react-feather'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
|
|
||||||
const Label = styled.div<{ color: string }>`
|
const Label = styled.div<{ color: string; backgroundColor: string }>`
|
||||||
padding: 4px 4px;
|
padding: 4px 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background-color: ${({ color }) => color + '1F'};
|
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: ${({ color }) => color};
|
color: ${({ color }) => color};
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -28,7 +28,7 @@ type TokenWarningLabelProps = {
|
|||||||
}
|
}
|
||||||
export default function TokenSafetyLabel({ level, canProceed, children }: TokenWarningLabelProps) {
|
export default function TokenSafetyLabel({ level, canProceed, children }: TokenWarningLabelProps) {
|
||||||
return (
|
return (
|
||||||
<Label color={useTokenWarningColor(level)}>
|
<Label color={useTokenWarningTextColor(level)} backgroundColor={useTokenWarningColor(level)}>
|
||||||
<Title marginRight="5px">{children}</Title>
|
<Title marginRight="5px">{children}</Title>
|
||||||
{canProceed ? <AlertTriangle strokeWidth={2.5} size="14px" /> : <Slash strokeWidth={2.5} size="14px" />}
|
{canProceed ? <AlertTriangle strokeWidth={2.5} size="14px" /> : <Slash strokeWidth={2.5} size="14px" />}
|
||||||
</Label>
|
</Label>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
|
import { getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
|
||||||
import { useTokenWarningColor } from 'hooks/useTokenWarningColor'
|
import { useTokenWarningColor, useTokenWarningTextColor } from 'hooks/useTokenWarningColor'
|
||||||
import { AlertTriangle, Slash } from 'react-feather'
|
import { AlertTriangle, Slash } from 'react-feather'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
import { ExternalLink } from 'theme'
|
import { ExternalLink } from 'theme'
|
||||||
|
|
||||||
const Label = styled.div<{ color: string }>`
|
const Label = styled.div<{ color: string; backgroundColor: string }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 20px 16px;
|
padding: 12px 20px 16px;
|
||||||
background-color: ${({ color }) => color + '1F'};
|
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: ${({ color }) => color};
|
color: ${({ color }) => color};
|
||||||
`
|
`
|
||||||
@@ -39,17 +39,18 @@ const StyledLink = styled(ExternalLink)`
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
`
|
`
|
||||||
|
|
||||||
type TokenWarningMessageProps = {
|
type TokenSafetyMessageProps = {
|
||||||
warning: Warning
|
warning: Warning
|
||||||
tokenAddress: string
|
tokenAddress: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarningMessageProps) {
|
export default function TokenSafetyMessage({ warning, tokenAddress }: TokenSafetyMessageProps) {
|
||||||
const color = useTokenWarningColor(warning.level)
|
const backgroundColor = useTokenWarningColor(warning.level)
|
||||||
|
const textColor = useTokenWarningTextColor(warning.level)
|
||||||
const { heading, description } = getWarningCopy(warning)
|
const { heading, description } = getWarningCopy(warning)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label color={color}>
|
<Label data-cy="token-safety-message" color={textColor} backgroundColor={backgroundColor}>
|
||||||
<TitleRow>
|
<TitleRow>
|
||||||
{warning.canProceed ? <AlertTriangle size="16px" /> : <Slash size="16px" />}
|
{warning.canProceed ? <AlertTriangle size="16px" /> : <Slash size="16px" />}
|
||||||
<Title marginLeft="7px">{warning.message}</Title>
|
<Title marginLeft="7px">{warning.message}</Title>
|
||||||
|
|||||||