Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a538bf0b69 | ||
|
|
135cb8fb34 | ||
|
|
bf50582d38 | ||
|
|
110e23d6eb | ||
|
|
7ab6a17b42 | ||
|
|
7ad13c96a8 | ||
|
|
4e99cc4d93 | ||
|
|
6d29815f59 | ||
|
|
4888fe23df | ||
|
|
ef9ecd9ce2 | ||
|
|
f5d0804c46 | ||
|
|
0bac257254 | ||
|
|
a77752ab83 | ||
|
|
bf31ca4f06 | ||
|
|
ed8afbd851 | ||
|
|
47b6a7c4d5 | ||
|
|
086fc65457 | ||
|
|
7df53f30a0 | ||
|
|
66497a0108 | ||
|
|
e0eb701bc0 | ||
|
|
36cb0668a3 | ||
|
|
810f42136e | ||
|
|
07b7d7f268 | ||
|
|
39b5bb37cd | ||
|
|
feed63b1b3 | ||
|
|
ee56382956 | ||
|
|
64e396d9e0 | ||
|
|
2ffc8a0bdf | ||
|
|
5ec9cdc5c4 | ||
|
|
4d85775d90 | ||
|
|
c1c59ca692 | ||
|
|
f29d97413e | ||
|
|
a078d94a38 | ||
|
|
c9c3329bc3 | ||
|
|
13d0b70fa8 | ||
|
|
b852e4e64a | ||
|
|
55bd3555be | ||
|
|
972a65066c | ||
|
|
39a212f762 | ||
|
|
c362f4fe39 | ||
|
|
271ef580e1 | ||
|
|
81ced4cb8b | ||
|
|
ab214a8133 | ||
|
|
1b2d86ae3a | ||
|
|
40cac44e07 | ||
|
|
4e6d28cff4 | ||
|
|
709fad0804 | ||
|
|
573f4c873a | ||
|
|
d300db669f | ||
|
|
fb8217ddea | ||
|
|
7b9a23d920 | ||
|
|
120ad935fa | ||
|
|
4eaf16b624 | ||
|
|
857e2915ab | ||
|
|
7410c81b42 | ||
|
|
fb05439d32 | ||
|
|
fb7eade70b | ||
|
|
bd2b2c487a | ||
|
|
2f004ed1d9 | ||
|
|
db257c73f2 | ||
|
|
7c37b9d00e | ||
|
|
7688c527f0 | ||
|
|
06dd41a9cd | ||
|
|
850fec40a9 | ||
|
|
3c7eabc3d8 | ||
|
|
048607080c | ||
|
|
a0f20c54d8 | ||
|
|
da79abbc0d | ||
|
|
5ac08e1142 | ||
|
|
d330eea375 | ||
|
|
35dace7bfe | ||
|
|
8ce8e17f62 | ||
|
|
eb105b6ec7 | ||
|
|
267e7de2b6 | ||
|
|
ab9f2af054 | ||
|
|
67b405dd42 | ||
|
|
281dbf4305 | ||
|
|
369f2d7dfa | ||
|
|
803c749b13 | ||
|
|
8818dadf24 | ||
|
|
d179fc6b84 | ||
|
|
18ec675c52 | ||
|
|
1a79bac893 |
27
.eslintrc.js
@@ -4,4 +4,31 @@ require('@uniswap/eslint-config/load')
|
||||
|
||||
module.exports = {
|
||||
extends: '@uniswap/eslint-config/react',
|
||||
overrides: [
|
||||
{
|
||||
// Configuration/typings typically export objects/definitions that are used outside of the transpiled package
|
||||
// (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely.
|
||||
files: ['**/*.config.*', '**/*.d.ts'],
|
||||
rules: {
|
||||
'import/no-unused-modules': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'zustand',
|
||||
importNames: ['default'],
|
||||
message: 'Default import from zustand is deprecated. Import `{ create }` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
@uniswap/web
|
||||
@uniswap/web-reviewers
|
||||
8
.github/actions/setup/action.yml
vendored
@@ -1,4 +1,6 @@
|
||||
name: Setup
|
||||
description: checkout repo, setup node, and install node_modules
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
@@ -10,12 +12,14 @@ runs:
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: yarn
|
||||
|
||||
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: node_modules/
|
||||
path: |
|
||||
node_modules
|
||||
!node_modules/.cache
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
shell: bash
|
||||
|
||||
59
.github/pull_request_template.md
vendored
@@ -1,24 +1,45 @@
|
||||
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
|
||||
<!-- Your PR title must follow conventional commits: https://github.com/Uniswap/interface#pr-title -->
|
||||
|
||||
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
|
||||
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
|
||||
- docs: Documentation only changes
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- perf: A code change that improves performance
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- test: Adding missing tests or correcting existing tests
|
||||
## Description
|
||||
<!-- Summary of change, including motivation and context. -->
|
||||
<!-- Use verb-driven language: "Fixes XYZ" instead of "This change fixes XYZ" -->
|
||||
|
||||
Example commit messages:
|
||||
|
||||
- feat: adds support for gnosis safe wallet
|
||||
- fix: removes a polling memory leak
|
||||
- chore: bumps redux version
|
||||
<!-- Delete inapplicable lines: -->
|
||||
_JIRA ticket:_
|
||||
_Slack thread:_
|
||||
_Relevant docs:_
|
||||
|
||||
Other things to note:
|
||||
|
||||
- Please describe the change using verb statements (ex: Removes X from Y)
|
||||
- PRs with multiple changes should use a list of verb statements
|
||||
- Add any relevant unit / integration tests
|
||||
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
|
||||
<!-- Delete this section if your change does not affect UI. -->
|
||||
## Screen capture
|
||||
|
||||
| Before | After (Desktop) | After (Mobile) |
|
||||
| ------------ |---------------- | -------------- |
|
||||
| paste_before | past_after | paste_after |
|
||||
|
||||
|
||||
## Test plan
|
||||
|
||||
<!-- Delete this section if your change is not a bug fix. -->
|
||||
### Reproducing the error
|
||||
|
||||
<!-- Include steps to reproduce the bug. -->
|
||||
1.
|
||||
|
||||
### QA (ie manual testing)
|
||||
|
||||
<!-- Include steps to test the change, ensuring no regression. -->
|
||||
- [ ] N/A
|
||||
|
||||
|
||||
#### Devices
|
||||
<!-- If applicable, include different devices and screen sizes that may be affected, and how you've tested them. -->
|
||||
|
||||
|
||||
### Automated testing
|
||||
|
||||
<!-- If N/A, check and note so it is obvious to your reviewers and does not show up as an incomplete task. -->
|
||||
<!-- eg - [x] Unit test N/A -->
|
||||
- [ ] Unit test
|
||||
- [ ] Integration/E2E test
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
@@ -42,6 +42,8 @@ jobs:
|
||||
needs: tag
|
||||
if: ${{ needs.tag.outputs.new_tag != null }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
|
||||
81
.github/workflows/test.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Test
|
||||
|
||||
# Many build steps have their own caches, so each job has its own cache to improve subsequent build times.
|
||||
# Build tools are configured to cache cache to node_modules/.cache, so this is cached independently of node_modules.
|
||||
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -14,7 +18,27 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: eslint-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn lint
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: tsc-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn typecheck
|
||||
|
||||
deps-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -28,6 +52,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: jest-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn test
|
||||
- uses: codecov/codecov-action@v3
|
||||
@@ -41,9 +71,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: build-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
@@ -55,31 +91,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
- run: yarn test:size
|
||||
|
||||
|
||||
cypress-build:
|
||||
runs-on: ubuntu-latest
|
||||
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /root/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
|
||||
- if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn cypress install
|
||||
yarn cypress info
|
||||
|
||||
cypress-test-matrix:
|
||||
needs: [build, cypress-build]
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
|
||||
strategy:
|
||||
@@ -89,17 +109,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /root/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
|
||||
- if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: yarn cypress install
|
||||
key: ${{ runner.os }}-cypress
|
||||
- run: |
|
||||
yarn cypress install
|
||||
yarn cypress info
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- uses: cypress-io/github-action@v4
|
||||
with:
|
||||
@@ -113,6 +136,12 @@ jobs:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
|
||||
cypress-tests:
|
||||
if: ${{ always() }}
|
||||
|
||||
4
.gitignore
vendored
@@ -29,6 +29,10 @@ schema.graphql
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
instrumented
|
||||
.nyc_output
|
||||
.nyc_output/**/*
|
||||
|
||||
/.netlify
|
||||
|
||||
npm-debug.log*
|
||||
|
||||
7
.nycrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"all": true,
|
||||
"report-dir": "coverage",
|
||||
"soureMap": false,
|
||||
"instrument": false
|
||||
}
|
||||
27
README.md
@@ -38,6 +38,33 @@ You can block an entire list of tokens by passing in a tokenlist like [here](./s
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
#### PR Title
|
||||
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
|
||||
|
||||
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
|
||||
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
|
||||
- docs: Documentation only changes
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- perf: A code change that improves performance
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- test: Adding missing tests or correcting existing tests
|
||||
|
||||
Example commit messages:
|
||||
|
||||
- feat: adds support for gnosis safe wallet
|
||||
- fix: removes a polling memory leak
|
||||
- chore: bumps redux version
|
||||
|
||||
Other things to note:
|
||||
|
||||
- Please describe the change using verb statements (ex: Removes X from Y)
|
||||
- PRs with multiple changes should use a list of verb statements
|
||||
- Add any relevant unit / integration tests
|
||||
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
|
||||
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
21
codecov.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
ignore:
|
||||
- "**/generated/**/*"
|
||||
- "**/generated/*"
|
||||
- "**/cypress/**/*"
|
||||
- "cypress/**/*"
|
||||
- "**/instrumented/**/*"
|
||||
- "**/styles/**/*"
|
||||
- "styles/**/*"
|
||||
- "**/constants/**/*"
|
||||
- "constants/**/*"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
if_ci_failed: error
|
||||
patch:
|
||||
default:
|
||||
target: 80%
|
||||
@@ -1,17 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-env node */
|
||||
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
|
||||
const { execSync } = require('child_process')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const { DefinePlugin } = require('webpack')
|
||||
|
||||
const commitHash = require('child_process').execSync('git rev-parse HEAD')
|
||||
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
// Linting and type checking are only necessary as part of development and testing.
|
||||
// Omit them from production builds, as they slow down the feedback loop.
|
||||
const shouldLintOrTypeCheck = !isProduction
|
||||
|
||||
module.exports = {
|
||||
babel: {
|
||||
plugins: ['@vanilla-extract/babel-plugin'],
|
||||
env: {
|
||||
test: {
|
||||
plugins: ['istanbul'],
|
||||
},
|
||||
development: {
|
||||
plugins: ['istanbul'],
|
||||
},
|
||||
},
|
||||
},
|
||||
eslint: {
|
||||
enable: shouldLintOrTypeCheck,
|
||||
pluginOptions(eslintConfig) {
|
||||
return Object.assign(eslintConfig, {
|
||||
cache: true,
|
||||
cacheLocation: 'node_modules/.cache/eslint/',
|
||||
})
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
enableTypeChecking: shouldLintOrTypeCheck,
|
||||
},
|
||||
jest: {
|
||||
configure(jestConfig) {
|
||||
return Object.assign({}, jestConfig, {
|
||||
return Object.assign(jestConfig, {
|
||||
cacheDirectory: 'node_modules/.cache/jest',
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
@@ -21,25 +48,29 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
webpack: {
|
||||
plugins: [
|
||||
new VanillaExtractPlugin({ identifiers: 'short' }),
|
||||
new DefinePlugin({
|
||||
'process.env.REACT_APP_GIT_COMMIT_HASH': JSON.stringify(commitHash.toString()),
|
||||
}),
|
||||
],
|
||||
plugins: [new VanillaExtractPlugin({ identifiers: 'short' })],
|
||||
configure: (webpackConfig) => {
|
||||
const instanceOfMiniCssExtractPlugin = webpackConfig.plugins.find(
|
||||
(plugin) => plugin instanceof MiniCssExtractPlugin
|
||||
)
|
||||
if (instanceOfMiniCssExtractPlugin !== undefined) instanceOfMiniCssExtractPlugin.options.ignoreOrder = true
|
||||
webpackConfig.plugins = webpackConfig.plugins.map((plugin) => {
|
||||
// Extend process.env with dynamic values (eg commit hash).
|
||||
// This will make dynamic values available to JavaScript only, not to interpolated HTML (ie index.html).
|
||||
if (plugin instanceof DefinePlugin) {
|
||||
Object.assign(plugin.definitions['process.env'], {
|
||||
REACT_APP_GIT_COMMIT_HASH: JSON.stringify(commitHash),
|
||||
})
|
||||
}
|
||||
|
||||
// We're currently on Webpack 4.x that doesn't support the `exports` field in package.json.
|
||||
// CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings.
|
||||
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
|
||||
if (plugin instanceof MiniCssExtractPlugin) {
|
||||
plugin.options.ignoreOrder = true
|
||||
}
|
||||
|
||||
return plugin
|
||||
})
|
||||
|
||||
// We're currently on Webpack 4.x which doesn't support the `exports` field in package.json.
|
||||
// Instead, we need to manually map the import path to the correct exports path (eg dist or build folder).
|
||||
// See https://github.com/webpack/webpack/issues/9509.
|
||||
//
|
||||
// In case you need to add more modules, make sure to remap them to the correct path.
|
||||
//
|
||||
// Map @uniswap/conedison to its dist folder.
|
||||
// This is required because conedison uses * to redirect all imports to its dist.
|
||||
webpackConfig.resolve.alias['@uniswap/conedison'] = '@uniswap/conedison/dist'
|
||||
|
||||
return webpackConfig
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
@@ -5,8 +6,10 @@ export default defineConfig({
|
||||
videoUploadOnPasses: false,
|
||||
defaultCommandTimeout: 24000, // 2x average block time
|
||||
chromeWebSecurity: false,
|
||||
retries: { runMode: 2 },
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
codeCoverageTask(on, config)
|
||||
return {
|
||||
...config,
|
||||
// Only enable Chrome.
|
||||
|
||||
@@ -26,7 +26,15 @@ describe('Landing Page', () => {
|
||||
|
||||
it('allows navigation to pool', () => {
|
||||
cy.viewport(2000, 1600)
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('pool-nav-link')).first().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
|
||||
it('allows navigation to pool on mobile', () => {
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('pool-nav-link')).last().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -53,4 +53,11 @@ describe('Testing nfts', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
||||
})
|
||||
|
||||
it('should close the sidebar when navigating to NFT details', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-nfts')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
11
cypress/e2e/position.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
describe('Position', () => {
|
||||
it('shows an valid state on a supported network', () => {
|
||||
cy.visit('/pools/1')
|
||||
cy.contains('UNI / ETH')
|
||||
})
|
||||
|
||||
it('shows an invalid state on a supported network', () => {
|
||||
cy.visit('/pools/788893')
|
||||
cy.contains('To view a position, you must be connected to the network it belongs to.')
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,7 @@ describe('Token explore', () => {
|
||||
|
||||
it('should load token leaderboard', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.eq', 100)
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.greaterThan', 0)
|
||||
// check sorted svg icon is present in volume cell, since tokens are sorted by volume by default
|
||||
cy.get(getTestSelector('header-row')).find(getTestSelector('volume-cell')).find('svg').should('exist')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
|
||||
|
||||
@@ -77,4 +77,20 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
|
||||
})
|
||||
|
||||
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
|
||||
visit(false)
|
||||
cy.viewport('iphone-6')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-buy-crypto')).click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
|
||||
visit(true)
|
||||
cy.viewport('iphone-6')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.root().click(15, 40)
|
||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// Import commands.ts using ES2015 syntax:
|
||||
import { injected } from './ethereum'
|
||||
import assert = require('assert')
|
||||
import '@cypress/code-coverage/support'
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
|
||||
|
||||
22
package.json
@@ -17,16 +17,17 @@
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
|
||||
"prepare": "yarn contracts:compile && yarn graphql:fetch && yarn graphql:generate && yarn i18n:compile",
|
||||
"postinstall": "patch-package",
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"serve": "serve build -l 3000",
|
||||
"deduplicate": "yarn-deduplicate --strategy=highest",
|
||||
"lint": "yarn eslint .",
|
||||
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "craco test --coverage",
|
||||
"test:size": "node scripts/test-size.js",
|
||||
"cypress:open": "cypress open --browser chrome --e2e",
|
||||
"cypress:run": "cypress run --browser chrome --e2e",
|
||||
"postinstall": "patch-package"
|
||||
"deduplicate": "yarn-deduplicate --strategy=highest"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
@@ -37,8 +38,12 @@
|
||||
"src/lib/utils/**/*.ts*",
|
||||
"src/pages/**/*.ts*",
|
||||
"src/state/**/*.ts*",
|
||||
"src/tracing/**/*.ts*",
|
||||
"src/utils/**/*.ts*"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".snap"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 4,
|
||||
@@ -66,6 +71,7 @@
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@typechain/ethers-v5": "^7.0.0",
|
||||
"@types/array.prototype.flat": "^1.2.1",
|
||||
"@types/array.prototype.flatmap": "^1.2.2",
|
||||
@@ -94,7 +100,7 @@
|
||||
"@uniswap/eslint-config": "^1.1.1",
|
||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
"cypress": "^10.3.1",
|
||||
"cypress": "10.3.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
@@ -113,6 +119,7 @@
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.2",
|
||||
"@coinbase/wallet-sdk": "^3.6.4",
|
||||
"@cypress/code-coverage": "^3.10.0",
|
||||
"@fontsource/ibm-plex-mono": "^4.5.1",
|
||||
"@fontsource/inter": "^4.5.1",
|
||||
"@graphql-codegen/cli": "^2.15.0",
|
||||
@@ -131,11 +138,11 @@
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@sentry/react": "^7.40.0",
|
||||
"@sentry/tracing": "^7.40.0",
|
||||
"@sentry/react": "^7.45.0",
|
||||
"@sentry/tracing": "^7.45.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "^1.3.1",
|
||||
"@uniswap/analytics-events": "^2.7.0",
|
||||
"@uniswap/analytics-events": "^2.10.0",
|
||||
"@uniswap/conedison": "^1.4.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
@@ -178,6 +185,7 @@
|
||||
"@web3-react/walletconnect": "8.1.2-beta.0",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"cids": "^1.0.0",
|
||||
"clsx": "^1.1.1",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 306 KiB |
19
src/assets/svg/wallet_banner_phone_image.svg
Normal file
|
After Width: | Height: | Size: 990 KiB |
@@ -12,12 +12,14 @@ import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight, Copy, CreditCard, IconProps, Info, Power, Settings } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { updateSelectedWallet } from 'state/user/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
@@ -28,13 +30,13 @@ import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import { useToggleWalletDrawer } from '.'
|
||||
import { useToggleAccountDrawer } from '.'
|
||||
import IconButton, { IconHoverText } from './IconButton'
|
||||
import MiniPortfolio from './MiniPortfolio'
|
||||
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
|
||||
|
||||
const AuthenticatedHeaderWrapper = styled.div`
|
||||
padding: 14px 12px 16px 16px;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -166,6 +168,8 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
|
||||
|
||||
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
|
||||
const isUnclaimed = useUserHasAvailableClaim(account)
|
||||
const getConnection = useGetConnection()
|
||||
@@ -180,7 +184,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
dispatch(updateSelectedWallet({ wallet: undefined }))
|
||||
}, [connector, dispatch])
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
const navigateToProfile = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
@@ -193,9 +197,10 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
|
||||
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
|
||||
const openFoRModalWithAnalytics = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_WIDGET_OPENED)
|
||||
openFiatOnrampModal()
|
||||
}, [openFiatOnrampModal])
|
||||
}, [openFiatOnrampModal, toggleWalletDrawer])
|
||||
|
||||
const [shouldCheck, setShouldCheck] = useState(false)
|
||||
const {
|
||||
@@ -283,11 +288,22 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
<LoadingBubble height="16px" width="100px" margin="4px 0 20px 0" />
|
||||
</Column>
|
||||
)}
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<HeaderButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
onClick={navigateToProfile}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</HeaderButton>
|
||||
)}
|
||||
<HeaderButton
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
onClick={handleBuyCryptoClick}
|
||||
disabled={disableBuyCryptoButton}
|
||||
data-testid="wallet-buy-crypto"
|
||||
>
|
||||
{error ? (
|
||||
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||
@@ -302,14 +318,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
</>
|
||||
)}
|
||||
</HeaderButton>
|
||||
<HeaderButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
onClick={navigateToProfile}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</HeaderButton>
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
@@ -1,7 +1,6 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { PropsWithChildren, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ClickableStyle } from 'theme'
|
||||
import { isIOS } from 'utils/userAgent'
|
||||
@@ -33,22 +32,38 @@ function BaseButton({ onClick, branded, children }: PropsWithChildren<{ onClick?
|
||||
)
|
||||
}
|
||||
|
||||
export const APP_STORE_LINK = 'https://apps.apple.com/us/app/uniswap-wallet-defi-nfts/id6443944476'
|
||||
const APP_STORE_LINK = 'https://apps.apple.com/us/app/uniswap-wallet/id6443944476'
|
||||
const MICROSITE_LINK = 'https://wallet.uniswap.org/'
|
||||
|
||||
const openAppStore = () => {
|
||||
window.open(APP_STORE_LINK, /* target = */ 'uniswap_wallet_appstore')
|
||||
}
|
||||
export const openWalletMicrosite = () => {
|
||||
sendAnalyticsEvent(InterfaceEventName.UNISWAP_WALLET_MICROSITE_OPENED)
|
||||
window.open(MICROSITE_LINK, /* target = */ 'uniswap_wallet_microsite')
|
||||
}
|
||||
|
||||
export function openDownloadApp(element: InterfaceElementName) {
|
||||
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { element })
|
||||
if (isIOS) openAppStore()
|
||||
else openWalletMicrosite()
|
||||
}
|
||||
|
||||
// Launches App Store if on an iOS device, else navigates to Uniswap Wallet microsite
|
||||
export function DownloadButton({ onClick, text = 'Download' }: { onClick?: () => void; text?: string }) {
|
||||
const navigate = useNavigate()
|
||||
const micrositeEnabled = useMGTMMicrositeEnabled()
|
||||
|
||||
export function DownloadButton({
|
||||
onClick,
|
||||
text = 'Download',
|
||||
element,
|
||||
}: {
|
||||
onClick?: () => void
|
||||
text?: string
|
||||
element: InterfaceElementName
|
||||
}) {
|
||||
const onButtonClick = useCallback(() => {
|
||||
// handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad
|
||||
onClick?.()
|
||||
|
||||
if (isIOS || !micrositeEnabled) {
|
||||
sendAnalyticsEvent('Uniswap wallet download clicked')
|
||||
window.open(APP_STORE_LINK)
|
||||
} else navigate('/wallet')
|
||||
}, [onClick, micrositeEnabled, navigate])
|
||||
openDownloadApp(element)
|
||||
}, [element, onClick])
|
||||
|
||||
return (
|
||||
<BaseButton branded onClick={onButtonClick}>
|
||||
@@ -56,8 +71,3 @@ export function DownloadButton({ onClick, text = 'Download' }: { onClick?: () =>
|
||||
</BaseButton>
|
||||
)
|
||||
}
|
||||
|
||||
export function LearnMoreButton() {
|
||||
const navigate = useNavigate()
|
||||
return <BaseButton onClick={() => navigate('/wallet')}>Learn More</BaseButton>
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import Column from 'components/Column'
|
||||
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
|
||||
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
|
||||
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import styled from 'styled-components/macro'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
import { PortfolioLogo } from '../PortfolioLogo'
|
||||
import PortfolioRow from '../PortfolioRow'
|
||||
import { useTimeSince } from './parseRemote'
|
||||
import { Activity } from './types'
|
||||
|
||||
const ActivityRowDescriptor = styled(ThemedText.BodySmall)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
${EllipsisStyle}
|
||||
`
|
||||
|
||||
const StyledTimestamp = styled(ThemedText.Caption)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-variant: small;
|
||||
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
|
||||
`
|
||||
|
||||
export function ActivityRow({
|
||||
activity: { chainId, status, title, descriptor, logos, otherAccount, currencies, timestamp, hash },
|
||||
}: {
|
||||
activity: Activity
|
||||
}) {
|
||||
const { ENSName } = useENSName(otherAccount)
|
||||
const timeSince = useTimeSince(timestamp)
|
||||
|
||||
const explorerUrl = getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)
|
||||
|
||||
return (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_ROW}
|
||||
properties={{ hash, chain_id: chainId, explorer_url: explorerUrl }}
|
||||
>
|
||||
<PortfolioRow
|
||||
left={
|
||||
<Column>
|
||||
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
|
||||
</Column>
|
||||
}
|
||||
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
|
||||
descriptor={
|
||||
<ActivityRowDescriptor color="textSecondary">
|
||||
{descriptor}
|
||||
{ENSName ?? otherAccount}
|
||||
</ActivityRowDescriptor>
|
||||
}
|
||||
right={
|
||||
status === TransactionStatus.Pending ? (
|
||||
<LoaderV2 />
|
||||
) : status === TransactionStatus.Confirmed ? (
|
||||
<StyledTimestamp>{timeSince}</StyledTimestamp>
|
||||
) : (
|
||||
<AlertTriangleFilled />
|
||||
)
|
||||
}
|
||||
onClick={() => window.open(explorerUrl, '_blank')}
|
||||
/>
|
||||
</TraceEvent>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,20 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
|
||||
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
|
||||
import { TransactionStatus, useTransactionListQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { PollingInterval } from 'graphql/data/util'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { PortfolioLogo } from '../PortfolioLogo'
|
||||
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
|
||||
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
|
||||
import { ActivityRow } from './ActivityRow'
|
||||
import { useLocalActivities } from './parseLocal'
|
||||
import { parseRemoteActivities, useTimeSince } from './parseRemote'
|
||||
import { parseRemoteActivities } from './parseRemote'
|
||||
import { Activity, ActivityMap } from './types'
|
||||
|
||||
interface ActivityGroup {
|
||||
@@ -101,11 +97,11 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
|
||||
const lastFetchedAtom = atom<number | undefined>(0)
|
||||
|
||||
export default function ActivityTab({ account }: { account: string }) {
|
||||
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
export function ActivityTab({ account }: { account: string }) {
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
|
||||
|
||||
const localMap = useLocalActivities()
|
||||
const localMap = useLocalActivities(account)
|
||||
|
||||
const { data, loading, refetch } = useTransactionListQuery({
|
||||
variables: { account },
|
||||
@@ -158,49 +154,3 @@ export default function ActivityTab({ account }: { account: string }) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const StyledDescriptor = styled(ThemedText.BodySmall)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
${EllipsisStyle}
|
||||
`
|
||||
|
||||
const StyledTimestamp = styled(ThemedText.Caption)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-variant: small;
|
||||
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
|
||||
`
|
||||
|
||||
function ActivityRow({ activity }: { activity: Activity }) {
|
||||
const { chainId, status, title, descriptor, logos, otherAccount, currencies } = activity
|
||||
const { ENSName } = useENSName(otherAccount)
|
||||
|
||||
const explorerUrl = getExplorerLink(activity.chainId, activity.hash, ExplorerDataType.TRANSACTION)
|
||||
const timeSince = useTimeSince(activity.timestamp)
|
||||
|
||||
return (
|
||||
<PortfolioRow
|
||||
left={
|
||||
<Column>
|
||||
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
|
||||
</Column>
|
||||
}
|
||||
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
|
||||
descriptor={
|
||||
<StyledDescriptor color="textSecondary">
|
||||
{descriptor}
|
||||
{ENSName ?? otherAccount}
|
||||
</StyledDescriptor>
|
||||
}
|
||||
right={
|
||||
status === TransactionStatus.Pending ? (
|
||||
<LoaderV2 />
|
||||
) : status === TransactionStatus.Confirmed ? (
|
||||
<StyledTimestamp>{timeSince}</StyledTimestamp>
|
||||
) : (
|
||||
<AlertTriangleFilled />
|
||||
)
|
||||
}
|
||||
onClick={() => window.open(explorerUrl, '_blank')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core'
|
||||
import { PERMIT2_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { DAI as MockDAI, nativeOnChain, USDC_MAINNET as MockUSDC_MAINNET } from 'constants/tokens'
|
||||
import { TransactionStatus as MockTxStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { TokenAddressMap } from 'state/lists/hooks'
|
||||
import {
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
TransactionDetails,
|
||||
TransactionInfo,
|
||||
TransactionType as MockTxType,
|
||||
} from 'state/transactions/types'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
|
||||
import { parseLocalActivity, useLocalActivities } from './parseLocal'
|
||||
|
||||
function mockSwapInfo(
|
||||
type: MockTradeType,
|
||||
inputCurrency: Token,
|
||||
inputCurrencyAmountRaw: string,
|
||||
outputCurrency: Token,
|
||||
outputCurrencyAmountRaw: string
|
||||
): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo {
|
||||
if (type === MockTradeType.EXACT_INPUT) {
|
||||
return {
|
||||
type: MockTxType.SWAP,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
inputCurrencyAmountRaw,
|
||||
outputCurrencyId: outputCurrency.address,
|
||||
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
|
||||
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: MockTxType.SWAP,
|
||||
tradeType: MockTradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
outputCurrencyId: outputCurrency.address,
|
||||
outputCurrencyAmountRaw,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockAccount1 = '0x000000000000000000000000000000000000000001'
|
||||
const mockAccount2 = '0x000000000000000000000000000000000000000002'
|
||||
const mockChainId = SupportedChainId.MAINNET
|
||||
const mockSpenderAddress = PERMIT2_ADDRESS[mockChainId]
|
||||
const mockCurrencyAmountRaw = '1000000000000000000'
|
||||
const mockCurrencyAmountRawUSDC = '1000000'
|
||||
|
||||
function mockHash(id: string, status: MockTxStatus = MockTxStatus.Confirmed) {
|
||||
return id + status
|
||||
}
|
||||
|
||||
function mockCommonFields(id: string, account = mockAccount2, status: MockTxStatus) {
|
||||
const hash = mockHash(id, status)
|
||||
return {
|
||||
hash,
|
||||
from: account,
|
||||
receipt:
|
||||
status === MockTxStatus.Pending
|
||||
? undefined
|
||||
: {
|
||||
transactionHash: hash,
|
||||
status: status === MockTxStatus.Confirmed ? 1 : 0,
|
||||
},
|
||||
addedTime: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function mockMultiStatus(info: TransactionInfo, id: string): [TransactionDetails, number][] {
|
||||
// Mocks a transaction with multiple statuses
|
||||
return [
|
||||
[
|
||||
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Pending) } as unknown as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Confirmed) } as unknown as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Failed) } as unknown as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
const mockTokenAddressMap: TokenAddressMap = {
|
||||
[mockChainId]: {
|
||||
[MockDAI.address]: { token: MockDAI },
|
||||
[MockUSDC_MAINNET.address]: { token: MockUSDC_MAINNET },
|
||||
} as TokenAddressMap[number],
|
||||
}
|
||||
|
||||
jest.mock('../../../../state/lists/hooks', () => ({
|
||||
useCombinedActiveList: () => mockTokenAddressMap,
|
||||
}))
|
||||
|
||||
jest.mock('../../../../state/transactions/hooks', () => {
|
||||
return {
|
||||
useMultichainTransactions: (): [TransactionDetails, number][] => {
|
||||
return [
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
...mockCommonFields('0x123', mockAccount1, MockTxStatus.Confirmed),
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
...mockMultiStatus(
|
||||
mockSwapInfo(
|
||||
MockTradeType.EXACT_OUTPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
'0xswap_exact_input'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
'0xswap_exact_output'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.APPROVAL,
|
||||
tokenAddress: MockDAI.address,
|
||||
spender: mockSpenderAddress,
|
||||
},
|
||||
'0xapproval'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.WRAP,
|
||||
unwrapped: false,
|
||||
currencyAmountRaw: mockCurrencyAmountRaw,
|
||||
chainId: mockChainId,
|
||||
},
|
||||
'0xwrap'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.WRAP,
|
||||
unwrapped: true,
|
||||
currencyAmountRaw: mockCurrencyAmountRaw,
|
||||
chainId: mockChainId,
|
||||
},
|
||||
'0xunwrap'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.ADD_LIQUIDITY_V3_POOL,
|
||||
createPool: false,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
feeAmount: 500,
|
||||
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
|
||||
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xadd_liquidity_v3'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.REMOVE_LIQUIDITY_V3,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
|
||||
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xremove_liquidity_v3'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.ADD_LIQUIDITY_V2_POOL,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
|
||||
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xadd_liquidity_v2'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.COLLECT_FEES,
|
||||
currencyId0: MockUSDC_MAINNET.address,
|
||||
currencyId1: MockDAI.address,
|
||||
expectedCurrencyOwed0: mockCurrencyAmountRawUSDC,
|
||||
expectedCurrencyOwed1: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xcollect_fees'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.MIGRATE_LIQUIDITY_V3,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
isFork: false,
|
||||
},
|
||||
'0xmigrate_v3_liquidity'
|
||||
),
|
||||
]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('parseLocalActivity', () => {
|
||||
it('returns swap activity fields with known tokens, exact input', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: mockCurrencyAmountRawUSDC,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
expectedOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
|
||||
minimumOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns swap activity fields with known tokens, exact output', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_OUTPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
status: 'CONFIRMED',
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns swap activity fields with unknown tokens', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = {} as TokenAddressMap
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({
|
||||
chainId: 1,
|
||||
currencies: [undefined, undefined],
|
||||
descriptor: 'Unknown for Unknown',
|
||||
status: 'CONFIRMED',
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('only returns activity for the current account', () => {
|
||||
const account1Activites = renderHook(() => useLocalActivities(mockAccount1)).result.current
|
||||
const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current
|
||||
|
||||
expect(Object.values(account1Activites)).toHaveLength(1)
|
||||
expect(Object.values(account2Activites)).toHaveLength(30)
|
||||
})
|
||||
|
||||
it('Properly uses correct tense of activity title based on tx status', () => {
|
||||
const activities = renderHook(() => useLocalActivities(mockAccount2)).result.current
|
||||
|
||||
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Pending)]?.title).toEqual('Swapping')
|
||||
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Confirmed)]?.title).toEqual('Swapped')
|
||||
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Failed)]?.title).toEqual('Swap failed')
|
||||
})
|
||||
|
||||
it('Adapts Swap exact input to Activity type', () => {
|
||||
const hash = mockHash('0xswap_exact_input')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Swapped',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Swap exact output to Activity type', () => {
|
||||
const hash = mockHash('0xswap_exact_output')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Swapped',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Approval to Activity type', () => {
|
||||
const hash = mockHash('0xapproval')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockDAI],
|
||||
title: 'Approved',
|
||||
descriptor: MockDAI.symbol,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Wrap to Activity type', () => {
|
||||
const hash = mockHash('0xwrap')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
const native = nativeOnChain(mockChainId)
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [native, native.wrapped],
|
||||
title: 'Wrapped',
|
||||
descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Unwrap to Activity type', () => {
|
||||
const hash = mockHash('0xunwrap')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
const native = nativeOnChain(mockChainId)
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [native.wrapped, native],
|
||||
title: 'Unwrapped',
|
||||
descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts AddLiquidityV3 to Activity type', () => {
|
||||
const hash = mockHash('0xadd_liquidity_v3')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Added liquidity',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts RemoveLiquidityV3 to Activity type', () => {
|
||||
const hash = mockHash('0xremove_liquidity_v3')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Removed liquidity',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts RemoveLiquidityV2 to Activity type', () => {
|
||||
const hash = mockHash('0xadd_liquidity_v2')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Added V2 liquidity',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts CollectFees to Activity type', () => {
|
||||
const hash = mockHash('0xcollect_fees')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Collected fees',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts MigrateLiquidityV3 to Activity type', () => {
|
||||
const hash = mockHash('0xmigrate_v3_liquidity')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Migrated liquidity',
|
||||
descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,6 @@ import { t } from '@lingui/macro'
|
||||
import { formatCurrencyAmount } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { nativeOnChain } from '@uniswap/smart-order-router'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TransactionPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useMemo } from 'react'
|
||||
@@ -26,20 +25,22 @@ import {
|
||||
import { getActivityTitle } from '../constants'
|
||||
import { Activity, ActivityMap } from './types'
|
||||
|
||||
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: TokenAddressMap) {
|
||||
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId][currencyId].token
|
||||
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: TokenAddressMap): Currency | undefined {
|
||||
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]?.token
|
||||
}
|
||||
|
||||
function buildCurrencyDescriptor(
|
||||
currencyA: Currency,
|
||||
currencyA: Currency | undefined,
|
||||
amtA: string,
|
||||
currencyB: Currency,
|
||||
currencyB: Currency | undefined,
|
||||
amtB: string,
|
||||
delimiter = t`for`
|
||||
) {
|
||||
const formattedA = formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyA, amtA))
|
||||
const formattedB = formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyB, amtB))
|
||||
return `${formattedA} ${currencyA.symbol} ${delimiter} ${formattedB} ${currencyB.symbol}`
|
||||
const formattedA = currencyA ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyA, amtA)) : t`Unknown`
|
||||
const symbolA = currencyA?.symbol ?? ''
|
||||
const formattedB = currencyB ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyB, amtB)) : t`Unknown`
|
||||
const symbolB = currencyB?.symbol ?? ''
|
||||
return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function parseSwap(
|
||||
@@ -79,7 +80,7 @@ function parseApproval(
|
||||
): Partial<Activity> {
|
||||
// TODO: Add 'amount' approved to ApproveTransactionInfo so we can distinguish between revoke and approve
|
||||
const currency = getCurrency(approval.tokenAddress, chainId, tokens)
|
||||
const descriptor = t`${currency.symbol ?? currency.name}`
|
||||
const descriptor = currency?.symbol ?? currency?.name ?? t`Unknown`
|
||||
return {
|
||||
descriptor,
|
||||
currencies: [currency],
|
||||
@@ -120,82 +121,82 @@ function parseMigrateCreateV3(
|
||||
tokens: TokenAddressMap
|
||||
): Partial<Activity> {
|
||||
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const quoteCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const descriptor = t`${baseCurrency.symbol} and ${quoteCurrency.symbol}`
|
||||
const baseSymbol = baseCurrency?.symbol ?? t`Unknown`
|
||||
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
|
||||
const quoteSymbol = quoteCurrency?.symbol ?? t`Unknown`
|
||||
const descriptor = t`${baseSymbol} and ${quoteSymbol}`
|
||||
|
||||
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
|
||||
}
|
||||
|
||||
function parseLocalActivity(
|
||||
export function parseLocalActivity(
|
||||
details: TransactionDetails,
|
||||
chainId: SupportedChainId,
|
||||
tokens: TokenAddressMap
|
||||
): Activity | undefined {
|
||||
const status = !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
try {
|
||||
const status = !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
|
||||
const receipt: TransactionPartsFragment | undefined = details.receipt
|
||||
? {
|
||||
id: details.receipt.transactionHash,
|
||||
...details.receipt,
|
||||
...details,
|
||||
status,
|
||||
}
|
||||
: undefined
|
||||
const receipt: TransactionPartsFragment | undefined = details.receipt
|
||||
? {
|
||||
id: details.receipt.transactionHash,
|
||||
...details.receipt,
|
||||
...details,
|
||||
status,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const defaultFields = {
|
||||
hash: details.hash,
|
||||
chainId,
|
||||
title: getActivityTitle(details.info.type, status),
|
||||
status,
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
receipt,
|
||||
const defaultFields = {
|
||||
hash: details.hash,
|
||||
chainId,
|
||||
title: getActivityTitle(details.info.type, status),
|
||||
status,
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
receipt,
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
const info = details.info
|
||||
if (info.type === TransactionType.SWAP) {
|
||||
additionalFields = parseSwap(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.APPROVAL) {
|
||||
additionalFields = parseApproval(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.WRAP) {
|
||||
additionalFields = parseWrap(info, chainId, status)
|
||||
} else if (
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
|
||||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
|
||||
) {
|
||||
additionalFields = parseLP(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.COLLECT_FEES) {
|
||||
additionalFields = parseCollectFees(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
|
||||
return { ...defaultFields, ...additionalFields }
|
||||
} catch (error) {
|
||||
console.debug(`Failed to parse transaction ${details.hash}`, error)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
const info = details.info
|
||||
if (info.type === TransactionType.SWAP) {
|
||||
additionalFields = parseSwap(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.APPROVAL) {
|
||||
additionalFields = parseApproval(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.WRAP) {
|
||||
additionalFields = parseWrap(info, chainId, status)
|
||||
} else if (
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
|
||||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
|
||||
) {
|
||||
additionalFields = parseLP(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.COLLECT_FEES) {
|
||||
additionalFields = parseCollectFees(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
|
||||
return { ...defaultFields, ...additionalFields }
|
||||
}
|
||||
|
||||
export function useLocalActivities(): ActivityMap | undefined {
|
||||
export function useLocalActivities(account: string): ActivityMap {
|
||||
const allTransactions = useMultichainTransactions()
|
||||
const { chainId } = useWeb3React()
|
||||
const tokens = useCombinedActiveList()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
chainId
|
||||
? allTransactions.reduce((acc: { [hash: string]: Activity }, [transaction, chainId]) => {
|
||||
try {
|
||||
const localActivity = parseLocalActivity(transaction, chainId, tokens)
|
||||
if (localActivity) acc[localActivity.hash] = localActivity
|
||||
} catch (error) {
|
||||
console.error('Failed to parse local activity', transaction)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
: undefined,
|
||||
[allTransactions, chainId, tokens]
|
||||
)
|
||||
return useMemo(() => {
|
||||
const activityByHash: ActivityMap = {}
|
||||
for (const [transaction, chainId] of allTransactions) {
|
||||
if (transaction.from !== account) continue
|
||||
|
||||
activityByHash[transaction.hash] = parseLocalActivity(transaction, chainId, tokens)
|
||||
}
|
||||
return activityByHash
|
||||
}, [account, allTransactions, tokens])
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export type Activity = {
|
||||
title: string
|
||||
descriptor?: string
|
||||
logos?: Array<string | undefined>
|
||||
currencies?: Array<Currency>
|
||||
currencies?: Array<Currency | undefined>
|
||||
otherAccount?: string
|
||||
receipt?: Receipt
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { NftCard } from 'nft/components/card'
|
||||
import { detailsHref } from 'nft/components/card/utils'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { WalletAsset } from 'nft/types'
|
||||
import { floorFormatter } from 'nft/utils'
|
||||
@@ -43,12 +46,13 @@ export function NFT({
|
||||
mediaShouldBePlaying: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}) {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const navigate = useNavigate()
|
||||
const trace = useTrace()
|
||||
|
||||
const navigateToNFTDetails = () => {
|
||||
navigate(`/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}`)
|
||||
toggleWalletDrawer()
|
||||
navigate(detailsHref(asset))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -59,12 +63,19 @@ export function NFT({
|
||||
display={{ disabledInfo: true }}
|
||||
isSelected={false}
|
||||
isDisabled={false}
|
||||
selectAsset={navigateToNFTDetails}
|
||||
unselectAsset={() => {
|
||||
/* */
|
||||
}}
|
||||
onCardClick={navigateToNFTDetails}
|
||||
sendAnalyticsEvent={() =>
|
||||
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
|
||||
element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM,
|
||||
collection_name: asset.collection?.name,
|
||||
collection_address: asset.collection?.address,
|
||||
token_id: asset.tokenId,
|
||||
...trace,
|
||||
})
|
||||
}
|
||||
mediaShouldBePlaying={mediaShouldBePlaying}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
testId="mini-portfolio-nft"
|
||||
/>
|
||||
<NFTDetails asset={asset} />
|
||||
</NFTContainer>
|
||||
@@ -5,13 +5,22 @@ import { useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useToggleWalletDrawer } from '..'
|
||||
import { DEFAULT_NFT_QUERY_AMOUNT } from './constants'
|
||||
import { NFT } from './NFT'
|
||||
import { useAccountDrawer } from '../..'
|
||||
import { DEFAULT_NFT_QUERY_AMOUNT } from '../constants'
|
||||
import { NFT } from './NFTItem'
|
||||
|
||||
export default function NFTs({ account }: { account: string }) {
|
||||
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(account, [], [], DEFAULT_NFT_QUERY_AMOUNT)
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(
|
||||
account,
|
||||
[],
|
||||
[],
|
||||
DEFAULT_NFT_QUERY_AMOUNT,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
!walletDrawerOpen
|
||||
)
|
||||
|
||||
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import Pools from '.'
|
||||
import useMultiChainPositions from './useMultiChainPositions'
|
||||
|
||||
jest.mock('./useMultiChainPositions')
|
||||
|
||||
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
|
||||
|
||||
const pool = new Pool(
|
||||
USDC_MAINNET,
|
||||
WETH9[SupportedChainId.MAINNET],
|
||||
FeeAmount.MEDIUM,
|
||||
'1851127709498178402383049949138810',
|
||||
'7076437181775065414',
|
||||
201189
|
||||
)
|
||||
|
||||
const position = new Position({
|
||||
pool,
|
||||
liquidity: 1341008833950736,
|
||||
tickLower: 200040,
|
||||
tickUpper: 202560,
|
||||
})
|
||||
const details = {
|
||||
nonce: BigNumber.from('0'),
|
||||
tokenId: BigNumber.from('0'),
|
||||
operator: '0x0',
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -100,
|
||||
tickUpper: 100,
|
||||
liquidity: BigNumber.from('9000'),
|
||||
feeGrowthInside0LastX128: BigNumber.from('0'),
|
||||
feeGrowthInside1LastX128: BigNumber.from('0'),
|
||||
tokensOwed0: BigNumber.from('0'),
|
||||
tokensOwed1: BigNumber.from('0'),
|
||||
}
|
||||
const useMultiChainPositionsReturnValue = {
|
||||
positions: [
|
||||
{
|
||||
owner,
|
||||
chainId: SupportedChainId.MAINNET,
|
||||
position,
|
||||
pool,
|
||||
details,
|
||||
inRange: true,
|
||||
closed: false,
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
})
|
||||
test('Pools should render LP positions', () => {
|
||||
const props = { account: owner }
|
||||
const { container } = render(<Pools {...props} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
@@ -1,10 +1,12 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { Position } from '@uniswap/v3-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
import { useCallback, useMemo, useReducer } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -31,7 +33,7 @@ export default function Pools({ account }: { account: string }) {
|
||||
return [openPositions, closedPositions]
|
||||
}, [positions])
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
if (!positions || loading) {
|
||||
return <PortfolioSkeleton />
|
||||
@@ -91,64 +93,75 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
|
||||
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const { chainId: walletChainId, connector } = useWeb3React()
|
||||
const onClick = useCallback(async () => {
|
||||
if (walletChainId !== chainId) await switchChain(connector, chainId)
|
||||
toggleWalletDrawer()
|
||||
navigate('/pool/' + details.tokenId)
|
||||
}, [walletChainId, chainId, connector, toggleWalletDrawer, navigate, details.tokenId])
|
||||
|
||||
const containsURL = useMemo(
|
||||
() =>
|
||||
[pool.token0.name, pool.token0.symbol, pool.token1.name, pool.token1.symbol].some((testString) =>
|
||||
hasURL(testString)
|
||||
),
|
||||
[pool]
|
||||
const analyticsEventProperties = useMemo(
|
||||
() => ({
|
||||
chain_id: chainId,
|
||||
pool_token_0_symbol: pool.token0.symbol,
|
||||
pool_token_1_symbol: pool.token1.symbol,
|
||||
pool_token_0_address: pool.token0.address,
|
||||
pool_token_1_address: pool.token1.address,
|
||||
}),
|
||||
[chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol]
|
||||
)
|
||||
|
||||
if (containsURL) {
|
||||
const shouldHidePosition = hasURL(pool.token0.symbol) || hasURL(pool.token1.symbol)
|
||||
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<PortfolioRow
|
||||
onClick={onClick}
|
||||
left={<PortfolioLogo chainId={chainId} currencies={[pool.token0, pool.token1]} />}
|
||||
title={
|
||||
<Row>
|
||||
<ThemedText.SubHeader fontWeight={500}>
|
||||
{pool.token0.symbol} / {pool.token1?.symbol}
|
||||
</ThemedText.SubHeader>
|
||||
</Row>
|
||||
}
|
||||
descriptor={<ThemedText.Caption>{`${pool.fee / 10000}%`}</ThemedText.Caption>}
|
||||
right={
|
||||
<>
|
||||
<MouseoverTooltip
|
||||
placement="left"
|
||||
text={
|
||||
<div style={{ padding: '4px 0px' }}>
|
||||
<ThemedText.Caption>{`${formatNumber(
|
||||
liquidityValue,
|
||||
NumberType.PortfolioBalance
|
||||
)} (liquidity) + ${formatNumber(feeValue, NumberType.PortfolioBalance)} (fees)`}</ThemedText.Caption>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.MINI_PORTFOLIO_POOLS_ROW}
|
||||
properties={analyticsEventProperties}
|
||||
>
|
||||
<PortfolioRow
|
||||
onClick={onClick}
|
||||
left={<PortfolioLogo chainId={chainId} currencies={[pool.token0, pool.token1]} />}
|
||||
title={
|
||||
<Row>
|
||||
<ThemedText.SubHeader fontWeight={500}>
|
||||
{formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)}
|
||||
{pool.token0.symbol} / {pool.token1?.symbol}
|
||||
</ThemedText.SubHeader>
|
||||
</MouseoverTooltip>
|
||||
|
||||
<Row justify="flex-end">
|
||||
<ThemedText.Caption color="textSecondary">
|
||||
{closed ? t`Closed` : inRange ? t`In range` : t`Out of range`}
|
||||
</ThemedText.Caption>
|
||||
<ActiveDot closed={closed} outOfRange={!inRange} />
|
||||
</Row>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
descriptor={<ThemedText.Caption>{`${pool.fee / 10000}%`}</ThemedText.Caption>}
|
||||
right={
|
||||
<>
|
||||
<MouseoverTooltip
|
||||
placement="left"
|
||||
text={
|
||||
<div style={{ padding: '4px 0px' }}>
|
||||
<ThemedText.Caption>{`${formatNumber(
|
||||
liquidityValue,
|
||||
NumberType.PortfolioBalance
|
||||
)} (liquidity) + ${formatNumber(feeValue, NumberType.PortfolioBalance)} (fees)`}</ThemedText.Caption>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ThemedText.SubHeader fontWeight={500}>
|
||||
{formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)}
|
||||
</ThemedText.SubHeader>
|
||||
</MouseoverTooltip>
|
||||
|
||||
<Row justify="flex-end">
|
||||
<ThemedText.Caption color="textSecondary">
|
||||
{closed ? t`Closed` : inRange ? t`In range` : t`Out of range`}
|
||||
</ThemedText.Caption>
|
||||
<ActiveDot closed={closed} outOfRange={!inRange} />
|
||||
</Row>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</TraceEvent>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { DAI_ARBITRUM } from '@uniswap/smart-order-router'
|
||||
import { DAI, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { PortfolioLogo } from './PortfolioLogo'
|
||||
|
||||
describe('PortfolioLogo', () => {
|
||||
it('renders without L2 icon', () => {
|
||||
const { container } = render(<PortfolioLogo chainId={SupportedChainId.MAINNET} currencies={[DAI, USDC_MAINNET]} />)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with L2 icon', () => {
|
||||
const { container } = render(
|
||||
<PortfolioLogo chainId={SupportedChainId.ARBITRUM_ONE} currencies={[DAI_ARBITRUM, USDC_ARBITRUM]} />
|
||||
)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,7 @@ import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import React from 'react'
|
||||
import { Loader } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
const UnknownContract = styled(UnknownStatus)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
@@ -39,7 +39,7 @@ const DoubleLogoContainer = styled.div`
|
||||
type MultiLogoProps = {
|
||||
chainId: SupportedChainId
|
||||
accountAddress?: string
|
||||
currencies?: Currency[]
|
||||
currencies?: Array<Currency | undefined>
|
||||
images?: (string | undefined)[]
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
@@ -57,34 +57,28 @@ const ENSAvatarImg = styled.img`
|
||||
width: 40px;
|
||||
`
|
||||
|
||||
const StyledChainLogo = styled.img<{ isSquare: boolean }>`
|
||||
height: ${({ isSquare }) => (isSquare ? '16px' : '14px')};
|
||||
width: ${({ isSquare }) => (isSquare ? '16px' : '14px')};
|
||||
margin-top: ${({ isSquare }) => (isSquare ? '0px' : '1px')};
|
||||
margin-left: ${({ isSquare }) => (isSquare ? '0px' : '1px')};
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 68%;
|
||||
const StyledChainLogo = styled.img`
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
`
|
||||
|
||||
const ChainLogoSquareBackground = styled.div`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border-radius: 4px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
const SquareChainLogo = styled.img`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const L2LogoContainer = styled.div<{ $backgroundColor?: string }>`
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
border-radius: 2px;
|
||||
height: 16px;
|
||||
left: 60%;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
left: 60%;
|
||||
`
|
||||
|
||||
const SquareBackgroundForNonSquareLogo = styled.div`
|
||||
height: 16px;
|
||||
outline: 2px solid ${({ theme }) => theme.backgroundSurface};
|
||||
width: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: ${({ theme }) => theme.textPrimary};
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 68%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
/**
|
||||
@@ -101,6 +95,7 @@ export function PortfolioLogo({
|
||||
const { squareLogoUrl, logoUrl } = getChainInfo(chainId)
|
||||
const chainLogo = squareLogoUrl ?? logoUrl
|
||||
const { avatar, loading } = useENSAvatar(accountAddress, false)
|
||||
const theme = useTheme()
|
||||
|
||||
const [src, nextSrc] = useTokenLogoSource(currencies?.[0]?.wrapped.address, chainId, currencies?.[0]?.isNative)
|
||||
const [src2, nextSrc2] = useTokenLogoSource(currencies?.[1]?.wrapped.address, chainId, currencies?.[1]?.isNative)
|
||||
@@ -147,13 +142,15 @@ export function PortfolioLogo({
|
||||
}
|
||||
|
||||
const L2Logo =
|
||||
chainId === SupportedChainId.MAINNET ? null : (
|
||||
<div>
|
||||
{chainLogo && <ChainLogoSquareBackground />}
|
||||
{!squareLogoUrl && logoUrl && <SquareBackgroundForNonSquareLogo />}
|
||||
{chainLogo && <StyledChainLogo isSquare={!!squareLogoUrl} src={chainLogo} alt="chainLogo" />}
|
||||
</div>
|
||||
)
|
||||
chainId !== SupportedChainId.MAINNET && chainLogo ? (
|
||||
<L2LogoContainer $backgroundColor={squareLogoUrl ? theme.backgroundSurface : theme.textPrimary}>
|
||||
{squareLogoUrl ? (
|
||||
<SquareChainLogo src={chainLogo} alt="chainLogo" />
|
||||
) : (
|
||||
<StyledChainLogo src={chainLogo} alt="chainLogo" />
|
||||
)}
|
||||
</L2LogoContainer>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<StyledLogoParentContainer>
|
||||
@@ -1,10 +1,9 @@
|
||||
import Column, { AutoColumn } from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { useMemo } from 'react'
|
||||
import styled, { css, keyframes } from 'styled-components/macro'
|
||||
|
||||
const RowWrapper = styled(Row)<{ onClick?: any }>`
|
||||
export const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>`
|
||||
gap: 12px;
|
||||
height: 68px;
|
||||
padding: 0 16px;
|
||||
@@ -14,7 +13,6 @@ const RowWrapper = styled(Row)<{ onClick?: any }>`
|
||||
${({ onClick }) => onClick && 'cursor: pointer'};
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.hoverDefault};
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
@@ -28,39 +26,30 @@ export default function PortfolioRow({
|
||||
title,
|
||||
descriptor,
|
||||
right,
|
||||
setIsHover,
|
||||
onClick,
|
||||
}: {
|
||||
left: React.ReactNode
|
||||
title: React.ReactNode
|
||||
descriptor?: React.ReactNode
|
||||
right: React.ReactNode
|
||||
right?: React.ReactNode
|
||||
setIsHover?: (b: boolean) => void
|
||||
onClick?: () => void
|
||||
}) {
|
||||
const onHover = useMemo(
|
||||
() =>
|
||||
setIsHover && {
|
||||
onMouseEnter: () => setIsHover?.(true),
|
||||
onMouseLeave: () => setIsHover?.(false),
|
||||
},
|
||||
[setIsHover]
|
||||
)
|
||||
return (
|
||||
<RowWrapper {...onHover} onClick={onClick}>
|
||||
<PortfolioRowWrapper onClick={onClick}>
|
||||
{left}
|
||||
<AutoColumn grow>
|
||||
{title}
|
||||
{descriptor}
|
||||
</AutoColumn>
|
||||
<EndColumn>{right}</EndColumn>
|
||||
</RowWrapper>
|
||||
{right && <EndColumn>{right}</EndColumn>}
|
||||
</PortfolioRowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) {
|
||||
return (
|
||||
<RowWrapper>
|
||||
<PortfolioRowWrapper>
|
||||
<LoadingBubble height="40px" width="40px" round />
|
||||
<AutoColumn grow gap="4px">
|
||||
<LoadingBubble height="16px" width="60px" delay="300ms" />
|
||||
@@ -76,7 +65,7 @@ function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) {
|
||||
</>
|
||||
)}
|
||||
</EndColumn>
|
||||
</RowWrapper>
|
||||
</PortfolioRowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import Row from 'components/Row'
|
||||
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
|
||||
@@ -10,12 +12,12 @@ import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
|
||||
import { useToggleWalletDrawer } from '..'
|
||||
import { PortfolioArrow } from '../AuthenticatedHeader'
|
||||
import { hideSmallBalancesAtom } from '../SmallBalanceToggle'
|
||||
import { ExpandoRow } from './ExpandoRow'
|
||||
import { PortfolioLogo } from './PortfolioLogo'
|
||||
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from './PortfolioRow'
|
||||
import { useToggleAccountDrawer } from '../..'
|
||||
import { PortfolioArrow } from '../../AuthenticatedHeader'
|
||||
import { hideSmallBalancesAtom } from '../../SmallBalanceToggle'
|
||||
import { ExpandoRow } from '../ExpandoRow'
|
||||
import { PortfolioLogo } from '../PortfolioLogo'
|
||||
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
|
||||
|
||||
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
|
||||
|
||||
@@ -24,7 +26,7 @@ function meetsThreshold(tokenBalance: TokenBalance, hideSmallBalances: boolean)
|
||||
}
|
||||
|
||||
export default function Tokens({ account }: { account: string }) {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
|
||||
const [showHiddenTokens, setShowHiddenTokens] = useState(false)
|
||||
|
||||
@@ -94,7 +96,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const navigateToTokenDetails = useCallback(async () => {
|
||||
navigate(getTokenDetailsURL(token))
|
||||
toggleWalletDrawer()
|
||||
@@ -102,28 +104,35 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
|
||||
const currency = gqlToCurrency(token)
|
||||
return (
|
||||
<PortfolioRow
|
||||
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
|
||||
title={<ThemedText.SubHeader fontWeight={500}>{token?.name}</ThemedText.SubHeader>}
|
||||
descriptor={
|
||||
<TokenBalanceText>
|
||||
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol}
|
||||
</TokenBalanceText>
|
||||
}
|
||||
onClick={navigateToTokenDetails}
|
||||
right={
|
||||
denominatedValue && (
|
||||
<>
|
||||
<ThemedText.SubHeader fontWeight={500}>
|
||||
{formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)}
|
||||
</ThemedText.SubHeader>
|
||||
<Row justify="flex-end">
|
||||
<PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} />
|
||||
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.MINI_PORTFOLIO_TOKEN_ROW}
|
||||
properties={{ chain_id: currency.chainId, token_name: token?.name, address: token?.address }}
|
||||
>
|
||||
<PortfolioRow
|
||||
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
|
||||
title={<ThemedText.SubHeader fontWeight={500}>{token?.name}</ThemedText.SubHeader>}
|
||||
descriptor={
|
||||
<TokenBalanceText>
|
||||
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol}
|
||||
</TokenBalanceText>
|
||||
}
|
||||
onClick={navigateToTokenDetails}
|
||||
right={
|
||||
denominatedValue && (
|
||||
<>
|
||||
<ThemedText.SubHeader fontWeight={500}>
|
||||
{formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)}
|
||||
</ThemedText.SubHeader>
|
||||
<Row justify="flex-end">
|
||||
<PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} />
|
||||
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TraceEvent>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PortfolioLogo renders with L2 icon 1`] = `
|
||||
.c3 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(n) {
|
||||
width: 19px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(1) {
|
||||
border-radius: 20px 0 0 20px;
|
||||
object-position: 0 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(2) {
|
||||
border-radius: 0 20px 20px 0;
|
||||
object-position: 100% 0;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
background-color: #0D111C;
|
||||
border-radius: 2px;
|
||||
height: 16px;
|
||||
left: 60%;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
width: 16px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1/logo.png"
|
||||
/>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<img
|
||||
alt="chainLogo"
|
||||
class="c5"
|
||||
src="arbitrum_logo.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PortfolioLogo renders without L2 icon 1`] = `
|
||||
.c3 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(n) {
|
||||
width: 19px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(1) {
|
||||
border-radius: 20px 0 0 20px;
|
||||
object-position: 0 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(2) {
|
||||
border-radius: 0 20px 20px 0;
|
||||
object-position: 100% 0;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
|
||||
/>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -5,13 +5,16 @@ import Column from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { useMiniPortfolioEnabled } from 'featureFlags/flags/miniPortfolio'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useState } from 'react'
|
||||
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import Activity from './Activity'
|
||||
import { ActivityTab } from './Activity'
|
||||
import NFTs from './NFTs'
|
||||
import Pools from './Pools'
|
||||
import { PortfolioRowWrapper } from './PortfolioRow'
|
||||
import Tokens from './Tokens'
|
||||
|
||||
const Wrapper = styled(Column)`
|
||||
@@ -20,6 +23,12 @@ const Wrapper = styled(Column)`
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
|
||||
${PortfolioRowWrapper} {
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.hoverDefault};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Nav = styled(AutoRow)`
|
||||
@@ -46,46 +55,67 @@ const PageWrapper = styled.div`
|
||||
|
||||
interface Page {
|
||||
title: React.ReactNode
|
||||
key: string
|
||||
component: ({ account }: { account: string }) => JSX.Element
|
||||
loggingElementName?: string
|
||||
loggingElementName: string
|
||||
}
|
||||
|
||||
const Pages: Array<Page> = [
|
||||
{
|
||||
title: <Trans>Tokens</Trans>,
|
||||
key: 'tokens',
|
||||
component: Tokens,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_TOKENS_TAB,
|
||||
},
|
||||
{ title: <Trans>NFTs</Trans>, component: NFTs, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB },
|
||||
{ title: <Trans>Pools</Trans>, component: Pools, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB },
|
||||
{ title: <Trans>Activity</Trans>, component: Activity },
|
||||
{
|
||||
title: <Trans>NFTs</Trans>,
|
||||
key: 'nfts',
|
||||
component: NFTs,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB,
|
||||
},
|
||||
{
|
||||
title: <Trans>Pools</Trans>,
|
||||
key: 'pools',
|
||||
component: Pools,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB,
|
||||
},
|
||||
{
|
||||
title: <Trans>Activity</Trans>,
|
||||
key: 'activity',
|
||||
component: ActivityTab,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_TAB,
|
||||
},
|
||||
]
|
||||
|
||||
function MiniPortfolio({ account }: { account: string }) {
|
||||
const isNftPage = useIsNftPage()
|
||||
const [currentPage, setCurrentPage] = useState(isNftPage ? 1 : 0)
|
||||
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
|
||||
|
||||
const Page = Pages[currentPage].component
|
||||
return (
|
||||
<Wrapper>
|
||||
<Nav>
|
||||
{Pages.map(({ title }, index) => (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.NAVBAR_CLICKED}
|
||||
element={Pages[index].loggingElementName}
|
||||
shouldLogImpression={!!Pages[index].loggingElementName}
|
||||
key={index}
|
||||
>
|
||||
<NavItem
|
||||
onClick={() => setCurrentPage(index)}
|
||||
active={currentPage === index}
|
||||
key={`Mini Portfolio page ${index}`}
|
||||
{Pages.map(({ title, loggingElementName, key }, index) => {
|
||||
if (shouldDisableNFTRoutes && loggingElementName.includes('nft')) return null
|
||||
return (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.NAVBAR_CLICKED}
|
||||
element={loggingElementName}
|
||||
key={index}
|
||||
>
|
||||
{title}
|
||||
</NavItem>
|
||||
</TraceEvent>
|
||||
))}
|
||||
<NavItem
|
||||
data-testid={`mini-portfolio-nav-${key}`}
|
||||
onClick={() => setCurrentPage(index)}
|
||||
active={currentPage === index}
|
||||
key={`Mini Portfolio page ${index}`}
|
||||
>
|
||||
{title}
|
||||
</NavItem>
|
||||
</TraceEvent>
|
||||
)
|
||||
})}
|
||||
</Nav>
|
||||
<PageWrapper>
|
||||
<Page account={account} />
|
||||
@@ -5,7 +5,7 @@ import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 're
|
||||
import { useAllTransactions } from 'state/transactions/hooks'
|
||||
import { TransactionDetails } from 'state/transactions/types'
|
||||
|
||||
import { useWalletDrawer } from '.'
|
||||
import { useAccountDrawer } from '.'
|
||||
|
||||
const isTxPending = (tx: TransactionDetails) => !tx.receipt
|
||||
function wasPending(previousTxs: { [hash: string]: TransactionDetails | undefined }, current: TransactionDetails) {
|
||||
@@ -39,7 +39,7 @@ function useHasUpdatedTx() {
|
||||
export default function PrefetchBalancesWrapper({ children }: PropsWithChildren) {
|
||||
const { account } = useWeb3React()
|
||||
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
|
||||
const [drawerOpen] = useWalletDrawer()
|
||||
const [drawerOpen] = useAccountDrawer()
|
||||
|
||||
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useState(true)
|
||||
const fetchBalances = useCallback(() => {
|
||||
@@ -3,7 +3,7 @@ import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/loca
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
|
||||
import { Check } from 'react-feather'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ClickableStyle, ThemedText } from 'theme'
|
||||
import ThemeToggle from 'theme/components/ThemeToggle'
|
||||
@@ -56,16 +56,13 @@ const BalanceToggleContainer = styled.div`
|
||||
export default function SettingsMenu({ onClose }: { onClose: () => void }) {
|
||||
const activeLocale = useActiveLocale()
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isWalletPage = pathname.includes('/wallet')
|
||||
|
||||
return (
|
||||
<SlideOutMenu title={<Trans>Settings</Trans>} onClose={onClose}>
|
||||
<SectionTitle>
|
||||
<Trans>Preferences</Trans>
|
||||
</SectionTitle>
|
||||
<ThemeToggleContainer>
|
||||
<ThemeToggle disabled={isWalletPage} />
|
||||
<ThemeToggle />
|
||||
</ThemeToggleContainer>
|
||||
<BalanceToggleContainer>
|
||||
<SmallBalanceToggle />
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { WalletConnect } from '@web3-react/walletconnect'
|
||||
import Column, { AutoColumn } from 'components/Column'
|
||||
@@ -95,7 +96,7 @@ export default function UniwalletModal() {
|
||||
)}
|
||||
</QRCodeWrapper>
|
||||
<Divider />
|
||||
<InfoSection onClose={onClose} />
|
||||
<InfoSection />
|
||||
</UniwalletConnectWrapper>
|
||||
</Modal>
|
||||
)
|
||||
@@ -108,7 +109,7 @@ const InfoSectionWrapper = styled(RowBetween)`
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
function InfoSection({ onClose }: { onClose: () => void }) {
|
||||
function InfoSection() {
|
||||
return (
|
||||
<InfoSectionWrapper>
|
||||
<AutoColumn gap="4px">
|
||||
@@ -117,12 +118,12 @@ function InfoSection({ onClose }: { onClose: () => void }) {
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ThemedText.Caption color="textSecondary">
|
||||
<Trans>
|
||||
Download in the App Store to safely store and send tokens and NFTs, swap tokens, and connect to crypto apps.
|
||||
Download in the App Store to safely store your tokens and NFTs, swap tokens, and connect to crypto apps.
|
||||
</Trans>
|
||||
</ThemedText.Caption>
|
||||
</AutoColumn>
|
||||
<Column>
|
||||
<DownloadButton onClick={onClose} />
|
||||
<DownloadButton element={InterfaceElementName.UNISWAP_WALLET_MODAL_DOWNLOAD_BUTTON} />
|
||||
</Column>
|
||||
</InfoSectionWrapper>
|
||||
)
|
||||
@@ -18,18 +18,18 @@ const DRAWER_MARGIN = '8px'
|
||||
const DRAWER_OFFSET = '10px'
|
||||
const DRAWER_TOP_MARGIN_MOBILE_WEB = '72px'
|
||||
|
||||
const walletDrawerOpenAtom = atom(false)
|
||||
const accountDrawerOpenAtom = atom(false)
|
||||
|
||||
export function useToggleWalletDrawer() {
|
||||
const updateWalletDrawerOpen = useUpdateAtom(walletDrawerOpenAtom)
|
||||
export function useToggleAccountDrawer() {
|
||||
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
|
||||
return useCallback(() => {
|
||||
updateWalletDrawerOpen((open) => !open)
|
||||
}, [updateWalletDrawerOpen])
|
||||
updateAccountDrawerOpen((open) => !open)
|
||||
}, [updateAccountDrawerOpen])
|
||||
}
|
||||
|
||||
export function useWalletDrawer(): [boolean, () => void] {
|
||||
const walletDrawerOpen = useAtomValue(walletDrawerOpenAtom)
|
||||
return [walletDrawerOpen, useToggleWalletDrawer()]
|
||||
export function useAccountDrawer(): [boolean, () => void] {
|
||||
const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom)
|
||||
return [accountDrawerOpen, useToggleAccountDrawer()]
|
||||
}
|
||||
|
||||
const ScrimBackground = styled.div<{ open: boolean }>`
|
||||
@@ -63,7 +63,7 @@ const Scrim = ({ onClick, open }: { onClick: () => void; open: boolean }) => {
|
||||
return <ScrimBackground onClick={onClick} open={open} />
|
||||
}
|
||||
|
||||
const WalletDropdownScrollWrapper = styled.div`
|
||||
const AccountDrawerScrollWrapper = styled.div`
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
@@ -76,32 +76,45 @@ const WalletDropdownScrollWrapper = styled.div`
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
right: ${DRAWER_MARGIN};
|
||||
top: ${DRAWER_MARGIN};
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH)};
|
||||
z-index: ${Z_INDEX.fixed};
|
||||
|
||||
overflow: hidden;
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
`
|
||||
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
const AccountDrawerWrapper = styled.div<{ open: boolean }>`
|
||||
margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH)};
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
z-index: ${Z_INDEX.modal};
|
||||
top: unset;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: ${({ open }) => (open ? 0 : `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))`)};
|
||||
position: absolute;
|
||||
margin-right: 0;
|
||||
top: ${({ open }) => (open ? `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))` : 0)};
|
||||
|
||||
width: 100%;
|
||||
height: calc(100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB});
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
box-shadow: unset;
|
||||
transition: top ${({ theme }) => theme.transition.duration.medium};
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1440px) {
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH_XL)};
|
||||
margin-right: ${({ open }) => (open ? 0 : `-${DRAWER_WIDTH_XL}`)};
|
||||
width: ${DRAWER_WIDTH_XL};
|
||||
}
|
||||
|
||||
@@ -112,8 +125,7 @@ const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
transition: right ${({ theme }) => theme.transition.duration.medium},
|
||||
bottom ${({ theme }) => theme.transition.duration.medium};
|
||||
transition: margin-right ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
|
||||
const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
|
||||
@@ -123,30 +135,24 @@ const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
|
||||
const CloseDrawer = styled.div`
|
||||
${ClickableStyle}
|
||||
cursor: pointer;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
position: fixed;
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH} - ${DRAWER_OFFSET});
|
||||
top: 4px;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
height: 100%;
|
||||
// When the drawer is not hovered, the icon should be 18px from the edge of the sidebar.
|
||||
padding: 24px calc(18px + ${DRAWER_OFFSET}) 24px 14px;
|
||||
border-radius: 20px 0 0 20px;
|
||||
transition: ${({ theme }) =>
|
||||
`${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color, ${theme.transition.duration.medium} ${theme.transition.timing.ease} margin`};
|
||||
&:hover {
|
||||
margin: 0 -4px 0 0;
|
||||
z-index: -1;
|
||||
margin: 0 -8px 0 0;
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 1440px) {
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH_XL} - ${DRAWER_OFFSET});
|
||||
}
|
||||
`
|
||||
|
||||
function WalletDropdown() {
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
function AccountDrawer() {
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!walletDrawerOpen) {
|
||||
@@ -187,7 +193,7 @@ function WalletDropdown() {
|
||||
}, [walletDrawerOpen, toggleWalletDrawer])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
{walletDrawerOpen && (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@@ -200,14 +206,14 @@ function WalletDropdown() {
|
||||
</TraceEvent>
|
||||
)}
|
||||
<Scrim onClick={toggleWalletDrawer} open={walletDrawerOpen} />
|
||||
<WalletDropdownWrapper open={walletDrawerOpen}>
|
||||
<AccountDrawerWrapper open={walletDrawerOpen}>
|
||||
{/* id used for child InfiniteScrolls to reference when it has reached the bottom of the component */}
|
||||
<WalletDropdownScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
|
||||
<AccountDrawerScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
|
||||
<DefaultMenu />
|
||||
</WalletDropdownScrollWrapper>
|
||||
</WalletDropdownWrapper>
|
||||
</>
|
||||
</AccountDrawerScrollWrapper>
|
||||
</AccountDrawerWrapper>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletDropdown
|
||||
export default AccountDrawer
|
||||
@@ -1,111 +1,147 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { openDownloadApp, openWalletMicrosite } from 'components/AccountDrawer/DownloadButton'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Row, { RowBetween } from 'components/Row'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import { DownloadButton, LearnMoreButton } from 'components/WalletDropdown/DownloadButton'
|
||||
import { OpacityHoverState } from 'components/Common'
|
||||
import Row from 'components/Row'
|
||||
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { useScreenSize } from 'hooks/useScreenSize'
|
||||
import { X } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useHideUniswapWalletBanner } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { isIOS } from 'utils/userAgent'
|
||||
|
||||
import bannerImageDark from '../../assets/images/uniswapWalletBannerDark.png'
|
||||
import bannerImageLight from '../../assets/images/uniswapWalletBannerLight.png'
|
||||
import { ReactComponent as AppleLogo } from '../../assets/svg/apple_logo.svg'
|
||||
import walletBannerPhoneImageSrc from '../../assets/svg/wallet_banner_phone_image.svg'
|
||||
|
||||
const PopupContainer = styled.div<{ show: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
${({ show }) => !show && 'display: none'};
|
||||
|
||||
box-shadow: ${({ theme }) =>
|
||||
theme.darkMode
|
||||
? '0px -16px 24px rgba(0, 0, 0, 0.4), 0px -8px 12px rgba(0, 0, 0, 0.4), 0px -4px 8px rgba(0, 0, 0, 0.32)'
|
||||
: '0px -12px 20px rgba(51, 53, 72, 0.04), 0px -6px 12px rgba(51, 53, 72, 0.02), 0px -4px 8px rgba(51, 53, 72, 0.04)'};
|
||||
|
||||
background-image: ${({ theme }) => (theme.darkMode ? `url(${bannerImageDark})` : `url(${bannerImageLight})`)};
|
||||
background: url(${walletBannerPhoneImageSrc});
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: bottom -1px right 15px;
|
||||
background-size: 166px;
|
||||
|
||||
cursor: pointer;
|
||||
:hover {
|
||||
background-size: 170px;
|
||||
}
|
||||
transition: background-size ${({ theme }) => theme.transition.duration.medium}
|
||||
${({ theme }) => theme.transition.timing.inOut};
|
||||
|
||||
background-color: ${({ theme }) => theme.promotional};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
position: fixed;
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.slow} opacity ${timing.in}`};
|
||||
width: 100%;
|
||||
bottom: 56px;
|
||||
height: 20%;
|
||||
`
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 24px 16px;
|
||||
padding: 24px 16px 16px;
|
||||
|
||||
border-radius: 20px;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 390px;
|
||||
height: 164px;
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
bottom: 62px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
width: unset;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const ButtonRow = styled(Row)`
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const StyledXButton = styled(X)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
&:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
&:active {
|
||||
opacity: ${({ theme }) => theme.opacity.click};
|
||||
}
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
right: 17px;
|
||||
|
||||
color: ${({ theme }) => theme.white};
|
||||
${OpacityHoverState};
|
||||
`
|
||||
|
||||
const BannerButton = styled(BaseButton)`
|
||||
height: 40px;
|
||||
border-radius: 16px;
|
||||
padding: 10px;
|
||||
${OpacityHoverState};
|
||||
`
|
||||
|
||||
export default function UniswapWalletBanner() {
|
||||
const [hideUniswapWalletBanner, toggleHideUniswapWalletBanner] = useHideUniswapWalletBanner()
|
||||
const [walletDrawerOpen] = useWalletDrawer()
|
||||
const mgtmEnabled = useMgtmEnabled()
|
||||
const location = useLocation()
|
||||
const isLandingScreen = location.search === '?intro=true' || location.pathname === '/'
|
||||
|
||||
const theme = useTheme()
|
||||
const shouldDisplay = Boolean(mgtmEnabled && !hideUniswapWalletBanner && !isLandingScreen)
|
||||
|
||||
const { pathname } = useLocation()
|
||||
// hardcodeToFalse hardcodes the banner to never display, temporarily:
|
||||
const hardcodeToFalse = false
|
||||
const shouldDisplay = Boolean(
|
||||
!walletDrawerOpen && !hideUniswapWalletBanner && isIOS && !pathname.startsWith('/wallet') && hardcodeToFalse
|
||||
)
|
||||
const screenSize = useScreenSize()
|
||||
|
||||
return (
|
||||
<PopupContainer show={shouldDisplay}>
|
||||
<InnerContainer>
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Get the power of Uniswap in your pocket</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<StyledXButton
|
||||
data-testid="uniswap-wallet-banner"
|
||||
color={theme.textSecondary}
|
||||
size={20}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleHideUniswapWalletBanner()
|
||||
}}
|
||||
/>
|
||||
</RowBetween>
|
||||
<ThemedText.BodySmall>
|
||||
<Trans>Download in the App Store today.</Trans>{' '}
|
||||
</ThemedText.BodySmall>
|
||||
</AutoColumn>
|
||||
<StyledXButton
|
||||
data-testid="uniswap-wallet-banner"
|
||||
size={20}
|
||||
onClick={(e) => {
|
||||
// prevent click from bubbling to UI on the page underneath, i.e. clicking a token row
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleHideUniswapWalletBanner()
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonRow>
|
||||
<LearnMoreButton />
|
||||
<DownloadButton onClick={() => toggleHideUniswapWalletBanner()} />
|
||||
</ButtonRow>
|
||||
</InnerContainer>
|
||||
<AutoColumn gap="8px">
|
||||
<ThemedText.HeadlineMedium fontSize="24px" lineHeight="28px" color="white" maxWidth="60%">
|
||||
<Trans>Uniswap in your pocket</Trans>
|
||||
</ThemedText.HeadlineMedium>
|
||||
</AutoColumn>
|
||||
|
||||
<ButtonRow>
|
||||
{isIOS ? (
|
||||
<>
|
||||
<BannerButton
|
||||
backgroundColor="white"
|
||||
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON)}
|
||||
>
|
||||
<AppleLogo width={14} height={14} />
|
||||
<ThemedText.LabelSmall color="black" marginLeft="5px">
|
||||
{!screenSize['xs'] ? <Trans>Download</Trans> : <Trans>Download app</Trans>}
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
|
||||
<BannerButton backgroundColor="black" onClick={openWalletMicrosite}>
|
||||
<ThemedText.LabelSmall color="white">
|
||||
<Trans>Learn more</Trans>
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
</>
|
||||
) : (
|
||||
<BannerButton backgroundColor="white" width="125px" onClick={openWalletMicrosite}>
|
||||
<ThemedText.LabelSmall color="black">
|
||||
<Trans>Learn more</Trans>
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
)}
|
||||
</ButtonRow>
|
||||
</PopupContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
|
||||
import { ChevronUpIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
import { Copy } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
|
||||
@@ -220,18 +218,16 @@ const updateServiceWorkerInBackground = async () => {
|
||||
}
|
||||
|
||||
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
|
||||
const { pathname } = useLocation()
|
||||
const { chainId } = useWeb3React()
|
||||
return (
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, eventId }) => <Fallback error={error} eventId={eventId} />}
|
||||
beforeCapture={(scope) => {
|
||||
scope.setLevel('fatal')
|
||||
scope.setTag('chain_id', chainId)
|
||||
}}
|
||||
onError={(error) => {
|
||||
onError={() => {
|
||||
updateServiceWorkerInBackground()
|
||||
if (pathname === '/swap') {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useBaseFlag, useUpdateFlag } from 'featureFlags'
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { MgtmVariant, useMgtmFlag } from 'featureFlags/flags/mgtm'
|
||||
import { useMiniPortfolioFlag } from 'featureFlags/flags/miniPortfolio'
|
||||
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
|
||||
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||
import { TaxServiceVariant, useTaxServiceBannerFlag } from 'featureFlags/flags/taxServiceBanner'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
@@ -212,12 +212,6 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.mgtm}
|
||||
label="Mobile Wallet go-to-market assets"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useBaseFlag(FeatureFlag.walletMicrosite)}
|
||||
featureFlag={FeatureFlag.walletMicrosite}
|
||||
label="Mobile Wallet microsite (requires mgtm to also be enabled)"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useMiniPortfolioFlag()}
|
||||
@@ -243,10 +237,10 @@ export default function FeatureFlagModal() {
|
||||
label="Migrate NFT read endpoints to GQL"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TaxServiceVariant}
|
||||
value={useTaxServiceBannerFlag()}
|
||||
featureFlag={FeatureFlag.taxService}
|
||||
label="Tax Service Banner"
|
||||
variant={DetailsV2Variant}
|
||||
value={useDetailsV2Flag()}
|
||||
featureFlag={FeatureFlag.detailsV2}
|
||||
label="Use the new details page for nfts"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
|
||||
24
src/components/Identicon/StatusIcon.test.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getConnections } from 'connection'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import StatusIcon from './StatusIcon'
|
||||
|
||||
jest.mock('../../hooks/useSocksBalance', () => ({
|
||||
useHasSocks: () => true,
|
||||
}))
|
||||
|
||||
describe('StatusIcon', () => {
|
||||
it('renders children in correct order, with no account and with socks', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with no account and showMiniIcons=false', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { Unicon } from 'components/Unicon'
|
||||
import { Connection, ConnectionType } from 'connection'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { flexColumnNoWrap } from 'theme/styles'
|
||||
|
||||
import sockImg from '../../assets/svg/socks.svg'
|
||||
@@ -62,88 +59,43 @@ const Socks = () => {
|
||||
}
|
||||
|
||||
const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<MiniIconContainer side={side}>
|
||||
<MiniImg src={connection.icon} alt={`${connection.name} icon`} />
|
||||
<MiniImg src={connection.getIcon?.(isDarkMode)} alt={`${connection.getName()} icon`} />
|
||||
</MiniIconContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const Divider = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
margin: 12px 0;
|
||||
`
|
||||
|
||||
function UniconTooltip({ children, enabled }: PropsWithChildren<{ enabled?: boolean }>) {
|
||||
return (
|
||||
<MouseoverTooltip
|
||||
offsetY={8}
|
||||
disableHover={!enabled}
|
||||
text={
|
||||
// TODO(cartcrom): add Learn More link when unicon microsite is polished
|
||||
<>
|
||||
<ThemedText.SubHeaderSmall color="textPrimary" paddingTop="4px">
|
||||
This is your Unicon
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<Divider />
|
||||
<ThemedText.Caption paddingBottom="4px">
|
||||
Unicons are avatars for your wallet, generated from your address.
|
||||
</ThemedText.Caption>
|
||||
</>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<div>{children}</div>
|
||||
</MouseoverTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const MainWalletIcon = ({
|
||||
connection,
|
||||
size,
|
||||
enableInfotips,
|
||||
}: {
|
||||
connection: Connection
|
||||
size: number
|
||||
enableInfotips?: boolean
|
||||
}) => {
|
||||
const MainWalletIcon = ({ connection, size }: { connection: Connection; size: number }) => {
|
||||
const { account } = useWeb3React()
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
if (!account) {
|
||||
return null
|
||||
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.name === 'MetaMask')) {
|
||||
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.getName() === 'MetaMask')) {
|
||||
return <Identicon size={size} />
|
||||
} else {
|
||||
return isMobile ? (
|
||||
<Unicon address={account} size={size} />
|
||||
) : (
|
||||
<UniconTooltip enabled={enableInfotips}>
|
||||
<Unicon address={account} size={size} />
|
||||
</UniconTooltip>
|
||||
)
|
||||
return <Unicon address={account} size={size} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function StatusIcon({
|
||||
connection,
|
||||
size = 16,
|
||||
enableInfotips,
|
||||
showMiniIcons = true,
|
||||
}: {
|
||||
connection: Connection
|
||||
size?: number
|
||||
enableInfotips?: boolean
|
||||
showMiniIcons?: boolean
|
||||
}) {
|
||||
const hasSocks = useHasSocks()
|
||||
|
||||
return (
|
||||
<IconWrapper size={size}>
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
<MainWalletIcon connection={connection} size={size} enableInfotips={enableInfotips} />
|
||||
<IconWrapper size={size} data-testid="StatusIconRoot">
|
||||
<MainWalletIcon connection={connection} size={size} />
|
||||
{showMiniIcons && <MiniWalletIcon connection={connection} side="right" />}
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
129
src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap
Normal file
@@ -0,0 +1,129 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusIcon renders children in correct order, with no account and with socks 1`] = `
|
||||
.c0 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-flow: column nowrap;
|
||||
-ms-flex-flow: column nowrap;
|
||||
flex-flow: column nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c0 > img,
|
||||
.c0 span {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (max-width:960px) {
|
||||
.c0 {
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c1 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c3 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-testid="StatusIconRoot"
|
||||
size="16"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
alt="MetaMask icon"
|
||||
class="c2"
|
||||
src="metamask.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
<img
|
||||
class="c2"
|
||||
src="socks.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,22 +1,30 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { ReactComponent as AppleLogo } from 'assets/svg/apple_logo.svg'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { openDownloadApp } from 'components/AccountDrawer/DownloadButton'
|
||||
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
|
||||
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
|
||||
import { APP_STORE_LINK } from 'components/WalletDropdown/DownloadButton'
|
||||
import NewBadge from 'components/WalletModal/NewBadge'
|
||||
import { useMgtmEnabled, useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { BarChartIcon, EllipsisIcon, GovernanceIcon, PoolIcon } from 'nft/components/icons'
|
||||
import {
|
||||
BarChartIcon,
|
||||
DiscordIconMenu,
|
||||
EllipsisIcon,
|
||||
GithubIconMenu,
|
||||
GovernanceIcon,
|
||||
PoolIcon,
|
||||
TwitterIconMenu,
|
||||
} from 'nft/components/icons'
|
||||
import { body, bodySmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { ReactNode, useReducer, useRef } from 'react'
|
||||
import { DollarSign, HelpCircle, Shield, Terminal } from 'react-feather'
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom'
|
||||
import { useToggleModal } from 'state/application/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||
|
||||
import { useToggleModal, useToggleTaxServiceModal } from '../../state/application/hooks'
|
||||
import { ReactComponent as AppleLogo } from '../../assets/svg/apple_logo.svg'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import * as styles from './MenuDropdown.css'
|
||||
import { NavDropdown } from './NavDropdown'
|
||||
@@ -27,30 +35,20 @@ const PrimaryMenuRow = ({
|
||||
href,
|
||||
close,
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
to?: NavLinkProps['to']
|
||||
href?: string
|
||||
close?: () => void
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{to ? (
|
||||
<NavLink to={to} className={styles.MenuRow} onClick={onClick}>
|
||||
<NavLink to={to} className={styles.MenuRow}>
|
||||
<Row onClick={close}>{children}</Row>
|
||||
</NavLink>
|
||||
) : (
|
||||
<Row
|
||||
as="a"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.MenuRow}
|
||||
onClick={onClick}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Row as="a" href={href} target="_blank" rel="noopener noreferrer" className={styles.MenuRow}>
|
||||
{children}
|
||||
</Row>
|
||||
)}
|
||||
@@ -97,6 +95,10 @@ const Separator = () => {
|
||||
return <Box className={styles.Separator} />
|
||||
}
|
||||
|
||||
const IconRow = ({ children }: { children: ReactNode }) => {
|
||||
return <Row className={styles.IconRow}>{children}</Row>
|
||||
}
|
||||
|
||||
const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
@@ -107,7 +109,7 @@ const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
|
||||
rel={href ? 'noopener noreferrer' : undefined}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
color="textSecondary"
|
||||
color="textPrimary"
|
||||
background="none"
|
||||
border="none"
|
||||
justifyContent="center"
|
||||
@@ -120,47 +122,21 @@ const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const StyledAppleLogo = styled(AppleLogo)`
|
||||
fill: ${({ theme }) => theme.textSecondary};
|
||||
padding: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const BadgeWrapper = styled.div`
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export const MenuDropdown = () => {
|
||||
const theme = useTheme()
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
const toggleTaxServiceModal = useToggleTaxServiceModal()
|
||||
const theme = useTheme()
|
||||
|
||||
const mgtmEnabled = useMgtmEnabled()
|
||||
const micrositeEnabled = useMGTMMicrositeEnabled()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="relative" ref={ref}>
|
||||
<NavIcon
|
||||
isActive={isOpen}
|
||||
onClick={toggleOpen}
|
||||
label={isOpen ? t`Show resources` : t`Hide resources`}
|
||||
activeBackground={isOpen}
|
||||
>
|
||||
<EllipsisIcon
|
||||
viewBox="0 0 20 20"
|
||||
width={24}
|
||||
height={24}
|
||||
color={isOpen ? theme.accentActive : theme.textSecondary}
|
||||
/>
|
||||
<NavIcon isActive={isOpen} onClick={toggleOpen} label={isOpen ? t`Show resources` : t`Hide resources`}>
|
||||
<EllipsisIcon viewBox="0 0 20 20" width={24} height={24} />
|
||||
</NavIcon>
|
||||
|
||||
{isOpen && (
|
||||
@@ -170,99 +146,100 @@ export const MenuDropdown = () => {
|
||||
<Box display={{ sm: 'none', lg: 'flex', xxl: 'none' }}>
|
||||
<PrimaryMenuRow to="/pool" close={toggleOpen}>
|
||||
<Icon>
|
||||
<PoolIcon width={24} height={24} color={theme.textSecondary} />
|
||||
<PoolIcon width={24} height={24} fill={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Pool</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
</Box>
|
||||
<Box
|
||||
display={mgtmEnabled ? 'flex' : 'none'}
|
||||
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_MODAL_DOWNLOAD_BUTTON)}
|
||||
>
|
||||
<PrimaryMenuRow close={toggleOpen}>
|
||||
<Icon>
|
||||
<AppleLogo width="24px" height="24px" fill={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Download Uniswap Wallet</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
</Box>
|
||||
<PrimaryMenuRow to="/vote" close={toggleOpen}>
|
||||
<Icon>
|
||||
<GovernanceIcon width={24} height={24} />
|
||||
<GovernanceIcon width={24} height={24} color={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Governance</Trans>
|
||||
<Trans>Vote in governance</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow href="https://info.uniswap.org/#/">
|
||||
<Icon>
|
||||
<BarChartIcon width={24} height={24} />
|
||||
<BarChartIcon width={24} height={24} color={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Token analytics</Trans>
|
||||
<Trans>View more analytics</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow href="https://help.uniswap.org/en/">
|
||||
<Icon>
|
||||
<HelpCircle color={theme.textSecondary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Help center</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow href="https://docs.uniswap.org/">
|
||||
<Icon>
|
||||
<Terminal color={theme.textSecondary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Documentation</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow
|
||||
</Column>
|
||||
<Separator />
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection={{ sm: 'row', md: 'column' }}
|
||||
flexWrap="wrap"
|
||||
alignItems={{ sm: 'center', md: 'flex-start' }}
|
||||
paddingX="8"
|
||||
>
|
||||
<SecondaryLinkedText href="https://help.uniswap.org/en/">
|
||||
<Trans>Help center</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText href="https://docs.uniswap.org/">
|
||||
<Trans>Documentation</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText href="https://uniswap.canny.io/feature-requests">
|
||||
<Trans>Feedback</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText
|
||||
onClick={() => {
|
||||
toggleOpen()
|
||||
togglePrivacyPolicy()
|
||||
}}
|
||||
>
|
||||
<Icon>
|
||||
<Shield color={theme.textSecondary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Legal & Privacy</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow
|
||||
onClick={() => {
|
||||
toggleTaxServiceModal()
|
||||
toggleOpen()
|
||||
}}
|
||||
>
|
||||
<Icon>
|
||||
<DollarSign size="24px" color={theme.textSecondary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Save on Tax Services</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
{mgtmEnabled && (
|
||||
<Box display={micrositeEnabled ? { xxl: 'flex', xxxl: 'none' } : 'flex'}>
|
||||
<PrimaryMenuRow
|
||||
to={micrositeEnabled ? '/wallet' : undefined}
|
||||
href={micrositeEnabled ? undefined : APP_STORE_LINK}
|
||||
close={toggleOpen}
|
||||
>
|
||||
<Icon>
|
||||
<StyledAppleLogo />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Uniswap Wallet</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
<BadgeWrapper>
|
||||
<NewBadge />
|
||||
</BadgeWrapper>
|
||||
</PrimaryMenuRow>
|
||||
</Box>
|
||||
)}
|
||||
<Trans>Legal & Privacy</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
{(isDevelopmentEnv() || isStagingEnv()) && (
|
||||
<>
|
||||
<Separator />
|
||||
<SecondaryLinkedText onClick={openFeatureFlagsModal}>
|
||||
<Trans>Feature Flags</Trans>
|
||||
</SecondaryLinkedText>
|
||||
</>
|
||||
<SecondaryLinkedText onClick={openFeatureFlagsModal}>
|
||||
<Trans>Feature Flags</Trans>
|
||||
</SecondaryLinkedText>
|
||||
)}
|
||||
</Column>
|
||||
</Box>
|
||||
<IconRow>
|
||||
<Icon href="https://discord.com/invite/FCfyBSbCU5">
|
||||
<DiscordIconMenu
|
||||
className={styles.hover}
|
||||
width={24}
|
||||
height={24}
|
||||
color={themeVars.colors.textSecondary}
|
||||
/>
|
||||
</Icon>
|
||||
<Icon href="https://twitter.com/Uniswap">
|
||||
<TwitterIconMenu
|
||||
className={styles.hover}
|
||||
width={24}
|
||||
height={24}
|
||||
color={themeVars.colors.textSecondary}
|
||||
/>
|
||||
</Icon>
|
||||
<Icon href="https://github.com/Uniswap">
|
||||
<GithubIconMenu
|
||||
className={styles.hover}
|
||||
width={24}
|
||||
height={24}
|
||||
color={themeVars.colors.textSecondary}
|
||||
/>
|
||||
</Icon>
|
||||
</IconRow>
|
||||
</Column>
|
||||
</NavDropdown>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { HistoryDuration, SafetyLevel } from 'graphql/data/__generated__/types-a
|
||||
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import useTrendingTokens from 'graphql/data/TrendingTokens'
|
||||
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
@@ -361,9 +360,7 @@ export const SearchBarDropdown = ({
|
||||
searchHistory,
|
||||
])
|
||||
|
||||
const showBNBComingSoonBadge = Boolean(
|
||||
chainId !== undefined && chainId === SupportedChainId.BNB && !isLoading && !CHAIN_ID_TO_BACKEND_NAME[chainId]
|
||||
)
|
||||
const showBNBComingSoonBadge = chainId === SupportedChainId.BNB && !isLoading
|
||||
|
||||
return (
|
||||
<Box className={styles.searchBarDropdownNft}>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import NewBadge from 'components/WalletModal/NewBadge'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { chainIdToBackendName } from 'graphql/data/util'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useIsPoolsPage } from 'hooks/useIsPoolsPage'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { UniIcon } from 'nft/components/icons'
|
||||
@@ -13,6 +12,7 @@ import { useProfilePageState } from 'nft/hooks'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { ReactNode } from 'react'
|
||||
import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { Bag } from './Bag'
|
||||
@@ -58,7 +58,8 @@ export const PageTabs = () => {
|
||||
|
||||
const isPoolActive = useIsPoolsPage()
|
||||
const isNftPage = useIsNftPage()
|
||||
const micrositeEnabled = useMGTMMicrositeEnabled()
|
||||
|
||||
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -68,22 +69,16 @@ export const PageTabs = () => {
|
||||
<MenuItem href={`/tokens/${chainName.toLowerCase()}`} isActive={pathname.startsWith('/tokens')}>
|
||||
<Trans>Tokens</Trans>
|
||||
</MenuItem>
|
||||
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
|
||||
<Trans>NFTs</Trans>
|
||||
</MenuItem>
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
|
||||
<Trans>NFTs</Trans>
|
||||
</MenuItem>
|
||||
)}
|
||||
<Box display={{ sm: 'flex', lg: 'none', xxl: 'flex' }} width="full">
|
||||
<MenuItem href="/pools" dataTestId="pool-nav-link" isActive={isPoolActive}>
|
||||
<Trans>Pools</Trans>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
{micrositeEnabled && (
|
||||
<Box display={{ sm: 'none', xxxl: 'flex' }}>
|
||||
<MenuItem href="/wallet" isActive={pathname.startsWith('/wallet')}>
|
||||
<Trans>Wallet</Trans>
|
||||
<NewBadge />
|
||||
</MenuItem>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginY={{ sm: '4', md: 'unset' }}>
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
@@ -12,26 +12,31 @@ const RowNoFlex = styled(AutoRow)`
|
||||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
const ColumnContainer = styled(AutoColumn)`
|
||||
margin: 0 12px;
|
||||
`
|
||||
|
||||
export const PopupAlertTriangle = styled(AlertTriangleFilled)`
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
`
|
||||
|
||||
export default function FailedNetworkSwitchPopup({ chainId }: { chainId: SupportedChainId }) {
|
||||
const chainInfo = getChainInfo(chainId)
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<RowNoFlex>
|
||||
<AutoColumn gap="sm">
|
||||
<RowNoFlex style={{ alignItems: 'center' }}>
|
||||
<div style={{ paddingRight: 13 }}>
|
||||
<AlertTriangle color={theme.accentWarning} size={24} display="flex" />
|
||||
</div>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Failed to switch networks</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</RowNoFlex>
|
||||
<RowNoFlex gap="12px">
|
||||
<PopupAlertTriangle />
|
||||
<ColumnContainer gap="sm">
|
||||
<ThemedText.SubHeader color="textSecondary">
|
||||
<Trans>Failed to switch networks</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
|
||||
<ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>To use Uniswap on {chainInfo.label}, switch the network in your wallet’s settings.</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
</AutoColumn>
|
||||
</ColumnContainer>
|
||||
</RowNoFlex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { animated } from 'react-spring'
|
||||
import { useSpring } from 'react-spring'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { PopupContent } from '../../state/application/reducer'
|
||||
import FailedNetworkSwitchPopup from './FailedNetworkSwitchPopup'
|
||||
import TransactionPopup from './TransactionPopup'
|
||||
|
||||
const StyledClose = styled(X)`
|
||||
const StyledClose = styled(X)<{ $padding: number }>`
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
right: ${({ $padding }) => `${$padding}px`};
|
||||
top: ${({ $padding }) => `${$padding}px`};
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
const Popup = styled.div`
|
||||
const PopupCss = css<{ show: boolean }>`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
visibility: ${({ show }) => (show ? 'visible' : 'hidden')};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
transition: ${({ theme }) => `visibility ${theme.transition.duration.fast} ease-in-out`};
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
min-width: 290px;
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
const Fader = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg3};
|
||||
min-width: 290px;
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const AnimatedFader = animated(Fader)
|
||||
const TransactionPopupContainer = styled.div`
|
||||
${PopupCss}
|
||||
padding: 2px 0px;
|
||||
`
|
||||
|
||||
const FailedSwitchNetworkPopupContainer = styled.div<{ show: boolean }>`
|
||||
${PopupCss}
|
||||
padding: 20px 35px 20px 20px;
|
||||
`
|
||||
|
||||
export default function PopupItem({
|
||||
removeAfterMs,
|
||||
@@ -56,36 +56,34 @@ export default function PopupItem({
|
||||
popKey: string
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
if (removeAfterMs === null) return undefined
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
removeThisPopup()
|
||||
removePopup(popKey)
|
||||
}, removeAfterMs)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [removeAfterMs, removeThisPopup])
|
||||
}, [popKey, removeAfterMs, removePopup])
|
||||
|
||||
const theme = useTheme()
|
||||
const faderStyle = useSpring({
|
||||
from: { width: '100%' },
|
||||
to: { width: '0%' },
|
||||
config: { duration: removeAfterMs ?? undefined },
|
||||
})
|
||||
|
||||
let popupContent
|
||||
if ('failedSwitchNetwork' in content) {
|
||||
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
|
||||
if ('txn' in content) {
|
||||
return (
|
||||
<TransactionPopupContainer show={true}>
|
||||
<StyledClose $padding={16} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
|
||||
<TransactionPopup hash={content.txn.hash} />
|
||||
</TransactionPopupContainer>
|
||||
)
|
||||
} else if ('failedSwitchNetwork' in content) {
|
||||
return (
|
||||
<FailedSwitchNetworkPopupContainer show={true}>
|
||||
<StyledClose $padding={20} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
|
||||
<FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
|
||||
</FailedSwitchNetworkPopupContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return popupContent ? (
|
||||
<Popup>
|
||||
<StyledClose color={theme.textSecondary} onClick={removeThisPopup} />
|
||||
{popupContent}
|
||||
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
|
||||
</Popup>
|
||||
) : null
|
||||
return null
|
||||
}
|
||||
|
||||
66
src/components/Popups/TransactionPopup.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { parseLocalActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
|
||||
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
|
||||
import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow'
|
||||
import Column from 'components/Column'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useCombinedActiveList } from 'state/lists/hooks'
|
||||
import { useTransaction } from 'state/transactions/hooks'
|
||||
import { TransactionDetails } from 'state/transactions/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
import { PopupAlertTriangle } from './FailedNetworkSwitchPopup'
|
||||
|
||||
const Descriptor = styled(ThemedText.BodySmall)`
|
||||
${EllipsisStyle}
|
||||
`
|
||||
|
||||
function TransactionPopupContent({ tx, chainId }: { tx: TransactionDetails; chainId: number }) {
|
||||
const success = tx.receipt?.status === 1
|
||||
const tokens = useCombinedActiveList()
|
||||
const activity = parseLocalActivity(tx, chainId, tokens)
|
||||
const { ENSName } = useENSName(activity?.otherAccount)
|
||||
|
||||
if (!activity) return null
|
||||
|
||||
const explorerUrl = getExplorerLink(chainId, tx.hash, ExplorerDataType.TRANSACTION)
|
||||
|
||||
return (
|
||||
<PortfolioRow
|
||||
left={
|
||||
success ? (
|
||||
<Column>
|
||||
<PortfolioLogo
|
||||
chainId={chainId}
|
||||
currencies={activity.currencies}
|
||||
images={activity.logos}
|
||||
accountAddress={activity.otherAccount}
|
||||
/>
|
||||
</Column>
|
||||
) : (
|
||||
<PopupAlertTriangle />
|
||||
)
|
||||
}
|
||||
title={<ThemedText.SubHeader fontWeight={500}>{activity.title}</ThemedText.SubHeader>}
|
||||
descriptor={
|
||||
<Descriptor color="textSecondary">
|
||||
{activity.descriptor}
|
||||
{ENSName ?? activity.otherAccount}
|
||||
</Descriptor>
|
||||
}
|
||||
onClick={() => window.open(explorerUrl, '_blank')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TransactionPopup({ hash }: { hash: string }) {
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const tx = useTransaction(hash)
|
||||
|
||||
if (!chainId || !tx) return null
|
||||
|
||||
return <TransactionPopupContent tx={tx} chainId={chainId} />
|
||||
}
|
||||
@@ -41,7 +41,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
|
||||
position: fixed;
|
||||
top: ${({ extraPadding }) => (extraPadding ? '72px' : '64px')};
|
||||
right: 1rem;
|
||||
max-width: 355px !important;
|
||||
max-width: 348px !important;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
|
||||
|
||||
@@ -1,38 +1,89 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { render, screen } from 'test-utils'
|
||||
import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { usePool } from 'hooks/usePools'
|
||||
import { PoolState } from 'hooks/usePools'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render } from 'test-utils/render'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('hooks/Tokens', () => {
|
||||
const originalModule = jest.requireActual('hooks/Tokens')
|
||||
const uniSDK = jest.requireActual('@uniswap/sdk-core')
|
||||
jest.mock('utils/unwrappedToken')
|
||||
|
||||
jest.mock('hooks/usePools')
|
||||
|
||||
jest.mock('hooks/Tokens')
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
jest.mock('components/DoubleLogo', () => () => <div />)
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
useToken: jest.fn(
|
||||
() =>
|
||||
new uniSDK.Token(
|
||||
1,
|
||||
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
8,
|
||||
'https://www.example.com',
|
||||
'example.com coin'
|
||||
)
|
||||
),
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
chainId: 1,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when the name contains a url', () => {
|
||||
const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
|
||||
|
||||
beforeEach(() => {
|
||||
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
|
||||
mocked(useToken).mockImplementation((tokenAddress?: string | null | undefined) => {
|
||||
if (!tokenAddress) return null
|
||||
if (tokenAddress === susToken0.address) return susToken0
|
||||
return new Token(1, tokenAddress, 8, 'symbol', 'name')
|
||||
})
|
||||
mocked(usePool).mockReturnValue([
|
||||
PoolState.EXISTS,
|
||||
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
|
||||
])
|
||||
mocked(unwrappedToken).mockReturnValue(susToken0)
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token0 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
||||
token0: susToken0Address,
|
||||
token1: USDC_MAINNET.address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
render(<PositionListItem {...positionDetails} />)
|
||||
screen.debug()
|
||||
expect(screen.queryByText('.com', { exact: false })).toBe(null)
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token1 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: susToken0Address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should render a position', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
@@ -203,12 +203,9 @@ export default function PositionListItem({
|
||||
|
||||
const removed = liquidity?.eq(0)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() => [token0?.name, token0?.symbol, token1?.name, token1?.symbol].some((testString) => hasURL(testString)),
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
|
||||
)
|
||||
const shouldHidePosition = hasURL(token0?.symbol) || hasURL(token1?.symbol)
|
||||
|
||||
if (containsURL) {
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import RoutingDiagram from './RoutingDiagram'
|
||||
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
color: #98A1C0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
margin-left: 4px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
color: #98A1C0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(auto,1fr) auto minmax(0,72px);
|
||||
grid-gap: 16px;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
background-color: #98A1C014;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
<div
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 168px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
>
|
||||
USD//C
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`renders loading rows when isLoading is true 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
-webkit-animation: fAQEyV 1.5s infinite;
|
||||
animation: fAQEyV 1.5s infinite;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient( to left,#F5F6FC 25%,#E8ECFB 50%,#F5F6FC 75% );
|
||||
background-size: 400%;
|
||||
border-radius: 12px;
|
||||
height: 2.4em;
|
||||
will-change: background-position;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
grid-column-gap: 0.5em;
|
||||
grid-template-columns: repeat(12,1fr);
|
||||
max-width: 960px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 1) {
|
||||
grid-column: 1 / 8;
|
||||
height: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 2) {
|
||||
grid-column: 12;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 3) {
|
||||
grid-column: 1 / 4;
|
||||
height: 0.75em;
|
||||
}
|
||||
|
||||
<div
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 560px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import * as mockJSBI from 'jsbi'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import CurrencyList from '.'
|
||||
|
||||
@@ -25,11 +26,11 @@ jest.mock(
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
account: '123',
|
||||
isActive: true,
|
||||
}),
|
||||
...web3React,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,37 +43,38 @@ jest.mock('../../../state/connection/hooks', () => {
|
||||
})
|
||||
|
||||
it('renders loading rows when isLoading is true', () => {
|
||||
const { asFragment } = render(
|
||||
const component = render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={true}
|
||||
searchQuery=""
|
||||
isAddressSearch=""
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(component.findByTestId('loading-rows')).toBeTruthy()
|
||||
expect(screen.queryByText('Wrapped BTC')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('DAI')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('USDC')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders currency rows correctly when currencies list is non-empty', () => {
|
||||
const { asFragment } = render(
|
||||
render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[DAI, USDC_MAINNET, WBTC]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={false}
|
||||
searchQuery=""
|
||||
isAddressSearch=""
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText('Wrapped BTC')).toBeInTheDocument()
|
||||
expect(screen.getByText('DAI')).toBeInTheDocument()
|
||||
expect(screen.getByText('USDC')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
|
||||
import Row, { RowFixed } from '../../Row'
|
||||
import { MouseoverTooltip } from '../../Tooltip'
|
||||
import { LoadingRows, MenuItem } from '../styleds'
|
||||
import * as styles from './index.css'
|
||||
import { scrollbarStyle } from './index.css'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency.isToken ? currency.address : 'ETHER'
|
||||
@@ -65,6 +65,10 @@ const WarningContainer = styled.div`
|
||||
margin-left: 0.3em;
|
||||
`
|
||||
|
||||
const ListWrapper = styled.div`
|
||||
padding-right: 0.25rem;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount<Currency> }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
@@ -212,7 +216,7 @@ export const formatAnalyticsEventProperties = (
|
||||
})
|
||||
|
||||
const LoadingRow = () => (
|
||||
<LoadingRows>
|
||||
<LoadingRows data-testid="loading-rows">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
@@ -290,10 +294,10 @@ export default function CurrencyList({
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ paddingRight: '4px' }}>
|
||||
<ListWrapper>
|
||||
{isLoading ? (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
className={scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
@@ -305,7 +309,7 @@ export default function CurrencyList({
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
className={scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
@@ -317,6 +321,6 @@ export default function CurrencyList({
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
</ListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,185 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button'
|
||||
import { bodySmall, subhead } from 'nft/css/common.css'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleTaxServiceModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import { useTaxServiceDismissal } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { opacify } from 'theme/utils'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import TaxServiceModal from '.'
|
||||
import CointrackerLogo from './CointrackerLogo.png'
|
||||
import TokenTaxLogo from './TokenTaxLogo.png'
|
||||
|
||||
const PopupContainer = styled.div<{ show: boolean; isDarkMode: boolean }>`
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 13px;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
display: ${({ show }) => (show ? 'flex' : 'none')};
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
right: clamp(0px, 1vw, 16px);
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.slow} opacity ${timing.in}`};
|
||||
width: 320px;
|
||||
height: 156px;
|
||||
bottom: 50px;
|
||||
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
|
||||
border-style: solid none;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
|
||||
background-image: url(${CointrackerLogo}), url(${TokenTaxLogo});
|
||||
background-size: 15%, 20%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: top right 75px, bottom 5px right 7px;
|
||||
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
|
||||
background-size: 48px, 64px;
|
||||
background-position: top right 75px, bottom 20px right 7px;
|
||||
}
|
||||
|
||||
opacity: ${({ isDarkMode }) => (isDarkMode ? '0.9' : '0.25')};
|
||||
}
|
||||
`
|
||||
|
||||
const InnerContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
border-radius: 12px;
|
||||
cursor: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background-color: ${({ isDarkMode, theme }) =>
|
||||
isDarkMode ? opacify(10, theme.accentAction) : opacify(4, theme.accentAction)};
|
||||
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const Button = styled(ThemeButton)`
|
||||
margin-top: auto;
|
||||
margin-right: auto;
|
||||
padding: 8px 24px;
|
||||
gap: 8px;
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export const StyledXButton = styled(X)`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
&:active {
|
||||
opacity: ${({ theme }) => theme.opacity.click};
|
||||
}
|
||||
`
|
||||
|
||||
const TAX_SERVICE_DISMISSED = 'TaxServiceToast-dismissed'
|
||||
|
||||
// TODO(lynnshaoyu): remove this count and change taxServiceDismissals in UserState to be a boolean
|
||||
// flag instead after upgrading to redux-persist.
|
||||
const MAX_RENDER_COUNT = 1
|
||||
|
||||
export default function TaxServiceBanner() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
const [dismissals, addTaxServiceDismissal] = useTaxServiceDismissal()
|
||||
const modalOpen = useModalIsOpen(ApplicationModal.TAX_SERVICE)
|
||||
const toggleTaxServiceModal = useToggleTaxServiceModal()
|
||||
|
||||
const sessionStorageTaxServiceDismissed = sessionStorage.getItem(TAX_SERVICE_DISMISSED)
|
||||
|
||||
if (!sessionStorageTaxServiceDismissed) {
|
||||
sessionStorage.setItem(TAX_SERVICE_DISMISSED, 'false')
|
||||
}
|
||||
const [bannerOpen, setBannerOpen] = useState(
|
||||
sessionStorageTaxServiceDismissed !== 'true' && (dismissals === undefined || dismissals < MAX_RENDER_COUNT)
|
||||
)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
sessionStorage.setItem(TAX_SERVICE_DISMISSED, 'true')
|
||||
setBannerOpen(false)
|
||||
dismissals === undefined ? addTaxServiceDismissal(1) : addTaxServiceDismissal(dismissals + 1)
|
||||
}, [addTaxServiceDismissal, dismissals])
|
||||
|
||||
const handleLearnMoreClick = useCallback(
|
||||
(e: any) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleTaxServiceModal()
|
||||
},
|
||||
[toggleTaxServiceModal]
|
||||
)
|
||||
|
||||
return (
|
||||
<PopupContainer show={bannerOpen} isDarkMode={isDarkMode}>
|
||||
<InnerContainer isDarkMode={isDarkMode} tabIndex={0}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<TextContainer data-testid="tax-service-description">
|
||||
<div className={subhead} style={{ paddingBottom: '12px' }}>
|
||||
<Trans>Save on your crypto taxes</Trans>
|
||||
</div>
|
||||
<div className={bodySmall} style={{ paddingBottom: '12px' }}>
|
||||
<Trans>Uniswap Labs can save you up to 20% on CoinTracker and TokenTax</Trans>{' '}
|
||||
</div>
|
||||
</TextContainer>
|
||||
<StyledXButton size={20} onClick={handleClose} />
|
||||
</div>
|
||||
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.TAX_SERVICE_BANNER_CTA_BUTTON}
|
||||
>
|
||||
<Button
|
||||
size={ButtonSize.small}
|
||||
emphasis={ButtonEmphasis.promotional}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
onClick={handleLearnMoreClick}
|
||||
data-testid="learn-more-button"
|
||||
>
|
||||
<Trans>Learn more</Trans>
|
||||
</Button>
|
||||
</TraceEvent>
|
||||
</InnerContainer>
|
||||
<TaxServiceModal isOpen={modalOpen} onDismiss={toggleTaxServiceModal} />
|
||||
</PopupContainer>
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,17 +0,0 @@
|
||||
import { render, screen } from '../../test-utils'
|
||||
import TaxServiceModal from './'
|
||||
import TaxServiceBanner from './TaxServiceBanner'
|
||||
|
||||
it('renders Tax Service Modal content', async () => {
|
||||
render(<TaxServiceModal isOpen={true} onDismiss={() => null} />)
|
||||
expect(screen.getByText('Save 10% on all plans')).toBeInTheDocument()
|
||||
expect(screen.getByText('New and existing users save up to 20%')).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('tax-service-option-button')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('renders Tax Service Banner', async () => {
|
||||
render(<TaxServiceBanner />)
|
||||
expect(screen.getByText('Save on your crypto taxes')).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('learn-more-button')).toHaveLength(1)
|
||||
expect(screen.getByText('Uniswap Labs can save you up to 20% on CoinTracker and TokenTax')).toBeInTheDocument()
|
||||
})
|
||||
@@ -1,141 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { ButtonEmphasis } from 'components/Button'
|
||||
import { ButtonSize, ThemeButton } from 'components/Button'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { bodySmall, subhead } from 'nft/css/common.css'
|
||||
import { memo } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Modal from '../Modal'
|
||||
import CointrackerFullLogo from './CointrackerFullLogo.png'
|
||||
import { StyledXButton } from './TaxServiceBanner'
|
||||
import TokenTaxFullLogo from './TokenTaxFullLogo.png'
|
||||
|
||||
interface TaxServiceModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
interface TaxServiceOptionProps {
|
||||
logo: any
|
||||
description: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
width: 420px;
|
||||
height: 268px;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
gap: 20px;
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
const TaxOptionContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const TaxOptionDescription = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const TaxOption = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
border-radius: 12px;
|
||||
cursor: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const StyledImageContainer = styled(Box)`
|
||||
width: 75%;
|
||||
height: 80%;
|
||||
cursor: auto;
|
||||
object-fit: contain;
|
||||
`
|
||||
|
||||
const Button = styled(ThemeButton)`
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
`
|
||||
|
||||
const TOKEN_TAX_URL = 'https://tokentax.co/uniswap?via=uniswap'
|
||||
const COINTRACKER_URL = 'https://www.cointracker.io/partner/uniswap?utm_source=uniswap'
|
||||
|
||||
const TOKEN_TAX_DESCRIPTION = 'Save 10% on all plans'
|
||||
const COINTRACKER_DESCRIPTION = 'New and existing users save up to 20%'
|
||||
|
||||
function TaxServiceOption({ description, logo, url }: TaxServiceOptionProps) {
|
||||
return (
|
||||
<TaxOption tabIndex={0}>
|
||||
<StyledImageContainer as="img" src={logo} draggable={false} />
|
||||
<TaxOptionDescription className={bodySmall}>{description}</TaxOptionDescription>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={
|
||||
url.includes('tokentax')
|
||||
? InterfaceElementName.TAX_SERVICE_TOKENTAX_BUTTON
|
||||
: InterfaceElementName.TAX_SERVICE_COINTRACKER_BUTTON
|
||||
}
|
||||
>
|
||||
<a href={url} target="_blank" rel="noreferrer" style={{ textDecoration: 'none' }}>
|
||||
<Button
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
data-testid="tax-service-option-button"
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
</a>
|
||||
</TraceEvent>
|
||||
</TaxOption>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(function TaxServiceModal({ isOpen, onDismiss }: TaxServiceModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} minHeight={false}>
|
||||
<InnerContainer>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', userSelect: 'none' }}>
|
||||
<div className={subhead}>
|
||||
<Trans>Save on your crypto taxes</Trans>
|
||||
</div>
|
||||
<StyledXButton
|
||||
size={20}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDismiss()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TaxOptionContainer>
|
||||
<TaxServiceOption description={COINTRACKER_DESCRIPTION} logo={CointrackerFullLogo} url={COINTRACKER_URL} />
|
||||
<TaxServiceOption description={TOKEN_TAX_DESCRIPTION} logo={TokenTaxFullLogo} url={TOKEN_TAX_URL} />
|
||||
</TaxOptionContainer>
|
||||
</InnerContainer>
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from 'test-utils'
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
|
||||
import { ResizingTextArea, TextInput } from './'
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
// TODO(WEB-3163): migrate noops throughout web to a shared util file.
|
||||
const noop = () => null
|
||||
|
||||
export const TooltipContainer = styled.div`
|
||||
max-width: 256px;
|
||||
cursor: default;
|
||||
padding: 0.6rem 1rem;
|
||||
pointer-events: auto;
|
||||
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 400;
|
||||
@@ -25,7 +29,6 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
text: ReactNode
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
noOp?: () => void
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
timeout?: number
|
||||
}
|
||||
@@ -33,17 +36,19 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
|
||||
content: ReactNode
|
||||
onOpen?: () => void
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
// whether to wrap the content in a `TooltipContainer`
|
||||
wrap?: boolean
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
}
|
||||
|
||||
export default function Tooltip({ text, open, close, noOp, disableHover, ...rest }: TooltipProps) {
|
||||
export default function Tooltip({ text, open, close, disableHover, ...rest }: TooltipProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
text && (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noOp : open} onMouseLeave={disableHover ? noOp : close}>
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
{text}
|
||||
</TooltipContainer>
|
||||
)
|
||||
@@ -53,15 +58,28 @@ export default function Tooltip({ text, open, close, noOp, disableHover, ...rest
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
|
||||
return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
|
||||
function TooltipContent({ content, wrap = false, open, close, disableHover, ...rest }: TooltipContentProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
wrap ? (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
{content}
|
||||
</TooltipContainer>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Standard text tooltip. */
|
||||
export function MouseoverTooltip({ text, disableHover, children, timeout, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => text && setShow(true), [text, setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
const open = () => text && setShow(true)
|
||||
const close = () => setShow(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (show && timeout) {
|
||||
@@ -76,18 +94,16 @@ export function MouseoverTooltip({ text, disableHover, children, timeout, ...res
|
||||
return
|
||||
}, [timeout, show])
|
||||
|
||||
const noOp = () => null
|
||||
return (
|
||||
<Tooltip
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
noOp={noOp}
|
||||
disableHover={disableHover}
|
||||
show={show}
|
||||
text={disableHover ? null : text}
|
||||
>
|
||||
<div onMouseEnter={disableHover ? noOp : open} onMouseLeave={disableHover || timeout ? noOp : close}>
|
||||
<div onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover || timeout ? noop : close}>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -103,18 +119,23 @@ export function MouseoverTooltipContent({
|
||||
...rest
|
||||
}: Omit<TooltipContentProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => {
|
||||
const open = () => {
|
||||
setShow(true)
|
||||
openCallback?.()
|
||||
}, [openCallback])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
}
|
||||
const close = () => {
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipContent {...rest} show={!disableHover && show} content={disableHover ? null : content}>
|
||||
<div
|
||||
style={{ display: 'inline-block', lineHeight: 0, padding: '0.25rem' }}
|
||||
onMouseEnter={open}
|
||||
onMouseLeave={close}
|
||||
>
|
||||
<TooltipContent
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
show={!disableHover && show}
|
||||
content={disableHover ? null : content}
|
||||
>
|
||||
<div onMouseEnter={open} onMouseLeave={close}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import UniwalletModal from 'components/AccountDrawer/UniwalletModal'
|
||||
import UniswapWalletBanner from 'components/Banner/UniswapWalletBanner'
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import FiatOnrampModal from 'components/FiatOnrampModal'
|
||||
import TaxServiceBanner from 'components/TaxServiceModal/TaxServiceBanner'
|
||||
import UniwalletModal from 'components/WalletDropdown/UniwalletModal'
|
||||
import { useTaxServiceBannerEnabled } from 'featureFlags/flags/taxServiceBanner'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useIsPoolsPage } from 'hooks/useIsPoolsPage'
|
||||
import { lazy } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
|
||||
@@ -25,13 +20,6 @@ export default function TopLevelModals() {
|
||||
const { account } = useWeb3React()
|
||||
useAccountRiskCheck(account)
|
||||
const accountBlocked = Boolean(blockedAccountModalOpen && account)
|
||||
const taxServiceEnabled = useTaxServiceBannerEnabled()
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isNftPage = useIsNftPage()
|
||||
const isPoolPage = useIsPoolsPage()
|
||||
|
||||
const isTaxModalServicePage = isNftPage || isPoolPage || pathname.startsWith('/swap')
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,7 +31,6 @@ export default function TopLevelModals() {
|
||||
<TransactionCompleteModal />
|
||||
<AirdropModal />
|
||||
<FiatOnrampModal />
|
||||
{taxServiceEnabled && isTaxModalServicePage && <TaxServiceBanner />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { Connection, ConnectionType } from 'connection'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
|
||||
|
||||
import NewBadge from './NewBadge'
|
||||
@@ -68,11 +69,12 @@ type OptionProps = {
|
||||
}
|
||||
export default function Option({ connection, pendingConnectionType, activate }: OptionProps) {
|
||||
const isPending = pendingConnectionType === connection.type
|
||||
const isDarkMode = useIsDarkMode()
|
||||
const content = (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.WALLET_SELECTED}
|
||||
properties={{ wallet_type: connection.name }}
|
||||
properties={{ wallet_type: connection.getName() }}
|
||||
element={InterfaceElementName.WALLET_TYPE_OPTION}
|
||||
>
|
||||
<OptionCardClickable
|
||||
@@ -83,9 +85,9 @@ export default function Option({ connection, pendingConnectionType, activate }:
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<IconWrapper>
|
||||
<img src={connection.icon} alt="Icon" />
|
||||
<img src={connection.getIcon?.(isDarkMode)} alt="Icon" />
|
||||
</IconWrapper>
|
||||
<HeaderText>{connection.name}</HeaderText>
|
||||
<HeaderText>{connection.getName()}</HeaderText>
|
||||
{connection.isNew && <NewBadge />}
|
||||
</OptionCardLeft>
|
||||
{isPending && <Loader />}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { sendAnalyticsEvent, user } from '@uniswap/analytics'
|
||||
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
|
||||
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import IconButton from 'components/AccountDrawer/IconButton'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import IconButton from 'components/WalletDropdown/IconButton'
|
||||
import { Connection, ConnectionType, networkConnection, useConnections } from 'connection'
|
||||
import { Connection, ConnectionType, getConnections, networkConnection } from 'connection'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { ErrorCode } from 'connection/utils'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
@@ -84,14 +84,14 @@ function didUserReject(connection: Connection, error: any): boolean {
|
||||
export default function WalletModal({ openSettings }: { openSettings: () => void }) {
|
||||
const dispatch = useAppDispatch()
|
||||
const { connector, account, chainId, provider } = useWeb3React()
|
||||
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
|
||||
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
|
||||
const [lastActiveWalletAddress, setLastActiveWalletAddress] = useState<string | undefined>(account)
|
||||
const [pendingConnection, setPendingConnection] = useState<Connection | undefined>()
|
||||
const [pendingError, setPendingError] = useState<any>()
|
||||
|
||||
const connections = useConnections()
|
||||
const connections = getConnections()
|
||||
const getConnection = useGetConnection()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,7 +116,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
|
||||
useEffect(() => {
|
||||
if (account && account !== lastActiveWalletAddress) {
|
||||
const walletName = getConnection(connector).name
|
||||
const walletName = getConnection(connector).getName()
|
||||
const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined
|
||||
const isReconnect =
|
||||
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletName).length > 0
|
||||
@@ -141,6 +141,9 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
|
||||
const tryActivation = useCallback(
|
||||
async (connection: Connection) => {
|
||||
// Skips wallet connection if the connection should override the default behavior, i.e. install metamask or launch coinbase app
|
||||
if (connection.overrideActivate?.()) return
|
||||
|
||||
// log selected wallet
|
||||
sendEvent({
|
||||
category: 'Wallet',
|
||||
@@ -153,19 +156,20 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
setPendingError(undefined)
|
||||
|
||||
await connection.connector.activate()
|
||||
console.debug(`connection activated: ${connection.getName()}`)
|
||||
dispatch(updateSelectedWallet({ wallet: connection.type }))
|
||||
if (drawerOpenRef.current) toggleWalletDrawer()
|
||||
} catch (error) {
|
||||
console.debug(`web3-react connection error: ${JSON.stringify(error)}`)
|
||||
// TODO(WEB-3162): re-add special treatment for already-pending injected errors
|
||||
if (didUserReject(connection, error)) {
|
||||
setPendingConnection(undefined)
|
||||
} // Prevents showing error caused by MetaMask being prompted twice
|
||||
else if (error?.code !== ErrorCode.MM_ALREADY_PENDING) {
|
||||
console.debug(`web3-react connection error: ${error}`)
|
||||
setPendingError(error.message)
|
||||
} else {
|
||||
setPendingError(error)
|
||||
|
||||
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
result: WalletConnectionResult.FAILED,
|
||||
wallet_type: connection.name,
|
||||
wallet_type: connection.getName(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -190,11 +194,11 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
<OptionGrid data-testid="option-grid">
|
||||
{connections.map((connection) =>
|
||||
// Hides Uniswap Wallet if mgtm is disabled
|
||||
connection.shouldDisplay && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
|
||||
connection.shouldDisplay() && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
|
||||
<Option
|
||||
key={connection.name}
|
||||
key={connection.getName()}
|
||||
connection={connection}
|
||||
activate={connection.overrideActivate ?? (() => tryActivation(connection))}
|
||||
activate={() => tryActivation(connection)}
|
||||
pendingConnectionType={pendingConnection?.type}
|
||||
/>
|
||||
) : null
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
const connections = useOrderedConnections()
|
||||
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
|
||||
|
||||
const key = useMemo(() => connections.map((connection) => connection.name).join('-'), [connections])
|
||||
const key = useMemo(() => connections.map((connection) => connection.getName()).join('-'), [connections])
|
||||
|
||||
return (
|
||||
<Web3ReactProvider connectors={connectors} key={key}>
|
||||
|
||||
@@ -2,19 +2,17 @@ import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import PortfolioDrawer, { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import WalletDropdown, { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import PrefetchBalancesWrapper from 'components/WalletDropdown/PrefetchBalancesWrapper'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { colors } from 'theme/colors'
|
||||
import { flexRowNoWrap } from 'theme/styles'
|
||||
@@ -153,16 +151,11 @@ function Web3StatusInner() {
|
||||
const { account, connector, chainId, ENSName } = useWeb3React()
|
||||
const getConnection = useGetConnection()
|
||||
const connection = getConnection(connector)
|
||||
const {
|
||||
trade: { state: tradeState, trade },
|
||||
inputError: swapInputError,
|
||||
} = useDerivedSwapInfo()
|
||||
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
|
||||
const [, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [, toggleAccountDrawer] = useAccountDrawer()
|
||||
const handleWalletDropdownClick = useCallback(() => {
|
||||
sendAnalyticsEvent(InterfaceEventName.ACCOUNT_DROPDOWN_BUTTON_CLICKED)
|
||||
toggleWalletDrawer()
|
||||
}, [toggleWalletDrawer])
|
||||
toggleAccountDrawer()
|
||||
}, [toggleAccountDrawer])
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
@@ -202,9 +195,7 @@ function Web3StatusInner() {
|
||||
pending={hasPendingTransactions}
|
||||
isClaimAvailable={isClaimAvailable}
|
||||
>
|
||||
{!hasPendingTransactions && (
|
||||
<StatusIcon enableInfotips={true} size={24} connection={connection} showMiniIcons={false} />
|
||||
)}
|
||||
{!hasPendingTransactions && <StatusIcon size={24} connection={connection} showMiniIcons={false} />}
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>
|
||||
@@ -225,7 +216,6 @@ function Web3StatusInner() {
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: validSwapQuote }}
|
||||
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<Web3StatusConnectWrapper
|
||||
@@ -243,13 +233,12 @@ function Web3StatusInner() {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export default function Web3Status() {
|
||||
return (
|
||||
<PrefetchBalancesWrapper>
|
||||
<Web3StatusInner />
|
||||
<Portal>
|
||||
<WalletDropdown />
|
||||
<PortfolioDrawer />
|
||||
</Portal>
|
||||
</PrefetchBalancesWrapper>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SwapWidgetSkeleton,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
@@ -67,7 +67,7 @@ export default function Widget({
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
|
||||
46
src/components/addLiquidity/OwnershipWarning.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const ExplainerText = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
const TitleRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: ${({ theme }) => theme.accentWarning};
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.accentWarningSoft};
|
||||
border-radius: 16px;
|
||||
margin-top: 12px;
|
||||
max-width: 480px;
|
||||
padding: 12px 20px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
interface OwnershipWarningProps {
|
||||
ownerAddress: string
|
||||
}
|
||||
|
||||
const OwnershipWarning = ({ ownerAddress }: OwnershipWarningProps) => (
|
||||
<Wrapper>
|
||||
<TitleRow>
|
||||
<AlertTriangle style={{ marginRight: '8px' }} />
|
||||
<ThemedText.SubHeader color="accentWarning">
|
||||
<Trans>Warning</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</TitleRow>
|
||||
<ExplainerText>
|
||||
<Trans>
|
||||
You are not the owner of this LP position. You will not be able to withdraw the liquidity from this position
|
||||
unless you own the following address: {ownerAddress}
|
||||
</Trans>
|
||||
</ExplainerText>
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default OwnershipWarning
|
||||
111
src/components/swap/SwapBuyFiatButton.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
|
||||
import { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
|
||||
import SwapBuyFiatButton, { MOONPAY_REGION_AVAILABILITY_ARTICLE } from './SwapBuyFiatButton'
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('../../state/application/hooks')
|
||||
const mockUseFiatOnrampAvailability = useFiatOnrampAvailability as jest.MockedFunction<typeof useFiatOnrampAvailability>
|
||||
const mockUseOpenModal = useOpenModal as jest.MockedFunction<typeof useOpenModal>
|
||||
|
||||
jest.mock('components/AccountDrawer')
|
||||
const mockuseAccountDrawer = useAccountDrawer as jest.MockedFunction<typeof useAccountDrawer>
|
||||
|
||||
const mockUseFiatOnRampsUnavailable = (shouldCheck: boolean) => {
|
||||
return {
|
||||
available: false,
|
||||
availabilityChecked: shouldCheck,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
const mockUseFiatOnRampsAvailable = (shouldCheck: boolean) => {
|
||||
if (shouldCheck) {
|
||||
return {
|
||||
available: true,
|
||||
availabilityChecked: true,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
available: false,
|
||||
availabilityChecked: false,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
describe('SwapBuyFiatButton.tsx', () => {
|
||||
let toggleWalletDrawer: jest.Mock<any, any>
|
||||
let useOpenModal: jest.Mock<any, any>
|
||||
|
||||
beforeAll(() => {
|
||||
toggleWalletDrawer = jest.fn()
|
||||
useOpenModal = jest.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
;(useWeb3React as jest.Mock).mockReturnValue({
|
||||
account: undefined,
|
||||
isActive: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('matches base snapshot', () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
const { asFragment } = render(<SwapBuyFiatButton />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fiat on ramps available in region, account unconnected', async () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
mockUseOpenModal.mockImplementation(() => useOpenModal)
|
||||
render(<SwapBuyFiatButton />)
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
expect(toggleWalletDrawer).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fiat on ramps available in region, account connected', async () => {
|
||||
;(useWeb3React as jest.Mock).mockReturnValue({
|
||||
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
|
||||
isActive: true,
|
||||
})
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
mockUseOpenModal.mockImplementation(() => useOpenModal)
|
||||
render(<SwapBuyFiatButton />)
|
||||
expect(screen.getByTestId('buy-fiat-flow-incomplete-indicator')).toBeInTheDocument()
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
expect(toggleWalletDrawer).toHaveBeenCalledTimes(0)
|
||||
expect(useOpenModal).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('buy-fiat-flow-incomplete-indicator')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fiat on ramps unavailable in region', async () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
render(<SwapBuyFiatButton />)
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
fireEvent.mouseOver(screen.getByTestId('buy-fiat-button'))
|
||||
expect(await screen.findByTestId('fiat-on-ramp-unavailable-tooltip')).toBeInTheDocument()
|
||||
expect(await screen.findByText(/Learn more/i)).toHaveAttribute('href', MOONPAY_REGION_AVAILABILITY_ARTICLE)
|
||||
expect(await screen.findByTestId('buy-fiat-button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
146
src/components/swap/SwapBuyFiatButton.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { ButtonText } from 'components/Button'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useBuyFiatFlowCompleted } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink } from 'theme'
|
||||
|
||||
import { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
|
||||
const Dot = styled.div`
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: ${({ theme }) => theme.accentActive};
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
export const MOONPAY_REGION_AVAILABILITY_ARTICLE =
|
||||
'https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-'
|
||||
|
||||
enum BuyFiatFlowState {
|
||||
// Default initial state. User is not actively trying to buy fiat.
|
||||
INACTIVE,
|
||||
// Buy fiat flow is active and region availability has been checked.
|
||||
ACTIVE_CHECKING_REGION,
|
||||
// Buy fiat flow is active, feature is available in user's region & needs wallet connection.
|
||||
ACTIVE_NEEDS_ACCOUNT,
|
||||
}
|
||||
|
||||
const StyledTextButton = styled(ButtonText)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
gap: 4px;
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function SwapBuyFiatButton() {
|
||||
const { account } = useWeb3React()
|
||||
const openFiatOnRampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
|
||||
const [buyFiatFlowCompleted, setBuyFiatFlowCompleted] = useBuyFiatFlowCompleted()
|
||||
const [checkFiatRegionAvailability, setCheckFiatRegionAvailability] = useState(false)
|
||||
const {
|
||||
available: fiatOnrampAvailable,
|
||||
availabilityChecked: fiatOnrampAvailabilityChecked,
|
||||
loading: fiatOnrampAvailabilityLoading,
|
||||
} = useFiatOnrampAvailability(checkFiatRegionAvailability)
|
||||
const [buyFiatFlowState, setBuyFiatFlowState] = useState(BuyFiatFlowState.INACTIVE)
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
|
||||
/*
|
||||
* Depending on the current state of the buy fiat flow the user is in (buyFiatFlowState),
|
||||
* the desired behavior of clicking the 'Buy' button is different.
|
||||
* 1) Initially upon first click, need to check the availability of the feature in the user's
|
||||
* region, and continue the flow.
|
||||
* 2) If the feature is available in the user's region, need to connect a wallet, and continue
|
||||
* the flow.
|
||||
* 3) If the feature is available and a wallet account is connected, show fiat on ramp modal.
|
||||
* 4) If the feature is unavailable, show feature unavailable tooltip.
|
||||
*/
|
||||
const handleBuyCrypto = useCallback(() => {
|
||||
if (!fiatOnrampAvailabilityChecked) {
|
||||
setCheckFiatRegionAvailability(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.ACTIVE_CHECKING_REGION)
|
||||
} else if (fiatOnrampAvailable && !account && !walletDrawerOpen) {
|
||||
toggleWalletDrawer()
|
||||
setBuyFiatFlowState(BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
} else if (fiatOnrampAvailable && account) {
|
||||
openFiatOnRampModal()
|
||||
setBuyFiatFlowCompleted(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.INACTIVE)
|
||||
} else if (!fiatOnrampAvailable) {
|
||||
setBuyFiatFlowCompleted(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.INACTIVE)
|
||||
}
|
||||
}, [
|
||||
fiatOnrampAvailabilityChecked,
|
||||
fiatOnrampAvailable,
|
||||
account,
|
||||
walletDrawerOpen,
|
||||
toggleWalletDrawer,
|
||||
openFiatOnRampModal,
|
||||
setBuyFiatFlowCompleted,
|
||||
])
|
||||
|
||||
// Continue buy fiat flow automatically when requisite state changes have occured.
|
||||
useEffect(() => {
|
||||
if (
|
||||
(buyFiatFlowState === BuyFiatFlowState.ACTIVE_CHECKING_REGION && fiatOnrampAvailabilityChecked) ||
|
||||
(account && buyFiatFlowState === BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
) {
|
||||
handleBuyCrypto()
|
||||
}
|
||||
}, [account, handleBuyCrypto, buyFiatFlowState, fiatOnrampAvailabilityChecked])
|
||||
|
||||
const buyCryptoButtonDisabled =
|
||||
(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) ||
|
||||
fiatOnrampAvailabilityLoading ||
|
||||
// When wallet drawer is open AND user is in the connect wallet step of the buy fiat flow, disable buy fiat button.
|
||||
(walletDrawerOpen && buyFiatFlowState === BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
|
||||
const fiatOnRampsUnavailableTooltipDisabled =
|
||||
!fiatOnrampAvailabilityChecked || (fiatOnrampAvailabilityChecked && fiatOnrampAvailable)
|
||||
|
||||
return (
|
||||
<MouseoverTooltipContent
|
||||
wrap
|
||||
content={
|
||||
<div data-testid="fiat-on-ramp-unavailable-tooltip">
|
||||
<Trans>Crypto purchases are not available in your region. </Trans>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.FIAT_ON_RAMP_LEARN_MORE_LINK}
|
||||
>
|
||||
<ExternalLink href={MOONPAY_REGION_AVAILABILITY_ARTICLE} style={{ paddingLeft: '4px' }}>
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</TraceEvent>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
disableHover={fiatOnRampsUnavailableTooltipDisabled}
|
||||
>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.FIAT_ON_RAMP_BUY_BUTTON}
|
||||
properties={{ account_connected: !!account }}
|
||||
>
|
||||
<StyledTextButton onClick={handleBuyCrypto} disabled={buyCryptoButtonDisabled} data-testid="buy-fiat-button">
|
||||
<Trans>Buy</Trans>
|
||||
{!buyFiatFlowCompleted && <Dot data-testid="buy-fiat-flow-incomplete-indicator" />}
|
||||
</StyledTextButton>
|
||||
</TraceEvent>
|
||||
</MouseoverTooltipContent>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SettingsTab from '../Settings'
|
||||
import SwapBuyFiatButton from './SwapBuyFiatButton'
|
||||
|
||||
const StyledSwapHeader = styled.div`
|
||||
padding: 8px 12px;
|
||||
@@ -13,14 +15,27 @@ const StyledSwapHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function SwapHeader({ allowedSlippage }: { allowedSlippage: Percent }) {
|
||||
const fiatOnRampButtonEnabled = useFiatOnRampButtonEnabled()
|
||||
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBlack fontWeight={500} fontSize={16} style={{ marginRight: '8px' }}>
|
||||
<RowFixed style={{ gap: '8px' }}>
|
||||
<TextHeader className={subhead}>
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</TextHeader>
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<SettingsTab placeholderSlippage={allowedSlippage} />
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: inherit;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
color: white;
|
||||
background-color: primary;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
color: #0D111C;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-flex-wrap: nowrap;
|
||||
-ms-flex-wrap: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
will-change: transform;
|
||||
-webkit-transition: -webkit-transform 450ms ease;
|
||||
-webkit-transition: transform 450ms ease;
|
||||
transition: transform 450ms ease;
|
||||
-webkit-transform: perspective(1px) translateZ(0);
|
||||
-ms-transform: perspective(1px) translateZ(0);
|
||||
transform: perspective(1px) translateZ(0);
|
||||
}
|
||||
|
||||
.c2:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c2 > * {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c2 > a {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 0;
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
background: none;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c3:focus {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.c3:active {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c3:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: #4C82FB;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
color: #7780A0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c4:focus {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c4:active {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="c1 c2 c3 c4"
|
||||
data-testid="buy-fiat-button"
|
||||
>
|
||||
Buy
|
||||
<div
|
||||
class="c5"
|
||||
data-testid="buy-fiat-flow-incomplete-indicator"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,111 +1,150 @@
|
||||
// eslint-disable-next-line jest/no-export
|
||||
export {}
|
||||
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
|
||||
import { ConnectionType, getConnections, useGetConnection } from 'connection'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('Non-injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
const UserAgentMock = jest.requireMock('utils/userAgent')
|
||||
jest.mock('utils/userAgent', () => ({
|
||||
isMobile: false,
|
||||
}))
|
||||
|
||||
it('MetaMask Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
describe('connection utility/metadata tests', () => {
|
||||
const createWalletEnvironment = (ethereum: Window['window']['ethereum'], isMobile = false) => {
|
||||
UserAgentMock.isMobile = isMobile
|
||||
global.window.ethereum = ethereum
|
||||
|
||||
it('Coinbase Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
const displayed = getConnections().filter((c) => c.shouldDisplay())
|
||||
const getConnection = renderHook(() => useGetConnection()).result.current
|
||||
const injected = getConnection(ConnectionType.INJECTED)
|
||||
const coinbase = getConnection(ConnectionType.COINBASE_WALLET)
|
||||
const uniswap = getConnection(ConnectionType.UNIWALLET)
|
||||
const walletconnect = getConnection(ConnectionType.WALLET_CONNECT)
|
||||
|
||||
it('Coinbase and MetaMask Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: true }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
return { displayed, injected, coinbase, uniswap, walletconnect }
|
||||
}
|
||||
|
||||
it('Generic Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
it('Non-injected Desktop', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment(undefined)
|
||||
|
||||
it('Generic Injected Mobile Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
|
||||
})
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
it('MetaMask Mobile Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
|
||||
})
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Coinbase Mobile Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
it('MetaMask-Injected Desktop', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true })
|
||||
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.coinbaseWalletConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
|
||||
})
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
it('mWeb Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(false)
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.coinbaseWalletConnection.overrideActivate).toBeDefined()
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(3)
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Coinbase-Injected Desktop', async () => {
|
||||
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true })
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Coinbase and MetaMask Injected Desktop', async () => {
|
||||
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true, isMetaMask: true })
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Generic Injected Desktop', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isTrustWallet: true })
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Generic Browser Wallet that injects as MetaMask', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isRabby: true, isMetaMask: true })
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Generic Wallet Browser with delayed injection', async () => {
|
||||
const { injected } = createWalletEnvironment(undefined)
|
||||
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
createWalletEnvironment({ isTrustWallet: true })
|
||||
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
})
|
||||
|
||||
const UNKNOWN_INJECTOR = { isRandomWallet: true } as Window['window']['ethereum']
|
||||
it('Generic Unknown Injected Wallet Browser', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment(UNKNOWN_INJECTOR, true)
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(injected.getIcon?.(/* isDarkMode */ false)).toBe(INJECTED_LIGHT_ICON)
|
||||
expect(injected.getIcon?.(/* isDarkMode */ true)).toBe(INJECTED_DARK_ICON)
|
||||
|
||||
// Ensures we provide multiple connection options if in an unknown injected browser
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('MetaMask Mobile Browser', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true }, true)
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
expect(displayed.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('Coinbase Mobile Browser', async () => {
|
||||
const { displayed, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true }, true)
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
// Expect coinbase option to not override activation in a the cb mobile browser
|
||||
expect(coinbase.overrideActivate?.()).toBeFalsy()
|
||||
expect(displayed.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('Uninjected mWeb Browser', async () => {
|
||||
const { displayed, injected, coinbase, walletconnect } = createWalletEnvironment(undefined, true)
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
expect(displayed.includes(walletconnect)).toBe(true)
|
||||
// Don't show injected connection on plain mWeb browser
|
||||
expect(displayed.includes(injected)).toBe(false)
|
||||
// Expect coinbase option to launch coinbase app in a regular mobile browser
|
||||
expect(coinbase.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
expect(displayed.length).toEqual(3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,22 +4,21 @@ import { GnosisSafe } from '@web3-react/gnosis-safe'
|
||||
import { MetaMask } from '@web3-react/metamask'
|
||||
import { Network } from '@web3-react/network'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import COINBASE_ICON_URL from 'assets/images/coinbaseWalletIcon.svg'
|
||||
import GNOSIS_ICON_URL from 'assets/images/gnosis.png'
|
||||
import METAMASK_ICON_URL from 'assets/images/metamask.svg'
|
||||
import UNIWALLET_ICON_URL from 'assets/images/uniwallet.svg'
|
||||
import WALLET_CONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg'
|
||||
import INJECTED_DARK_ICON_URL from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON_URL from 'assets/svg/browser-wallet-light.svg'
|
||||
import UNISWAP_LOGO_URL from 'assets/svg/logo.svg'
|
||||
import COINBASE_ICON from 'assets/images/coinbaseWalletIcon.svg'
|
||||
import GNOSIS_ICON from 'assets/images/gnosis.png'
|
||||
import METAMASK_ICON from 'assets/images/metamask.svg'
|
||||
import UNIWALLET_ICON from 'assets/images/uniwallet.svg'
|
||||
import WALLET_CONNECT_ICON from 'assets/images/walletConnectIcon.svg'
|
||||
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
|
||||
import UNISWAP_LOGO from 'assets/svg/logo.svg'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useCallback } from 'react'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
|
||||
|
||||
import { RPC_URLS } from '../constants/networks'
|
||||
import { RPC_PROVIDERS } from '../constants/providers'
|
||||
import { isCoinbaseWallet, isInjected, isMetaMaskWallet } from './utils'
|
||||
import { getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
|
||||
import { UniwalletConnect, WalletConnectPopup } from './WalletConnect'
|
||||
|
||||
export enum ConnectionType {
|
||||
@@ -32,13 +31,13 @@ export enum ConnectionType {
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
name: string
|
||||
getName(): string
|
||||
connector: Connector
|
||||
hooks: Web3ReactHooks
|
||||
type: ConnectionType
|
||||
icon?: string
|
||||
shouldDisplay?: boolean
|
||||
overrideActivate?: () => void
|
||||
getIcon?(isDarkMode: boolean): string
|
||||
shouldDisplay(): boolean
|
||||
overrideActivate?: () => boolean
|
||||
isNew?: boolean
|
||||
}
|
||||
|
||||
@@ -50,73 +49,73 @@ const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
|
||||
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
|
||||
)
|
||||
export const networkConnection: Connection = {
|
||||
name: 'Network',
|
||||
getName: () => 'Network',
|
||||
connector: web3Network,
|
||||
hooks: web3NetworkHooks,
|
||||
type: ConnectionType.NETWORK,
|
||||
shouldDisplay: false,
|
||||
shouldDisplay: () => false,
|
||||
}
|
||||
|
||||
const isCoinbaseWalletBrowser = isMobile && isCoinbaseWallet
|
||||
const isMetaMaskBrowser = isMobile && isMetaMaskWallet
|
||||
const getIsInjectedMobileBrowser = isCoinbaseWalletBrowser || isMetaMaskBrowser
|
||||
const getIsCoinbaseWalletBrowser = () => isMobile && getIsCoinbaseWallet()
|
||||
const getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet()
|
||||
const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser()
|
||||
|
||||
const getShouldAdvertiseMetaMask = !isMetaMaskWallet && !isMobile && (!isInjected || isCoinbaseWallet)
|
||||
const isGenericInjector = isInjected && !isMetaMaskWallet && !isCoinbaseWallet
|
||||
const getShouldAdvertiseMetaMask = () =>
|
||||
!getIsMetaMaskWallet() && !isMobile && (!getIsInjected() || getIsCoinbaseWallet())
|
||||
const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() && !getIsCoinbaseWallet()
|
||||
|
||||
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
|
||||
const baseInjectedConnection: Omit<Connection, 'icon'> = {
|
||||
name: isGenericInjector ? 'Browser Wallet' : 'MetaMask',
|
||||
|
||||
const injectedConnection: Connection = {
|
||||
// TODO(WEB-3131) re-add "Install MetaMask" string when no injector is present
|
||||
getName: () => (getIsGenericInjector() ? 'Browser Wallet' : 'MetaMask'),
|
||||
connector: web3Injected,
|
||||
hooks: web3InjectedHooks,
|
||||
type: ConnectionType.INJECTED,
|
||||
shouldDisplay: isMetaMaskWallet || getShouldAdvertiseMetaMask || isGenericInjector,
|
||||
getIcon: (isDarkMode: boolean) =>
|
||||
getIsGenericInjector() ? (isDarkMode ? INJECTED_DARK_ICON : INJECTED_LIGHT_ICON) : METAMASK_ICON,
|
||||
shouldDisplay: () => getIsMetaMaskWallet() || getShouldAdvertiseMetaMask() || getIsGenericInjector(),
|
||||
// If on non-injected, non-mobile browser, prompt user to install Metamask
|
||||
overrideActivate: getShouldAdvertiseMetaMask ? () => window.open('https://metamask.io/', 'inst_metamask') : undefined,
|
||||
overrideActivate: () => {
|
||||
if (getShouldAdvertiseMetaMask()) {
|
||||
window.open('https://metamask.io/', 'inst_metamask')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const darkInjectedConnection: Connection = {
|
||||
...baseInjectedConnection,
|
||||
icon: isGenericInjector ? INJECTED_DARK_ICON_URL : METAMASK_ICON_URL,
|
||||
}
|
||||
|
||||
export const lightInjectedConnection: Connection = {
|
||||
...baseInjectedConnection,
|
||||
icon: isGenericInjector ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL,
|
||||
}
|
||||
|
||||
const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
|
||||
export const gnosisSafeConnection: Connection = {
|
||||
name: 'Gnosis Safe',
|
||||
getName: () => 'Gnosis Safe',
|
||||
connector: web3GnosisSafe,
|
||||
hooks: web3GnosisSafeHooks,
|
||||
type: ConnectionType.GNOSIS_SAFE,
|
||||
icon: GNOSIS_ICON_URL,
|
||||
shouldDisplay: false,
|
||||
getIcon: () => GNOSIS_ICON,
|
||||
shouldDisplay: () => false,
|
||||
}
|
||||
|
||||
const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnectPopup>(
|
||||
(actions) => new WalletConnectPopup({ actions, onError })
|
||||
)
|
||||
export const walletConnectConnection: Connection = {
|
||||
name: 'WalletConnect',
|
||||
getName: () => 'WalletConnect',
|
||||
connector: web3WalletConnect,
|
||||
hooks: web3WalletConnectHooks,
|
||||
type: ConnectionType.WALLET_CONNECT,
|
||||
icon: WALLET_CONNECT_ICON_URL,
|
||||
shouldDisplay: !getIsInjectedMobileBrowser,
|
||||
getIcon: () => WALLET_CONNECT_ICON,
|
||||
shouldDisplay: () => !getIsInjectedMobileBrowser(),
|
||||
}
|
||||
|
||||
const [web3UniwalletConnect, web3UniwalletConnectHooks] = initializeConnector<UniwalletConnect>(
|
||||
(actions) => new UniwalletConnect({ actions, onError })
|
||||
)
|
||||
export const uniwalletConnectConnection: Connection = {
|
||||
name: 'Uniswap Wallet',
|
||||
getName: () => 'Uniswap Wallet',
|
||||
connector: web3UniwalletConnect,
|
||||
hooks: web3UniwalletConnectHooks,
|
||||
type: ConnectionType.UNIWALLET,
|
||||
icon: UNIWALLET_ICON_URL,
|
||||
shouldDisplay: Boolean(!getIsInjectedMobileBrowser && !isNonIOSPhone),
|
||||
getIcon: () => UNIWALLET_ICON,
|
||||
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
@@ -127,31 +126,35 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba
|
||||
options: {
|
||||
url: RPC_URLS[SupportedChainId.MAINNET][0],
|
||||
appName: 'Uniswap',
|
||||
appLogoUrl: UNISWAP_LOGO_URL,
|
||||
appLogoUrl: UNISWAP_LOGO,
|
||||
reloadOnDisconnect: false,
|
||||
},
|
||||
onError,
|
||||
})
|
||||
)
|
||||
|
||||
export const coinbaseWalletConnection: Connection = {
|
||||
name: 'Coinbase Wallet',
|
||||
const coinbaseWalletConnection: Connection = {
|
||||
getName: () => 'Coinbase Wallet',
|
||||
connector: web3CoinbaseWallet,
|
||||
hooks: web3CoinbaseWalletHooks,
|
||||
type: ConnectionType.COINBASE_WALLET,
|
||||
icon: COINBASE_ICON_URL,
|
||||
shouldDisplay: Boolean((isMobile && !getIsInjectedMobileBrowser) || !isMobile || isCoinbaseWalletBrowser),
|
||||
getIcon: () => COINBASE_ICON,
|
||||
shouldDisplay: () =>
|
||||
Boolean((isMobile && !getIsInjectedMobileBrowser()) || !isMobile || getIsCoinbaseWalletBrowser()),
|
||||
// If on a mobile browser that isn't the coinbase wallet browser, deeplink to the coinbase wallet app
|
||||
overrideActivate:
|
||||
isMobile && !getIsInjectedMobileBrowser
|
||||
? () => window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
|
||||
: undefined,
|
||||
overrideActivate: () => {
|
||||
if (isMobile && !getIsInjectedMobileBrowser()) {
|
||||
window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export function getConnections(isDarkMode: boolean) {
|
||||
export function getConnections() {
|
||||
return [
|
||||
uniwalletConnectConnection,
|
||||
isDarkMode ? darkInjectedConnection : lightInjectedConnection,
|
||||
injectedConnection,
|
||||
walletConnectConnection,
|
||||
coinbaseWalletConnection,
|
||||
gnosisSafeConnection,
|
||||
@@ -159,38 +162,29 @@ export function getConnections(isDarkMode: boolean) {
|
||||
]
|
||||
}
|
||||
|
||||
export function useConnections() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return getConnections(isDarkMode)
|
||||
}
|
||||
|
||||
export function useGetConnection() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return useCallback(
|
||||
(c: Connector | ConnectionType) => {
|
||||
if (c instanceof Connector) {
|
||||
const connection = getConnections(isDarkMode).find((connection) => connection.connector === c)
|
||||
if (!connection) {
|
||||
throw Error('unsupported connector')
|
||||
}
|
||||
return connection
|
||||
} else {
|
||||
switch (c) {
|
||||
case ConnectionType.INJECTED:
|
||||
return isDarkMode ? darkInjectedConnection : lightInjectedConnection
|
||||
case ConnectionType.COINBASE_WALLET:
|
||||
return coinbaseWalletConnection
|
||||
case ConnectionType.WALLET_CONNECT:
|
||||
return walletConnectConnection
|
||||
case ConnectionType.UNIWALLET:
|
||||
return uniwalletConnectConnection
|
||||
case ConnectionType.NETWORK:
|
||||
return networkConnection
|
||||
case ConnectionType.GNOSIS_SAFE:
|
||||
return gnosisSafeConnection
|
||||
}
|
||||
return useCallback((c: Connector | ConnectionType) => {
|
||||
if (c instanceof Connector) {
|
||||
const connection = getConnections().find((connection) => connection.connector === c)
|
||||
if (!connection) {
|
||||
throw Error('unsupported connector')
|
||||
}
|
||||
},
|
||||
[isDarkMode]
|
||||
)
|
||||
return connection
|
||||
} else {
|
||||
switch (c) {
|
||||
case ConnectionType.INJECTED:
|
||||
return injectedConnection
|
||||
case ConnectionType.COINBASE_WALLET:
|
||||
return coinbaseWalletConnection
|
||||
case ConnectionType.WALLET_CONNECT:
|
||||
return walletConnectConnection
|
||||
case ConnectionType.UNIWALLET:
|
||||
return uniwalletConnectConnection
|
||||
case ConnectionType.NETWORK:
|
||||
return networkConnection
|
||||
case ConnectionType.GNOSIS_SAFE:
|
||||
return gnosisSafeConnection
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
export const isInjected = Boolean(window.ethereum)
|
||||
export const getIsInjected = () => Boolean(window.ethereum)
|
||||
|
||||
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
|
||||
// This variable should be true only when using the MetaMask extension
|
||||
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
|
||||
type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet'
|
||||
const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet']
|
||||
export const isMetaMaskWallet = Boolean(
|
||||
window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag])
|
||||
)
|
||||
type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet' | 'isLedgerConnect'
|
||||
const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet', 'isLedgerConnect']
|
||||
export const getIsMetaMaskWallet = () =>
|
||||
Boolean(window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag]))
|
||||
|
||||
export const isCoinbaseWallet = Boolean(window.ethereum?.isCoinbaseWallet)
|
||||
export const getIsCoinbaseWallet = () => Boolean(window.ethereum?.isCoinbaseWallet)
|
||||
|
||||
// https://eips.ethereum.org/EIPS/eip-1193#provider-errors
|
||||
export enum ErrorCode {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30
|
||||
export const L2_DEADLINE_FROM_NOW = 60 * 5
|
||||
|
||||
// transaction popup dismisal amounts
|
||||
export const DEFAULT_TXN_DISMISS_MS = 25000
|
||||
export const DEFAULT_TXN_DISMISS_MS = 10000
|
||||
export const L2_TXN_DISMISS_MS = 5000
|
||||
|
||||
export const BIG_INT_ZERO = JSBI.BigInt(0)
|
||||
|
||||
@@ -489,7 +489,7 @@ class ExtendedEther extends Ether {
|
||||
public get wrapped(): Token {
|
||||
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
if (wrapped) return wrapped
|
||||
throw new Error('Unsupported chain ID')
|
||||
throw new Error(`Unsupported chain ID: ${this.chainId}`)
|
||||
}
|
||||
|
||||
private static _cachedExtendedEther: { [chainId: number]: NativeCurrency } = {}
|
||||
|
||||
@@ -5,11 +5,11 @@ export enum FeatureFlag {
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
|
||||
swapWidget = 'swap_widget_replacement_enabled',
|
||||
statsigDummy = 'web_dummy_gate_amplitude_id',
|
||||
nftGraphql = 'nft_graphql_migration',
|
||||
taxService = 'tax_service_banner',
|
||||
mgtm = 'web_mobile_go_to_market_enabled',
|
||||
walletMicrosite = 'walletMicrosite',
|
||||
miniPortfolio = 'miniPortfolio',
|
||||
detailsV2 = 'details_v2',
|
||||
}
|
||||
|
||||