Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbf39e4932 | ||
|
|
975570fa97 | ||
|
|
d6aa0e98a4 | ||
|
|
4644cd7b0a | ||
|
|
9ddedd8dab | ||
|
|
95030a52c5 | ||
|
|
1911f72536 | ||
|
|
85217452db | ||
|
|
f7a1a2ab58 | ||
|
|
66a2006284 | ||
|
|
610b7f4464 | ||
|
|
ce12635332 | ||
|
|
2182e18f85 | ||
|
|
ad2c7dfdff | ||
|
|
cb36c9103e | ||
|
|
0a1459ee83 | ||
|
|
8896a042f0 | ||
|
|
61ad07c3f2 | ||
|
|
81a5164d99 | ||
|
|
467e80a42f | ||
|
|
58f25aa439 | ||
|
|
377c71f2e5 | ||
|
|
7cf25ac7c8 | ||
|
|
09b54570e1 | ||
|
|
73580de922 | ||
|
|
e32fd3a8fc | ||
|
|
057417c666 | ||
|
|
f1b300af70 | ||
|
|
600049bc6e | ||
|
|
6e91311489 | ||
|
|
f6a464cb3b | ||
|
|
e589c751d7 | ||
|
|
0f91af1df2 | ||
|
|
10ef04510a | ||
|
|
e3b3d9e825 | ||
|
|
3050e967f7 | ||
|
|
2150450760 | ||
|
|
1b07e95885 | ||
|
|
9bb50d6a7b | ||
|
|
b08bb7eaff | ||
|
|
3a36ac5538 | ||
|
|
2962cd0e14 | ||
|
|
6a311aa6d7 | ||
|
|
e78b6d61f2 | ||
|
|
365b429c0b | ||
|
|
32d300009e | ||
|
|
806623c602 | ||
|
|
3272f8e9db | ||
|
|
010ef108eb | ||
|
|
19b1e9e399 | ||
|
|
6287b95b92 | ||
|
|
4e8a6e2a4c | ||
|
|
848c7b418b | ||
|
|
f619cf4353 | ||
|
|
877db71e2a | ||
|
|
f4b5727fdb | ||
|
|
1fd6b1e659 | ||
|
|
6570beef32 | ||
|
|
b57f58ab35 | ||
|
|
2f40c4f614 | ||
|
|
3f9c34d37d | ||
|
|
1d5c6530e3 | ||
|
|
78f294c340 | ||
|
|
90d24a26f3 | ||
|
|
7a3a5bd546 | ||
|
|
081ae15aa8 | ||
|
|
f5a5c5e70d | ||
|
|
e05e0206b7 | ||
|
|
344b4340ae | ||
|
|
eeef306bdd | ||
|
|
63a491d4b1 | ||
|
|
6831a73fdf | ||
|
|
a4aef02747 | ||
|
|
c26716047f | ||
|
|
0fa238af0b | ||
|
|
21c1484c0e | ||
|
|
8a845ee0e9 | ||
|
|
f5229ca838 | ||
|
|
875203f0ef | ||
|
|
91a8202737 | ||
|
|
0b4819d165 | ||
|
|
e7d3289754 | ||
|
|
0698e0f82a | ||
|
|
0350cc4701 | ||
|
|
997052869d | ||
|
|
9ec16c2ba8 | ||
|
|
e2cf8f1642 | ||
|
|
ed6952d1f7 | ||
|
|
3277d70e93 | ||
|
|
d1a31fe763 | ||
|
|
f88af029ae | ||
|
|
9f3e49b4d8 | ||
|
|
d4911d1054 | ||
|
|
90df9c4ced | ||
|
|
14f15d1fd6 | ||
|
|
69818ace1f | ||
|
|
42906d6709 | ||
|
|
2f8936a980 | ||
|
|
f5c4468c3c | ||
|
|
852e8f749f | ||
|
|
6694e5e398 | ||
|
|
2c9a50a372 | ||
|
|
0fc0cba6de | ||
|
|
041c86c04d | ||
|
|
123373e671 | ||
|
|
eb1732deee | ||
|
|
3c13321a71 | ||
|
|
58703f31a0 | ||
|
|
58721fb191 | ||
|
|
678cd1a06f | ||
|
|
a5ff3beb92 | ||
|
|
35ccf425f6 | ||
|
|
fe030412cd | ||
|
|
4d5a43351f | ||
|
|
ac1bc3b3a6 | ||
|
|
d1063d50ed | ||
|
|
46fc74e90f | ||
|
|
2c4f4092d8 | ||
|
|
aac7268dc8 | ||
|
|
fd162a72ff | ||
|
|
e20936709c | ||
|
|
2fda2c8c15 | ||
|
|
1f09757c49 | ||
|
|
7e49babff7 | ||
|
|
b35653ade1 | ||
|
|
57b53013d1 | ||
|
|
bafd3f3c05 | ||
|
|
29db0a50b3 | ||
|
|
9566fb888e | ||
|
|
13c8903e8f | ||
|
|
1d06b47e8d | ||
|
|
0089c2ee43 | ||
|
|
9869a9fcb7 | ||
|
|
631f29d66d | ||
|
|
97deebad37 | ||
|
|
e667615449 | ||
|
|
4ab61faeae | ||
|
|
0004db3d4a | ||
|
|
c133c472be |
2
.env
2
.env
@@ -1,2 +1,2 @@
|
||||
REACT_APP_CHAIN_ID="1"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/acb7e55995d04c49bfb52b7141599467"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"
|
||||
@@ -1,5 +1,5 @@
|
||||
REACT_APP_CHAIN_ID="1"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/febcb10ca2754433a61e0805bc6c047d"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1"
|
||||
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -4,11 +4,18 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
|
||||
YOUR ISSUE WILL BE DELETED.
|
||||
SEE https://github.com/Uniswap/default-token-list#adding-a-token
|
||||
|
||||
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
|
||||
-->
|
||||
|
||||
**Bug Description**
|
||||
A clear and concise description of what the bug is.
|
||||
A clear and concise description of the bug.
|
||||
|
||||
**Steps to Reproduce**
|
||||
1. Go to ...
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
9
.github/ISSUE_TEMPLATE/feature-request.md
vendored
9
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -4,9 +4,16 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
|
||||
YOUR ISSUE WILL BE DELETED.
|
||||
SEE https://github.com/Uniswap/default-token-list#adding-a-token
|
||||
|
||||
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
@@ -4,7 +4,15 @@ about: Tell us something else
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
|
||||
YOUR ISSUE WILL BE DELETED.
|
||||
SEE https://github.com/Uniswap/default-token-list#adding-a-token
|
||||
|
||||
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/token-request.md
vendored
27
.github/ISSUE_TEMPLATE/token-request.md
vendored
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Token Request
|
||||
about: Request a token addition
|
||||
title: ''
|
||||
labels: token request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Please provide the following information for your token.**
|
||||
|
||||
Token Address:
|
||||
Token Name (from contract):
|
||||
Token Decimals (from contract):
|
||||
Token Symbol (from contract):
|
||||
Uniswap Exchange Address of Token:
|
||||
|
||||
Link to the official homepage of token:
|
||||
Link to CoinMarketCap or CoinGecko page of token:
|
||||
|
||||
Some tokens (e.g. BNB) do not work with Uniswap v1. In order to assess if your token works correctly, please complete small-value transactions of each of the types below, and submit the Etherscan transaction links for our review.
|
||||
Test `addLiquidity` transaction:
|
||||
Test `swap` transaction:
|
||||
Test `removeLiquidity` transaction:
|
||||
|
||||
Are you willing to add liquidity to the liquidity pool for this token? (Y/N):
|
||||
If so, how much liquidity are you willing to add?:
|
||||
46
.github/workflows/release.yaml
vendored
46
.github/workflows/release.yaml
vendored
@@ -2,14 +2,10 @@ name: Release
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
- cron: '0 12 * * 1-4'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
@@ -43,14 +39,14 @@ jobs:
|
||||
node-version: '12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --ignore-scripts --frozen-lockfile
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build the IPFS bundle
|
||||
run: yarn ipfs-build
|
||||
run: yarn build
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: upload
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
|
||||
with:
|
||||
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
|
||||
path: './build'
|
||||
@@ -64,14 +60,14 @@ jobs:
|
||||
cidv0: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Update DNS with new IPFS hash
|
||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
||||
env:
|
||||
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
|
||||
RECORD_DOMAIN: 'uniswap.org'
|
||||
RECORD_NAME: '_dnslink.app'
|
||||
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
|
||||
with:
|
||||
domain: 'uniswap.org'
|
||||
subdomain: '_dnslink.app'
|
||||
record-type: 'TXT'
|
||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
||||
token: ${{ secrets.VERCEL_TOKEN }}
|
||||
team-name: 'uniswap'
|
||||
cid: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
@@ -82,19 +78,21 @@ jobs:
|
||||
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
||||
body: |
|
||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
||||
|
||||
The IPFS hash of the bundle is:
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||
|
||||
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
|
||||
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
The preferred URLs below are safe to use to access this specific release.
|
||||
|
||||
Preferred URLs:
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
|
||||
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
|
||||
|
||||
Other IPFS gateways:
|
||||
|
||||
14
.github/workflows/tests.yaml
vendored
14
.github/workflows/tests.yaml
vendored
@@ -2,10 +2,10 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
- master
|
||||
jobs:
|
||||
integration-tests:
|
||||
name: Integration tests
|
||||
@@ -26,7 +26,11 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn install
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn cypress install
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_NETWORK_URL: "https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"
|
||||
- run: yarn integration-test
|
||||
|
||||
unit-tests:
|
||||
@@ -48,7 +52,7 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn test
|
||||
|
||||
lint:
|
||||
@@ -70,6 +74,6 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn lint
|
||||
|
||||
|
||||
48
README.md
48
README.md
@@ -1,11 +1,12 @@
|
||||
# Uniswap Frontend
|
||||
# Uniswap Interface
|
||||
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://github.com/Uniswap/uniswap-interface/actions?query=workflow%3ATests)
|
||||
[](https://prettier.io/)
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://uniswap.org/docs/)
|
||||
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
@@ -13,11 +14,17 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
|
||||
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
|
||||
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
|
||||
## Accessing the frontend
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
To access the front end, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
|
||||
or visit [uniswap.exchange](https://uniswap.exchange).
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
## Listing a token
|
||||
|
||||
Please see the
|
||||
[@uniswap/default-token-list](https://github.com/uniswap/default-token-list)
|
||||
repository.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -27,31 +34,32 @@ or visit [uniswap.exchange](https://uniswap.exchange).
|
||||
yarn
|
||||
```
|
||||
|
||||
### Configure Environment (optional)
|
||||
|
||||
Copy `.env` to `.env.local` and change the appropriate variables.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
To have the frontend default to a different network, make a copy of `.env` named `.env.local`,
|
||||
change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g.
|
||||
`"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
|
||||
### Configuring the environment (optional)
|
||||
|
||||
Note that the front end only works properly on testnets where both
|
||||
To have the interface default to a different network when a wallet is not connected:
|
||||
|
||||
1. Make a copy of `.env` named `.env.local`
|
||||
2. Change `REACT_APP_NETWORK_ID` to `"{YOUR_NETWORK_ID}"`
|
||||
3. Change `REACT_APP_NETWORK_URL` to e.g. `"https://{YOUR_NETWORK_ID}.infura.io/v3/{YOUR_INFURA_KEY}"`
|
||||
|
||||
Note that the interface only works on testnets where both
|
||||
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
||||
[multicall](https://github.com/makerdao/multicall) are deployed.
|
||||
The frontend will not work on other networks.
|
||||
The interface will not work on other networks.
|
||||
|
||||
## Contributions
|
||||
|
||||
**Please open all pull requests against the `v2` branch.**
|
||||
CI checks will run against all PRs.
|
||||
**Please open all pull requests against the `master` branch.**
|
||||
CI checks will run against all PRs.
|
||||
|
||||
## Accessing Uniswap V1 interface
|
||||
## Accessing Uniswap Interface V1
|
||||
|
||||
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways linked
|
||||
from the [v1.0.0 release](https://github.com/Uniswap/uniswap-frontend/releases/tag/v1.0.0).
|
||||
The Uniswap Interface supports swapping against, and migrating or removing liquidity from Uniswap V1. However,
|
||||
if you would like to use Uniswap V1, the Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
|
||||
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"pluginsFile": false,
|
||||
"fixturesFolder": false,
|
||||
"supportFile": "cypress/support/index.js",
|
||||
"video": false
|
||||
"video": false,
|
||||
"defaultCommandTimeout": 10000
|
||||
}
|
||||
|
||||
@@ -16,4 +16,35 @@ describe('Add Liquidity', () => {
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('single token can be selected', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('redirects /add/token-token to add/token/token', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.url().should(
|
||||
'contain',
|
||||
'/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects /add/WETH-token to /add/WETH-address/token', () => {
|
||||
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.url().should(
|
||||
'contain',
|
||||
'/add/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects /add/token-WETH to /add/token/WETH-address', () => {
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.url().should(
|
||||
'contain',
|
||||
'/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TEST_ADDRESS_NEVER_USE } from '../support/commands'
|
||||
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
@@ -10,11 +10,6 @@ describe('Landing Page', () => {
|
||||
cy.url().should('include', '/swap')
|
||||
})
|
||||
|
||||
it('allows navigation to send', () => {
|
||||
cy.get('#send-nav-link').click()
|
||||
cy.url().should('include', '/send')
|
||||
})
|
||||
|
||||
it('allows navigation to pool', () => {
|
||||
cy.get('#pool-nav-link').click()
|
||||
cy.url().should('include', '/pool')
|
||||
@@ -22,6 +17,6 @@ describe('Landing Page', () => {
|
||||
|
||||
it('is connected', () => {
|
||||
cy.get('#web3-status-connected').click()
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE)
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
})
|
||||
|
||||
24
cypress/integration/lists.test.ts
Normal file
24
cypress/integration/lists.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
describe('Swap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('list selection persists', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.reload()
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').should('not.exist')
|
||||
})
|
||||
|
||||
it('change list', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
|
||||
cy.get('#currency-search-change-list-button').click()
|
||||
cy.get('#list-row-tokens-1inch-eth .select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', '1inch')
|
||||
})
|
||||
})
|
||||
8
cypress/integration/migrate-v1.test.ts
Normal file
8
cypress/integration/migrate-v1.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
describe('Migrate V1 Liquidity', () => {
|
||||
describe('Remove V1 liquidity', () => {
|
||||
it('renders the correct page', () => {
|
||||
cy.visit('/remove/v1/0x93bB63aFe1E0180d0eF100D774B473034fd60C36')
|
||||
cy.get('#remove-v1-exchange').should('contain', 'MKR/ETH')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,12 @@
|
||||
describe('Pool', () => {
|
||||
beforeEach(() => cy.visit('/pool'))
|
||||
it('can search for a pool', () => {
|
||||
it('add liquidity links to /add/ETH', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.get('#token-search-input').type('DAI')
|
||||
cy.url().should('contain', '/add/ETH')
|
||||
})
|
||||
|
||||
it('can import a pool', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.get('#import-pool-link').click({ force: true }) // blocked by the grid element in the search box
|
||||
cy.url().should('include', '/find')
|
||||
it('import pool links to /import', () => {
|
||||
cy.get('#import-pool-link').click()
|
||||
cy.url().should('contain', '/find')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
describe('Remove Liquidity', () => {
|
||||
it('redirects', () => {
|
||||
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.url().should(
|
||||
'contain',
|
||||
'/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
|
||||
)
|
||||
})
|
||||
|
||||
it('eth remove', () => {
|
||||
cy.visit('/remove/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('eth remove swap order', () => {
|
||||
cy.visit('/remove/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'MKR')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
describe('Send', () => {
|
||||
beforeEach(() => cy.visit('/send'))
|
||||
it('should redirect', () => {
|
||||
cy.visit('/send')
|
||||
cy.url().should('include', '/swap')
|
||||
})
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#sending-no-swap-input')
|
||||
.type('0.001')
|
||||
.should('have.value', '0.001')
|
||||
it('should redirect with url params', () => {
|
||||
cy.visit('/send?outputCurrency=ETH&recipient=bob.argent.xyz')
|
||||
cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,44 +1,77 @@
|
||||
describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('\\')
|
||||
.type('\\', { delay: 200 })
|
||||
.should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero output amount', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('can swap ETH for DAI', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 })
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.get('#show-advanced').click()
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||
})
|
||||
|
||||
it('add a recipient does not exist unless in expert mode', () => {
|
||||
cy.get('#add-recipient-button').should('not.exist')
|
||||
})
|
||||
|
||||
describe('expert mode', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'prompt').returns('confirm')
|
||||
})
|
||||
cy.get('#open-settings-dialog-button').click()
|
||||
cy.get('#toggle-expert-mode-button').click()
|
||||
cy.get('#confirm-expert-mode').click()
|
||||
})
|
||||
|
||||
it('add a recipient is visible', () => {
|
||||
cy.get('#add-recipient-button').should('be.visible')
|
||||
})
|
||||
|
||||
it('add a recipient', () => {
|
||||
cy.get('#add-recipient-button').click()
|
||||
cy.get('#recipient').should('exist')
|
||||
})
|
||||
|
||||
it('remove recipient', () => {
|
||||
cy.get('#add-recipient-button').click()
|
||||
cy.get('#remove-recipient-button').click()
|
||||
cy.get('#recipient').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
17
cypress/integration/token-warning.ts
Normal file
17
cypress/integration/token-warning.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
describe('Warning', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap?outputCurrency=0x0a40f26d74274b7f22b28556a27b35d97ce08e0a')
|
||||
})
|
||||
|
||||
it('Check that warning is displayed', () => {
|
||||
cy.get('.token-warning-container').should('be.visible')
|
||||
})
|
||||
|
||||
it('Check that warning hides after button dismissal', () => {
|
||||
cy.get('.token-dismiss-button').should('be.disabled')
|
||||
cy.get('.understand-checkbox').click()
|
||||
cy.get('.token-dismiss-button').should('not.be.disabled')
|
||||
cy.get('.token-dismiss-button').click()
|
||||
cy.get('.token-warning-container').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
2
cypress/support/commands.d.ts
vendored
2
cypress/support/commands.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
export const TEST_ADDRESS_NEVER_USE: string
|
||||
|
||||
export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
|
||||
|
||||
// declare namespace Cypress {
|
||||
// // eslint-disable-next-line @typescript-eslint/class-name-casing
|
||||
// interface cy {
|
||||
|
||||
@@ -14,6 +14,8 @@ const PRIVATE_KEY_TEST_NEVER_USE = '0xad20c82497421e9784f18460ad2fe84f73569068e9
|
||||
// address of the above key
|
||||
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
|
||||
|
||||
export const TEST_ADDRESS_NEVER_USE_SHORTENED = '0x0fF2...F4a5'
|
||||
|
||||
class CustomizedBridge extends _Eip1193Bridge {
|
||||
async sendAsync(...args) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
@@ -67,11 +69,12 @@ class CustomizedBridge extends _Eip1193Bridge {
|
||||
|
||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||
Cypress.Commands.overwrite('visit', (original, url, options) => {
|
||||
return original(url, {
|
||||
return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url, {
|
||||
...options,
|
||||
onBeforeLoad(win) {
|
||||
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
|
||||
win.localStorage.clear()
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
|
||||
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
|
||||
win.ethereum = new CustomizedBridge(signer, provider)
|
||||
}
|
||||
|
||||
27
netlify.toml
27
netlify.toml
@@ -1,27 +0,0 @@
|
||||
# block some countries
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/451.html"
|
||||
status = 451
|
||||
force = true
|
||||
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
|
||||
headers = {Link="<https://uniswap.exchange>"}
|
||||
|
||||
# forward migrate
|
||||
[[redirects]]
|
||||
from = "https://migrate.uniswap.exchange/*"
|
||||
to = "https://uniswap.exchange/migrate/v1"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
# forward v2 subdomain to apex
|
||||
[[redirects]]
|
||||
from = "https://v2.uniswap.exchange/*"
|
||||
to = "https://uniswap.exchange/:splat"
|
||||
status = 301
|
||||
|
||||
# support SPA setup
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
52
package.json
52
package.json
@@ -1,61 +1,62 @@
|
||||
{
|
||||
"name": "@uniswap/interface",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": "https://uniswap.exchange",
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@ethersproject/address": "^5.0.0-beta.134",
|
||||
"@ethersproject/bignumber": "^5.0.0-beta.138",
|
||||
"@ethersproject/constants": "^5.0.0-beta.133",
|
||||
"@ethersproject/contracts": "^5.0.0-beta.151",
|
||||
"@ethersproject/experimental": "^5.0.0-beta.141",
|
||||
"@ethersproject/providers": "5.0.0-beta.162",
|
||||
"@ethersproject/strings": "^5.0.0-beta.136",
|
||||
"@ethersproject/units": "^5.0.0-beta.132",
|
||||
"@ethersproject/wallet": "^5.0.0-beta.141",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"@ethersproject/experimental": "^5.0.1",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@reduxjs/toolkit": "^1.3.5",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/lodash.flatmap": "^4.5.6",
|
||||
"@types/multicodec": "^1.0.0",
|
||||
"@types/node": "^13.13.5",
|
||||
"@types/qs": "^6.9.2",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.8",
|
||||
"@types/react-router-dom": "^5.0.0",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.0",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/rebass": "^4.0.5",
|
||||
"@types/styled-components": "^4.2.0",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/sdk": "^2.0.5",
|
||||
"@uniswap/default-token-list": "^1.3.1",
|
||||
"@uniswap/sdk": "3.0.3-beta.1",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.15",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "1.0.0-beta.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"ajv": "^6.12.3",
|
||||
"cids": "^1.0.0",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cypress": "^4.5.0",
|
||||
"cypress": "^4.11.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"ethers": "^5.0.7",
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"inter-ui": "^3.13.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"multicodec": "^2.0.0",
|
||||
"multihashes": "^3.0.1",
|
||||
"polished": "^3.3.2",
|
||||
"prettier": "^1.17.0",
|
||||
"qrcode.react": "^0.9.3",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.13.1",
|
||||
"react-device-detect": "^1.6.2",
|
||||
@@ -69,26 +70,25 @@
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"serve": "^11.3.0",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"styled-components": "^4.2.0",
|
||||
"typescript": "^3.8.3",
|
||||
"use-media": "^1.4.0"
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@walletconnect/web3-provider": "1.1.1-alpha.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
|
||||
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
|
||||
"cy:run": "cypress run",
|
||||
"serve:build": "serve -s build -l 3000",
|
||||
"integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run"
|
||||
"integration-test": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run'"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
|
||||
75
public/locales/iw.json
Normal file
75
public/locales/iw.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"noWallet": "לא נמצא ארנק",
|
||||
"wrongNetwork": "נבחרה רשת לא נכונה",
|
||||
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
|
||||
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
|
||||
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
|
||||
"disconnected": "מנותק",
|
||||
"swap": "המרה",
|
||||
"send": "שליחה",
|
||||
"pool": "להפקיד",
|
||||
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
|
||||
"input": "מוכר",
|
||||
"output": "אקבל",
|
||||
"estimated": "הערכה",
|
||||
"balance": "בארנק שלי {{ balanceInput }}",
|
||||
"unlock": "שחרור נעילת ארנק",
|
||||
"pending": "ממתין לאישור",
|
||||
"selectToken": "בחרו את הטוקן להמרה",
|
||||
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
|
||||
"noExchange": "לא מתאפשרת המרה",
|
||||
"exchangeRate": "שער המרה",
|
||||
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
|
||||
"selectTokenCont": "בחרו טוקן כדי להמשיך",
|
||||
"noLiquidity": "אין נזילות",
|
||||
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
|
||||
"transactionDetails": "פרטי הטרנזקציה",
|
||||
"hideDetails": "הסתר פרטים נוספים",
|
||||
"youAreSelling": "למכירה",
|
||||
"orTransFail": "או שהטרנזקציה תיכשל",
|
||||
"youWillReceive": "תוצר המרה מינימלי",
|
||||
"youAreBuying": "קונה",
|
||||
"itWillCost": "זה יעלה",
|
||||
"insufficientBalance": "אין בחשבון מספיק מטבעות",
|
||||
"inputNotValid": "קלט לא תקין",
|
||||
"differentToken": "יש צורך בטוקנים שונים",
|
||||
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
|
||||
"invalidRecipient": "לא הוכנסה כתובת תקינה",
|
||||
"recipientAddress": "כתובת יעד",
|
||||
"youAreSending": "כמות לשליחה",
|
||||
"willReceive": "יתקבל לכל הפחות",
|
||||
"to": "אל",
|
||||
"addLiquidity": "להוספת נזילות למאגר",
|
||||
"deposit": "הפקדה",
|
||||
"currentPoolSize": "גודל מאגר הנזילות הכולל",
|
||||
"yourPoolShare": "חלקך במאגר הנזילות",
|
||||
"noZero": "אפס אינו ערך תקין",
|
||||
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
|
||||
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
|
||||
"youAreAdding": "מתווספים למאגר",
|
||||
"and": "וגם",
|
||||
"intoPool": "לתוך הנזילות",
|
||||
"outPool": "מתוך",
|
||||
"youWillMint": "יונפקו לכם",
|
||||
"liquidityTokens": "טוקנים של נזילות",
|
||||
"totalSupplyIs": "חלקך במאגר הנזילות",
|
||||
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||
"firstLiquidity": "אתה הראשוןה שמזרים נזילות למאגר",
|
||||
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||
"removeLiquidity": "הוצאה של נזילות",
|
||||
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
|
||||
"youAreRemoving": "יוסרו",
|
||||
"youWillRemove": "יוסרו",
|
||||
"createExchange": "ליצירת זוג מסחר",
|
||||
"invalidTokenAddress": "כתובת טוקן לא נכונה",
|
||||
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
|
||||
"invalidSymbol": "תו שגוי",
|
||||
"invalidDecimals": "ספרות עשרוניות שגויות",
|
||||
"tokenAddress": "כתובת הטוקן",
|
||||
"label": "שם",
|
||||
"decimals": "ספרות עשרויות",
|
||||
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
|
||||
}
|
||||
@@ -19,8 +19,8 @@
|
||||
"pending": "Đang chờ xử lý",
|
||||
"selectToken": "Chọn một đồng tiền ảo",
|
||||
"searchOrPaste": "Tìm kiếm tên, biểu tượng, hoặc địa chỉ của đồng tiền ảo",
|
||||
"searchOrPasteMobile": "Tên, Biểu tương, hoặc Địa chỉ",
|
||||
"noExchange": "Không Tìm Thấy Giao Dịch",
|
||||
"searchOrPasteMobile": "Tên, Biểu tượng, hoặc Địa chỉ",
|
||||
"noExchange": "Không tìm thấy giao dịch",
|
||||
"exchangeRate": "Tỷ giá",
|
||||
"unknownError": "Rất tiếc! Xảy ra lỗi không xác định. Vui lòng làm mới trang, hoặc truy cập từ trình duyệt hay thiết bị khác.",
|
||||
"enterValueCont": "Nhập một giá trị {{ missingCurrencyValue }} để tiếp tục.",
|
||||
@@ -29,7 +29,7 @@
|
||||
"insufficientLiquidity": "Không đủ tính thanh khoản.",
|
||||
"unlockTokenCont": "Vui lòng mở khoá đồng tiền ảo để tiếp tục",
|
||||
"transactionDetails": "Chi tiết nâng cao",
|
||||
"hideDetails": "Che giấu chi tiết",
|
||||
"hideDetails": "Ẩn chi tiết",
|
||||
"slippageWarning": "Cảnh báo trượt giá",
|
||||
"highSlippageWarning": "Cảnh báo trượt giá cao",
|
||||
"youAreSelling": "Bạn đang bán",
|
||||
@@ -59,7 +59,7 @@
|
||||
"intoPool": "vào nhóm thanh khoản.",
|
||||
"outPool": "từ nhóm thanh khoản.",
|
||||
"youWillMint": "Bạn sẽ đúc tiền",
|
||||
"liquidityTokens": "dồng thanh khoản.",
|
||||
"liquidityTokens": "đồng thanh khoản.",
|
||||
"totalSupplyIs": "Tổng cung hiện tại của đồng thanh khoản là",
|
||||
"youAreSettingExRate": "Bạn đang đặt tỷ giá hối đoái ban đầu thành",
|
||||
"totalSupplyIs0": "Tổng cung hiện tại của đồng thanh khoản là 0.",
|
||||
|
||||
BIN
src/assets/images/token-list/lists-dark.png
Normal file
BIN
src/assets/images/token-list/lists-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/images/token-list/lists-light.png
Normal file
BIN
src/assets/images/token-list/lists-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -6,21 +6,21 @@ import { LinkStyledButton } from '../../theme'
|
||||
import { CheckCircle, Copy } from 'react-feather'
|
||||
|
||||
const CopyIcon = styled(LinkStyledButton)`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
color: ${({ theme }) => theme.text3};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
margin-right: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: none;
|
||||
font-size: 0.825rem;
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
}
|
||||
`
|
||||
const TransactionStatusText = styled.span`
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.825rem;
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
`
|
||||
@@ -30,7 +30,6 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
|
||||
|
||||
return (
|
||||
<CopyIcon onClick={() => setCopied(props.toCopy)}>
|
||||
{props.children}
|
||||
{isCopied ? (
|
||||
<TransactionStatusText>
|
||||
<CheckCircle size={'16'} />
|
||||
@@ -41,6 +40,7 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
|
||||
<Copy size={'16'} />
|
||||
</TransactionStatusText>
|
||||
)}
|
||||
{isCopied ? '' : props.children}
|
||||
</CopyIcon>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,73 +1,60 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Check, Triangle } from 'react-feather'
|
||||
import { CheckCircle, Triangle } from 'react-feather'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { ExternalLink, Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
|
||||
import { transparentize } from 'polished'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { RowFixed } from '../Row'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const TransactionWrapper = styled.div`
|
||||
margin-top: 0.75rem;
|
||||
`
|
||||
const TransactionWrapper = styled.div``
|
||||
|
||||
const TransactionStatusText = styled.div`
|
||||
margin-right: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-decoration: none !important;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.25rem 0rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid;
|
||||
|
||||
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
|
||||
|
||||
border-color: ${({ pending, success, theme }) =>
|
||||
pending
|
||||
? transparentize(0.75, theme.primary1)
|
||||
: success
|
||||
? transparentize(0.75, theme.green1)
|
||||
: transparentize(0.75, theme.red1)};
|
||||
|
||||
:hover {
|
||||
border-color: ${({ pending, success, theme }) =>
|
||||
pending
|
||||
? transparentize(0, theme.primary1)
|
||||
: success
|
||||
? transparentize(0, theme.green1)
|
||||
: transparentize(0, theme.red1)};
|
||||
}
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
flex-shrink: 0;
|
||||
const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
|
||||
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
|
||||
`
|
||||
|
||||
export default function Transaction({ hash }: { hash: string }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
const summary = allTransactions?.[hash]?.summary
|
||||
const pending = !allTransactions?.[hash]?.receipt
|
||||
const success =
|
||||
!pending &&
|
||||
(allTransactions[hash].receipt.status === 1 || typeof allTransactions[hash].receipt.status === 'undefined')
|
||||
const tx = allTransactions?.[hash]
|
||||
const summary = tx?.summary
|
||||
const pending = !tx?.receipt
|
||||
const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined')
|
||||
|
||||
if (!chainId) return null
|
||||
|
||||
return (
|
||||
<TransactionWrapper>
|
||||
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
||||
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
|
||||
<IconWrapper>
|
||||
{pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />}
|
||||
<RowFixed>
|
||||
<TransactionStatusText>{summary ?? hash} ↗</TransactionStatusText>
|
||||
</RowFixed>
|
||||
<IconWrapper pending={pending} success={success}>
|
||||
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
|
||||
</IconWrapper>
|
||||
</TransactionState>
|
||||
</TransactionWrapper>
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { useCallback, useContext } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { clearAllTransactions } from '../../state/transactions/actions'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { AutoRow } from '../Row'
|
||||
import Copy from './Copy'
|
||||
import Transaction from './Transaction'
|
||||
@@ -18,9 +18,8 @@ import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import Identicon from '../Identicon'
|
||||
|
||||
import { ButtonEmpty } from '../Button'
|
||||
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
@@ -55,31 +54,31 @@ const UpperSection = styled.div`
|
||||
|
||||
const InfoCard = styled.div`
|
||||
padding: 1rem;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-row-gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const AccountGroupingRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
|
||||
div {
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const AccountSection = styled.div`
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
padding: 0rem 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
|
||||
`
|
||||
|
||||
const YourAccount = styled.div`
|
||||
@@ -94,28 +93,6 @@ const YourAccount = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const GreenCircle = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:first-child {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
margin-left: 12px;
|
||||
margin-right: 2px;
|
||||
background-color: ${({ theme }) => theme.green1};
|
||||
border-radius: 50%;
|
||||
}
|
||||
`
|
||||
|
||||
const CircleWrapper = styled.div`
|
||||
color: ${({ theme }) => theme.green1};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LowerSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
padding: 1.5rem;
|
||||
@@ -132,13 +109,14 @@ const LowerSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
const AccountControl = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)};
|
||||
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')};
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
@@ -146,22 +124,22 @@ const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
|
||||
|
||||
p {
|
||||
min-width: 0;
|
||||
margin: 0.5rem 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
const ConnectButtonRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
|
||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
margin-left: 1rem;
|
||||
font-size: 0.825rem;
|
||||
display: flex;
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.text2};
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
@@ -181,14 +159,17 @@ const CloseColor = styled(Close)`
|
||||
`
|
||||
|
||||
const WalletName = styled.div`
|
||||
padding-left: 0.5rem;
|
||||
width: initial;
|
||||
font-size: 0.825rem;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
& > img,
|
||||
span {
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
@@ -203,10 +184,12 @@ const TransactionListWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
`
|
||||
|
||||
const WalletAction = styled.div`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
margin-left: 16px;
|
||||
const WalletAction = styled(ButtonSecondary)`
|
||||
width: fit-content;
|
||||
font-weight: 400;
|
||||
margin-left: 8px;
|
||||
font-size: 0.825rem;
|
||||
padding: 4px 6px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
@@ -217,7 +200,7 @@ const MainWalletAction = styled(WalletAction)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
function renderTransactions(transactions) {
|
||||
function renderTransactions(transactions: string[]) {
|
||||
return (
|
||||
<TransactionListWrapper>
|
||||
{transactions.map((hash, i) => {
|
||||
@@ -229,8 +212,8 @@ function renderTransactions(transactions) {
|
||||
|
||||
interface AccountDetailsProps {
|
||||
toggleWalletModal: () => void
|
||||
pendingTransactions: any[]
|
||||
confirmedTransactions: any[]
|
||||
pendingTransactions: string[]
|
||||
confirmedTransactions: string[]
|
||||
ENSName?: string
|
||||
openOptions: () => void
|
||||
}
|
||||
@@ -255,39 +238,39 @@ export default function AccountDetails({
|
||||
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
|
||||
)
|
||||
.map(k => SUPPORTED_WALLETS[k].name)[0]
|
||||
return <WalletName>{name}</WalletName>
|
||||
return <WalletName>Connected with {name}</WalletName>
|
||||
}
|
||||
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<Identicon /> {formatConnectorName()}
|
||||
<Identicon />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={WalletConnectIcon} alt={'wallet connect logo'} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={CoinbaseWalletIcon} alt={'coinbase wallet logo'} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={FortmaticIcon} alt={'fortmatic logo'} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<>
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={PortisIcon} alt={'portis logo'} />
|
||||
<MainWalletAction
|
||||
onClick={() => {
|
||||
portis.portis.showPortis()
|
||||
@@ -299,15 +282,12 @@ export default function AccountDetails({
|
||||
</>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const clearAllTransactionsCallback = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
dispatch(clearAllTransactions({ chainId }))
|
||||
},
|
||||
[dispatch, chainId]
|
||||
)
|
||||
const clearAllTransactionsCallback = useCallback(() => {
|
||||
if (chainId) dispatch(clearAllTransactions({ chainId }))
|
||||
}, [dispatch, chainId])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -320,10 +300,11 @@ export default function AccountDetails({
|
||||
<YourAccount>
|
||||
<InfoCard>
|
||||
<AccountGroupingRow>
|
||||
{getStatusIcon()}
|
||||
{formatConnectorName()}
|
||||
<div>
|
||||
{connector !== injected && connector !== walletlink && (
|
||||
<WalletAction
|
||||
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
|
||||
onClick={() => {
|
||||
;(connector as any).close()
|
||||
}}
|
||||
@@ -331,73 +312,89 @@ export default function AccountDetails({
|
||||
Disconnect
|
||||
</WalletAction>
|
||||
)}
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
<div />
|
||||
</GreenCircle>
|
||||
</CircleWrapper>
|
||||
<WalletAction
|
||||
style={{ fontSize: '.825rem', fontWeight: 400 }}
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</WalletAction>
|
||||
</div>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow id="web3-account-identifier-row">
|
||||
{ENSName ? (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={true}>
|
||||
<p>{ENSName}</p> <Copy toCopy={account} />
|
||||
</AccountControl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<p>{account}</p> <Copy toCopy={account} />
|
||||
</AccountControl>
|
||||
</>
|
||||
)}
|
||||
<AccountControl>
|
||||
{ENSName ? (
|
||||
<>
|
||||
<div>
|
||||
{getStatusIcon()}
|
||||
<p> {ENSName}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{getStatusIcon()}
|
||||
<p> {account && shortenAddress(account)}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AccountControl>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow>
|
||||
{ENSName ? (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<AddressLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
View on Etherscan ↗
|
||||
</AddressLink>
|
||||
<AccountControl>
|
||||
<div>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={true}
|
||||
href={chainId && getEtherscanLink(chainId, ENSName, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
)}
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={false}
|
||||
href={getEtherscanLink(chainId, account, 'address')}
|
||||
>
|
||||
View on Etherscan ↗
|
||||
</AddressLink>
|
||||
<AccountControl>
|
||||
<div>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={false}
|
||||
href={getEtherscanLink(chainId, account, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
)}
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
)}
|
||||
</AccountGroupingRow>
|
||||
</InfoCard>
|
||||
</YourAccount>
|
||||
|
||||
{!(isMobile && (window.web3 || window.ethereum)) && (
|
||||
<ConnectButtonRow>
|
||||
<ButtonEmpty
|
||||
style={{ fontWeight: 400 }}
|
||||
padding={'12px'}
|
||||
width={'260px'}
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
>
|
||||
Connect to a different wallet
|
||||
</ButtonEmpty>
|
||||
</ConnectButtonRow>
|
||||
)}
|
||||
</AccountSection>
|
||||
</UpperSection>
|
||||
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||
<LowerSection>
|
||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
||||
<AutoRow mb={'1rem'} style={{ justifyContent: 'space-between' }}>
|
||||
<TYPE.body>Recent Transactions</TYPE.body>
|
||||
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
|
||||
</AutoRow>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import React, { useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import useENS from '../../hooks/useENS'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
@@ -24,6 +22,8 @@ const ContainerRow = styled.div<{ error: boolean }>`
|
||||
align-items: center;
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)};
|
||||
transition: border-color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')},
|
||||
color 500ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
`
|
||||
|
||||
@@ -39,6 +39,7 @@ const Input = styled.input<{ error?: boolean }>`
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -65,119 +66,59 @@ const Input = styled.input<{ error?: boolean }>`
|
||||
`
|
||||
|
||||
export default function AddressInputPanel({
|
||||
initialInput = '',
|
||||
onChange,
|
||||
onError
|
||||
id,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
initialInput?: string
|
||||
onChange: (val: { address: string; name?: string }) => void
|
||||
onError: (error: boolean, input: string) => void
|
||||
id?: string
|
||||
// the typed string value
|
||||
value: string
|
||||
// triggers whenever the typed value changes
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [input, setInput] = useState(initialInput ? initialInput : '')
|
||||
const debouncedInput = useDebounce(input, 200)
|
||||
const { address, loading, name } = useENS(value)
|
||||
|
||||
const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined })
|
||||
const [error, setError] = useState<boolean>(false)
|
||||
const handleInput = useCallback(
|
||||
event => {
|
||||
const input = event.target.value
|
||||
const withoutSpaces = input.replace(/\s+/g, '')
|
||||
onChange(withoutSpaces)
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
// keep data and errors in sync
|
||||
useEffect(() => {
|
||||
onChange({ address: data.address, name: data.name })
|
||||
}, [onChange, data.address, data.name])
|
||||
useEffect(() => {
|
||||
onError(error, input)
|
||||
}, [onError, error, input])
|
||||
|
||||
// run parser on debounced input
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
// if the input is an address, try to look up its name
|
||||
if (isAddress(debouncedInput)) {
|
||||
library
|
||||
.lookupAddress(debouncedInput)
|
||||
.then(name => {
|
||||
if (stale) return
|
||||
// if an ENS name exists, set it as the destination
|
||||
if (name) {
|
||||
setInput(name)
|
||||
} else {
|
||||
setData({ address: debouncedInput, name: '' })
|
||||
setError(null)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (stale) return
|
||||
setData({ address: debouncedInput, name: '' })
|
||||
setError(null)
|
||||
})
|
||||
}
|
||||
// otherwise try to look up the address of the input, treated as an ENS name
|
||||
else {
|
||||
if (debouncedInput !== '') {
|
||||
library
|
||||
.resolveName(debouncedInput)
|
||||
.then(address => {
|
||||
if (stale) return
|
||||
// if the debounced input name resolves to an address
|
||||
if (address) {
|
||||
setData({ address: address, name: debouncedInput })
|
||||
setError(null)
|
||||
} else {
|
||||
setError(true)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (stale) return
|
||||
setError(true)
|
||||
})
|
||||
} else if (debouncedInput === '') {
|
||||
setError(true)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [debouncedInput, library])
|
||||
|
||||
function onInput(event) {
|
||||
setData({ address: undefined, name: undefined })
|
||||
setError(false)
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace
|
||||
setInput(checksummedInput || input)
|
||||
}
|
||||
const error = Boolean(value.length > 0 && !loading && !address)
|
||||
|
||||
return (
|
||||
<InputPanel>
|
||||
<ContainerRow error={input !== '' && error}>
|
||||
<InputPanel id={id}>
|
||||
<ContainerRow error={error}>
|
||||
<InputContainer>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||
Recipient
|
||||
</TYPE.black>
|
||||
{data.address && (
|
||||
<ExternalLink
|
||||
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
|
||||
style={{ fontSize: '14px' }}
|
||||
>
|
||||
{address && chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, name ?? address, 'address')} style={{ fontSize: '14px' }}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
)}
|
||||
</RowBetween>
|
||||
<Input
|
||||
className="recipient-address-input"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
placeholder="Wallet Address or ENS name"
|
||||
error={input !== '' && error}
|
||||
onChange={onInput}
|
||||
value={input}
|
||||
error={error}
|
||||
pattern="^(0x[a-fA-F0-9]{40})$"
|
||||
onChange={handleInput}
|
||||
value={value}
|
||||
/>
|
||||
</AutoColumn>
|
||||
</InputContainer>
|
||||
|
||||
@@ -6,7 +6,12 @@ import { RowBetween } from '../Row'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Button as RebassButton, ButtonProps } from 'rebass/styled-components'
|
||||
|
||||
const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadius?: string }>`
|
||||
const Base = styled(RebassButton)<{
|
||||
padding?: string
|
||||
width?: string
|
||||
borderRadius?: string
|
||||
altDisabledStyle?: boolean
|
||||
}>`
|
||||
padding: ${({ padding }) => (padding ? padding : '18px')};
|
||||
width: ${({ width }) => (width ? width : '100%')};
|
||||
font-weight: 500;
|
||||
@@ -16,11 +21,14 @@ const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadi
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
&:disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
@@ -45,10 +53,13 @@ export const ButtonPrimary = styled(Base)`
|
||||
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.text3}
|
||||
background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)};
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -68,6 +79,16 @@ export const ButtonLight = styled(Base)`
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||
}
|
||||
:disabled {
|
||||
opacity: 0.4;
|
||||
:hover {
|
||||
cursor: auto;
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonGray = styled(Base)`
|
||||
@@ -180,7 +201,6 @@ export const ButtonEmpty = styled(Base)`
|
||||
export const ButtonWhite = styled(Base)`
|
||||
border: 1px solid #edeef2;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
};
|
||||
color: black;
|
||||
|
||||
&:focus {
|
||||
@@ -228,14 +248,21 @@ const ButtonErrorStyle = styled(Base)`
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
background-color: ${({ theme }) => theme.red1};
|
||||
border: 1px solid ${({ theme }) => theme.red1};
|
||||
}
|
||||
`
|
||||
|
||||
export function ButtonConfirmed({ confirmed, ...rest }: { confirmed?: boolean } & ButtonProps) {
|
||||
export function ButtonConfirmed({
|
||||
confirmed,
|
||||
altDisabledStyle,
|
||||
...rest
|
||||
}: { confirmed?: boolean; altDisabledStyle?: boolean } & ButtonProps) {
|
||||
if (confirmed) {
|
||||
return <ButtonConfirmedStyle {...rest} />
|
||||
} else {
|
||||
return <ButtonPrimary {...rest} />
|
||||
return <ButtonPrimary {...rest} altDisabledStyle={altDisabledStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +274,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonPrimary {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
@@ -258,7 +285,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonOutlined {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
|
||||
@@ -19,11 +19,11 @@ export const LightCard = styled(Card)`
|
||||
`
|
||||
|
||||
export const GreyCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const OutlineCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.advancedBG};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const YellowCard = styled(Card)`
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import React, { useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import Modal from '../Modal'
|
||||
import Loader from '../Loader'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { CloseIcon } from '../../theme/components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { ArrowUpCircle } from 'react-feather'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
const Section = styled(AutoColumn)`
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const BottomSection = styled(Section)`
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
hash: string
|
||||
topContent: () => React.ReactChild
|
||||
bottomContent: () => React.ReactChild
|
||||
attemptingTxn: boolean
|
||||
pendingConfirmation: boolean
|
||||
pendingText: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export default function ConfirmationModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
hash,
|
||||
topContent,
|
||||
bottomContent,
|
||||
attemptingTxn,
|
||||
pendingConfirmation,
|
||||
pendingText,
|
||||
title = ''
|
||||
}: ConfirmationModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
{!attemptingTxn ? (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{title}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
{topContent()}
|
||||
</Section>
|
||||
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
{pendingConfirmation ? (
|
||||
<Loader size="90px" />
|
||||
) : (
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
)}
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
|
||||
</Text>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
|
||||
{pendingText}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
{!pendingConfirmation && (
|
||||
<>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
View on Etherscan
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pendingConfirmation && (
|
||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||
Confirm this transaction in your wallet
|
||||
</Text>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</Wrapper>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import { Currency, Pair } from '@uniswap/sdk'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import SearchModal from '../SearchModal'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE, CursorPointer } from '../../theme'
|
||||
import { TYPE } from '../../theme'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
|
||||
@@ -49,7 +48,6 @@ const LabelRow = styled.div`
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0.75rem 1rem 0 1rem;
|
||||
height: 20px;
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => darken(0.2, theme.text2)};
|
||||
@@ -118,48 +116,48 @@ const StyledBalanceMax = styled.button`
|
||||
|
||||
interface CurrencyInputPanelProps {
|
||||
value: string
|
||||
field: string
|
||||
onUserInput: (field: string, val: string) => void
|
||||
onUserInput: (value: string) => void
|
||||
onMax?: () => void
|
||||
showMaxButton: boolean
|
||||
label?: string
|
||||
onTokenSelection?: (tokenAddress: string) => void
|
||||
token?: Token | null
|
||||
disableTokenSelect?: boolean
|
||||
onCurrencySelect?: (currency: Currency) => void
|
||||
currency?: Currency | null
|
||||
disableCurrencySelect?: boolean
|
||||
hideBalance?: boolean
|
||||
isExchange?: boolean
|
||||
pair?: Pair | null
|
||||
hideInput?: boolean
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedTokenAddress?: string | null
|
||||
otherCurrency?: Currency | null
|
||||
id: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export default function CurrencyInputPanel({
|
||||
value,
|
||||
field,
|
||||
onUserInput,
|
||||
onMax,
|
||||
showMaxButton,
|
||||
label = 'Input',
|
||||
onTokenSelection = null,
|
||||
token = null,
|
||||
disableTokenSelect = false,
|
||||
onCurrencySelect,
|
||||
currency,
|
||||
disableCurrencySelect = false,
|
||||
hideBalance = false,
|
||||
isExchange = false,
|
||||
pair = null, // used for double token logo
|
||||
hideInput = false,
|
||||
showSendWithSwap = false,
|
||||
otherSelectedTokenAddress = null,
|
||||
id
|
||||
otherCurrency,
|
||||
id,
|
||||
showCommonBases
|
||||
}: CurrencyInputPanelProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account } = useActiveWeb3React()
|
||||
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
}, [setModalOpen])
|
||||
|
||||
return (
|
||||
<InputPanel id={id}>
|
||||
<Container hideInput={hideInput}>
|
||||
@@ -170,83 +168,77 @@ export default function CurrencyInputPanel({
|
||||
{label}
|
||||
</TYPE.body>
|
||||
{account && (
|
||||
<CursorPointer>
|
||||
<TYPE.body
|
||||
onClick={onMax}
|
||||
color={theme.text2}
|
||||
fontWeight={500}
|
||||
fontSize={14}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
{!hideBalance && !!token && userTokenBalance
|
||||
? 'Balance: ' + userTokenBalance?.toSignificant(6)
|
||||
: ' -'}
|
||||
</TYPE.body>
|
||||
</CursorPointer>
|
||||
<TYPE.body
|
||||
onClick={onMax}
|
||||
color={theme.text2}
|
||||
fontWeight={500}
|
||||
fontSize={14}
|
||||
style={{ display: 'inline', cursor: 'pointer' }}
|
||||
>
|
||||
{!hideBalance && !!currency && selectedCurrencyBalance
|
||||
? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
|
||||
: ' -'}
|
||||
</TYPE.body>
|
||||
)}
|
||||
</RowBetween>
|
||||
</LabelRow>
|
||||
)}
|
||||
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableTokenSelect}>
|
||||
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableCurrencySelect}>
|
||||
{!hideInput && (
|
||||
<>
|
||||
<NumericalInput
|
||||
className="token-amount-input"
|
||||
value={value}
|
||||
onUserInput={val => {
|
||||
onUserInput(field, val)
|
||||
onUserInput(val)
|
||||
}}
|
||||
/>
|
||||
{account && !!token?.address && showMaxButton && label !== 'To' && (
|
||||
{account && currency && showMaxButton && label !== 'To' && (
|
||||
<StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CurrencySelect
|
||||
selected={!!token}
|
||||
selected={!!currency}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (!disableTokenSelect) {
|
||||
if (!disableCurrencySelect) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Aligner>
|
||||
{isExchange ? (
|
||||
<DoubleLogo a0={pair?.token0.address} a1={pair?.token1.address} size={24} margin={true} />
|
||||
) : token?.address ? (
|
||||
<TokenLogo address={token?.address} size={'24px'} />
|
||||
{pair ? (
|
||||
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
|
||||
) : currency ? (
|
||||
<CurrencyLogo currency={currency} size={'24px'} />
|
||||
) : null}
|
||||
{isExchange ? (
|
||||
{pair ? (
|
||||
<StyledTokenName className="pair-name-container">
|
||||
{pair?.token0.symbol}:{pair?.token1.symbol}
|
||||
</StyledTokenName>
|
||||
) : (
|
||||
<StyledTokenName className="token-symbol-container" active={Boolean(token && token.symbol)}>
|
||||
{(token && token.symbol && token.symbol.length > 20
|
||||
? token.symbol.slice(0, 4) +
|
||||
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
|
||||
{(currency && currency.symbol && currency.symbol.length > 20
|
||||
? currency.symbol.slice(0, 4) +
|
||||
'...' +
|
||||
token.symbol.slice(token.symbol.length - 5, token.symbol.length)
|
||||
: token?.symbol) || t('selectToken')}
|
||||
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
|
||||
: currency?.symbol) || t('selectToken')}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
{!disableTokenSelect && <StyledDropDown selected={!!token?.address} />}
|
||||
{!disableCurrencySelect && <StyledDropDown selected={!!currency} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
</Container>
|
||||
{!disableTokenSelect && (
|
||||
<SearchModal
|
||||
{!disableCurrencySelect && onCurrencySelect && (
|
||||
<CurrencySearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={() => {
|
||||
setModalOpen(false)
|
||||
}}
|
||||
filterType="tokens"
|
||||
onTokenSelect={onTokenSelection}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hiddenToken={token?.address}
|
||||
otherSelectedTokenAddress={otherSelectedTokenAddress}
|
||||
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
|
||||
onDismiss={handleDismissSearch}
|
||||
onCurrencySelect={onCurrencySelect}
|
||||
selectedCurrency={currency}
|
||||
otherSelectedCurrency={otherCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
|
||||
54
src/components/CurrencyLogo/index.tsx
Normal file
54
src/components/CurrencyLogo/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
import useHttpLocations from '../../hooks/useHttpLocations'
|
||||
import { WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import Logo from '../Logo'
|
||||
|
||||
const getTokenLogoURL = (address: string) =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
|
||||
const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
border-radius: 24px;
|
||||
`
|
||||
|
||||
const StyledLogo = styled(Logo)<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
export default function CurrencyLogo({
|
||||
currency,
|
||||
size = '24px',
|
||||
style
|
||||
}: {
|
||||
currency?: Currency
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
|
||||
|
||||
const srcs: string[] = useMemo(() => {
|
||||
if (currency === ETHER) return []
|
||||
|
||||
if (currency instanceof Token) {
|
||||
if (currency instanceof WrappedTokenInfo) {
|
||||
return [...uriLocations, getTokenLogoURL(currency.address)]
|
||||
}
|
||||
|
||||
return [getTokenLogoURL(currency.address)]
|
||||
}
|
||||
return []
|
||||
}, [currency, uriLocations])
|
||||
|
||||
if (currency === ETHER) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} style={style} />
|
||||
}
|
||||
|
||||
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} />
|
||||
}
|
||||
@@ -1,34 +1,40 @@
|
||||
import { Currency } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
|
||||
const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>`
|
||||
const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'};
|
||||
`
|
||||
|
||||
interface DoubleTokenLogoProps {
|
||||
interface DoubleCurrencyLogoProps {
|
||||
margin?: boolean
|
||||
size?: number
|
||||
a0: string
|
||||
a1: string
|
||||
currency0?: Currency
|
||||
currency1?: Currency
|
||||
}
|
||||
|
||||
const HigherLogo = styled(TokenLogo)`
|
||||
const HigherLogo = styled(CurrencyLogo)`
|
||||
z-index: 2;
|
||||
`
|
||||
const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>`
|
||||
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
|
||||
position: absolute;
|
||||
left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'};
|
||||
`
|
||||
|
||||
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
|
||||
export default function DoubleCurrencyLogo({
|
||||
currency0,
|
||||
currency1,
|
||||
size = 16,
|
||||
margin = false
|
||||
}: DoubleCurrencyLogoProps) {
|
||||
return (
|
||||
<TokenWrapper sizeraw={size} margin={margin}>
|
||||
<HigherLogo address={a0} size={size.toString() + 'px'} />
|
||||
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />
|
||||
</TokenWrapper>
|
||||
<Wrapper sizeraw={size} margin={margin}>
|
||||
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
|
||||
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Send, Sun, Moon } from 'react-feather'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
const FooterFrame = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Footer() {
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<FooterFrame>
|
||||
<form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank">
|
||||
<ButtonSecondary p="8px 12px">
|
||||
<Send size={16} style={{ marginRight: '8px' }} /> Feedback
|
||||
</ButtonSecondary>
|
||||
</form>
|
||||
<ButtonSecondary onClick={toggleDarkMode} p="8px 12px" ml="0.5rem" width="min-content">
|
||||
{darkMode ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</ButtonSecondary>
|
||||
</FooterFrame>
|
||||
)
|
||||
}
|
||||
76
src/components/Header/VersionSwitch.tsx
Normal file
76
src/components/Header/VersionSwitch.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
|
||||
const VersionLabel = styled.span<{ enabled: boolean }>`
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
|
||||
font-size: 1rem;
|
||||
font-weight: ${({ enabled }) => (enabled ? '500' : '400')};
|
||||
:hover {
|
||||
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
|
||||
}
|
||||
`
|
||||
|
||||
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
|
||||
border-radius: 12px;
|
||||
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
|
||||
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
|
||||
background: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function VersionSwitch() {
|
||||
const version = useToggledVersion()
|
||||
const location = useLocation()
|
||||
const query = useParsedQueryString()
|
||||
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
|
||||
|
||||
const toggleDest = useMemo(() => {
|
||||
return versionSwitchAvailable
|
||||
? {
|
||||
...location,
|
||||
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
|
||||
}
|
||||
: location
|
||||
}, [location, query, version, versionSwitchAvailable])
|
||||
|
||||
const handleClick = useCallback(
|
||||
e => {
|
||||
if (!versionSwitchAvailable) e.preventDefault()
|
||||
},
|
||||
[versionSwitchAvailable]
|
||||
)
|
||||
|
||||
const toggle = (
|
||||
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
|
||||
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
|
||||
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)
|
||||
return versionSwitchAvailable ? (
|
||||
toggle
|
||||
) : (
|
||||
<MouseoverTooltip text="This page is only compatible with Uniswap V2.">{toggle}</MouseoverTooltip>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { Link as HistoryLink } from 'react-router-dom'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import Row from '../Row'
|
||||
import Menu from '../Menu'
|
||||
import Web3Status from '../Web3Status'
|
||||
|
||||
import { ExternalLink, StyledInternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { YellowCard } from '../Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import Logo from '../../assets/svg/logo.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import LogoDark from '../../assets/svg/logo_white.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
import { useETHBalances } from '../../state/wallet/hooks'
|
||||
|
||||
import { YellowCard } from '../Card'
|
||||
import Settings from '../Settings'
|
||||
import Menu from '../Menu'
|
||||
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import Web3Status from '../Web3Status'
|
||||
import VersionSwitch from './VersionSwitch'
|
||||
|
||||
const HeaderFrame = styled.div`
|
||||
display: flex;
|
||||
@@ -31,15 +29,12 @@ const HeaderFrame = styled.div`
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
z-index: 2;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
padding: 12px 0 0 0;
|
||||
width: calc(100%);
|
||||
position: relative;
|
||||
`};
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const HeaderElement = styled.div`
|
||||
@@ -47,7 +42,16 @@ const HeaderElement = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
const HeaderElementWrap = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
margin-top: 0.5rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const Title = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
@@ -72,6 +76,7 @@ const AccountElement = styled.div<{ active: boolean }>`
|
||||
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
:focus {
|
||||
border: 1px solid blue;
|
||||
@@ -82,10 +87,7 @@ const TestnetWrapper = styled.div`
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
margin-left: 10px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
const NetworkCard = styled(YellowCard)`
|
||||
@@ -95,125 +97,82 @@ const NetworkCard = styled(YellowCard)`
|
||||
padding: 8px 12px;
|
||||
`
|
||||
|
||||
const UniIcon = styled(HistoryLink)<{ to: string }>`
|
||||
const UniIcon = styled.div`
|
||||
transition: transform 0.3s ease;
|
||||
:hover {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
img {
|
||||
width: 4.5rem;
|
||||
}
|
||||
`};
|
||||
`
|
||||
|
||||
const MigrateBanner = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
const HeaderControls = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
color: ${({ theme }) => theme.primaryText1};
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
pointer-events: auto;
|
||||
a {
|
||||
color: ${({ theme }) => theme.primaryText1};
|
||||
}
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
const BalanceText = styled(Text)`
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const VersionLabel = styled.span<{ isV2?: boolean }>`
|
||||
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
|
||||
}
|
||||
`
|
||||
|
||||
const VersionToggle = styled.a`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
|
||||
[ChainId.MAINNET]: null,
|
||||
[ChainId.RINKEBY]: 'Rinkeby',
|
||||
[ChainId.ROPSTEN]: 'Ropsten',
|
||||
[ChainId.GÖRLI]: 'Görli',
|
||||
[ChainId.KOVAN]: 'Kovan'
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const userEthBalance = useTokenBalanceTreatingWETHasETH(account, WETH[chainId])
|
||||
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
|
||||
const [isDark] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<HeaderFrame>
|
||||
<MigrateBanner>
|
||||
Uniswap V2 is live! Read the
|
||||
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<b>blog post ↗</b>
|
||||
</ExternalLink>
|
||||
or
|
||||
<StyledInternalLink to="/migrate/v1">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</StyledInternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween padding="1rem">
|
||||
<RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
|
||||
<HeaderElement>
|
||||
<Title>
|
||||
<UniIcon id="link" to="/">
|
||||
<Title href=".">
|
||||
<UniIcon>
|
||||
<img src={isDark ? LogoDark : Logo} alt="logo" />
|
||||
</UniIcon>
|
||||
{!isMobile && (
|
||||
<TitleText>
|
||||
<HistoryLink id="link" to="/">
|
||||
<img
|
||||
style={{ marginLeft: '4px', marginTop: '4px' }}
|
||||
src={isDark ? WordmarkDark : Wordmark}
|
||||
alt="logo"
|
||||
/>
|
||||
</HistoryLink>
|
||||
</TitleText>
|
||||
)}
|
||||
<TitleText>
|
||||
<img style={{ marginLeft: '4px', marginTop: '4px' }} src={isDark ? WordmarkDark : Wordmark} alt="logo" />
|
||||
</TitleText>
|
||||
</Title>
|
||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
|
||||
{!isMobile && (
|
||||
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
|
||||
<VersionLabel isV2={true}>V2</VersionLabel>
|
||||
<VersionLabel isV2={false}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)}
|
||||
</TestnetWrapper>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
<div style={{ pointerEvents: 'auto' }}>
|
||||
<HeaderControls>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && chainId && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<BalanceText style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</BalanceText>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
</HeaderElement>
|
||||
<HeaderElementWrap>
|
||||
<VersionSwitch />
|
||||
<Settings />
|
||||
<Menu />
|
||||
</div>
|
||||
</HeaderElement>
|
||||
</HeaderElementWrap>
|
||||
</HeaderControls>
|
||||
</RowBetween>
|
||||
</HeaderFrame>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import Jazzicon from 'jazzicon'
|
||||
|
||||
const StyledIdenticon = styled.div`
|
||||
const StyledIdenticonContainer = styled.div`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 1.125rem;
|
||||
@@ -24,5 +24,6 @@ export default function Identicon() {
|
||||
}
|
||||
}, [account])
|
||||
|
||||
return <StyledIdenticon ref={ref} />
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
|
||||
return <StyledIdenticonContainer ref={ref as any} />
|
||||
}
|
||||
|
||||
26
src/components/ListLogo/index.tsx
Normal file
26
src/components/ListLogo/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useHttpLocations from '../../hooks/useHttpLocations'
|
||||
|
||||
import Logo from '../Logo'
|
||||
|
||||
const StyledListLogo = styled(Logo)<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
export default function ListLogo({
|
||||
logoURI,
|
||||
style,
|
||||
size = '24px',
|
||||
alt
|
||||
}: {
|
||||
logoURI: string
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
alt?: string
|
||||
}) {
|
||||
const srcs: string[] = useHttpLocations(logoURI)
|
||||
|
||||
return <StyledListLogo alt={alt} size={size} srcs={srcs} style={style} />
|
||||
}
|
||||
@@ -1,15 +1,38 @@
|
||||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`
|
||||
|
||||
export default function Loader({ size }: { size: string }) {
|
||||
return <SpinnerWrapper src={Circle} alt="loader" size={size} />
|
||||
const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
|
||||
animation: 2s ${rotate} linear infinite;
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
path {
|
||||
stroke: ${({ stroke, theme }) => stroke ?? theme.primary1};
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Takes in custom size and stroke for circle color, default to primary color as fill,
|
||||
* need ...rest for layered styles on top
|
||||
*/
|
||||
export default function Loader({ size = '16px', stroke, ...rest }: { size?: string; stroke?: string }) {
|
||||
return (
|
||||
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
|
||||
<path
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</StyledSVG>
|
||||
)
|
||||
}
|
||||
|
||||
34
src/components/Logo/index.tsx
Normal file
34
src/components/Logo/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react'
|
||||
import { HelpCircle } from 'react-feather'
|
||||
import { ImageProps } from 'rebass'
|
||||
|
||||
const BAD_SRCS: { [tokenAddress: string]: true } = {}
|
||||
|
||||
export interface LogoProps extends Pick<ImageProps, 'style' | 'alt' | 'className'> {
|
||||
srcs: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
|
||||
*/
|
||||
export default function Logo({ srcs, alt, ...rest }: LogoProps) {
|
||||
const [, refresh] = useState<number>(0)
|
||||
|
||||
const src: string | undefined = srcs.find(src => !BAD_SRCS[src])
|
||||
|
||||
if (src) {
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
alt={alt}
|
||||
src={src}
|
||||
onError={() => {
|
||||
if (src) BAD_SRCS[src] = true
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <HelpCircle {...rest} />
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { Info, BookOpen, Code, PieChart, MessageCircle } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
@@ -77,35 +78,17 @@ const MenuItem = styled(ExternalLink)`
|
||||
}
|
||||
`
|
||||
|
||||
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
|
||||
: 'https://github.com/Uniswap/uniswap-frontend'
|
||||
const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
|
||||
|
||||
export default function Menu() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [open, toggle] = useToggle(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
|
||||
<StyledMenu ref={node as any}>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuIcon />
|
||||
</StyledMenuButton>
|
||||
@@ -123,7 +106,7 @@ export default function Menu() {
|
||||
<Code size={14} />
|
||||
Code
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://discord.gg/vXCdddD">
|
||||
<MenuItem id="link" href="https://discord.gg/EwFs3Pp">
|
||||
<MessageCircle size={14} />
|
||||
Discord
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { animated, useTransition, useSpring } from 'react-spring'
|
||||
import { Spring } from 'react-spring/renderprops'
|
||||
|
||||
import { DialogOverlay, DialogContent } from '@reach/dialog'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import '@reach/dialog/styles.css'
|
||||
@@ -11,39 +9,28 @@ import { useGesture } from 'react-use-gesture'
|
||||
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverlay {...rest} />)<{ mobile: boolean }>`
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: 2;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
css`
|
||||
align-items: flex-end;
|
||||
`}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: ${({ theme }) => theme.modalBG};
|
||||
opacity: 0.5;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
}
|
||||
background-color: ${({ theme }) => theme.modalBG};
|
||||
}
|
||||
`
|
||||
|
||||
const AnimatedDialogContent = animated(DialogContent)
|
||||
// destructure to not pass custom props to Dialog DOM element
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
|
||||
<DialogContent aria-label="content" {...rest} />
|
||||
))`
|
||||
<AnimatedDialogContent {...rest} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog'
|
||||
})`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.bg1};
|
||||
@@ -51,6 +38,9 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)};
|
||||
padding: 0px;
|
||||
width: 50vw;
|
||||
overflow: hidden;
|
||||
|
||||
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
|
||||
|
||||
max-width: 420px;
|
||||
${({ maxHeight }) =>
|
||||
@@ -67,12 +57,10 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
width: 65vw;
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
||||
width: 85vw;
|
||||
max-height: 66vh;
|
||||
${mobile &&
|
||||
css`
|
||||
width: 100vw;
|
||||
@@ -84,14 +72,6 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
}
|
||||
`
|
||||
|
||||
const HiddenCloseButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
`
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
@@ -106,104 +86,53 @@ export default function Modal({
|
||||
onDismiss,
|
||||
minHeight = false,
|
||||
maxHeight = 50,
|
||||
initialFocusRef = null,
|
||||
initialFocusRef,
|
||||
children
|
||||
}: ModalProps) {
|
||||
const transitions = useTransition(isOpen, null, {
|
||||
const fadeTransition = useTransition(isOpen, null, {
|
||||
config: { duration: 200 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 }
|
||||
})
|
||||
|
||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
||||
const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
|
||||
const bind = useGesture({
|
||||
onDrag: state => {
|
||||
let velocity = state.velocity
|
||||
if (velocity < 1) {
|
||||
velocity = 1
|
||||
}
|
||||
if (velocity > 8) {
|
||||
velocity = 8
|
||||
}
|
||||
set({
|
||||
xy: state.down ? state.movement : [0, 0],
|
||||
config: { mass: 1, tension: 210, friction: 20 }
|
||||
y: state.down ? state.movement[1] : 0
|
||||
})
|
||||
if (velocity > 3 && state.direction[1] > 0) {
|
||||
if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
{transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={true}
|
||||
return (
|
||||
<>
|
||||
{fadeTransition.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent
|
||||
{...(isMobile
|
||||
? {
|
||||
...bind(),
|
||||
style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
|
||||
}
|
||||
: {})}
|
||||
aria-label="dialog content"
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
||||
}}
|
||||
to={{
|
||||
transform: isOpen ? 'translateY(0px)' : 'translateY(200px)'
|
||||
}}
|
||||
>
|
||||
{props => (
|
||||
<animated.div
|
||||
{...bind()}
|
||||
style={{
|
||||
transform: (xy as any).interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`)
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
)}
|
||||
</Spring>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={false}
|
||||
>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,18 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
|
||||
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
|
||||
import { NavLink, Link as HistoryLink } from 'react-router-dom'
|
||||
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
|
||||
const tabOrder = [
|
||||
{
|
||||
path: '/swap',
|
||||
textKey: 'swap',
|
||||
regex: /\/swap/
|
||||
},
|
||||
{
|
||||
path: '/send',
|
||||
textKey: 'send',
|
||||
regex: /\/send/
|
||||
},
|
||||
{
|
||||
path: '/pool',
|
||||
textKey: 'pool',
|
||||
regex: /\/pool/
|
||||
}
|
||||
]
|
||||
|
||||
const Tabs = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
border-radius: 3rem;
|
||||
justify-content: space-evenly;
|
||||
`
|
||||
|
||||
const activeClassName = 'ACTIVE'
|
||||
@@ -43,7 +24,6 @@ const StyledNavLink = styled(NavLink).attrs({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3rem;
|
||||
flex: 1 0 auto;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@@ -68,89 +48,54 @@ const ActiveText = styled.div`
|
||||
font-size: 20px;
|
||||
`
|
||||
|
||||
const ArrowLink = styled(ArrowLeft)`
|
||||
const StyledArrowLeft = styled(ArrowLeft)`
|
||||
color: ${({ theme }) => theme.text1};
|
||||
`
|
||||
|
||||
function NavigationTabs({ location: { pathname }, history }: RouteComponentProps<{}>) {
|
||||
export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const navigate = useCallback(
|
||||
direction => {
|
||||
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
|
||||
history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path)
|
||||
},
|
||||
[pathname, history]
|
||||
)
|
||||
const navigateRight = useCallback(() => {
|
||||
navigate(1)
|
||||
}, [navigate])
|
||||
const navigateLeft = useCallback(() => {
|
||||
navigate(-1)
|
||||
}, [navigate])
|
||||
|
||||
useBodyKeyDown('ArrowRight', navigateRight)
|
||||
useBodyKeyDown('ArrowLeft', navigateLeft)
|
||||
|
||||
const adding = pathname.match('/add')
|
||||
const removing = pathname.match('/remove')
|
||||
const finding = pathname.match('/find')
|
||||
const creating = pathname.match('/create')
|
||||
|
||||
return (
|
||||
<>
|
||||
{adding || removing ? (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<CursorPointer onClick={() => history.push('/pool')}>
|
||||
<ArrowLink />
|
||||
</CursorPointer>
|
||||
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
) : finding ? (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<ArrowLink />
|
||||
</HistoryLink>
|
||||
<ActiveText>Import Pool</ActiveText>
|
||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
) : creating ? (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<ArrowLink />
|
||||
</HistoryLink>
|
||||
<ActiveText>Create Pool</ActiveText>
|
||||
<QuestionHelper text={'Use this interface to create a new pool.'} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
) : (
|
||||
<Tabs style={{ marginBottom: '20px' }}>
|
||||
{tabOrder.map(({ path, textKey, regex }) => (
|
||||
<StyledNavLink
|
||||
id={`${textKey}-nav-link`}
|
||||
key={path}
|
||||
to={path}
|
||||
isActive={(_, { pathname }) => !!pathname.match(regex)}
|
||||
>
|
||||
{t(textKey)}
|
||||
</StyledNavLink>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</>
|
||||
<Tabs style={{ marginBottom: '20px' }}>
|
||||
<StyledNavLink id={`swap-nav-link`} to={'/swap'} isActive={() => active === 'swap'}>
|
||||
{t('swap')}
|
||||
</StyledNavLink>
|
||||
<StyledNavLink id={`pool-nav-link`} to={'/pool'} isActive={() => active === 'pool'}>
|
||||
{t('pool')}
|
||||
</StyledNavLink>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(NavigationTabs)
|
||||
export function FindPoolTabs() {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>Import Pool</ActiveText>
|
||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export function AddRemoveTabs({ adding }: { adding: boolean }) {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const Input = React.memo(function InnerInput({
|
||||
...rest
|
||||
}: {
|
||||
value: string | number
|
||||
onUserInput: (string) => void
|
||||
onUserInput: (input: string) => void
|
||||
error?: boolean
|
||||
fontSize?: string
|
||||
align?: 'right' | 'left'
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import styled from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import Portal from '@reach/portal'
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity : 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 1;
|
||||
}
|
||||
`
|
||||
|
||||
const fadeOut = keyframes`
|
||||
from {
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 0;
|
||||
}
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 9999;
|
||||
|
||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
||||
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
|
||||
transition: visibility 150ms linear;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
@@ -103,9 +83,9 @@ export interface PopoverProps {
|
||||
}
|
||||
|
||||
export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) {
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>(null)
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>(null)
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement>(null)
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
|
||||
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement,
|
||||
strategy: 'fixed',
|
||||
@@ -114,17 +94,20 @@ export default function Popover({ content, show, children, placement = 'auto' }:
|
||||
{ name: 'arrow', options: { element: arrowElement } }
|
||||
]
|
||||
})
|
||||
useInterval(update, show ? 100 : null)
|
||||
const updateCallback = useCallback(() => {
|
||||
update && update()
|
||||
}, [update])
|
||||
useInterval(updateCallback, show ? 100 : null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
|
||||
<ReferenceElement ref={setReferenceElement as any}>{children}</ReferenceElement>
|
||||
<Portal>
|
||||
<PopoverContainer show={show} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<PopoverContainer show={show} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrowElement}
|
||||
ref={setArrowElement as any}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
|
||||
104
src/components/Popups/ListUpdatePopup.tsx
Normal file
104
src/components/Popups/ListUpdatePopup.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { diffTokenLists, TokenList } from '@uniswap/token-lists'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { acceptListUpdate } from '../../state/lists/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
export default function ListUpdatePopup({
|
||||
popKey,
|
||||
listUrl,
|
||||
oldList,
|
||||
newList,
|
||||
auto
|
||||
}: {
|
||||
popKey: string
|
||||
listUrl: string
|
||||
oldList: TokenList
|
||||
newList: TokenList
|
||||
auto: boolean
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const handleAcceptUpdate = useCallback(() => {
|
||||
if (auto) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from Popup',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
removeThisPopup()
|
||||
}, [auto, dispatch, listUrl, removeThisPopup])
|
||||
|
||||
const { added: tokensAdded, changed: tokensChanged, removed: tokensRemoved } = useMemo(() => {
|
||||
return diffTokenLists(oldList.tokens, newList.tokens)
|
||||
}, [newList.tokens, oldList.tokens])
|
||||
const numTokensChanged = useMemo(
|
||||
() =>
|
||||
Object.keys(tokensChanged).reduce((memo, chainId: any) => memo + Object.keys(tokensChanged[chainId]).length, 0),
|
||||
[tokensChanged]
|
||||
)
|
||||
|
||||
return (
|
||||
<AutoRow>
|
||||
<AutoColumn style={{ flex: '1' }} gap="8px">
|
||||
{auto ? (
|
||||
<TYPE.body fontWeight={500}>
|
||||
The token list "{oldList.name}" has been updated to{' '}
|
||||
<strong>{listVersionLabel(newList.version)}</strong>.
|
||||
</TYPE.body>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Text>
|
||||
An update is available for the token list "{oldList.name}" (
|
||||
{listVersionLabel(oldList.version)} to {listVersionLabel(newList.version)}).
|
||||
</Text>
|
||||
<ul>
|
||||
{tokensAdded.length > 0 ? (
|
||||
<li>
|
||||
{tokensAdded.map(token => (
|
||||
<strong key={`${token.chainId}-${token.address}`} title={token.address}>
|
||||
{token.symbol}
|
||||
</strong>
|
||||
))}{' '}
|
||||
added
|
||||
</li>
|
||||
) : null}
|
||||
{tokensRemoved.length > 0 ? (
|
||||
<li>
|
||||
{tokensRemoved.map(token => (
|
||||
<strong key={`${token.chainId}-${token.address}`} title={token.address}>
|
||||
{token.symbol}
|
||||
</strong>
|
||||
))}{' '}
|
||||
removed
|
||||
</li>
|
||||
) : null}
|
||||
{numTokensChanged > 0 ? <li>{numTokensChanged} tokens updated</li> : null}
|
||||
</ul>
|
||||
</div>
|
||||
<AutoRow>
|
||||
<div style={{ flexGrow: 1, marginRight: 12 }}>
|
||||
<ButtonSecondary onClick={handleAcceptUpdate}>Accept update</ButtonSecondary>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<ButtonSecondary onClick={removeThisPopup}>Dismiss</ButtonSecondary>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
)
|
||||
}
|
||||
97
src/components/Popups/PopupItem.tsx
Normal file
97
src/components/Popups/PopupItem.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useCallback, useContext, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { animated } from 'react-spring'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import ListUpdatePopup from './ListUpdatePopup'
|
||||
import TransactionPopup from './TransactionPopup'
|
||||
|
||||
export const StyledClose = styled(X)`
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
export const Popup = styled.div`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
min-width: 290px;
|
||||
`}
|
||||
`
|
||||
const Fader = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
const AnimatedFader = animated(Fader)
|
||||
|
||||
export default function PopupItem({
|
||||
removeAfterMs,
|
||||
content,
|
||||
popKey
|
||||
}: {
|
||||
removeAfterMs: number | null
|
||||
content: PopupContent
|
||||
popKey: string
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
useEffect(() => {
|
||||
if (removeAfterMs === null) return undefined
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
removeThisPopup()
|
||||
}, removeAfterMs)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [removeAfterMs, removeThisPopup])
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash, success, summary }
|
||||
} = content
|
||||
popupContent = <TransactionPopup hash={hash} success={success} summary={summary} />
|
||||
} else if ('listUpdate' in content) {
|
||||
const {
|
||||
listUpdate: { listUrl, oldList, newList, auto }
|
||||
} = content
|
||||
popupContent = <ListUpdatePopup popKey={popKey} listUrl={listUrl} oldList={oldList} newList={newList} auto={auto} />
|
||||
}
|
||||
|
||||
const faderStyle = useSpring({
|
||||
from: { width: '100%' },
|
||||
to: { width: '0%' },
|
||||
config: { duration: removeAfterMs ?? undefined }
|
||||
})
|
||||
|
||||
return (
|
||||
<Popup>
|
||||
<StyledClose color={theme.text2} onClick={removeThisPopup} />
|
||||
{popupContent}
|
||||
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
41
src/components/Popups/TransactionPopup.tsx
Normal file
41
src/components/Popups/TransactionPopup.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
const RowNoFlex = styled(AutoRow)`
|
||||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
export default function TransactionPopup({
|
||||
hash,
|
||||
success,
|
||||
summary
|
||||
}: {
|
||||
hash: string
|
||||
success?: boolean
|
||||
summary?: string
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<RowNoFlex>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
|
||||
{chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</RowNoFlex>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,8 @@
|
||||
import { ChainId, Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useMediaLayout } from 'use-media'
|
||||
|
||||
import { X } from 'react-feather'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useActivePopups } from '../../state/application/hooks'
|
||||
import { AutoColumn } from '../Column'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import Row from '../Row'
|
||||
import TxnPopup from '../TxnPopup'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
const StyledClose = styled(X)`
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
import PopupItem from './PopupItem'
|
||||
|
||||
const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
position: relative;
|
||||
@@ -29,6 +10,11 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
height: ${({ height }) => height};
|
||||
margin: ${({ height }) => (height ? '0 auto;' : 0)};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
|
||||
|
||||
display: none;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: block;
|
||||
`};
|
||||
`
|
||||
|
||||
const MobilePopupInner = styled.div`
|
||||
@@ -44,123 +30,39 @@ const MobilePopupInner = styled.div`
|
||||
`
|
||||
|
||||
const FixedPopupColumn = styled(AutoColumn)`
|
||||
position: absolute;
|
||||
top: 112px;
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 1rem;
|
||||
max-width: 355px !important;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const Popup = styled.div`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
min-width: 290px;
|
||||
`}
|
||||
`
|
||||
|
||||
function PoolPopup({
|
||||
token0,
|
||||
token1
|
||||
}: {
|
||||
token0: { address?: string; symbol?: string }
|
||||
token1: { address?: string; symbol?: string }
|
||||
}) {
|
||||
const pairAddress: string | null = useMemo(() => {
|
||||
if (!token0 || !token1) return null
|
||||
// just mock it out
|
||||
return Pair.getAddress(
|
||||
new Token(ChainId.MAINNET, token0.address, 18),
|
||||
new Token(ChainId.MAINNET, token1.address, 18)
|
||||
)
|
||||
}, [token0, token1])
|
||||
|
||||
return (
|
||||
<AutoColumn gap={'10px'}>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Pool Imported
|
||||
</Text>
|
||||
<Row>
|
||||
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
UNI {token0?.symbol} / {token1?.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
{pairAddress ? (
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash, success, summary }
|
||||
} = content
|
||||
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
|
||||
} else if ('poolAdded' in content) {
|
||||
const {
|
||||
poolAdded: { token0, token1 }
|
||||
} = content
|
||||
|
||||
return <PoolPopup token0={token0} token1={token1} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function Popups() {
|
||||
const theme = useContext(ThemeContext)
|
||||
// get all popups
|
||||
const activePopups = useActivePopups()
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
// switch view settings on mobile
|
||||
const isMobile = useMediaLayout({ maxWidth: '600px' })
|
||||
|
||||
if (!isMobile) {
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<FixedPopupColumn gap="20px">
|
||||
{activePopups.map(item => {
|
||||
return (
|
||||
<Popup key={item.key}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
|
||||
<PopupItem content={item.content} popKey={item.key} />
|
||||
</Popup>
|
||||
)
|
||||
})}
|
||||
{activePopups.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</FixedPopupColumn>
|
||||
)
|
||||
}
|
||||
//mobile
|
||||
else
|
||||
return (
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
|
||||
<MobilePopupInner>
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map(item => {
|
||||
return (
|
||||
<Popup key={item.key}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
|
||||
<PopupItem content={item.content} popKey={item.key} />
|
||||
</Popup>
|
||||
)
|
||||
})}
|
||||
.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</MobilePopupInner>
|
||||
</MobilePopupWrapper>
|
||||
)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
69
src/components/PositionCard/V1.tsx
Normal file
69
src/components/PositionCard/V1.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Link, RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
|
||||
import { Text } from 'rebass'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { FixedHeightRow, HoverCard } from './index'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
||||
token: Token
|
||||
V1LiquidityBalance: TokenAmount
|
||||
}
|
||||
|
||||
function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<DoubleCurrencyLogo currency0={token} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
|
||||
{`${chainId && token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
ml="0.5rem"
|
||||
px="0.75rem"
|
||||
py="0.25rem"
|
||||
style={{ borderRadius: '1rem' }}
|
||||
backgroundColor={theme.yellow1}
|
||||
color={'black'}
|
||||
>
|
||||
V1
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary width="68%" as={Link} to={`/migrate/v1/${V1LiquidityBalance.token.address}`}>
|
||||
Migrate
|
||||
</ButtonSecondary>
|
||||
|
||||
<ButtonSecondary
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
width="28%"
|
||||
as={Link}
|
||||
to={`/remove/v1/${V1LiquidityBalance.token.address}`}
|
||||
>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(V1PositionCard)
|
||||
@@ -1,50 +1,138 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { JSBI, Pair, Percent } from '@uniswap/sdk'
|
||||
import { darken } from 'polished'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Percent, Pair, JSBI } from '@uniswap/sdk'
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import { unwrappedToken } from '../../utils/wrappedCurrency'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
import Card, { GreyCard } from '../Card'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import { Text } from 'rebass'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import { Dots } from '../swap/styleds'
|
||||
|
||||
const FixedHeightRow = styled(RowBetween)`
|
||||
export const FixedHeightRow = styled(RowBetween)`
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const HoverCard = styled(Card)`
|
||||
export const HoverCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
|
||||
}
|
||||
`
|
||||
|
||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
||||
interface PositionCardProps {
|
||||
pair: Pair
|
||||
minimal?: boolean
|
||||
showUnwrapped?: boolean
|
||||
border?: string
|
||||
}
|
||||
|
||||
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) {
|
||||
export function MinimalPositionCard({ pair, showUnwrapped = false, border }: PositionCardProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const token0 = pair?.token0
|
||||
const token1 = pair?.token1
|
||||
const currency0 = showUnwrapped ? pair.token0 : unwrappedToken(pair.token0)
|
||||
const currency1 = showUnwrapped ? pair.token1 : unwrappedToken(pair.token1)
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
|
||||
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
|
||||
|
||||
const [token0Deposited, token1Deposited] =
|
||||
!!pair &&
|
||||
!!totalPoolTokens &&
|
||||
!!userPoolBalance &&
|
||||
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
|
||||
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
? [
|
||||
pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
|
||||
pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
|
||||
]
|
||||
: [undefined, undefined]
|
||||
|
||||
return (
|
||||
<>
|
||||
{userPoolBalance && (
|
||||
<GreyCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Your position
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
|
||||
<RowFixed>
|
||||
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{currency0.symbol}/{currency1.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<AutoColumn gap="4px">
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{currency0.symbol}:
|
||||
</Text>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{currency1.symbol}:
|
||||
</Text>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</GreyCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const currency0 = unwrappedToken(pair.token0)
|
||||
const currency1 = unwrappedToken(pair.token1)
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
|
||||
|
||||
const poolTokenPercentage =
|
||||
!!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
@@ -58,179 +146,99 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
|
||||
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
? [
|
||||
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
|
||||
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
|
||||
pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
|
||||
pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
|
||||
]
|
||||
: [undefined, undefined]
|
||||
|
||||
if (minimal) {
|
||||
return (
|
||||
<>
|
||||
{userPoolBalance && (
|
||||
<GreyCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
return (
|
||||
<HoverCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
|
||||
<RowFixed>
|
||||
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
{showMore ? (
|
||||
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
||||
) : (
|
||||
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
|
||||
)}
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
{showMore && (
|
||||
<AutoColumn gap="8px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {currency0.symbol}:
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Your current position
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency0} />
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<AutoColumn gap="4px">
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token0?.symbol}:
|
||||
</Text>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token0?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token1?.symbol}:
|
||||
</Text>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token1?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</GreyCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
} else
|
||||
return (
|
||||
<HoverCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
{showMore ? (
|
||||
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
||||
) : (
|
||||
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
|
||||
'-'
|
||||
)}
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
{showMore && (
|
||||
<AutoColumn gap="8px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token0?.symbol}:
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
</FixedHeightRow>
|
||||
|
||||
<FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {currency1.symbol}:
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token1?.symbol}:
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency1} />
|
||||
</RowFixed>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoRow justify="center" marginTop={'10px'}>
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
|
||||
View pool information ↗
|
||||
</ExternalLink>
|
||||
</AutoRow>
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary
|
||||
width="48%"
|
||||
onClick={() => {
|
||||
history.push('/add/' + token0?.address + '-' + token1?.address)
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</ButtonSecondary>
|
||||
<ButtonSecondary
|
||||
width="48%"
|
||||
onClick={() => {
|
||||
history.push('/remove/' + token0?.address + '-' + token1?.address)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
<AutoRow justify="center" marginTop={'10px'}>
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pair.liquidityToken.address}`}>
|
||||
View pool information ↗
|
||||
</ExternalLink>
|
||||
</AutoRow>
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary as={Link} to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`} width="48%">
|
||||
Add
|
||||
</ButtonSecondary>
|
||||
<ButtonSecondary as={Link} width="48%" to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(PositionCard)
|
||||
|
||||
79
src/components/ProgressSteps/index.tsx
Normal file
79
src/components/ProgressSteps/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { transparentize } from 'polished'
|
||||
|
||||
const Wrapper = styled(AutoColumn)`
|
||||
margin-top: 1.25rem;
|
||||
`
|
||||
|
||||
const Grouping = styled(RowBetween)`
|
||||
width: 50%;
|
||||
`
|
||||
|
||||
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
background-color: ${({ theme, confirmed, disabled }) =>
|
||||
disabled ? theme.bg4 : confirmed ? theme.green1 : theme.primary1};
|
||||
border-radius: 50%;
|
||||
color: ${({ theme }) => theme.white};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 8px;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
const CircleRow = styled.div`
|
||||
width: calc(100% - 20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Connector = styled.div<{ prevConfirmed?: boolean }>`
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme, prevConfirmed }) => transparentize(0.5, prevConfirmed ? theme.green1 : theme.primary1)} 0%,
|
||||
${({ theme, prevConfirmed }) => (prevConfirmed ? theme.primary1 : theme.bg4)} 80%
|
||||
);
|
||||
opacity: 0.6;
|
||||
`
|
||||
|
||||
interface ProgressCirclesProps {
|
||||
steps: 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 }: ProgressCirclesProps) {
|
||||
return (
|
||||
<Wrapper justify={'center'}>
|
||||
<Grouping>
|
||||
{steps.map((step, i) => {
|
||||
return (
|
||||
<CircleRow key={i}>
|
||||
<Circle confirmed={step} disabled={!steps[i - 1] && i !== 0}>
|
||||
{step ? '✓' : i + 1}
|
||||
</Circle>
|
||||
<Connector prevConfirmed={step} />
|
||||
</CircleRow>
|
||||
)
|
||||
})}
|
||||
<Circle disabled={!steps[steps.length - 1]}>{steps.length + 1}</Circle>
|
||||
</Grouping>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const QuestionWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
|
||||
export default function QuestionHelper({ text }: { text: string }) {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
@@ -30,7 +30,7 @@ export default function QuestionHelper({ text, disabled }: { text: string; disab
|
||||
|
||||
return (
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
<Tooltip text={text} show={show && !disabled}>
|
||||
<Tooltip text={text} show={show}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
|
||||
@@ -5,14 +5,13 @@ const Row = styled(Box)<{ align?: string; padding?: string; border?: string; bor
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
align-items: ${({ align }) => align && align};
|
||||
align-items: ${({ align }) => (align ? align : 'center')};
|
||||
padding: ${({ padding }) => padding};
|
||||
border: ${({ border }) => border};
|
||||
border-radius: ${({ borderRadius }) => borderRadius};
|
||||
`
|
||||
|
||||
export const RowBetween = styled(Row)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
|
||||
export const RowBetween = styled(Row)`
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
|
||||
@@ -1,41 +1,66 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { ChainId, Currency, currencyEquals, ETHER, Token } from '@uniswap/sdk'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SUGGESTED_BASES } from '../../constants'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { BaseWrapper } from './styleds'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
|
||||
const BaseWrapper = styled.div<{ disable?: boolean }>`
|
||||
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
|
||||
align-items: center;
|
||||
:hover {
|
||||
cursor: ${({ disable }) => !disable && 'pointer'};
|
||||
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
||||
}
|
||||
|
||||
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
||||
opacity: ${({ disable }) => disable && '0.4'};
|
||||
`
|
||||
|
||||
export default function CommonBases({
|
||||
chainId,
|
||||
onSelect,
|
||||
selectedTokenAddress
|
||||
selectedCurrency
|
||||
}: {
|
||||
chainId: number
|
||||
selectedTokenAddress: string
|
||||
onSelect: (tokenAddress: string) => void
|
||||
chainId?: ChainId
|
||||
selectedCurrency?: Currency | null
|
||||
onSelect: (currency: Currency) => void
|
||||
}) {
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<AutoRow>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Common Bases
|
||||
<Text fontWeight={500} fontSize={14}>
|
||||
Common bases
|
||||
</Text>
|
||||
<QuestionHelper text="These tokens are commonly used in pairs." />
|
||||
<QuestionHelper text="These tokens are commonly paired with other tokens." />
|
||||
</AutoRow>
|
||||
<AutoRow gap="10px">
|
||||
{(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => {
|
||||
<AutoRow gap="4px">
|
||||
<BaseWrapper
|
||||
onClick={() => {
|
||||
if (!selectedCurrency || !currencyEquals(selectedCurrency, ETHER)) {
|
||||
onSelect(ETHER)
|
||||
}
|
||||
}}
|
||||
disable={selectedCurrency === ETHER}
|
||||
>
|
||||
<CurrencyLogo currency={ETHER} style={{ marginRight: 8 }} />
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
ETH
|
||||
</Text>
|
||||
</BaseWrapper>
|
||||
{(chainId ? SUGGESTED_BASES[chainId] : []).map((token: Token) => {
|
||||
const selected = selectedCurrency instanceof Token && selectedCurrency.address === token.address
|
||||
return (
|
||||
<BaseWrapper
|
||||
gap="6px"
|
||||
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
|
||||
disable={selectedTokenAddress === token.address}
|
||||
key={token.address}
|
||||
>
|
||||
<TokenLogo address={token.address} />
|
||||
<BaseWrapper onClick={() => !selected && onSelect(token)} disable={selected} key={token.address}>
|
||||
<CurrencyLogo currency={token} style={{ marginRight: 8 }} />
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{token.symbol}
|
||||
</Text>
|
||||
|
||||
210
src/components/SearchModal/CurrencyList.tsx
Normal file
210
src/components/SearchModal/CurrencyList.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Currency, CurrencyAmount, currencyEquals, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { useIsUserAddedToken } from '../../hooks/Tokens'
|
||||
import Column from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import { FadedSpan, MenuItem } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
|
||||
}
|
||||
|
||||
const StyledBalanceText = styled(Text)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 5rem;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const Tag = styled.div`
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.3rem 0.25rem 0.3rem;
|
||||
max-width: 6rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
justify-self: flex-end;
|
||||
margin-right: 4px;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
|
||||
const TagContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
function TokenTags({ currency }: { currency: Currency }) {
|
||||
if (!(currency instanceof WrappedTokenInfo)) {
|
||||
return <span />
|
||||
}
|
||||
|
||||
const tags = currency.tags
|
||||
if (!tags || tags.length === 0) return <span />
|
||||
|
||||
const tag = tags[0]
|
||||
|
||||
return (
|
||||
<TagContainer>
|
||||
<MouseoverTooltip text={tag.description}>
|
||||
<Tag key={tag.id}>{tag.name}</Tag>
|
||||
</MouseoverTooltip>
|
||||
{tags.length > 1 ? (
|
||||
<MouseoverTooltip
|
||||
text={tags
|
||||
.slice(1)
|
||||
.map(({ name, description }) => `${name}: ${description}`)
|
||||
.join('; \n')}
|
||||
>
|
||||
<Tag>...</Tag>
|
||||
</MouseoverTooltip>
|
||||
) : null}
|
||||
</TagContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrencyRow({
|
||||
currency,
|
||||
onSelect,
|
||||
isSelected,
|
||||
otherSelected,
|
||||
style
|
||||
}: {
|
||||
currency: Currency
|
||||
onSelect: () => void
|
||||
isSelected: boolean
|
||||
otherSelected: boolean
|
||||
style: CSSProperties
|
||||
}) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const key = currencyKey(currency)
|
||||
const selectedTokenList = useSelectedTokenList()
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
|
||||
const customAdded = useIsUserAddedToken(currency)
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
const addToken = useAddUserToken()
|
||||
|
||||
// only show add or remove buttons if not on selected list
|
||||
return (
|
||||
<MenuItem
|
||||
style={style}
|
||||
className={`token-item-${key}`}
|
||||
onClick={() => (isSelected ? null : onSelect())}
|
||||
disabled={isSelected}
|
||||
selected={otherSelected}
|
||||
>
|
||||
<CurrencyLogo currency={currency} size={'24px'} />
|
||||
<Column>
|
||||
<Text title={currency.name} fontWeight={500}>
|
||||
{currency.symbol}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
{!isOnSelectedList && customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Added by user
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (chainId && currency instanceof Token) removeToken(chainId, currency.address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isOnSelectedList && !customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Found by address
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (currency instanceof Token) addToken(currency)
|
||||
}}
|
||||
>
|
||||
(Add)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
<TokenTags currency={currency} />
|
||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
|
||||
</RowFixed>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CurrencyList({
|
||||
height,
|
||||
currencies,
|
||||
selectedCurrency,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
fixedListRef,
|
||||
showETH
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherCurrency?: Currency | null
|
||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||
showETH: boolean
|
||||
}) {
|
||||
const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH])
|
||||
|
||||
const Row = useCallback(
|
||||
({ data, index, style }) => {
|
||||
const currency: Currency = data[index]
|
||||
const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency))
|
||||
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
|
||||
const handleSelect = () => onCurrencySelect(currency)
|
||||
return (
|
||||
<CurrencyRow
|
||||
style={style}
|
||||
currency={currency}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[onCurrencySelect, otherCurrency, selectedCurrency]
|
||||
)
|
||||
|
||||
const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), [])
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
itemData={itemData}
|
||||
itemCount={itemData.length}
|
||||
itemSize={56}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
214
src/components/SearchModal/CurrencySearch.tsx
Normal file
214
src/components/SearchModal/CurrencySearch.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useSelectedListInfo } from '../../state/lists/hooks'
|
||||
import { CloseIcon, LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Card from '../Card'
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import CommonBases from './CommonBases'
|
||||
import CurrencyList from './CurrencyList'
|
||||
import { filterTokens } from './filtering'
|
||||
import SortButton from './SortButton'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
|
||||
interface CurrencySearchProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
onChangeList: () => void
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
selectedCurrency,
|
||||
onCurrencySelect,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onChangeList
|
||||
}: CurrencySearchProps) {
|
||||
const { t } = useTranslation()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const fixedList = useRef<FixedSizeList>()
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(searchQuery)
|
||||
const searchToken = useToken(searchQuery)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddressSearch) {
|
||||
ReactGA.event({
|
||||
category: 'Currency Select',
|
||||
action: 'Search by address',
|
||||
label: isAddressSearch
|
||||
})
|
||||
}
|
||||
}, [isAddressSearch])
|
||||
|
||||
const showETH: boolean = useMemo(() => {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
return s === '' || s === 'e' || s === 'et' || s === 'eth'
|
||||
}, [searchQuery])
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (isAddressSearch) return searchToken ? [searchToken] : []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isAddressSearch, searchToken, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol?.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
fixedList.current?.scrollTo(0)
|
||||
}, [])
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
if (s === 'eth') {
|
||||
handleCurrencySelect(ETHER)
|
||||
} else if (filteredSortedTokens.length > 0) {
|
||||
if (
|
||||
filteredSortedTokens[0].symbol?.toLowerCase() === searchQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens.length === 1
|
||||
) {
|
||||
handleCurrencySelect(filteredSortedTokens[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredSortedTokens, handleCurrencySelect, searchQuery]
|
||||
)
|
||||
|
||||
const selectedListInfo = useSelectedListInfo()
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper text="Find a token by searching for its name or symbol or by pasting its address below." />
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={selectedCurrency} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div style={{ flex: '1' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CurrencyList
|
||||
height={height}
|
||||
showETH={showETH}
|
||||
currencies={filteredSortedTokens}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={selectedCurrency}
|
||||
fixedListRef={fixedList}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Card>
|
||||
<RowBetween>
|
||||
{selectedListInfo.current ? (
|
||||
<Row>
|
||||
{selectedListInfo.current.logoURI ? (
|
||||
<ListLogo
|
||||
style={{ marginRight: 12 }}
|
||||
logoURI={selectedListInfo.current.logoURI}
|
||||
alt={`${selectedListInfo.current.name} list logo`}
|
||||
/>
|
||||
) : null}
|
||||
<TYPE.main id="currency-search-selected-list-name">{selectedListInfo.current.name}</TYPE.main>
|
||||
</Row>
|
||||
) : null}
|
||||
<LinkStyledButton
|
||||
style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }}
|
||||
onClick={onChangeList}
|
||||
id="currency-search-change-list-button"
|
||||
>
|
||||
{selectedListInfo.current ? 'Change' : 'Select a list'}
|
||||
</LinkStyledButton>
|
||||
</RowBetween>
|
||||
</Card>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
85
src/components/SearchModal/CurrencySearchModal.tsx
Normal file
85
src/components/SearchModal/CurrencySearchModal.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Currency } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { useSelectedListUrl } from '../../state/lists/hooks'
|
||||
import Modal from '../Modal'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import ListIntroduction from './ListIntroduction'
|
||||
import { ListSelect } from './ListSelect'
|
||||
|
||||
interface CurrencySearchModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export default function CurrencySearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onCurrencySelect,
|
||||
selectedCurrency,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases = false
|
||||
}: CurrencySearchModalProps) {
|
||||
const [listView, setListView] = useState<boolean>(false)
|
||||
const lastOpen = useLast(isOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !lastOpen) {
|
||||
setListView(false)
|
||||
}
|
||||
}, [isOpen, lastOpen])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
const handleClickChangeList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Change Lists'
|
||||
})
|
||||
setListView(true)
|
||||
}, [])
|
||||
const handleClickBack = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Back'
|
||||
})
|
||||
setListView(false)
|
||||
}, [])
|
||||
const handleSelectListIntroduction = useCallback(() => {
|
||||
setListView(true)
|
||||
}, [])
|
||||
|
||||
const selectedListUrl = useSelectedListUrl()
|
||||
const noListSelected = !selectedListUrl
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} minHeight={listView ? 40 : noListSelected ? 0 : 80}>
|
||||
{listView ? (
|
||||
<ListSelect onDismiss={onDismiss} onBack={handleClickBack} />
|
||||
) : noListSelected ? (
|
||||
<ListIntroduction onSelectList={handleSelectListIntroduction} />
|
||||
) : (
|
||||
<CurrencySearch
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
onChangeList={handleClickChangeList}
|
||||
selectedCurrency={selectedCurrency}
|
||||
otherSelectedCurrency={otherSelectedCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
47
src/components/SearchModal/ListIntroduction.tsx
Normal file
47
src/components/SearchModal/ListIntroduction.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { OutlineCard } from '../Card'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { PaddedColumn } from './styleds'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import listLight from '../../assets/images/token-list/lists-light.png'
|
||||
import listDark from '../../assets/images/token-list/lists-dark.png'
|
||||
|
||||
export default function ListIntroduction({ onSelectList }: { onSelectList: () => void }) {
|
||||
const [isDark] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn>
|
||||
<AutoColumn gap="14px">
|
||||
<img
|
||||
style={{ width: '120px', margin: '0 auto' }}
|
||||
src={isDark ? listDark : listLight}
|
||||
alt="token-list-preview"
|
||||
/>
|
||||
<img
|
||||
style={{ width: '100%', borderRadius: '12px' }}
|
||||
src="https://cloudflare-ipfs.com/ipfs/QmRf1rAJcZjV3pwKTHfPdJh4RxR8yvRHkdLjZCsmp7T6hA"
|
||||
alt="token-list-preview"
|
||||
/>
|
||||
<Text style={{ marginBottom: '8px', textAlign: 'center' }}>
|
||||
Uniswap now supports token lists. You can add your own custom lists via IPFS, HTTPS and ENS.{' '}
|
||||
</Text>
|
||||
<ButtonPrimary onClick={onSelectList} id="list-introduction-choose-a-list">
|
||||
Choose a list
|
||||
</ButtonPrimary>
|
||||
<OutlineCard style={{ marginBottom: '8px', padding: '1rem' }}>
|
||||
<Text fontWeight={400} fontSize={14} style={{ textAlign: 'center' }}>
|
||||
Token lists are an{' '}
|
||||
<ExternalLink href="https://github.com/uniswap/token-lists">open specification</ExternalLink>. Check out{' '}
|
||||
<ExternalLink href="https://tokenlists.org">tokenlists.org</ExternalLink> to learn more.
|
||||
</Text>
|
||||
</OutlineCard>
|
||||
</AutoColumn>
|
||||
</PaddedColumn>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
379
src/components/SearchModal/ListSelect.tsx
Normal file
379
src/components/SearchModal/ListSelect.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
import { AppDispatch, AppState } from '../../state'
|
||||
import { acceptListUpdate, removeList, selectList } from '../../state/lists/actions'
|
||||
import { useSelectedListUrl } from '../../state/lists/hooks'
|
||||
import { CloseIcon, ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { parseENSAddress } from '../../utils/parseENSAddress'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import { ButtonOutlined, ButtonPrimary, ButtonSecondary } from '../Button'
|
||||
|
||||
import Column from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 100;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 8px;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
`
|
||||
|
||||
const StyledListUrlText = styled.div`
|
||||
max-width: 160px;
|
||||
opacity: 0.6;
|
||||
margin-right: 0.5rem;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
function ListOrigin({ listUrl }: { listUrl: string }) {
|
||||
const ensName = useMemo(() => parseENSAddress(listUrl)?.ensName, [listUrl])
|
||||
const host = useMemo(() => {
|
||||
if (ensName) return undefined
|
||||
const lowerListUrl = listUrl.toLowerCase()
|
||||
if (lowerListUrl.startsWith('ipfs://') || lowerListUrl.startsWith('ipns://')) {
|
||||
return listUrl
|
||||
}
|
||||
try {
|
||||
const url = new URL(listUrl)
|
||||
return url.host
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}, [listUrl, ensName])
|
||||
return <>{ensName ?? host}</>
|
||||
}
|
||||
|
||||
function listUrlRowHTMLId(listUrl: string) {
|
||||
return `list-row-${listUrl.replace(/\./g, '-')}`
|
||||
}
|
||||
|
||||
const ListRow = memo(function ListRow({ listUrl, onBack }: { listUrl: string; onBack: () => void }) {
|
||||
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const selectedListUrl = useSelectedListUrl()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
|
||||
|
||||
const isSelected = listUrl === selectedListUrl
|
||||
|
||||
const [open, toggle] = useToggle(false)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }]
|
||||
})
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const selectThisList = useCallback(() => {
|
||||
if (isSelected) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Select List',
|
||||
label: listUrl
|
||||
})
|
||||
|
||||
dispatch(selectList(listUrl))
|
||||
onBack()
|
||||
}, [dispatch, isSelected, listUrl, onBack])
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(removeList(listUrl))
|
||||
}
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
if (!list) return null
|
||||
|
||||
return (
|
||||
<Row key={listUrl} align="center" padding="16px" id={listUrlRowHTMLId(listUrl)}>
|
||||
{list.logoURI ? (
|
||||
<ListLogo style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
|
||||
) : (
|
||||
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
|
||||
)}
|
||||
<Column style={{ flex: '1' }}>
|
||||
<Row>
|
||||
<Text
|
||||
fontWeight={isSelected ? 500 : 400}
|
||||
fontSize={16}
|
||||
style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{list.name}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
marginTop: '4px'
|
||||
}}
|
||||
>
|
||||
<StyledListUrlText title={listUrl}>
|
||||
<ListOrigin listUrl={listUrl} />
|
||||
</StyledListUrlText>
|
||||
</Row>
|
||||
</Column>
|
||||
<StyledMenu ref={node as any}>
|
||||
<ButtonOutlined
|
||||
style={{
|
||||
width: '2rem',
|
||||
padding: '.8rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px',
|
||||
marginRight: '0.5rem'
|
||||
}}
|
||||
onClick={toggle}
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
<DropDown />
|
||||
</ButtonOutlined>
|
||||
|
||||
{open && (
|
||||
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
<div>{list && listVersionLabel(list.version)}</div>
|
||||
<SeparatorDark />
|
||||
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>View list</ExternalLink>
|
||||
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
|
||||
Remove list
|
||||
</UnpaddedLinkStyledButton>
|
||||
{pending && (
|
||||
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>Update list</UnpaddedLinkStyledButton>
|
||||
)}
|
||||
</PopoverContainer>
|
||||
)}
|
||||
</StyledMenu>
|
||||
{isSelected ? (
|
||||
<ButtonPrimary
|
||||
disabled={true}
|
||||
className="select-button"
|
||||
style={{ width: '5rem', minWidth: '5rem', padding: '0.5rem .35rem', borderRadius: '12px', fontSize: '14px' }}
|
||||
>
|
||||
Selected
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<>
|
||||
<ButtonPrimary
|
||||
className="select-button"
|
||||
style={{
|
||||
width: '5rem',
|
||||
minWidth: '4.5rem',
|
||||
padding: '0.5rem .35rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
onClick={selectThisList}
|
||||
>
|
||||
Select
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
|
||||
const AddListButton = styled(ButtonSecondary)`
|
||||
/* height: 1.8rem; */
|
||||
max-width: 4rem;
|
||||
margin-left: 1rem;
|
||||
border-radius: 12px;
|
||||
padding: 10px 18px;
|
||||
`
|
||||
|
||||
const ListContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export function ListSelect({ onDismiss, onBack }: { onDismiss: () => void; onBack: () => void }) {
|
||||
const [listUrlInput, setListUrlInput] = useState<string>('')
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
|
||||
const adding = Boolean(lists[listUrlInput]?.loadingRequestId)
|
||||
const [addError, setAddError] = useState<string | null>(null)
|
||||
|
||||
const handleInput = useCallback(e => {
|
||||
setListUrlInput(e.target.value)
|
||||
setAddError(null)
|
||||
}, [])
|
||||
const fetchList = useFetchListCallback()
|
||||
|
||||
const handleAddList = useCallback(() => {
|
||||
if (adding) return
|
||||
setAddError(null)
|
||||
fetchList(listUrlInput)
|
||||
.then(() => {
|
||||
setListUrlInput('')
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listUrlInput
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listUrlInput
|
||||
})
|
||||
setAddError(error.message)
|
||||
dispatch(removeList(listUrlInput))
|
||||
})
|
||||
}, [adding, dispatch, fetchList, listUrlInput])
|
||||
|
||||
const validUrl: boolean = useMemo(() => {
|
||||
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
|
||||
}, [listUrlInput])
|
||||
|
||||
const handleEnterKey = useCallback(
|
||||
e => {
|
||||
if (validUrl && e.key === 'Enter') {
|
||||
handleAddList()
|
||||
}
|
||||
},
|
||||
[handleAddList, validUrl]
|
||||
)
|
||||
|
||||
const sortedLists = useMemo(() => {
|
||||
const listUrls = Object.keys(lists)
|
||||
return listUrls
|
||||
.filter(listUrl => {
|
||||
return Boolean(lists[listUrl].current)
|
||||
})
|
||||
.sort((u1, u2) => {
|
||||
const { current: l1 } = lists[u1]
|
||||
const { current: l2 } = lists[u2]
|
||||
if (l1 && l2) {
|
||||
return l1.name.toLowerCase() < l2.name.toLowerCase()
|
||||
? -1
|
||||
: l1.name.toLowerCase() === l2.name.toLowerCase()
|
||||
? 0
|
||||
: 1
|
||||
}
|
||||
if (l1) return -1
|
||||
if (l2) return 1
|
||||
return 0
|
||||
})
|
||||
}, [lists])
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn>
|
||||
<RowBetween>
|
||||
<div>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} />
|
||||
</div>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Manage Lists
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<PaddedColumn gap="14px">
|
||||
<Text fontWeight={600}>
|
||||
Add a list{' '}
|
||||
<QuestionHelper text="Token lists are an open specification for lists of ERC20 tokens. You can use any token list by entering its URL below. Beware that third party token lists can contain fake or malicious ERC20 tokens." />
|
||||
</Text>
|
||||
<Row>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="list-add-input"
|
||||
placeholder="https:// or ipfs:// or ENS name"
|
||||
value={listUrlInput}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnterKey}
|
||||
style={{ height: '2.75rem', borderRadius: 12, padding: '12px' }}
|
||||
/>
|
||||
<AddListButton onClick={handleAddList} disabled={!validUrl}>
|
||||
Add
|
||||
</AddListButton>
|
||||
</Row>
|
||||
{addError ? (
|
||||
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
|
||||
{addError}
|
||||
</TYPE.error>
|
||||
) : null}
|
||||
</PaddedColumn>
|
||||
|
||||
<Separator />
|
||||
|
||||
<ListContainer>
|
||||
{sortedLists.map(listUrl => (
|
||||
<ListRow key={listUrl} listUrl={listUrl} onBack={onBack} />
|
||||
))}
|
||||
</ListContainer>
|
||||
<Separator />
|
||||
|
||||
<div style={{ padding: '16px', textAlign: 'center' }}>
|
||||
<ExternalLink href="https://tokenlists.org">Browse lists</ExternalLink>
|
||||
</div>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { JSBI, Pair, TokenAmount } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import { RowFixed } from '../Row'
|
||||
import { MenuItem, ModalInfo } from './styleds'
|
||||
|
||||
export default function PairList({
|
||||
pairs,
|
||||
focusTokenAddress,
|
||||
pairBalances,
|
||||
onSelectPair,
|
||||
onAddLiquidity = onSelectPair
|
||||
}: {
|
||||
pairs: Pair[]
|
||||
focusTokenAddress?: string
|
||||
pairBalances: { [pairAddress: string]: TokenAmount }
|
||||
onSelectPair: (pair: Pair) => void
|
||||
onAddLiquidity: (pair: Pair) => void
|
||||
}) {
|
||||
if (pairs.length === 0) {
|
||||
return <ModalInfo>No Pools Found</ModalInfo>
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
itemSize={54}
|
||||
height={500}
|
||||
itemCount={pairs.length}
|
||||
width="100%"
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const pair = pairs[index]
|
||||
|
||||
// the focused token is shown first
|
||||
const tokenA = focusTokenAddress === pair.token1.address ? pair.token1 : pair.token0
|
||||
const tokenB = tokenA === pair.token0 ? pair.token1 : pair.token0
|
||||
|
||||
const pairAddress = pair.liquidityToken.address
|
||||
const balance = pairBalances[pairAddress]?.toSignificant(6)
|
||||
const zeroBalance = pairBalances[pairAddress]?.raw && JSBI.equal(pairBalances[pairAddress].raw, JSBI.BigInt(0))
|
||||
|
||||
const selectPair = () => onSelectPair(pair)
|
||||
const addLiquidity = () => onAddLiquidity(pair)
|
||||
|
||||
return (
|
||||
<MenuItem style={style} onClick={selectPair}>
|
||||
<RowFixed>
|
||||
<DoubleTokenLogo a0={tokenA.address} a1={tokenB.address} size={24} margin={true} />
|
||||
<Text fontWeight={500} fontSize={16}>{`${tokenA.symbol}/${tokenB.symbol}`}</Text>
|
||||
</RowFixed>
|
||||
|
||||
<ButtonPrimary padding={'6px 8px'} width={'fit-content'} borderRadius={'12px'} onClick={addLiquidity}>
|
||||
{balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'}
|
||||
</ButtonPrimary>
|
||||
</MenuItem>
|
||||
)
|
||||
}}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { FadedSpan, GreySpan, MenuItem, SpinnerWrapper, ModalInfo } from './styleds'
|
||||
|
||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||
const address = isAddress(tokenAddress)
|
||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||
}
|
||||
|
||||
export default function TokenList({
|
||||
tokens,
|
||||
allTokenBalances,
|
||||
selectedToken,
|
||||
onTokenSelect,
|
||||
otherToken,
|
||||
showSendWithSwap,
|
||||
onRemoveAddedToken,
|
||||
otherSelectedText,
|
||||
hideRemove
|
||||
}: {
|
||||
tokens: Token[]
|
||||
selectedToken: string
|
||||
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
||||
onTokenSelect: (tokenAddress: string) => void
|
||||
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
|
||||
otherToken: string
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedText: string
|
||||
hideRemove?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return <ModalInfo>{t('noToken')}</ModalInfo>
|
||||
}
|
||||
return (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={tokens.length}
|
||||
itemSize={50}
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const { address, symbol } = tokens[index]
|
||||
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
const balance = allTokenBalances[address]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={style}
|
||||
key={address}
|
||||
className={`token-item-${address}`}
|
||||
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
|
||||
disabled={selectedToken && selectedToken === address}
|
||||
selected={otherToken === address}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
{symbol}
|
||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && !hideRemove && (
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onRemoveAddedToken(chainId, address)
|
||||
}}
|
||||
style={{ marginLeft: '4px', fontWeight: 400 }}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
)}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<AutoColumn gap="4px" justify="end">
|
||||
{balance ? (
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
<ButtonSecondary padding={'4px 8px'}>
|
||||
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
Send With Swap
|
||||
</Text>
|
||||
</ButtonSecondary>
|
||||
) : balance ? (
|
||||
balance.toSignificant(6)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
) : account ? (
|
||||
<SpinnerWrapper src={Circle} alt="loader" />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</AutoColumn>
|
||||
</MenuItem>
|
||||
)
|
||||
}}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isAddress } from '../../utils'
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
|
||||
export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
if (search.length === 0) return tokens
|
||||
@@ -10,41 +10,27 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
return tokens.filter(token => token.address === searchingAddress)
|
||||
}
|
||||
|
||||
const lowerSearchParts = searchingAddress ? [] : search.toLowerCase().split(/\s+/)
|
||||
const lowerSearchParts = search
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
|
||||
if (lowerSearchParts.length === 0) {
|
||||
return tokens
|
||||
}
|
||||
|
||||
const matchesSearch = (s: string): boolean => {
|
||||
const sParts = s.toLowerCase().split(/\s+/)
|
||||
const sParts = s
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
|
||||
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p)))
|
||||
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p)))
|
||||
}
|
||||
|
||||
return tokens.filter(token => {
|
||||
const { symbol, name } = token
|
||||
|
||||
return matchesSearch(symbol) || matchesSearch(name)
|
||||
})
|
||||
}
|
||||
|
||||
export function filterPairs(pairs: Pair[], search: string): Pair[] {
|
||||
if (search.trim().length === 0) return pairs
|
||||
|
||||
const addressSearch = isAddress(search)
|
||||
if (addressSearch) {
|
||||
return pairs.filter(p => {
|
||||
return (
|
||||
p.token0.address === addressSearch ||
|
||||
p.token1.address === addressSearch ||
|
||||
p.liquidityToken.address === addressSearch
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const lowerSearch = search.toLowerCase()
|
||||
return pairs.filter(pair => {
|
||||
const pairExpressionA = `${pair.token0.symbol}/${pair.token1.symbol}`.toLowerCase()
|
||||
if (pairExpressionA.startsWith(lowerSearch)) return true
|
||||
const pairExpressionB = `${pair.token1.symbol}/${pair.token0.symbol}`.toLowerCase()
|
||||
if (pairExpressionB.startsWith(lowerSearch)) return true
|
||||
return filterTokens([pair.token0, pair.token1], search).length > 0
|
||||
return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterPairs, filterTokens } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { useTokenComparator, pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
filterType?: 'tokens'
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: SearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const isTokenView = filterType === 'tokens'
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const sortedTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return Object.values(allTokens).sort(tokenComparator)
|
||||
}, [allTokens, isTokenView, tokenComparator])
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return filterTokens(sortedTokens, searchQuery)
|
||||
}, [isTokenView, sortedTokens, searchQuery])
|
||||
|
||||
function _onTokenSelect(address: string) {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return allPairs.sort((a, b): number => {
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [isTokenView, allPairs, allPairBalances])
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [isTokenView, searchQuery, sortedPairList])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{isTokenView ? 'Select a token' : 'Select a pool'}
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text={
|
||||
isTokenView
|
||||
? 'Find a token by searching for its name or symbol or by pasting its address below.'
|
||||
: 'Find a pair by searching for its name below.'
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type={'text'}
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{isTokenView && (
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
)}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
{isTokenView ? (
|
||||
<TokenList
|
||||
tokens={filteredTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onRemoveAddedToken={removeTokenByAddress}
|
||||
onTokenSelect={_onTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hideRemove={Boolean(isAddress(searchQuery))}
|
||||
/>
|
||||
) : (
|
||||
<PairList
|
||||
pairs={filteredPairs}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
)}
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
{isTokenView ? (
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
) : (
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(SearchModal)
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
|
||||
import { useAllTokenBalances } from '../../state/wallet/hooks'
|
||||
|
||||
// compare two token amounts with highest one coming first
|
||||
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
@@ -16,40 +14,13 @@ function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// compare two pairs, favoring "pinned" pairs, and falling back to balances
|
||||
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
const aShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
|
||||
) ?? false
|
||||
const bShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
|
||||
) ?? false
|
||||
|
||||
if (aShouldBePinned && !bShouldBePinned) {
|
||||
return -1
|
||||
} else if (!aShouldBePinned && bShouldBePinned) {
|
||||
return 1
|
||||
} else {
|
||||
return balanceComparator(balanceA, balanceB)
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenComparator(
|
||||
weth: Token | undefined,
|
||||
balances: { [tokenAddress: string]: TokenAmount }
|
||||
): (tokenA: Token, tokenB: Token) => number {
|
||||
function getTokenComparator(balances: {
|
||||
[tokenAddress: string]: TokenAmount | undefined
|
||||
}): (tokenA: Token, tokenB: Token) => number {
|
||||
return function sortTokens(tokenA: Token, tokenB: Token): number {
|
||||
// -1 = a is first
|
||||
// 1 = b is first
|
||||
|
||||
// sort ETH first
|
||||
if (weth) {
|
||||
if (tokenA.equals(weth)) return -1
|
||||
if (tokenB.equals(weth)) return 1
|
||||
}
|
||||
|
||||
// sort by balances
|
||||
const balanceA = balances[tokenA.address]
|
||||
const balanceB = balances[tokenB.address]
|
||||
@@ -57,16 +28,18 @@ function getTokenComparator(
|
||||
const balanceComp = balanceComparator(balanceA, balanceB)
|
||||
if (balanceComp !== 0) return balanceComp
|
||||
|
||||
// sort by symbol
|
||||
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||
if (tokenA.symbol && tokenB.symbol) {
|
||||
// sort by symbol
|
||||
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||
} else {
|
||||
return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const weth = WETH[chainId]
|
||||
const balances = useAllTokenBalancesTreatingWETHasETH()
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
|
||||
const balances = useAllTokenBalances()
|
||||
const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances])
|
||||
return useMemo(() => {
|
||||
if (inverted) {
|
||||
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import styled from 'styled-components'
|
||||
import { Spinner } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export const ModalInfo = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
@@ -9,8 +8,8 @@ export const ModalInfo = styled.div`
|
||||
padding: 1rem 1rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
min-height: 200px;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
@@ -18,18 +17,26 @@ export const FadedSpan = styled(RowFixed)`
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
export const GreySpan = styled.span`
|
||||
color: ${({ theme }) => theme.text3};
|
||||
font-weight: 400;
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
export const SpinnerWrapper = styled(Spinner)`
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
color: ${({ theme }) => theme.text4};
|
||||
opacity: 0.6;
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px);
|
||||
grid-gap: 16px;
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
||||
}
|
||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||
`
|
||||
|
||||
export const Input = styled.input`
|
||||
export const SearchInput = styled.input`
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
@@ -50,46 +57,20 @@ export const Input = styled.input`
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text3};
|
||||
}
|
||||
`
|
||||
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
const PaddedItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(PaddedItem)`
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
||||
}
|
||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||
`
|
||||
|
||||
export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
||||
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
width: 120px;
|
||||
|
||||
:hover {
|
||||
cursor: ${({ disable }) => !disable && 'pointer'};
|
||||
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
||||
}
|
||||
|
||||
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
||||
opacity: ${({ disable }) => disable && '0.4'};
|
||||
`
|
||||
|
||||
export const SearchInput = styled(Input)`
|
||||
transition: border 100ms;
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
export const Separator = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
||||
export const SeparatorDark = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
244
src/components/Settings/index.tsx
Normal file
244
src/components/Settings/index.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import React, { useRef, useContext, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import {
|
||||
useUserSlippageTolerance,
|
||||
useExpertModeManager,
|
||||
useUserDeadline,
|
||||
useDarkModeManager
|
||||
} from '../../state/user/hooks'
|
||||
import TransactionSettings from '../TransactionSettings'
|
||||
import { RowFixed, RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Toggle from '../Toggle'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonError } from '../Button'
|
||||
import { useSettingsMenuOpen, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { Text } from 'rebass'
|
||||
import Modal from '../Modal'
|
||||
|
||||
const StyledMenuIcon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledCloseIcon = styled(X)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
const EmojiWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
right: 0px;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const MenuFlyout = styled.span`
|
||||
min-width: 20.125rem;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
min-width: 18.125rem;
|
||||
right: -46px;
|
||||
`};
|
||||
`
|
||||
|
||||
const Break = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
const ModalContentWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
export default function SettingsTab() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const open = useSettingsMenuOpen()
|
||||
const toggle = useToggleSettingsMenu()
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
const [userSlippageTolerance, setUserslippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
const [deadline, setDeadline] = useUserDeadline()
|
||||
|
||||
const [expertMode, toggleExpertMode] = useExpertModeManager()
|
||||
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
return (
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
|
||||
<StyledMenu ref={node as any}>
|
||||
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
|
||||
<ModalContentWrapper>
|
||||
<AutoColumn gap="lg">
|
||||
<RowBetween style={{ padding: '0 2rem' }}>
|
||||
<div />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Are you sure?
|
||||
</Text>
|
||||
<StyledCloseIcon onClick={() => setShowConfirmation(false)} />
|
||||
</RowBetween>
|
||||
<Break />
|
||||
<AutoColumn gap="lg" style={{ padding: '0 2rem' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Expert mode turns off the confirm transaction prompt and allows high slippage trades that often result
|
||||
in bad rates and lost funds.
|
||||
</Text>
|
||||
<Text fontWeight={600} fontSize={20}>
|
||||
ONLY USE THIS MODE IF YOU KNOW WHAT YOU ARE DOING.
|
||||
</Text>
|
||||
<ButtonError
|
||||
error={true}
|
||||
padding={'12px'}
|
||||
onClick={() => {
|
||||
if (window.prompt(`Please type the word "confirm" to enable expert mode.`) === 'confirm') {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500} id="confirm-expert-mode">
|
||||
Turn On Expert Mode
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</ModalContentWrapper>
|
||||
</Modal>
|
||||
<StyledMenuButton onClick={toggle} id="open-settings-dialog-button">
|
||||
<StyledMenuIcon />
|
||||
{expertMode && (
|
||||
<EmojiWrapper>
|
||||
<span role="img" aria-label="wizard-icon">
|
||||
🧙
|
||||
</span>
|
||||
</EmojiWrapper>
|
||||
)}
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuFlyout>
|
||||
<AutoColumn gap="md" style={{ padding: '1rem' }}>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Transaction Settings
|
||||
</Text>
|
||||
<TransactionSettings
|
||||
rawSlippage={userSlippageTolerance}
|
||||
setRawSlippage={setUserslippageTolerance}
|
||||
deadline={deadline}
|
||||
setDeadline={setDeadline}
|
||||
/>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Interface Settings
|
||||
</Text>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Expert Mode
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Bypasses confirmation modals and allows high slippage trades. Use at your own risk." />
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
id="toggle-expert-mode-button"
|
||||
isActive={expertMode}
|
||||
toggle={
|
||||
expertMode
|
||||
? () => {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
: () => {
|
||||
toggle()
|
||||
setShowConfirmation(true)
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Dark Mode
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
<Toggle isActive={darkMode} toggle={toggleDarkMode} />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</MenuFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledRangeInput = styled.input<{ value: number }>`
|
||||
const StyledRangeInput = styled.input<{ size: number }>`
|
||||
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
||||
width: 100%; /* Specific width is required for Firefox. */
|
||||
background: transparent; /* Otherwise white in Chrome */
|
||||
@@ -17,8 +17,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
@@ -33,8 +33,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
@@ -48,8 +48,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
@@ -62,24 +62,12 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
@@ -102,26 +90,31 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
interface InputSliderProps {
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
step?: number
|
||||
min?: number
|
||||
max?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export default function InputSlider({ value, onChange }: InputSliderProps) {
|
||||
export default function Slider({ value, onChange, min = 0, step = 1, max = 100, size = 28 }: InputSliderProps) {
|
||||
const changeCallback = useCallback(
|
||||
e => {
|
||||
onChange(e.target.value)
|
||||
onChange(parseInt(e.target.value))
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledRangeInput
|
||||
size={size}
|
||||
type="range"
|
||||
value={value}
|
||||
style={{ width: '90%', marginLeft: 15, marginRight: 15, padding: '15px 0' }}
|
||||
onChange={changeCallback}
|
||||
aria-labelledby="input-slider"
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
aria-labelledby="input slider"
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
41
src/components/Toggle/index.tsx
Normal file
41
src/components/Toggle/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')};
|
||||
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
id?: string
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Toggle({ id, isActive, toggle }: ToggleProps) {
|
||||
return (
|
||||
<StyledToggle id={id} isActive={isActive} onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
On
|
||||
</ToggleElement>
|
||||
<ToggleElement isActive={!isActive} isOnSwitch={false}>
|
||||
Off
|
||||
</ToggleElement>
|
||||
</StyledToggle>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { isAddress } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { WETH } from '@uniswap/sdk'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
|
||||
const TOKEN_ICON_API = address =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
const BAD_IMAGES = {}
|
||||
|
||||
const Image = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
background-color: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
`
|
||||
|
||||
const Emoji = styled.span<{ size?: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
margin-bottom: -4px;
|
||||
`
|
||||
|
||||
const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
border-radius: 24px;
|
||||
`
|
||||
|
||||
export default function TokenLogo({
|
||||
address,
|
||||
size = '24px',
|
||||
...rest
|
||||
}: {
|
||||
address?: string
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const [error, setError] = useState(false)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// mock rinkeby DAI
|
||||
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
|
||||
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
|
||||
}
|
||||
|
||||
let path = ''
|
||||
// hard code to show ETH instead of WETH in UI
|
||||
if (address === WETH[chainId].address) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
|
||||
path = TOKEN_ICON_API(address)
|
||||
} else {
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
<span role="img" aria-label="Thinking">
|
||||
🤔
|
||||
</span>
|
||||
</Emoji>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...rest}
|
||||
// alt={address}
|
||||
src={path}
|
||||
size={size}
|
||||
onError={() => {
|
||||
BAD_IMAGES[address] = true
|
||||
setError(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)};
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-row-gap: 14px;
|
||||
`
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: flex-start;
|
||||
& > * {
|
||||
margin-right: 6px;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
color: #aeaeae;
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
& > * {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
const HELP_TEXT = `
|
||||
The Uniswap V2 smart contracts are designed to support any ERC20 token on Ethereum. Any token can be
|
||||
loaded into the interface by entering its Ethereum address into the search field or passing it as a URL
|
||||
parameter.
|
||||
`
|
||||
|
||||
const DUPLICATE_NAME_HELP_TEXT = `${HELP_TEXT} This token has the same name or symbol as another token in your list.`
|
||||
|
||||
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
|
||||
token?: Token
|
||||
}
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const isDefaultToken = Boolean(
|
||||
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
|
||||
)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (isDefaultToken || !token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
if (userToken.equals(token)) {
|
||||
return false
|
||||
}
|
||||
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
||||
})
|
||||
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefaultToken || !token || dismissed) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
{duplicateNameOrSymbol ? null : (
|
||||
<CloseIcon onClick={dismissTokenWarning}>
|
||||
<CloseColor />
|
||||
</CloseIcon>
|
||||
)}
|
||||
<Row>
|
||||
<TYPE.subHeader>{duplicateNameOrSymbol ? 'Duplicate token name or symbol' : 'Imported token'}</TYPE.subHeader>
|
||||
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
|
||||
</Row>
|
||||
<Row>
|
||||
<TokenLogo address={token.address} />
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</div>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
<Row>
|
||||
<TYPE.italic>Verify this is the correct token before making any transactions.</TYPE.italic>
|
||||
</Row>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
`
|
||||
|
||||
export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) {
|
||||
return (
|
||||
<WarningContainer>
|
||||
{Object.keys(tokens).map(field =>
|
||||
tokens[field] ? <TokenWarningCard style={{ marginBottom: 14 }} key={field} token={tokens[field]} /> : null
|
||||
)}
|
||||
</WarningContainer>
|
||||
)
|
||||
}
|
||||
153
src/components/TokenWarningModal/index.tsx
Normal file
153
src/components/TokenWarningModal/index.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink, shortenAddress } from '../../utils'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import Modal from '../Modal'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ButtonError } from '../Button'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme }) => transparentize(0.6, theme.bg3)};
|
||||
padding: 0.75rem;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: rgba(242, 150, 2, 0.05);
|
||||
border: 1px solid #f3841e;
|
||||
border-radius: 20px;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const StyledWarningIcon = styled(AlertTriangle)`
|
||||
stroke: ${({ theme }) => theme.red2};
|
||||
`
|
||||
|
||||
interface TokenWarningCardProps {
|
||||
token?: Token
|
||||
}
|
||||
|
||||
function TokenWarningCard({ token }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (!token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
if (userToken.equals(token)) {
|
||||
return false
|
||||
}
|
||||
return userToken.symbol?.toLowerCase() === tokenSymbol || userToken.name?.toLowerCase() === tokenName
|
||||
})
|
||||
}, [token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (!token) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol}>
|
||||
<AutoRow gap="6px">
|
||||
<AutoColumn gap="24px">
|
||||
<CurrencyLogo currency={token} size={'16px'} />
|
||||
<div> </div>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="10px" justify="flex-start">
|
||||
<TYPE.main>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}{' '}
|
||||
</TYPE.main>
|
||||
{chainId && (
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
<TYPE.blue title={token.address}>{shortenAddress(token.address)} (View on Etherscan)</TYPE.blue>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenWarningModal({
|
||||
isOpen,
|
||||
tokens,
|
||||
onConfirm
|
||||
}: {
|
||||
isOpen: boolean
|
||||
tokens: Token[]
|
||||
onConfirm: () => void
|
||||
}) {
|
||||
const [understandChecked, setUnderstandChecked] = useState(false)
|
||||
const toggleUnderstand = useCallback(() => setUnderstandChecked(uc => !uc), [])
|
||||
|
||||
const handleDismiss = useCallback(() => null, [])
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={handleDismiss} maxHeight={90}>
|
||||
<WarningContainer className="token-warning-container">
|
||||
<AutoColumn gap="lg">
|
||||
<AutoRow gap="6px">
|
||||
<StyledWarningIcon />
|
||||
<TYPE.main color={'red2'}>Token imported</TYPE.main>
|
||||
</AutoRow>
|
||||
<TYPE.body color={'red2'}>
|
||||
Anyone can create an ERC20 token on Ethereum with <em>any</em> name, including creating fake versions of
|
||||
existing tokens and tokens that claim to represent projects that do not have a token.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
This interface can load arbitrary tokens by token addresses. Please take extra caution and do your research
|
||||
when interacting with arbitrary ERC20 tokens.
|
||||
</TYPE.body>
|
||||
<TYPE.body color={'red2'}>
|
||||
If you purchase an arbitrary token, <strong>you may be unable to sell it back.</strong>
|
||||
</TYPE.body>
|
||||
{tokens.map(token => {
|
||||
return <TokenWarningCard key={token.address} token={token} />
|
||||
})}
|
||||
<RowBetween>
|
||||
<div>
|
||||
<label style={{ cursor: 'pointer', userSelect: 'none' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="understand-checkbox"
|
||||
checked={understandChecked}
|
||||
onChange={toggleUnderstand}
|
||||
/>{' '}
|
||||
I understand
|
||||
</label>
|
||||
</div>
|
||||
<ButtonError
|
||||
disabled={!understandChecked}
|
||||
error={true}
|
||||
width={'140px'}
|
||||
padding="0.5rem 1rem"
|
||||
className="token-dismiss-button"
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onClick={() => {
|
||||
onConfirm()
|
||||
}}
|
||||
>
|
||||
<TYPE.body color="white">Continue</TYPE.body>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</WarningContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
@@ -16,3 +16,16 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
export default function Tooltip({ text, ...rest }: TooltipProps) {
|
||||
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
|
||||
}
|
||||
|
||||
export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
return (
|
||||
<Tooltip {...rest} show={show}>
|
||||
<div onMouseEnter={open} onMouseLeave={close}>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
197
src/components/TransactionConfirmationModal/index.tsx
Normal file
197
src/components/TransactionConfirmationModal/index.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import Modal from '../Modal'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { CloseIcon, Spinner } from '../../theme/components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { AlertTriangle, ArrowUpCircle } from 'react-feather'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
const Section = styled(AutoColumn)`
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const BottomSection = styled(Section)`
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
`
|
||||
|
||||
function ConfirmationPendingContent({ onDismiss, pendingText }: { onDismiss: () => void; pendingText: string }) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Waiting For Confirmation
|
||||
</Text>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
|
||||
{pendingText}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||
Confirm this transaction in your wallet
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function TransactionSubmittedContent({
|
||||
onDismiss,
|
||||
chainId,
|
||||
hash
|
||||
}: {
|
||||
onDismiss: () => void
|
||||
hash: string | undefined
|
||||
chainId: ChainId
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Transaction Submitted
|
||||
</Text>
|
||||
|
||||
{chainId && hash && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
View on Etherscan
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
)}
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConfirmationModalContent({
|
||||
title,
|
||||
bottomContent,
|
||||
onDismiss,
|
||||
topContent
|
||||
}: {
|
||||
title: string
|
||||
onDismiss: () => void
|
||||
topContent: () => React.ReactNode
|
||||
bottomContent: () => React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{title}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
{topContent()}
|
||||
</Section>
|
||||
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function TransactionErrorContent({ message, onDismiss }: { message: string; onDismiss: () => void }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Error
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
|
||||
<AlertTriangle color={theme.red1} style={{ strokeWidth: 1.5 }} size={64} />
|
||||
<Text fontWeight={500} fontSize={16} color={theme.red1} style={{ textAlign: 'center', width: '85%' }}>
|
||||
{message}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
<BottomSection gap="12px">
|
||||
<ButtonPrimary onClick={onDismiss}>Dismiss</ButtonPrimary>
|
||||
</BottomSection>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
hash: string | undefined
|
||||
content: () => React.ReactNode
|
||||
attemptingTxn: boolean
|
||||
pendingText: string
|
||||
}
|
||||
|
||||
export default function TransactionConfirmationModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
attemptingTxn,
|
||||
hash,
|
||||
pendingText,
|
||||
content
|
||||
}: ConfirmationModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
if (!chainId) return null
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
{attemptingTxn ? (
|
||||
<ConfirmationPendingContent onDismiss={onDismiss} pendingText={pendingText} />
|
||||
) : hash ? (
|
||||
<TransactionSubmittedContent chainId={chainId} hash={hash} onDismiss={onDismiss} />
|
||||
) : (
|
||||
content()
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -21,15 +21,15 @@ enum DeadlineError {
|
||||
const FancyButton = styled.button`
|
||||
color: ${({ theme }) => theme.text1};
|
||||
align-items: center;
|
||||
min-width: 55px;
|
||||
height: 2rem;
|
||||
border-radius: 36px;
|
||||
font-size: 12px;
|
||||
width: auto;
|
||||
min-width: 3rem;
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
outline: none;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
:hover {
|
||||
cursor: inherit;
|
||||
border: 1px solid ${({ theme }) => theme.bg4};
|
||||
}
|
||||
:focus {
|
||||
@@ -48,9 +48,8 @@ const Option = styled(FancyButton)<{ active: boolean }>`
|
||||
|
||||
const Input = styled.input`
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
flex-grow: 1;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
font-size: 16px;
|
||||
width: auto;
|
||||
outline: none;
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
@@ -64,6 +63,7 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
padding: 0 0.75rem;
|
||||
flex: 1;
|
||||
border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`};
|
||||
:hover {
|
||||
border: ${({ theme, active, warning }) =>
|
||||
@@ -78,8 +78,11 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
||||
}
|
||||
`
|
||||
|
||||
const SlippageSelector = styled.div`
|
||||
padding: 0 20px;
|
||||
const SlippageEmojiContainer = styled.span`
|
||||
color: #f3841e;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`}
|
||||
`
|
||||
|
||||
export interface SlippageTabsProps {
|
||||
@@ -101,60 +104,55 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
|
||||
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
|
||||
|
||||
let slippageError: SlippageError
|
||||
let slippageError: SlippageError | undefined
|
||||
if (slippageInput !== '' && !slippageInputIsValid) {
|
||||
slippageError = SlippageError.InvalidInput
|
||||
} else if (slippageInputIsValid && rawSlippage < 50) {
|
||||
slippageError = SlippageError.RiskyLow
|
||||
} else if (slippageInputIsValid && rawSlippage > 500) {
|
||||
slippageError = SlippageError.RiskyHigh
|
||||
} else {
|
||||
slippageError = undefined
|
||||
}
|
||||
|
||||
let deadlineError: DeadlineError
|
||||
let deadlineError: DeadlineError | undefined
|
||||
if (deadlineInput !== '' && !deadlineInputIsValid) {
|
||||
deadlineError = DeadlineError.InvalidInput
|
||||
} else {
|
||||
deadlineError = undefined
|
||||
}
|
||||
|
||||
function parseCustomSlippage(event) {
|
||||
setSlippageInput(event.target.value)
|
||||
function parseCustomSlippage(value: string) {
|
||||
setSlippageInput(value)
|
||||
|
||||
let valueAsIntFromRoundedFloat: number
|
||||
try {
|
||||
valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
|
||||
const valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(value) * 100).toString())
|
||||
if (!Number.isNaN(valueAsIntFromRoundedFloat) && valueAsIntFromRoundedFloat < 5000) {
|
||||
setRawSlippage(valueAsIntFromRoundedFloat)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (
|
||||
typeof valueAsIntFromRoundedFloat === 'number' &&
|
||||
!Number.isNaN(valueAsIntFromRoundedFloat) &&
|
||||
valueAsIntFromRoundedFloat < 5000
|
||||
) {
|
||||
setRawSlippage(valueAsIntFromRoundedFloat)
|
||||
}
|
||||
}
|
||||
|
||||
function parseCustomDeadline(event) {
|
||||
setDeadlineInput(event.target.value)
|
||||
function parseCustomDeadline(value: string) {
|
||||
setDeadlineInput(value)
|
||||
|
||||
let valueAsInt: number
|
||||
try {
|
||||
valueAsInt = Number.parseInt(event.target.value) * 60
|
||||
const valueAsInt: number = Number.parseInt(value) * 60
|
||||
if (!Number.isNaN(valueAsInt) && valueAsInt > 0) {
|
||||
setDeadline(valueAsInt)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
|
||||
setDeadline(valueAsInt)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Set slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
|
||||
<SlippageSelector>
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
@@ -187,18 +185,21 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
<RowBetween>
|
||||
{!!slippageInput &&
|
||||
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
|
||||
<span role="img" aria-label="warning" style={{ color: '#F3841E' }}>
|
||||
⚠️
|
||||
</span>
|
||||
<SlippageEmojiContainer>
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>
|
||||
</SlippageEmojiContainer>
|
||||
) : null}
|
||||
{/* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 */}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
ref={inputRef as any}
|
||||
placeholder={(rawSlippage / 100).toFixed(2)}
|
||||
value={slippageInput}
|
||||
onBlur={() => {
|
||||
parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
|
||||
parseCustomSlippage((rawSlippage / 100).toFixed(2))
|
||||
}}
|
||||
onChange={parseCustomSlippage}
|
||||
onChange={e => parseCustomSlippage(e.target.value)}
|
||||
color={!slippageInputIsValid ? 'red' : ''}
|
||||
/>
|
||||
%
|
||||
@@ -220,25 +221,25 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</SlippageSelector>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontSize={14} color={theme.text2}>
|
||||
Deadline
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Transaction deadline
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
|
||||
</RowFixed>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<RowFixed>
|
||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||
<Input
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
onBlur={() => {
|
||||
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
|
||||
parseCustomDeadline((deadline / 60).toString())
|
||||
}}
|
||||
placeholder={(deadline / 60).toString()}
|
||||
value={deadlineInput}
|
||||
onChange={parseCustomDeadline}
|
||||
onChange={e => parseCustomDeadline(e.target.value)}
|
||||
/>
|
||||
</OptionCustom>
|
||||
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
|
||||
@@ -246,6 +247,6 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
</>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
const Fader = styled.div<{ count: number }>`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
|
||||
height: 2px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
transition: width 100ms linear;
|
||||
`
|
||||
|
||||
const delay = 100
|
||||
|
||||
export default function TxnPopup({
|
||||
hash,
|
||||
success,
|
||||
summary,
|
||||
popKey
|
||||
}: {
|
||||
hash: string
|
||||
success?: boolean
|
||||
summary?: string
|
||||
popKey?: string
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const [isRunning, setIsRunning] = useState(true)
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
count > 150 ? removeThisPopup() : setCount(count + 1)
|
||||
},
|
||||
isRunning ? delay : null
|
||||
)
|
||||
|
||||
return (
|
||||
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
|
||||
{success ? (
|
||||
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
|
||||
) : (
|
||||
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
|
||||
)}
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>
|
||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
||||
</TYPE.body>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
</AutoColumn>
|
||||
<Fader count={count} />
|
||||
</AutoRow>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ const InfoCard = styled.button<{ active?: boolean }>`
|
||||
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
|
||||
`
|
||||
|
||||
const OptionCard = styled(InfoCard)`
|
||||
const OptionCard = styled(InfoCard as any)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -30,7 +30,7 @@ const OptionCardLeft = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const OptionCardClickable = styled(OptionCard)<{ clickable?: boolean }>`
|
||||
const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>`
|
||||
margin-top: 0;
|
||||
&:hover {
|
||||
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
|
||||
@@ -73,7 +73,7 @@ const SubHeader = styled.div`
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
const IconWrapper = styled.div<{ size?: number | null }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -90,7 +90,7 @@ const IconWrapper = styled.div<{ size?: number }>`
|
||||
export default function Option({
|
||||
link = null,
|
||||
clickable = true,
|
||||
size = null,
|
||||
size,
|
||||
onClick = null,
|
||||
color,
|
||||
header,
|
||||
@@ -114,7 +114,6 @@ export default function Option({
|
||||
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color}>
|
||||
{' '}
|
||||
{active ? (
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
|
||||
@@ -3,11 +3,9 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import WalletConnectData from './WalletConnectData'
|
||||
import { walletconnect, injected } from '../../connectors'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { injected } from '../../connectors'
|
||||
import { darken } from 'polished'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const PendingSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -19,14 +17,8 @@ const PendingSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
font-size: 4rem;
|
||||
const StyledLoader = styled(Loader)`
|
||||
margin-right: 1rem;
|
||||
svg {
|
||||
path {
|
||||
color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||
@@ -72,44 +64,39 @@ const LoadingWrapper = styled.div`
|
||||
`
|
||||
|
||||
export default function PendingView({
|
||||
uri = '',
|
||||
size,
|
||||
connector,
|
||||
error = false,
|
||||
setPendingError,
|
||||
tryActivation
|
||||
}: {
|
||||
uri?: string
|
||||
size?: number
|
||||
connector?: AbstractConnector
|
||||
error?: boolean
|
||||
setPendingError: (error: boolean) => void
|
||||
tryActivation: (connector: AbstractConnector) => void
|
||||
}) {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
const isMetamask = window?.ethereum?.isMetaMask
|
||||
|
||||
return (
|
||||
<PendingSection>
|
||||
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingWrapper>
|
||||
{!error && <SpinnerWrapper src={Circle} />}
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>Error connecting.</div>
|
||||
<ErrorButton
|
||||
onClick={() => {
|
||||
setPendingError(false)
|
||||
tryActivation(connector)
|
||||
connector && tryActivation(connector)
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</ErrorButton>
|
||||
</ErrorGroup>
|
||||
) : connector === walletconnect ? (
|
||||
'Scan QR code with a compatible wallet...'
|
||||
) : (
|
||||
'Initializing...'
|
||||
<>
|
||||
<StyledLoader />
|
||||
Initializing...
|
||||
</>
|
||||
)}
|
||||
</LoadingWrapper>
|
||||
</LoadingMessage>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
interface WalletConnectDataProps {
|
||||
uri?: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
|
||||
return <QRCodeWrapper>{uri && <QRCode size={size} value={uri} />}</QRCodeWrapper>
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import ReactGA from 'react-ga'
|
||||
import styled from 'styled-components'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||
|
||||
@@ -15,8 +14,10 @@ import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletconnect, fortmatic, portis } from '../../connectors'
|
||||
import { injected, fortmatic, portis } from '../../connectors'
|
||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
@@ -128,7 +129,7 @@ export default function WalletModal({
|
||||
|
||||
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
|
||||
|
||||
const [pendingWallet, setPendingWallet] = useState()
|
||||
const [pendingWallet, setPendingWallet] = useState<AbstractConnector | undefined>()
|
||||
|
||||
const [pendingError, setPendingError] = useState<boolean>()
|
||||
|
||||
@@ -152,19 +153,6 @@ export default function WalletModal({
|
||||
}
|
||||
}, [walletModalOpen])
|
||||
|
||||
// set up uri listener for walletconnect
|
||||
const [uri, setUri] = useState()
|
||||
useEffect(() => {
|
||||
const activateWC = uri => {
|
||||
setUri(uri)
|
||||
// setWalletView(WALLET_VIEWS.PENDING)
|
||||
}
|
||||
walletconnect.on(URI_AVAILABLE, activateWC)
|
||||
return () => {
|
||||
walletconnect.off(URI_AVAILABLE, activateWC)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// close modal when a connection is successful
|
||||
const activePrevious = usePrevious(active)
|
||||
const connectorPrevious = usePrevious(connector)
|
||||
@@ -174,7 +162,7 @@ export default function WalletModal({
|
||||
}
|
||||
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
|
||||
|
||||
const tryActivation = async connector => {
|
||||
const tryActivation = async (connector: AbstractConnector | undefined) => {
|
||||
let name = ''
|
||||
Object.keys(SUPPORTED_WALLETS).map(key => {
|
||||
if (connector === SUPPORTED_WALLETS[key].connector) {
|
||||
@@ -190,13 +178,20 @@ export default function WalletModal({
|
||||
})
|
||||
setPendingWallet(connector) // set wallet for pending view
|
||||
setWalletView(WALLET_VIEWS.PENDING)
|
||||
activate(connector, undefined, true).catch(error => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
} else {
|
||||
setPendingError(true)
|
||||
}
|
||||
})
|
||||
|
||||
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
|
||||
if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) {
|
||||
connector.walletConnectProvider = undefined
|
||||
}
|
||||
|
||||
connector &&
|
||||
activate(connector, undefined, true).catch(error => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
} else {
|
||||
setPendingError(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// close wallet modal if fortmatic modal is active
|
||||
@@ -345,8 +340,6 @@ export default function WalletModal({
|
||||
<ContentWrapper>
|
||||
{walletView === WALLET_VIEWS.PENDING ? (
|
||||
<PendingView
|
||||
uri={uri}
|
||||
size={220}
|
||||
connector={pendingWallet}
|
||||
error={pendingError}
|
||||
setPendingError={setPendingError}
|
||||
@@ -358,9 +351,7 @@ export default function WalletModal({
|
||||
{walletView !== WALLET_VIEWS.PENDING && (
|
||||
<Blurb>
|
||||
<span>New to Ethereum? </span>{' '}
|
||||
<ExternalLink href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
|
||||
Learn more about wallets
|
||||
</ExternalLink>
|
||||
<ExternalLink href="https://ethereum.org/wallets/">Learn more about wallets</ExternalLink>
|
||||
</Blurb>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
@@ -369,7 +360,7 @@ export default function WalletModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={walletModalOpen} onDismiss={toggleWalletModal} minHeight={null} maxHeight={90}>
|
||||
<Modal isOpen={walletModalOpen} onDismiss={toggleWalletModal} minHeight={false} maxHeight={90}>
|
||||
<Wrapper>{getModalContent()}</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { network } from '../../connectors'
|
||||
import { useEagerConnect, useInactiveListener } from '../../hooks'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const MessageWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -20,17 +19,7 @@ const Message = styled.h2`
|
||||
color: ${({ theme }) => theme.secondary1};
|
||||
`
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
font-size: 4rem;
|
||||
|
||||
svg {
|
||||
path {
|
||||
color: ${({ theme }) => theme.secondary1};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Web3ReactManager({ children }) {
|
||||
export default function Web3ReactManager({ children }: { children: JSX.Element }) {
|
||||
const { t } = useTranslation()
|
||||
const { active } = useWeb3React()
|
||||
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName)
|
||||
@@ -78,7 +67,7 @@ export default function Web3ReactManager({ children }) {
|
||||
if (!active && !networkActive) {
|
||||
return showLoader ? (
|
||||
<MessageWrapper>
|
||||
<SpinnerWrapper src={Circle} />
|
||||
<Loader />
|
||||
</MessageWrapper>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { darken, lighten } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Activity } from 'react-feather'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import useENSName from '../../hooks/useENSName'
|
||||
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { TransactionDetails } from '../../state/transactions/reducer'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
import Identicon from '../Identicon'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import WalletModal from '../WalletModal'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
|
||||
import { Spinner } from '../../theme'
|
||||
import LightCircle from '../../assets/svg/lightcircle.svg'
|
||||
import Loader from '../Loader'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
`
|
||||
import WalletModal from '../WalletModal'
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -123,104 +119,123 @@ const NetworkIcon = styled(Activity)`
|
||||
`
|
||||
|
||||
// we want the latest one to come first, so return negative if a is after b
|
||||
function newTranscationsFirst(a: TransactionDetails, b: TransactionDetails) {
|
||||
function newTransactionsFirst(a: TransactionDetails, b: TransactionDetails) {
|
||||
return b.addedTime - a.addedTime
|
||||
}
|
||||
|
||||
function recentTransactionsOnly(a: TransactionDetails) {
|
||||
return new Date().getTime() - a.addedTime < 86_400_000
|
||||
const SOCK = (
|
||||
<span role="img" aria-label="has socks emoji" style={{ marginTop: -4, marginBottom: -4 }}>
|
||||
🧦
|
||||
</span>
|
||||
)
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
function StatusIcon({ connector }: { connector: AbstractConnector }) {
|
||||
if (connector === injected) {
|
||||
return <Identicon />
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default function Web3Status() {
|
||||
function Web3StatusInner() {
|
||||
const { t } = useTranslation()
|
||||
const { active, account, connector, error } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
const { account, connector, error } = useWeb3React()
|
||||
|
||||
const ENSName = useENSName(account)
|
||||
const { ENSName } = useENSName(account ?? undefined)
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
const sortedRecentTransactions = useMemo(() => {
|
||||
const txs = Object.values(allTransactions)
|
||||
return txs.filter(recentTransactionsOnly).sort(newTranscationsFirst)
|
||||
return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
|
||||
}, [allTransactions])
|
||||
|
||||
const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash)
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
const hasSocks = useHasSocks()
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
if (account) {
|
||||
return (
|
||||
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
|
||||
</RowBetween>
|
||||
) : (
|
||||
<>
|
||||
{hasSocks ? SOCK : null}
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
</>
|
||||
)}
|
||||
{!hasPendingTransactions && connector && <StatusIcon connector={connector} />}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error'}</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
|
||||
<Text>{t('Connect to a wallet')}</Text>
|
||||
</Web3StatusConnect>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default function Web3Status() {
|
||||
const { active, account } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
|
||||
const { ENSName } = useENSName(account ?? undefined)
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
const sortedRecentTransactions = useMemo(() => {
|
||||
const txs = Object.values(allTransactions)
|
||||
return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
|
||||
}, [allTransactions])
|
||||
|
||||
const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash)
|
||||
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// handle the logo we want to show with the account
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return <Identicon />
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getWeb3Status() {
|
||||
if (account) {
|
||||
return (
|
||||
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>{pending?.length} Pending</Text> <SpinnerWrapper src={LightCircle} alt="loader" />
|
||||
</RowBetween>
|
||||
) : (
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
)}
|
||||
{!hasPendingTransactions && getStatusIcon()}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error'}</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
|
||||
<Text>{t('Connect to a wallet')}</Text>
|
||||
</Web3StatusConnect>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!contextNetwork.active && !active) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{getWeb3Status()}
|
||||
<WalletModal ENSName={ENSName} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
<Web3StatusInner />
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
// fires a GA pageview every time the route changes
|
||||
export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps) {
|
||||
export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps): null {
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(`${pathname}${search}`)
|
||||
}, [pathname, search])
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronUp, ChevronRight } from 'react-feather'
|
||||
import { Text, Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { SectionBreak } from './styleds'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { SectionBreak } from './styleds'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
@@ -34,8 +31,10 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
||||
<RowFixed>
|
||||
<TYPE.black color={theme.text1} fontSize={14}>
|
||||
{isExactIn
|
||||
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.token.symbol}` ?? '-'
|
||||
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.token.symbol}` ?? '-'}
|
||||
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.currency.symbol}` ??
|
||||
'-'
|
||||
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.currency.symbol}` ??
|
||||
'-'}
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
@@ -57,83 +56,45 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
||||
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
|
||||
</RowFixed>
|
||||
<TYPE.black fontSize={14} color={theme.text1}>
|
||||
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.token.symbol}` : '-'}
|
||||
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.currency.symbol}` : '-'}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
|
||||
<SectionBreak />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
export interface AdvancedSwapDetailsProps {
|
||||
trade?: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const showRoute = Boolean(trade && trade.route.path.length > 2)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
|
||||
<SectionBreak />
|
||||
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
|
||||
|
||||
<SlippageTabs {...slippageTabProps} />
|
||||
|
||||
{trade?.route?.path?.length > 2 && (
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
{flatMap(
|
||||
trade.route.path,
|
||||
// add a null in-between each item
|
||||
(token, i, array) => {
|
||||
const lastItem = i === array.length - 1
|
||||
return lastItem ? [token] : [token, null]
|
||||
}
|
||||
).map((token, i) => {
|
||||
// use null as an indicator to insert chevrons
|
||||
if (token === null) {
|
||||
return <ChevronRight key={i} color={theme.text2} />
|
||||
} else {
|
||||
return (
|
||||
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
|
||||
<TokenLogo address={token.address} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Flex>
|
||||
</AutoColumn>
|
||||
{trade && (
|
||||
<>
|
||||
<TradeSummary trade={trade} allowedSlippage={allowedSlippage} />
|
||||
{showRoute && (
|
||||
<>
|
||||
<SectionBreak />
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
<SwapRoute trade={trade} />
|
||||
</AutoColumn>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
)
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { RowBetween } from '../Row'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useLastTruthy } from '../../hooks/useLast'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
import { AdvancedDropdown } from './styleds'
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({
|
||||
showAdvanced,
|
||||
setShowAdvanced,
|
||||
...rest
|
||||
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
|
||||
showAdvanced: boolean
|
||||
setShowAdvanced: (showAdvanced: boolean) => void
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
|
||||
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||
transition: transform 300ms ease-in-out;
|
||||
`
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
|
||||
const lastTrade = useLastTruthy(trade)
|
||||
|
||||
return (
|
||||
<AdvancedDropdown>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
||||
) : (
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
|
||||
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Show Advanced
|
||||
</Text>
|
||||
<ChevronDown color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
)}
|
||||
</AdvancedDropdown>
|
||||
<AdvancedDetailsFooter show={Boolean(trade)}>
|
||||
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade ?? undefined} />
|
||||
</AdvancedDetailsFooter>
|
||||
)
|
||||
}
|
||||
|
||||
40
src/components/swap/BetterTradeLink.tsx
Normal file
40
src/components/swap/BetterTradeLink.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { useLocation } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
import { StyledInternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function BetterTradeLink({ version }: { version: Version }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const location = useLocation()
|
||||
const search = useParsedQueryString()
|
||||
|
||||
const linkDestination = useMemo(() => {
|
||||
return {
|
||||
...location,
|
||||
search: `?${stringify({
|
||||
...search,
|
||||
use: version !== DEFAULT_VERSION ? version : undefined
|
||||
})}`
|
||||
}
|
||||
}, [location, search, version])
|
||||
|
||||
return (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<StyledInternalLink to={linkDestination}>
|
||||
<b>Uniswap {version.toUpperCase()} ↗</b>
|
||||
</StyledInternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
109
src/components/swap/ConfirmSwapModal.tsx
Normal file
109
src/components/swap/ConfirmSwapModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { currencyEquals, Trade } from '@uniswap/sdk'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import TransactionConfirmationModal, {
|
||||
ConfirmationModalContent,
|
||||
TransactionErrorContent
|
||||
} from '../TransactionConfirmationModal'
|
||||
import SwapModalFooter from './SwapModalFooter'
|
||||
import SwapModalHeader from './SwapModalHeader'
|
||||
|
||||
/**
|
||||
* Returns true if the trade requires a confirmation of details before we can submit it
|
||||
* @param tradeA trade A
|
||||
* @param tradeB trade B
|
||||
*/
|
||||
function tradeMeaningfullyDiffers(tradeA: Trade, tradeB: Trade): boolean {
|
||||
return (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
|
||||
!tradeA.inputAmount.equalTo(tradeB.inputAmount) ||
|
||||
!currencyEquals(tradeA.outputAmount.currency, tradeB.outputAmount.currency) ||
|
||||
!tradeA.outputAmount.equalTo(tradeB.outputAmount)
|
||||
)
|
||||
}
|
||||
|
||||
export default function ConfirmSwapModal({
|
||||
trade,
|
||||
originalTrade,
|
||||
onAcceptChanges,
|
||||
allowedSlippage,
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
recipient,
|
||||
swapErrorMessage,
|
||||
isOpen,
|
||||
attemptingTxn,
|
||||
txHash
|
||||
}: {
|
||||
isOpen: boolean
|
||||
trade: Trade | undefined
|
||||
originalTrade: Trade | undefined
|
||||
attemptingTxn: boolean
|
||||
txHash: string | undefined
|
||||
recipient: string | null
|
||||
allowedSlippage: number
|
||||
onAcceptChanges: () => void
|
||||
onConfirm: () => void
|
||||
swapErrorMessage: string | undefined
|
||||
onDismiss: () => void
|
||||
}) {
|
||||
const showAcceptChanges = useMemo(
|
||||
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
|
||||
[originalTrade, trade]
|
||||
)
|
||||
|
||||
const modalHeader = useCallback(() => {
|
||||
return trade ? (
|
||||
<SwapModalHeader
|
||||
trade={trade}
|
||||
allowedSlippage={allowedSlippage}
|
||||
recipient={recipient}
|
||||
showAcceptChanges={showAcceptChanges}
|
||||
onAcceptChanges={onAcceptChanges}
|
||||
/>
|
||||
) : null
|
||||
}, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade])
|
||||
|
||||
const modalBottom = useCallback(() => {
|
||||
return trade ? (
|
||||
<SwapModalFooter
|
||||
onConfirm={onConfirm}
|
||||
trade={trade}
|
||||
disabledConfirm={showAcceptChanges}
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
allowedSlippage={allowedSlippage}
|
||||
/>
|
||||
) : null
|
||||
}, [allowedSlippage, onConfirm, showAcceptChanges, swapErrorMessage, trade])
|
||||
|
||||
// text to show while loading
|
||||
const pendingText = `Swapping ${trade?.inputAmount?.toSignificant(6)} ${
|
||||
trade?.inputAmount?.currency?.symbol
|
||||
} for ${trade?.outputAmount?.toSignificant(6)} ${trade?.outputAmount?.currency?.symbol}`
|
||||
|
||||
const confirmationContent = useCallback(
|
||||
() =>
|
||||
swapErrorMessage ? (
|
||||
<TransactionErrorContent onDismiss={onDismiss} message={swapErrorMessage} />
|
||||
) : (
|
||||
<ConfirmationModalContent
|
||||
title="Confirm Swap"
|
||||
onDismiss={onDismiss}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
/>
|
||||
),
|
||||
[onDismiss, modalBottom, modalHeader, swapErrorMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<TransactionConfirmationModal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
attemptingTxn={attemptingTxn}
|
||||
hash={txHash}
|
||||
content={confirmationContent}
|
||||
pendingText={pendingText}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import { ONE_BIPS } from '../../constants'
|
||||
import { warningSeverity } from '../../utils/prices'
|
||||
import { ErrorText } from './styleds'
|
||||
|
||||
/**
|
||||
* Formatted version of price impact text with warning colors
|
||||
*/
|
||||
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
|
||||
return (
|
||||
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
|
||||
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
|
||||
{priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'}
|
||||
</ErrorText>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export function PriceSlippageWarningCard({ priceSlippage }: { priceSlippage: Percent }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<YellowCard style={{ padding: '20px', paddingTop: '10px' }}>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<RowFixed style={{ paddingTop: '8px' }}>
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>{' '}
|
||||
<Text fontWeight={500} marginLeft="4px" color={theme.text1}>
|
||||
Price Warning
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
|
||||
This trade will move the price by ~{priceSlippage.toFixed(2)}%.
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +1,45 @@
|
||||
import { Percent, TokenAmount, Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext, useMemo, useState } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import { formatExecutionPrice } from '../../utils/prices'
|
||||
import {
|
||||
computeSlippageAdjustedAmounts,
|
||||
computeTradePriceBreakdown,
|
||||
formatExecutionPrice,
|
||||
warningSeverity
|
||||
} from '../../utils/prices'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
import { StyledBalanceMaxMini, SwapCallbackError } from './styleds'
|
||||
|
||||
export default function SwapModalFooter({
|
||||
trade,
|
||||
showInverted,
|
||||
setShowInverted,
|
||||
severity,
|
||||
slippageAdjustedAmounts,
|
||||
onSwap,
|
||||
parsedAmounts,
|
||||
realizedLPFee,
|
||||
priceImpactWithoutFee,
|
||||
confirmText
|
||||
onConfirm,
|
||||
allowedSlippage,
|
||||
swapErrorMessage,
|
||||
disabledConfirm
|
||||
}: {
|
||||
trade?: Trade
|
||||
showInverted: boolean
|
||||
setShowInverted: (inverted: boolean) => void
|
||||
severity: number
|
||||
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
|
||||
onSwap: () => any
|
||||
parsedAmounts?: { [field in Field]?: TokenAmount }
|
||||
realizedLPFee?: TokenAmount
|
||||
priceImpactWithoutFee?: Percent
|
||||
confirmText: string
|
||||
trade: Trade
|
||||
allowedSlippage: number
|
||||
onConfirm: () => void
|
||||
swapErrorMessage: string | undefined
|
||||
disabledConfirm: boolean
|
||||
}) {
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
const theme = useContext(ThemeContext)
|
||||
const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [
|
||||
allowedSlippage,
|
||||
trade
|
||||
])
|
||||
const { priceImpactWithoutFee, realizedLPFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade])
|
||||
const severity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoColumn gap="0px">
|
||||
@@ -66,23 +69,21 @@ export default function SwapModalFooter({
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum sent' : 'Maximum sold'}
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14}>
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
|
||||
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
|
||||
</TYPE.black>
|
||||
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
|
||||
<TYPE.black fontSize={14} marginLeft={'4px'}>
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT
|
||||
? parsedAmounts[Field.OUTPUT]?.token?.symbol
|
||||
: parsedAmounts[Field.INPUT]?.token?.symbol}
|
||||
</TYPE.black>
|
||||
)}
|
||||
<TYPE.black fontSize={14} marginLeft={'4px'}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? trade.outputAmount.currency.symbol
|
||||
: trade.inputAmount.currency.symbol}
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
@@ -102,17 +103,25 @@ export default function SwapModalFooter({
|
||||
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
|
||||
</RowFixed>
|
||||
<TYPE.black fontSize={14}>
|
||||
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.token?.symbol : '-'}
|
||||
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade.inputAmount.currency.symbol : '-'}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoRow>
|
||||
<ButtonError onClick={onSwap} error={severity > 2} style={{ margin: '10px 0 0 0' }} id="confirm-swap-or-send">
|
||||
<ButtonError
|
||||
onClick={onConfirm}
|
||||
disabled={disabledConfirm}
|
||||
error={severity > 2}
|
||||
style={{ margin: '10px 0 0 0' }}
|
||||
id="confirm-swap-or-send"
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{confirmText}
|
||||
{severity > 2 ? 'Swap Anyway' : 'Confirm Swap'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
|
||||
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</AutoRow>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,62 +1,107 @@
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { ArrowDown, AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { isAddress, shortenAddress } from '../../utils'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { TruncatedText } from './styleds'
|
||||
import { TruncatedText, SwapShowAcceptChanges } from './styleds'
|
||||
|
||||
export default function SwapModalHeader({
|
||||
formattedAmounts,
|
||||
tokens,
|
||||
slippageAdjustedAmounts,
|
||||
priceImpactSeverity,
|
||||
independentField
|
||||
trade,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
showAcceptChanges,
|
||||
onAcceptChanges
|
||||
}: {
|
||||
formattedAmounts?: { [field in Field]?: string }
|
||||
tokens?: { [field in Field]?: Token }
|
||||
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
|
||||
priceImpactSeverity: number
|
||||
independentField: Field
|
||||
trade: Trade
|
||||
allowedSlippage: number
|
||||
recipient: string | null
|
||||
showAcceptChanges: boolean
|
||||
onAcceptChanges: () => void
|
||||
}) {
|
||||
const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [
|
||||
trade,
|
||||
allowedSlippage
|
||||
])
|
||||
const { priceImpactWithoutFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade])
|
||||
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
|
||||
<RowBetween align="flex-end">
|
||||
<TruncatedText fontSize={24} fontWeight={500}>
|
||||
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]}
|
||||
</TruncatedText>
|
||||
<RowFixed gap="4px">
|
||||
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
|
||||
<RowFixed gap={'0px'}>
|
||||
<CurrencyLogo currency={trade.inputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
|
||||
<TruncatedText
|
||||
fontSize={24}
|
||||
fontWeight={500}
|
||||
color={showAcceptChanges && trade.tradeType === TradeType.EXACT_OUTPUT ? theme.primary1 : ''}
|
||||
>
|
||||
{trade.inputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap={'0px'}>
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.INPUT]?.symbol || ''}
|
||||
{trade.inputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowFixed>
|
||||
<ArrowDown size="16" color={theme.text2} />
|
||||
<ArrowDown size="16" color={theme.text2} style={{ marginLeft: '4px', minWidth: '16px' }} />
|
||||
</RowFixed>
|
||||
<RowBetween align="flex-end">
|
||||
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
|
||||
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]}
|
||||
</TruncatedText>
|
||||
<RowFixed gap="4px">
|
||||
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
|
||||
<RowFixed gap={'0px'}>
|
||||
<CurrencyLogo currency={trade.outputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
|
||||
<TruncatedText
|
||||
fontSize={24}
|
||||
fontWeight={500}
|
||||
color={
|
||||
priceImpactSeverity > 2
|
||||
? theme.red1
|
||||
: showAcceptChanges && trade.tradeType === TradeType.EXACT_INPUT
|
||||
? theme.primary1
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{trade.outputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap={'0px'}>
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.OUTPUT]?.symbol || ''}
|
||||
{trade.outputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
{showAcceptChanges ? (
|
||||
<SwapShowAcceptChanges justify="flex-start" gap={'0px'}>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<AlertTriangle size={20} style={{ marginRight: '8px', minWidth: 24 }} />
|
||||
<TYPE.main color={theme.primary1}> Price Updated</TYPE.main>
|
||||
</RowFixed>
|
||||
<ButtonPrimary
|
||||
style={{ padding: '.5rem', width: 'fit-content', fontSize: '0.825rem', borderRadius: '12px' }}
|
||||
onClick={onAcceptChanges}
|
||||
>
|
||||
Accept
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
</SwapShowAcceptChanges>
|
||||
) : null}
|
||||
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
|
||||
{independentField === Field.INPUT ? (
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
|
||||
{`Output is estimated. You will receive at least `}
|
||||
<b>
|
||||
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
|
||||
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {trade.outputAmount.currency.symbol}
|
||||
</b>
|
||||
{' or the transaction will revert.'}
|
||||
</TYPE.italic>
|
||||
@@ -64,12 +109,20 @@ export default function SwapModalHeader({
|
||||
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
|
||||
{`Input is estimated. You will sell at most `}
|
||||
<b>
|
||||
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {tokens[Field.INPUT]?.symbol}
|
||||
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {trade.inputAmount.currency.symbol}
|
||||
</b>
|
||||
{' or the transaction will revert.'}
|
||||
</TYPE.italic>
|
||||
)}
|
||||
</AutoColumn>
|
||||
{recipient !== null ? (
|
||||
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
|
||||
<TYPE.main>
|
||||
Output will be sent to{' '}
|
||||
<b title={recipient}>{isAddress(recipient) ? shortenAddress(recipient) : recipient}</b>
|
||||
</TYPE.main>
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
38
src/components/swap/SwapRoute.tsx
Normal file
38
src/components/swap/SwapRoute.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import React, { Fragment, memo, useContext } from 'react'
|
||||
import { ChevronRight } from 'react-feather'
|
||||
import { Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { TYPE } from '../../theme'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
|
||||
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
{trade.route.path.map((token, i, path) => {
|
||||
const isLastItem: boolean = i === path.length - 1
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
|
||||
<CurrencyLogo currency={token} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
{isLastItem ? null : <ChevronRight color={theme.text2} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import { Price } from '@uniswap/sdk'
|
||||
import { useContext } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -7,23 +7,20 @@ import { ThemeContext } from 'styled-components'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
|
||||
interface TradePriceProps {
|
||||
trade?: Trade
|
||||
price?: Price
|
||||
showInverted: boolean
|
||||
setShowInverted: (showInverted: boolean) => void
|
||||
}
|
||||
|
||||
export default function TradePrice({ trade, showInverted, setShowInverted }: TradePriceProps) {
|
||||
export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const inputToken = trade?.inputAmount?.token
|
||||
const outputToken = trade?.outputAmount?.token
|
||||
|
||||
const price = showInverted
|
||||
? trade?.executionPrice?.toSignificant(6)
|
||||
: trade?.executionPrice?.invert()?.toSignificant(6)
|
||||
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
|
||||
|
||||
const show = Boolean(price?.baseCurrency && price?.quoteCurrency)
|
||||
const label = showInverted
|
||||
? `${outputToken?.symbol} per ${inputToken?.symbol}`
|
||||
: `${inputToken?.symbol} per ${outputToken?.symbol}`
|
||||
? `${price?.quoteCurrency?.symbol} per ${price?.baseCurrency?.symbol}`
|
||||
: `${price?.baseCurrency?.symbol} per ${price?.quoteCurrency?.symbol}`
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -32,10 +29,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
|
||||
color={theme.text2}
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
>
|
||||
{price && `${price} ${label}`}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
{show ? (
|
||||
<>
|
||||
{formattedPrice ?? '-'} {label}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { TokenAmount } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import Copy from '../AccountDetails/Copy'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
|
||||
export function TransferModalHeader({
|
||||
recipient,
|
||||
ENSName,
|
||||
amount
|
||||
}: {
|
||||
recipient: string
|
||||
ENSName: string
|
||||
amount: TokenAmount
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '40px' }}>
|
||||
<RowBetween>
|
||||
<Text fontSize={36} fontWeight={500}>
|
||||
{amount?.toSignificant(6)} {amount?.token?.symbol}
|
||||
</Text>
|
||||
<TokenLogo address={amount?.token?.address} size={'30px'} />
|
||||
</RowBetween>
|
||||
<TYPE.darkGray fontSize={20}>To</TYPE.darkGray>
|
||||
{ENSName ? (
|
||||
<AutoColumn gap="lg">
|
||||
<TYPE.blue fontSize={36}>{ENSName}</TYPE.blue>
|
||||
<AutoRow gap="10px">
|
||||
<ExternalLink href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
<TYPE.blue fontSize={18}>
|
||||
{recipient?.slice(0, 8)}...{recipient?.slice(34, 42)}↗
|
||||
</TYPE.blue>
|
||||
</ExternalLink>
|
||||
<Copy toCopy={recipient} />
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
) : (
|
||||
<AutoRow gap="10px">
|
||||
<ExternalLink href={getEtherscanLink(chainId, recipient, 'address')}>
|
||||
<TYPE.blue fontSize={36}>
|
||||
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}↗
|
||||
</TYPE.blue>
|
||||
</ExternalLink>
|
||||
<Copy toCopy={recipient} />
|
||||
</AutoRow>
|
||||
)}
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user