Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b9624bca | ||
|
|
04a0479236 | ||
|
|
55f1e35ffc | ||
|
|
01a3aa1c92 | ||
|
|
7121b4aa1c | ||
|
|
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 |
@@ -5,6 +5,14 @@ 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: {
|
||||
|
||||
8
.github/actions/setup/action.yml
vendored
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
|
||||
|
||||
10
.github/pull_request_template.md
vendored
10
.github/pull_request_template.md
vendored
@@ -14,9 +14,9 @@ _Relevant docs:_
|
||||
<!-- Delete this section if your change does not affect UI. -->
|
||||
## Screen capture
|
||||
|
||||
| Before | After (Desktop) | After (Mobile) |
|
||||
| ----------------------- |----------------------- | ---------------------- |
|
||||
| <!-- Paste "before" --> | <!-- Paste "after" --> | <!-- Paste "after" --> |
|
||||
| Before | After (Desktop) | After (Mobile) |
|
||||
| ------------ |---------------- | -------------- |
|
||||
| paste_before | past_after | paste_after |
|
||||
|
||||
|
||||
## Test plan
|
||||
@@ -39,7 +39,7 @@ _Relevant docs:_
|
||||
|
||||
### Automated testing
|
||||
|
||||
<!-- If N/A, do not check nor delete, but strike through. -->
|
||||
<!-- eg - [ ] <s>Unit test</s> -->
|
||||
<!-- 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
|
||||
|
||||
75
.github/workflows/test.yml
vendored
75
.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:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ schema.graphql
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/cache
|
||||
|
||||
# builds
|
||||
/build
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
/* 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: {
|
||||
@@ -17,9 +23,22 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
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',
|
||||
@@ -29,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,13 +1,16 @@
|
||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
||||
import { defineConfig } from 'cypress'
|
||||
import { setupHardhatEvents } from 'cypress-hardhat'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'yp82ef',
|
||||
videoUploadOnPasses: false,
|
||||
defaultCommandTimeout: 24000, // 2x average block time
|
||||
chromeWebSecurity: false,
|
||||
retries: { runMode: 2 },
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
async setupNodeEvents(on, config) {
|
||||
await setupHardhatEvents(on, config)
|
||||
codeCoverageTask(on, config)
|
||||
return {
|
||||
...config,
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
11
cypress/e2e/position.test.ts
Normal file
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.')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
22
hardhat.config.js
Normal file
22
hardhat.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-env node */
|
||||
require('dotenv').config()
|
||||
|
||||
const mainnetFork = {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: 17023328,
|
||||
httpHeaders: {
|
||||
Origin: 'localhost:3000', // infura allowlists requests by origin
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
networks: {
|
||||
hardhat: {
|
||||
chainId: 1,
|
||||
forking: mainnetFork,
|
||||
accounts: {
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
32
package.json
32
package.json
@@ -17,28 +17,28 @@
|
||||
"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": [
|
||||
"src/components/**/*.ts*",
|
||||
"src/hooks/**/*.ts*",
|
||||
"src/lib/hooks/**/*.ts*",
|
||||
"src/lib/state/**/*.ts*",
|
||||
"src/lib/utils/**/*.ts*",
|
||||
"src/pages/**/*.ts*",
|
||||
"src/state/**/*.ts*",
|
||||
"src/tracing/**/*.ts*",
|
||||
"src/utils/**/*.ts*"
|
||||
"src/**/*.ts*",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/abis/types/**",
|
||||
"!src/constants/**/*.ts",
|
||||
"!src/graphql/**/__generated__/**",
|
||||
"!src/locales/**",
|
||||
"!src/test-utils/**",
|
||||
"!src/types/v3/**"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".snap"
|
||||
@@ -70,6 +70,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",
|
||||
@@ -98,9 +99,11 @@
|
||||
"@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",
|
||||
"cypress-hardhat": "^1.0.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"hardhat": "^2.14.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"ms.macro": "^2.0.0",
|
||||
@@ -138,9 +141,10 @@
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@sentry/react": "^7.45.0",
|
||||
"@sentry/tracing": "^7.45.0",
|
||||
"@sentry/types": "^7.45.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "^1.3.1",
|
||||
"@uniswap/analytics-events": "^2.8.0",
|
||||
"@uniswap/analytics-events": "^2.10.0",
|
||||
"@uniswap/conedison": "^1.4.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 558 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 306 KiB |
19
src/assets/svg/wallet_banner_phone_image.svg
Normal file
19
src/assets/svg/wallet_banner_phone_image.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 990 KiB |
@@ -30,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;
|
||||
@@ -184,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()
|
||||
@@ -197,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 {
|
||||
@@ -287,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>
|
||||
@@ -306,16 +318,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
</>
|
||||
)}
|
||||
</HeaderButton>
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<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?
|
||||
)
|
||||
}
|
||||
|
||||
const APP_STORE_LINK = 'https://apps.apple.com/us/app/uniswap-wallet-defi-nfts/id6443944476'
|
||||
const APP_STORE_LINK = 'https://apps.apple.com/app/apple-store/id6443944476?pt=123625782&ct=In-App-Banners&mt=8'
|
||||
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>
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
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'
|
||||
@@ -98,10 +98,10 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
const lastFetchedAtom = atom<number | undefined>(0)
|
||||
|
||||
export function ActivityTab({ account }: { account: string }) {
|
||||
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
|
||||
|
||||
const localMap = useLocalActivities()
|
||||
const localMap = useLocalActivities(account)
|
||||
|
||||
const { data, loading, refetch } = useTransactionListQuery({
|
||||
variables: { account },
|
||||
@@ -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'
|
||||
@@ -123,7 +122,7 @@ function parseMigrateCreateV3(
|
||||
): Partial<Activity> {
|
||||
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const baseSymbol = baseCurrency?.symbol ?? t`Unknown`
|
||||
const quoteCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
|
||||
const quoteSymbol = quoteCurrency?.symbol ?? t`Unknown`
|
||||
const descriptor = t`${baseSymbol} and ${quoteSymbol}`
|
||||
|
||||
@@ -135,71 +134,69 @@ export function parseLocalActivity(
|
||||
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])
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
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'
|
||||
@@ -46,7 +46,7 @@ export function NFT({
|
||||
mediaShouldBePlaying: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}) {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const navigate = useNavigate()
|
||||
const trace = useTrace()
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -2,13 +2,13 @@ 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 { render } from 'test-utils'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import Pools from '.'
|
||||
import useMultiChainPositions from './useMultiChainPositions'
|
||||
|
||||
jest.mock('./useMultiChainPositions')
|
||||
const mockUseMultiChainPositions = useMultiChainPositions as jest.MockedFunction<typeof useMultiChainPositions>
|
||||
|
||||
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
|
||||
|
||||
@@ -58,7 +58,7 @@ const useMultiChainPositionsReturnValue = {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseMultiChainPositions.mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
})
|
||||
test('Pools should render LP positions', () => {
|
||||
const props = { account: owner }
|
||||
@@ -4,9 +4,9 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
|
||||
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'
|
||||
@@ -33,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 />
|
||||
@@ -93,7 +93,7 @@ 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)
|
||||
@@ -1,7 +1,7 @@
|
||||
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'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { PortfolioLogo } from './PortfolioLogo'
|
||||
|
||||
@@ -12,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
|
||||
|
||||
@@ -26,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)
|
||||
|
||||
@@ -96,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()
|
||||
@@ -11,7 +11,7 @@ import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { ActivityTab } from './Activity/ActivityTab'
|
||||
import { ActivityTab } from './Activity'
|
||||
import NFTs from './NFTs'
|
||||
import Pools from './Pools'
|
||||
import { PortfolioRowWrapper } from './PortfolioRow'
|
||||
@@ -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,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
|
||||
import { ChevronUpIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
@@ -217,11 +218,13 @@ const updateServiceWorkerInBackground = async () => {
|
||||
}
|
||||
|
||||
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
|
||||
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={() => {
|
||||
updateServiceWorkerInBackground()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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'
|
||||
@@ -211,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()}
|
||||
@@ -241,6 +236,12 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.nftGraphql}
|
||||
label="Migrate NFT read endpoints to GQL"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={DetailsV2Variant}
|
||||
value={useDetailsV2Flag()}
|
||||
featureFlag={FeatureFlag.detailsV2}
|
||||
label="Use the new details page for nfts"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getConnections } from 'connection'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import StatusIcon from './StatusIcon'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Unicon } from 'components/Unicon'
|
||||
import { Connection, ConnectionType } from 'connection'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { flexColumnNoWrap } from 'theme/styles'
|
||||
|
||||
import sockImg from '../../assets/svg/socks.svg'
|
||||
@@ -58,9 +59,10 @@ const Socks = () => {
|
||||
}
|
||||
|
||||
const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<MiniIconContainer side={side}>
|
||||
<MiniImg src={connection.getIcon?.()} alt={`${connection.getName()} icon`} />
|
||||
<MiniImg src={connection.getIcon?.(isDarkMode)} alt={`${connection.getName()} icon`} />
|
||||
</MiniIconContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { openDownloadApp } from 'components/AccountDrawer/DownloadButton'
|
||||
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
|
||||
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
|
||||
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
@@ -21,6 +24,7 @@ import { useToggleModal } from 'state/application/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||
|
||||
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'
|
||||
@@ -126,6 +130,8 @@ export const MenuDropdown = () => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
|
||||
const mgtmEnabled = useMgtmEnabled()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="relative" ref={ref}>
|
||||
@@ -140,16 +146,29 @@ 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} color={theme.textSecondary} />
|
||||
<GovernanceIcon width={24} height={24} color={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Vote in governance</Trans>
|
||||
@@ -157,7 +176,7 @@ export const MenuDropdown = () => {
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow href="https://info.uniswap.org/#/">
|
||||
<Icon>
|
||||
<BarChartIcon width={24} height={24} color={theme.textSecondary} />
|
||||
<BarChartIcon width={24} height={24} color={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>View more analytics</Trans>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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'
|
||||
@@ -60,7 +58,6 @@ export const PageTabs = () => {
|
||||
|
||||
const isPoolActive = useIsPoolsPage()
|
||||
const isNftPage = useIsNftPage()
|
||||
const micrositeEnabled = useMGTMMicrositeEnabled()
|
||||
|
||||
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
|
||||
|
||||
@@ -82,14 +79,6 @@ export const PageTabs = () => {
|
||||
<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,38 +1,49 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
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: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
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;
|
||||
}
|
||||
`}
|
||||
min-width: 290px;
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const TransactionPopupContainer = styled.div`
|
||||
${PopupCss}
|
||||
padding: 2px 0px;
|
||||
`
|
||||
|
||||
const FailedSwitchNetworkPopupContainer = styled.div<{ show: boolean }>`
|
||||
${PopupCss}
|
||||
padding: 20px 35px 20px 20px;
|
||||
`
|
||||
|
||||
export default function PopupItem({
|
||||
@@ -45,32 +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()
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
popupContent = <TransactionPopup hash={content.txn.hash} />
|
||||
return (
|
||||
<TransactionPopupContainer show={true}>
|
||||
<StyledClose $padding={16} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
|
||||
<TransactionPopup hash={content.txn.hash} />
|
||||
</TransactionPopupContainer>
|
||||
)
|
||||
} else if ('failedSwitchNetwork' in content) {
|
||||
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
|
||||
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}
|
||||
</Popup>
|
||||
) : null
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 { parseLocalActivity } from 'components/WalletDropdown/MiniPortfolio/Activity/parseLocal'
|
||||
import { PortfolioLogo } from 'components/WalletDropdown/MiniPortfolio/PortfolioLogo'
|
||||
import PortfolioRow from 'components/WalletDropdown/MiniPortfolio/PortfolioRow'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useCombinedActiveList } from 'state/lists/hooks'
|
||||
import { useTransaction } from 'state/transactions/hooks'
|
||||
|
||||
@@ -41,7 +41,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
|
||||
position: fixed;
|
||||
top: ${({ extraPadding }) => (extraPadding ? '72px' : '64px')};
|
||||
right: 1rem;
|
||||
max-width: 376px !important;
|
||||
max-width: 348px !important;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
|
||||
|
||||
@@ -5,19 +5,17 @@ import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { usePool } from 'hooks/usePools'
|
||||
import { PoolState } from 'hooks/usePools'
|
||||
import { render } from 'test-utils'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render } from 'test-utils/render'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('utils/unwrappedToken')
|
||||
const mockUnwrappedToken = unwrappedToken as jest.MockedFunction<typeof unwrappedToken>
|
||||
|
||||
jest.mock('hooks/usePools')
|
||||
const mockUsePool = usePool as jest.MockedFunction<typeof usePool>
|
||||
|
||||
jest.mock('hooks/Tokens')
|
||||
const mockUseToken = useToken as jest.MockedFunction<typeof useToken>
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
jest.mock('components/DoubleLogo', () => () => <div />)
|
||||
@@ -36,16 +34,16 @@ const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
|
||||
|
||||
beforeEach(() => {
|
||||
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
|
||||
mockUseToken.mockImplementation((tokenAddress?: string | null | undefined) => {
|
||||
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')
|
||||
})
|
||||
mockUsePool.mockReturnValue([
|
||||
mocked(usePool).mockReturnValue([
|
||||
PoolState.EXISTS,
|
||||
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
|
||||
])
|
||||
mockUnwrappedToken.mockReturnValue(susToken0)
|
||||
mocked(unwrappedToken).mockReturnValue(susToken0)
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token0 symbol contains a url', () => {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen } from 'test-utils'
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import { ResizingTextArea, TextInput } from './'
|
||||
|
||||
@@ -8,7 +9,7 @@ describe('TextInput', () => {
|
||||
<TextInput
|
||||
className="testing"
|
||||
value="My test input"
|
||||
onUserInput={() => null}
|
||||
onUserInput={noop}
|
||||
placeholder="Test Placeholder"
|
||||
fontSize="12"
|
||||
/>
|
||||
@@ -41,7 +42,7 @@ describe('ResizableTextArea', () => {
|
||||
<ResizingTextArea
|
||||
className="testing"
|
||||
value="My test input"
|
||||
onUserInput={() => null}
|
||||
onUserInput={noop}
|
||||
placeholder="Test Placeholder"
|
||||
fontSize="12"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
@@ -8,6 +9,7 @@ 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 +27,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 +34,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 +56,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 +92,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 +117,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,9 +1,9 @@
|
||||
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 UniwalletModal from 'components/WalletDropdown/UniwalletModal'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { lazy } from 'react'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
// jest unit tests for the parseLocalActivity function
|
||||
|
||||
import { SupportedChainId, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { TokenAddressMap } from 'state/lists/hooks'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import {
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
TransactionDetails,
|
||||
TransactionType,
|
||||
} from 'state/transactions/types'
|
||||
|
||||
import { parseLocalActivity } from './parseLocal'
|
||||
|
||||
const oneUSDCRaw = '1000000'
|
||||
const oneDAIRaw = '1000000000000000000'
|
||||
|
||||
function buildSwapInfo(
|
||||
type: TradeType,
|
||||
inputCurrency: Token,
|
||||
inputCurrencyAmountRaw: string,
|
||||
outputCurrency: Token,
|
||||
outputCurrencyAmountRaw: string
|
||||
): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo {
|
||||
if (type === TradeType.EXACT_INPUT) {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
inputCurrencyAmountRaw,
|
||||
outputCurrencyId: outputCurrency.address,
|
||||
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
|
||||
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
outputCurrencyId: outputCurrency.address,
|
||||
outputCurrencyAmountRaw,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildTokenAddressMap(...tokens: WrappedTokenInfo[]): TokenAddressMap {
|
||||
return {
|
||||
[SupportedChainId.MAINNET]: Object.fromEntries(tokens.map((token) => [token.address, { token }])),
|
||||
}
|
||||
}
|
||||
|
||||
describe('parseLocalActivity', () => {
|
||||
it('returns swap activity fields with known tokens, exact input', () => {
|
||||
const details = {
|
||||
info: buildSwapInfo(TradeType.EXACT_INPUT, USDC_MAINNET, oneUSDCRaw, DAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = buildTokenAddressMap(USDC_MAINNET as WrappedTokenInfo, DAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [USDC_MAINNET, DAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: USDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: DAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
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: buildSwapInfo(TradeType.EXACT_OUTPUT, USDC_MAINNET, oneUSDCRaw, DAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = buildTokenAddressMap(USDC_MAINNET as WrappedTokenInfo, DAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [USDC_MAINNET, DAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: USDC_MAINNET.address,
|
||||
expectedInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
maximumInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: DAI.address,
|
||||
outputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns swap activity fields with unknown tokens', () => {
|
||||
const details = {
|
||||
info: buildSwapInfo(TradeType.EXACT_INPUT, USDC_MAINNET, oneUSDCRaw, DAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = {} as TokenAddressMap
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [undefined, undefined],
|
||||
descriptor: 'Unknown for Unknown',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: USDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: DAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,6 +69,7 @@ type OptionProps = {
|
||||
}
|
||||
export default function Option({ connection, pendingConnectionType, activate }: OptionProps) {
|
||||
const isPending = pendingConnectionType === connection.type
|
||||
const isDarkMode = useIsDarkMode()
|
||||
const content = (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@@ -83,7 +85,7 @@ export default function Option({ connection, pendingConnectionType, activate }:
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<IconWrapper>
|
||||
<img src={connection.getIcon?.()} alt="Icon" />
|
||||
<img src={connection.getIcon?.(isDarkMode)} alt="Icon" />
|
||||
</IconWrapper>
|
||||
<HeaderText>{connection.getName()}</HeaderText>
|
||||
{connection.isNew && <NewBadge />}
|
||||
|
||||
@@ -2,11 +2,11 @@ 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, getConnections, networkConnection } from 'connection'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { ErrorCode } from 'connection/utils'
|
||||
@@ -84,7 +84,7 @@ 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)
|
||||
@@ -156,15 +156,16 @@ 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,
|
||||
|
||||
@@ -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])
|
||||
@@ -223,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
|
||||
@@ -241,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
|
||||
|
||||
111
src/components/swap/SwapBuyFiatButton.test.tsx
Normal file
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
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,5 +1,7 @@
|
||||
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'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
@@ -108,6 +110,9 @@ describe('connection utility/metadata tests', () => {
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -4,13 +4,14 @@ 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_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 { isMobile, isNonIOSPhone } from 'utils/userAgent'
|
||||
@@ -34,8 +35,7 @@ export interface Connection {
|
||||
connector: Connector
|
||||
hooks: Web3ReactHooks
|
||||
type: ConnectionType
|
||||
// TODO(WEB-3130): add darkmode check for icons
|
||||
getIcon?(): string
|
||||
getIcon?(isDarkMode: boolean): string
|
||||
shouldDisplay(): boolean
|
||||
overrideActivate?: () => boolean
|
||||
isNew?: boolean
|
||||
@@ -65,13 +65,15 @@ const getShouldAdvertiseMetaMask = () =>
|
||||
const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() && !getIsCoinbaseWallet()
|
||||
|
||||
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
|
||||
|
||||
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,
|
||||
getIcon: () => (getIsGenericInjector() ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL),
|
||||
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: () => {
|
||||
@@ -82,14 +84,13 @@ const injectedConnection: Connection = {
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
|
||||
export const gnosisSafeConnection: Connection = {
|
||||
getName: () => 'Gnosis Safe',
|
||||
connector: web3GnosisSafe,
|
||||
hooks: web3GnosisSafeHooks,
|
||||
type: ConnectionType.GNOSIS_SAFE,
|
||||
getIcon: () => GNOSIS_ICON_URL,
|
||||
getIcon: () => GNOSIS_ICON,
|
||||
shouldDisplay: () => false,
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ export const walletConnectConnection: Connection = {
|
||||
connector: web3WalletConnect,
|
||||
hooks: web3WalletConnectHooks,
|
||||
type: ConnectionType.WALLET_CONNECT,
|
||||
getIcon: () => WALLET_CONNECT_ICON_URL,
|
||||
getIcon: () => WALLET_CONNECT_ICON,
|
||||
shouldDisplay: () => !getIsInjectedMobileBrowser(),
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ export const uniwalletConnectConnection: Connection = {
|
||||
connector: web3UniwalletConnect,
|
||||
hooks: web3UniwalletConnectHooks,
|
||||
type: ConnectionType.UNIWALLET,
|
||||
getIcon: () => UNIWALLET_ICON_URL,
|
||||
getIcon: () => UNIWALLET_ICON,
|
||||
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
|
||||
isNew: true,
|
||||
}
|
||||
@@ -125,7 +126,7 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba
|
||||
options: {
|
||||
url: RPC_URLS[SupportedChainId.MAINNET][0],
|
||||
appName: 'Uniswap',
|
||||
appLogoUrl: UNISWAP_LOGO_URL,
|
||||
appLogoUrl: UNISWAP_LOGO,
|
||||
reloadOnDisconnect: false,
|
||||
},
|
||||
onError,
|
||||
@@ -137,7 +138,7 @@ const coinbaseWalletConnection: Connection = {
|
||||
connector: web3CoinbaseWallet,
|
||||
hooks: web3CoinbaseWalletHooks,
|
||||
type: ConnectionType.COINBASE_WALLET,
|
||||
getIcon: () => COINBASE_ICON_URL,
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,10 +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',
|
||||
mgtm = 'web_mobile_go_to_market_enabled',
|
||||
walletMicrosite = 'walletMicrosite',
|
||||
miniPortfolio = 'miniPortfolio',
|
||||
detailsV2 = 'details_v2',
|
||||
}
|
||||
|
||||
9
src/featureFlags/flags/fiatOnRampButton.ts
Normal file
9
src/featureFlags/flags/fiatOnRampButton.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
function useFiatOnRampButtonFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.fiatOnRampButtonOnSwap)
|
||||
}
|
||||
|
||||
export function useFiatOnRampButtonEnabled(): boolean {
|
||||
return useFiatOnRampButtonFlag() === BaseVariant.Enabled
|
||||
}
|
||||
@@ -10,9 +10,4 @@ export function useMgtmEnabled(): boolean {
|
||||
return useMgtmFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export function useMGTMMicrositeEnabled() {
|
||||
const mgtmEnabled = useMgtmEnabled()
|
||||
return useBaseFlag(FeatureFlag.walletMicrosite) === BaseVariant.Enabled && mgtmEnabled
|
||||
}
|
||||
|
||||
export { BaseVariant as MgtmVariant }
|
||||
|
||||
11
src/featureFlags/flags/nftDetails.ts
Normal file
11
src/featureFlags/flags/nftDetails.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useDetailsV2Flag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.detailsV2)
|
||||
}
|
||||
|
||||
export function useDetailsV2Enabled(): boolean {
|
||||
return useDetailsV2Flag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as DetailsV2Variant }
|
||||
@@ -39,33 +39,18 @@ gql`
|
||||
node {
|
||||
id
|
||||
name
|
||||
ownerAddress
|
||||
image {
|
||||
url
|
||||
}
|
||||
smallImage {
|
||||
url
|
||||
}
|
||||
originalImage {
|
||||
url
|
||||
}
|
||||
tokenId
|
||||
description
|
||||
animationUrl
|
||||
suspiciousFlag
|
||||
collection {
|
||||
name
|
||||
isVerified
|
||||
image {
|
||||
url
|
||||
}
|
||||
creator {
|
||||
address
|
||||
profileImage {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
}
|
||||
nftContracts {
|
||||
address
|
||||
standard
|
||||
@@ -98,11 +83,8 @@ gql`
|
||||
}
|
||||
}
|
||||
rarities {
|
||||
provider
|
||||
rank
|
||||
score
|
||||
}
|
||||
metadataUrl
|
||||
}
|
||||
cursor
|
||||
}
|
||||
|
||||
@@ -122,7 +122,8 @@ export function useNftBalance(
|
||||
first?: number,
|
||||
after?: string,
|
||||
last?: number,
|
||||
before?: string
|
||||
before?: string,
|
||||
skip = false
|
||||
) {
|
||||
const { data, loading, fetchMore } = useNftBalanceQuery({
|
||||
variables: {
|
||||
@@ -140,6 +141,7 @@ export function useNftBalance(
|
||||
last,
|
||||
before,
|
||||
},
|
||||
skip,
|
||||
})
|
||||
|
||||
const hasNext = data?.nftBalances?.pageInfo?.hasNextPage
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { useClientSideRouter } from 'state/user/hooks'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
|
||||
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
|
||||
import useAutoRouterSupported from './useAutoRouterSupported'
|
||||
@@ -29,53 +30,45 @@ jest.mock('./useIsWindowVisible')
|
||||
jest.mock('state/routing/useRoutingAPITrade')
|
||||
jest.mock('state/user/hooks')
|
||||
|
||||
const mockUseDebounce = useDebounce as jest.MockedFunction<typeof useDebounce>
|
||||
const mockUseAutoRouterSupported = useAutoRouterSupported as jest.MockedFunction<typeof useAutoRouterSupported>
|
||||
const mockUseIsWindowVisible = useIsWindowVisible as jest.MockedFunction<typeof useIsWindowVisible>
|
||||
|
||||
const mockUseRoutingAPITrade = useRoutingAPITrade as jest.MockedFunction<typeof useRoutingAPITrade>
|
||||
const mockUseClientSideRouter = useClientSideRouter as jest.MockedFunction<typeof useClientSideRouter>
|
||||
const mockUseClientSideV3Trade = useClientSideV3Trade as jest.MockedFunction<typeof useClientSideV3Trade>
|
||||
|
||||
// helpers to set mock expectations
|
||||
const expectRouterMock = (state: TradeState) => {
|
||||
mockUseRoutingAPITrade.mockReturnValue({ state, trade: undefined })
|
||||
mocked(useRoutingAPITrade).mockReturnValue({ state, trade: undefined })
|
||||
}
|
||||
|
||||
const expectClientSideMock = (state: TradeState) => {
|
||||
mockUseClientSideV3Trade.mockReturnValue({ state, trade: undefined })
|
||||
mocked(useClientSideV3Trade).mockReturnValue({ state, trade: undefined })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// ignore debounced value
|
||||
mockUseDebounce.mockImplementation((value) => value)
|
||||
mocked(useDebounce).mockImplementation((value) => value)
|
||||
|
||||
mockUseIsWindowVisible.mockReturnValue(true)
|
||||
mockUseAutoRouterSupported.mockReturnValue(true)
|
||||
mockUseClientSideRouter.mockReturnValue([true, () => undefined])
|
||||
mocked(useIsWindowVisible).mockReturnValue(true)
|
||||
mocked(useAutoRouterSupported).mockReturnValue(true)
|
||||
mocked(useClientSideRouter).mockReturnValue([true, () => undefined])
|
||||
})
|
||||
|
||||
describe('#useBestV3Trade ExactIn', () => {
|
||||
it('does not compute routing api trade when routing API is not supported', async () => {
|
||||
mockUseAutoRouterSupported.mockReturnValue(false)
|
||||
mocked(useAutoRouterSupported).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute routing api trade when window is not focused', async () => {
|
||||
mockUseIsWindowVisible.mockReturnValue(false)
|
||||
mocked(useIsWindowVisible).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -85,7 +78,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -94,7 +87,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -103,7 +96,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
@@ -115,7 +108,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
})
|
||||
|
||||
it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => {
|
||||
@@ -124,7 +117,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
})
|
||||
@@ -132,30 +125,30 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
describe('#useBestV3Trade ExactOut', () => {
|
||||
it('does not compute routing api trade when routing API is not supported', () => {
|
||||
mockUseAutoRouterSupported.mockReturnValue(false)
|
||||
mocked(useAutoRouterSupported).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(
|
||||
TradeType.EXACT_OUTPUT,
|
||||
undefined,
|
||||
USDC_MAINNET,
|
||||
RouterPreference.CLIENT
|
||||
)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute routing api trade when window is not focused', () => {
|
||||
mockUseIsWindowVisible.mockReturnValue(false)
|
||||
mocked(useIsWindowVisible).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(
|
||||
TradeType.EXACT_OUTPUT,
|
||||
undefined,
|
||||
USDC_MAINNET,
|
||||
@@ -169,7 +162,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -178,7 +171,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -187,7 +180,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
@@ -199,7 +192,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
})
|
||||
|
||||
it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => {
|
||||
@@ -208,7 +201,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { renderHook } from 'test-utils'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
import { lightTheme } from 'theme/colors'
|
||||
|
||||
import { useTokenWarningColor, useTokenWarningTextColor } from './useTokenWarningColor'
|
||||
|
||||
@@ -18,7 +18,9 @@ describe('fetchTokenList', () => {
|
||||
fetch.mockOnceIf(url, () => {
|
||||
throw new Error()
|
||||
})
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`failed to fetch list: ${url}`)
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -33,9 +35,63 @@ describe('fetchTokenList', () => {
|
||||
expect(resolver).toHaveBeenCalledWith(url)
|
||||
})
|
||||
|
||||
it('throws an error when the ENS resolver throws', async () => {
|
||||
const url = 'example.eth'
|
||||
const error = new Error('ENS resolver error')
|
||||
resolver.mockRejectedValue(error)
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`failed to resolve ENS name: ${url}`)
|
||||
expect(resolver).toHaveBeenCalledWith(url)
|
||||
})
|
||||
|
||||
it('fetches and validates a list from an ENS address', async () => {
|
||||
jest.mock('../../utils/contenthashToUri', () =>
|
||||
jest.fn().mockImplementation(() => 'ipfs://QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1')
|
||||
)
|
||||
const url = 'example.eth'
|
||||
const contenthash = '0xe3010170122013e051d1cfff20606de36845d4fe28deb9861a319a5bc8596fa4e610e8803918'
|
||||
const translatedUri = 'https://cloudflare-ipfs.com/ipfs/QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1/'
|
||||
resolver.mockResolvedValue(contenthash)
|
||||
fetch.mockOnceIf(translatedUri, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
})
|
||||
|
||||
it('throws for an unrecognized list URL protocol', async () => {
|
||||
const url = 'unknown://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve(''))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`Unrecognized list URL protocol.`)
|
||||
})
|
||||
|
||||
it('logs a debug statement if the response is not successful', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve({ status: 404 }))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fetches and validates the default token list', async () => {
|
||||
fetch.mockOnceIf(DEFAULT_TOKEN_LIST, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(DEFAULT_TOKEN_LIST, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('throws for a list with invalid json response', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve('invalid json'))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses cached value the second time', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,11 @@ export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.o
|
||||
|
||||
const listCache = new Map<string, TokenList>()
|
||||
|
||||
/** Fetches and validates a token list. */
|
||||
/**
|
||||
* Fetches and validates a token list.
|
||||
* For a given token list URL, we try to fetch the list from all the possible HTTP URLs.
|
||||
* For example, IPFS URLs can be fetched through multiple gateways.
|
||||
*/
|
||||
export default async function fetchTokenList(
|
||||
listUrl: string,
|
||||
resolveENSContentHash: (ensName: string) => Promise<string>,
|
||||
@@ -43,31 +47,38 @@ export default async function fetchTokenList(
|
||||
urls = uriToHttp(listUrl)
|
||||
}
|
||||
|
||||
if (urls.length === 0) {
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
}
|
||||
|
||||
// Try each of the derived URLs until one succeeds.
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i]
|
||||
const isLast = i === urls.length - 1
|
||||
let response
|
||||
try {
|
||||
response = await fetch(url, { credentials: 'omit' })
|
||||
} catch (error) {
|
||||
const message = `failed to fetch list: ${listUrl}`
|
||||
console.debug(message, error)
|
||||
if (isLast) throw new Error(message)
|
||||
console.debug(`failed to fetch list: ${listUrl} (${url})`, error)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `failed to fetch list: ${listUrl}`
|
||||
console.debug(message, response.statusText)
|
||||
if (isLast) throw new Error(message)
|
||||
console.debug(`failed to fetch list ${listUrl} (${url})`, response.statusText)
|
||||
continue
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
const list = skipValidation ? json : await validateTokenList(json)
|
||||
listCache?.set(listUrl, list)
|
||||
return list
|
||||
try {
|
||||
// The content of the result is sometimes invalid even with a 200 status code.
|
||||
// A response can be invalid if it's not a valid JSON or if it doesn't match the TokenList schema.
|
||||
const json = await response.json()
|
||||
const list = skipValidation ? json : await validateTokenList(json)
|
||||
listCache?.set(listUrl, list)
|
||||
return list
|
||||
} catch (error) {
|
||||
console.debug(`failed to parse and validate list response: ${listUrl} (${url})`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
throw new Error(`No valid token list found at any URLs derived from ${listUrl}.`)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { i18n } from '@lingui/core'
|
||||
import { I18nProvider } from '@lingui/react'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
|
||||
import {
|
||||
af,
|
||||
@@ -83,9 +82,8 @@ export async function dynamicActivate(locale: SupportedLocale) {
|
||||
const catalog = await import(`locales/${locale}.js`)
|
||||
// Bundlers will either export it as default or as a named export named default.
|
||||
i18n.load(locale, catalog.messages || catalog.default.messages)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Sentry.captureException(new Error(`Unable to load locale (${locale})`))
|
||||
} catch (error: unknown) {
|
||||
console.error(new Error(`Unable to load locale (${locale}): ${error}`))
|
||||
}
|
||||
i18n.activate(locale)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
@@ -12,7 +13,6 @@ import Row from 'components/Row'
|
||||
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
@@ -287,7 +287,7 @@ const PENDING_BAG_STATUSES = [
|
||||
]
|
||||
|
||||
export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) => {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const theme = useTheme()
|
||||
const { account, chainId, connector } = useWeb3React()
|
||||
const connected = Boolean(account && chainId)
|
||||
@@ -334,7 +334,8 @@ export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) =
|
||||
const { allowance, isAllowancePending, isApprovalLoading, updateAllowance } = usePermit2Approval(
|
||||
trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined,
|
||||
maximumAmountIn,
|
||||
shouldUsePayWithAnyToken
|
||||
shouldUsePayWithAnyToken,
|
||||
true
|
||||
)
|
||||
usePayWithAnyTokenSwap(trade, allowance, allowedSlippage)
|
||||
const priceImpact = usePriceImpact(trade)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import Bag from './Bag'
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
|
||||
isActive: true,
|
||||
}),
|
||||
...web3React,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Markets } from 'nft/types'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { MarketplaceContainer } from './icons'
|
||||
|
||||
|
||||
@@ -313,18 +313,16 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
'no-cache'
|
||||
)
|
||||
|
||||
// TODO simplify typecasting when removing graphql flag
|
||||
const lastSalePrice = isNftGraphqlEnabled ? gqlPriceData?.[0]?.price : priceData?.events[0]?.price
|
||||
const formattedEthprice = isNftGraphqlEnabled
|
||||
? formatEth(parseFloat(lastSalePrice ?? ''))
|
||||
: formatEthPrice(lastSalePrice) || 0
|
||||
const formattedPrice = isNftGraphqlEnabled
|
||||
? formattedEthprice
|
||||
: lastSalePrice
|
||||
? putCommas(parseFloat(formattedEthprice.toString())).toString()
|
||||
: null
|
||||
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
|
||||
let formattedPrice
|
||||
if (isNftGraphqlEnabled) {
|
||||
const weiPrice = gqlPriceData?.[0]?.price
|
||||
formattedPrice = weiPrice ? formatEth(parseFloat(weiPrice)) : undefined
|
||||
} else {
|
||||
const ethPrice = priceData?.events[0]?.price
|
||||
formattedPrice = ethPrice ? putCommas(formatEthPrice(priceData?.events[0]?.price)).toString() : undefined
|
||||
}
|
||||
|
||||
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
|
||||
const Filter = useCallback(
|
||||
function ActivityFilter({ eventType }: { eventType: ActivityEventType }) {
|
||||
const isActive = activeFilters[eventType]
|
||||
|
||||
14
src/nft/components/details/NftDetails.tsx
Normal file
14
src/nft/components/details/NftDetails.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
|
||||
|
||||
interface NftDetailsProps {
|
||||
asset: GenieAsset
|
||||
collection: CollectionInfoForAsset
|
||||
}
|
||||
|
||||
export const NftDetails = ({ asset, collection }: NftDetailsProps) => {
|
||||
return (
|
||||
<div>
|
||||
Details page for {asset.name} from {collection.collectionName}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -114,8 +114,8 @@ export const EthCell = ({
|
||||
denomination: Denomination
|
||||
usdPrice?: number
|
||||
}) => {
|
||||
const denominatedValue = getDenominatedValue(denomination, true, value, usdPrice)
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const denominatedValue = getDenominatedValue(denomination, !isNftGraphqlEnabled, value, usdPrice)
|
||||
const formattedValue = denominatedValue
|
||||
? denomination === Denomination.ETH
|
||||
? isNftGraphqlEnabled
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { CollectionTableColumn, TimePeriod } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
import { CellProps, Column, Row } from 'react-table'
|
||||
@@ -26,14 +27,19 @@ const compareFloats = (a?: number, b?: number): 1 | -1 => {
|
||||
}
|
||||
|
||||
const CollectionTable = ({ data, timePeriod }: { data: CollectionTableColumn[]; timePeriod: TimePeriod }) => {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const floorSort = useMemo(() => {
|
||||
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
|
||||
const aFloor = BigNumber.from(rowA.original.floor.value ?? 0)
|
||||
const bFloor = BigNumber.from(rowB.original.floor.value ?? 0)
|
||||
if (isNftGraphqlEnabled) {
|
||||
return compareFloats(rowA.original.floor.value, rowB.original.floor.value)
|
||||
} else {
|
||||
const aFloor = BigNumber.from(rowA.original.floor.value ?? 0)
|
||||
const bFloor = BigNumber.from(rowB.original.floor.value ?? 0)
|
||||
|
||||
return aFloor.gte(bFloor) ? 1 : -1
|
||||
return aFloor.gte(bFloor) ? 1 : -1
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [isNftGraphqlEnabled])
|
||||
|
||||
const floorChangeSort = useMemo(() => {
|
||||
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import { Box } from '../Box'
|
||||
import * as styles from './Overlay.css'
|
||||
|
||||
@@ -10,6 +12,6 @@ export const stopPropagation = (event: React.SyntheticEvent<HTMLElement>) => {
|
||||
event.nativeEvent.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
export const Overlay = ({ onClick = () => null }: OverlayProps) => {
|
||||
export const Overlay = ({ onClick = noop }: OverlayProps) => {
|
||||
return <Box className={styles.overlay} onClick={onClick} />
|
||||
}
|
||||
|
||||
17
src/nft/components/profile/view/EmptyWalletContent.test.tsx
Normal file
17
src/nft/components/profile/view/EmptyWalletContent.test.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { EmptyWalletModule } from './EmptyWalletContent'
|
||||
|
||||
describe('EmptyWalletContent.tsx', () => {
|
||||
it('matches base snapshot', () => {
|
||||
const { asFragment } = render(
|
||||
<div>
|
||||
<EmptyWalletModule type="nft" />
|
||||
<EmptyWalletModule type="token" />
|
||||
<EmptyWalletModule type="activity" />
|
||||
<EmptyWalletModule type="pool" />
|
||||
</div>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { EmptyActivityIcon, EmptyNftsIcon, EmptyTokensIcon } from './icons'
|
||||
import { EmptyActivityIcon, EmptyNftsIcon, EmptyPoolsIcon, EmptyTokensIcon } from './icons'
|
||||
|
||||
const EmptyWalletContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -74,11 +74,11 @@ const EMPTY_WALLET_CONTENT: { [key in EmptyWalletContentType]: EmptyWalletConten
|
||||
icon: <EmptyActivityIcon />,
|
||||
},
|
||||
pool: {
|
||||
title: <Trans>No positions yet</Trans>,
|
||||
title: <Trans>No pools yet</Trans>,
|
||||
subtitle: <Trans>Open a new position or create a pool to get started.</Trans>,
|
||||
actionText: <Trans>+ New position</Trans>,
|
||||
urlPath: '/pool',
|
||||
icon: <EmptyTokensIcon />,
|
||||
icon: <EmptyPoolsIcon />,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import InfiniteLoader from 'react-window-infinite-loader'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { TRANSITION_DURATIONS } from 'theme/styles'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import { WALLET_COLLECTIONS_PAGINATION_LIMIT } from './ProfilePage'
|
||||
import * as styles from './ProfilePage.css'
|
||||
@@ -190,7 +191,7 @@ const CollectionSelect = ({
|
||||
|
||||
// Only load 1 page of items at a time.
|
||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
||||
const loadMoreItems = isFetchingNextPage ? () => null : fetchNextPage
|
||||
const loadMoreItems = isFetchingNextPage ? noop : fetchNextPage
|
||||
|
||||
// Every row is loaded except for our loading indicator row.
|
||||
const isItemLoaded = useCallback(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user