Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cc6879638 | ||
|
|
a5534803a1 | ||
|
|
a06f885724 | ||
|
|
de7cfc93e6 | ||
|
|
aa6c469042 | ||
|
|
dc478ce37e | ||
|
|
3f3f16c366 | ||
|
|
8e84a53e57 | ||
|
|
e88a50d309 | ||
|
|
102d99afa2 | ||
|
|
73e4202497 | ||
|
|
6e228185b4 | ||
|
|
bb92a9ee36 | ||
|
|
4936ec5cfc | ||
|
|
bb1db9048a | ||
|
|
20dbb2a9f7 | ||
|
|
fd4430fe69 | ||
|
|
167cc695c9 | ||
|
|
e175bff7f4 | ||
|
|
4442933eac | ||
|
|
8447b30327 | ||
|
|
22ce55ec46 | ||
|
|
b25da9de2d | ||
|
|
ae1fb4367f | ||
|
|
1410edda32 | ||
|
|
3bc7f015ee | ||
|
|
802714377c | ||
|
|
352daf959e | ||
|
|
92c21c2811 | ||
|
|
e70723aaf3 | ||
|
|
1802f50163 | ||
|
|
2aa1b18d14 | ||
|
|
a286e5b114 | ||
|
|
62361647e0 | ||
|
|
deee278439 | ||
|
|
6340deb201 | ||
|
|
28b154ebe8 | ||
|
|
3bde2165f4 | ||
|
|
78c8fd2359 | ||
|
|
c378752910 | ||
|
|
0bf7b92013 | ||
|
|
283479f76e | ||
|
|
d3c30e2f6b | ||
|
|
32d226f78e | ||
|
|
96744505c0 | ||
|
|
97236033d4 | ||
|
|
86e62dc4b9 | ||
|
|
e584a5fa36 | ||
|
|
332ef6e6c8 | ||
|
|
8cbd111e65 | ||
|
|
55ffcbd465 | ||
|
|
404775e86d | ||
|
|
0ae9fe28a2 | ||
|
|
89c0caae43 | ||
|
|
c8086e3c76 | ||
|
|
1c2842e5a0 | ||
|
|
a2c6d3f475 | ||
|
|
841ea7f8a1 | ||
|
|
804692b114 | ||
|
|
6282298d13 | ||
|
|
7a5b855097 | ||
|
|
c9908748cf | ||
|
|
79b77deee1 | ||
|
|
a554af6670 | ||
|
|
1843f214b1 | ||
|
|
3e0788092e | ||
|
|
d14c49df0d | ||
|
|
c098ad1ffe | ||
|
|
48114ef51d | ||
|
|
cb7132ee17 | ||
|
|
0fa4859a09 | ||
|
|
f8bb5046f0 | ||
|
|
7d1589d1df | ||
|
|
26b603cc2e | ||
|
|
ece68a0ec7 | ||
|
|
fd212477ce | ||
|
|
a16d2387cc | ||
|
|
cae56ec385 | ||
|
|
d16b3473e0 | ||
|
|
f66f249dba | ||
|
|
08afd888d0 | ||
|
|
b427be2673 | ||
|
|
f753a5e325 | ||
|
|
46d9d8e3df | ||
|
|
680d3a3f26 | ||
|
|
e4c625ee71 |
7
.env
@@ -1,7 +1,12 @@
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||
REACT_APP_AWS_API_REGION="us-east-2"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
||||
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
||||
REACT_APP_SENTRY_ENABLED=false
|
||||
ESLINT_NO_DEV_ERRORS=true
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink?platform=web"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||
REACT_APP_SENTRY_ENABLED=true
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
*.config.ts
|
||||
*.d.ts
|
||||
/src/graphql/data/__generated__/types-and-hooks.ts
|
||||
/src/graphql/thegraph/__generated__/types-and-hooks.ts
|
||||
/src/schema/schema.graphql
|
||||
|
||||
7
.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/* eslint-env node */
|
||||
|
||||
require('@uniswap/eslint-config/load')
|
||||
|
||||
module.exports = {
|
||||
extends: '@uniswap/eslint-config/react',
|
||||
}
|
||||
109
.eslintrc.json
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
// Allows for the parsing of JSX
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
},
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"src/types/v3",
|
||||
"src/abis/types",
|
||||
"src/locales/**/*.js",
|
||||
"src/locales/**/en-US.po",
|
||||
"node_modules",
|
||||
"coverage",
|
||||
"build",
|
||||
"dist",
|
||||
".DS_Store",
|
||||
".env.local",
|
||||
".env.development.local",
|
||||
".env.test.local",
|
||||
".env.production.local",
|
||||
".idea/",
|
||||
".vscode/",
|
||||
"package-lock.json",
|
||||
"yarn.lock"
|
||||
],
|
||||
"extends": [
|
||||
"react-app",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"plugins": ["import", "simple-import-sort", "unused-imports"],
|
||||
"rules": {
|
||||
"import/no-unused-modules": [2, { "unusedExports": true }],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"prettier/prettier": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-curly-brace-presence": ["error", { "props": "never", "children": "never" }],
|
||||
"object-shorthand": ["error", "always"],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "ethers",
|
||||
"message": "Please import from '@ethersproject/module' directly to support tree-shaking."
|
||||
},
|
||||
{
|
||||
"name": "styled-components",
|
||||
"message": "Please import from styled-components/macro."
|
||||
},
|
||||
{
|
||||
"name": "@lingui/macro",
|
||||
"importNames": ["t"],
|
||||
"message": "Please use <Trans> instead of t."
|
||||
}
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["**/dist"],
|
||||
"message": "Do not import from dist/ - this is an implementation detail, and breaks tree-shaking."
|
||||
},
|
||||
{
|
||||
"group": ["!styled-components/macro"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "@ethersproject/providers",
|
||||
"message": "Please only use Providers instantiated in constants/providers to improve traceability.",
|
||||
"allowTypeImports": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
5
.github/workflows/release.yaml
vendored
@@ -112,7 +112,8 @@ jobs:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
||||
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
@@ -120,4 +121,4 @@ jobs:
|
||||
with:
|
||||
environment: production
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
|
||||
1
.gitignore
vendored
@@ -9,7 +9,6 @@
|
||||
/src/locales/**/pseudo.po
|
||||
|
||||
# generated graphql types
|
||||
__generated__/
|
||||
schema.graphql
|
||||
|
||||
# dependencies
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/src/schema/schema.graphql
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
7
.vscode/settings.json
vendored
@@ -5,15 +5,12 @@
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.tabCompletion": "on",
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.inlineSuggest.enabled": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"eslint.enable": true,
|
||||
"eslint.debug": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
"eslint.debug": true
|
||||
}
|
||||
|
||||
25
apollo-codegen.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-env node */
|
||||
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli'
|
||||
|
||||
// Generates TS objects from the schemas returned by graphql queries
|
||||
// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
schema: './src/graphql/data/schema.graphql',
|
||||
documents: ['./src/graphql/data/**', '!./src/graphql/data/__generated__/**', '!**/thegraph/**'],
|
||||
generates: {
|
||||
'src/graphql/data/__generated__/types-and-hooks.ts': {
|
||||
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
|
||||
config: {
|
||||
withHooks: true,
|
||||
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
|
||||
maybeValue: 'T',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// This is used in package.json when generating apollo schemas however the linter stills flags this as unused
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export default config
|
||||
25
apollo-codegen_thegraph.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-env node */
|
||||
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli'
|
||||
|
||||
// Generates TS objects from the schemas returned by graphql queries
|
||||
// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
schema: './src/graphql/thegraph/schema.graphql',
|
||||
documents: ['!./src/graphql/data/**', '!./src/graphql/thegraph/__generated__/**', './src/graphql/thegraph/**'],
|
||||
generates: {
|
||||
'src/graphql/thegraph/__generated__/types-and-hooks.ts': {
|
||||
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
|
||||
config: {
|
||||
withHooks: true,
|
||||
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
|
||||
maybeValue: 'T',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// This is used in package.json when generating apollo schemas however the linter stills flags this as unused
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export default config
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
it('loads swap page', () => {
|
||||
cy.get('#swap-page')
|
||||
it('shows landing page when no selectedWallet', () => {
|
||||
cy.visit('/', { noWallet: true })
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
cy.screenshot()
|
||||
})
|
||||
|
||||
it('redirects to swap page when selectedWallet is INJECTED', () => {
|
||||
cy.visit('/', { selectedWallet: 'INJECTED' })
|
||||
cy.get('#swap-page')
|
||||
cy.url().should('include', '/swap')
|
||||
cy.screenshot()
|
||||
})
|
||||
|
||||
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => {
|
||||
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' })
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
})
|
||||
|
||||
it('shows landing page when the unicorn icon in nav is selected', () => {
|
||||
cy.get(getTestSelector('uniswap-logo')).click()
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
})
|
||||
|
||||
it('allows navigation to pool', () => {
|
||||
cy.get('#pool-nav-link').click()
|
||||
cy.url().should('include', '/pool')
|
||||
|
||||
@@ -3,8 +3,9 @@ import { getTestSelector } from '../utils'
|
||||
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
|
||||
})
|
||||
|
||||
it('should load nft leaderboard', () => {
|
||||
@@ -22,13 +23,8 @@ describe('Testing nfts', () => {
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('exist')
|
||||
})
|
||||
|
||||
it('should be able to open bag and open sweep', () => {
|
||||
cy.get(getTestSelector('nft-sweep-button')).first().click()
|
||||
cy.get(getTestSelector('nft-empty-bag')).should('exist')
|
||||
cy.get(getTestSelector('nft-sweep-slider')).should('exist')
|
||||
})
|
||||
|
||||
it('should be able to navigate to activity', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-activity')).first().click()
|
||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||
})
|
||||
@@ -45,20 +41,11 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/8565`)
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||
cy.get(getTestSelector('nft-details-toggle-bag')).eq(1).click()
|
||||
cy.get(getTestSelector('nft-bag')).should('exist')
|
||||
})
|
||||
|
||||
it('should go view my nfts', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
||||
cy.get(getTestSelector('nft-explore-nfts-button')).should('exist')
|
||||
cy.get(getTestSelector('nft-no-nfts-selected')).should('exist')
|
||||
cy.get(getTestSelector('nft-bag-close-icon')).click()
|
||||
cy.get(getTestSelector('nft-explore-nfts-button')).click()
|
||||
cy.get(getTestSelector('nft-welcome-modal')).should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Pool', () => {
|
||||
beforeEach(() => cy.visit('/pool'))
|
||||
|
||||
it('add liquidity links to /add/ETH', () => {
|
||||
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.url().should('contain', '/add/ETH')
|
||||
})
|
||||
|
||||
@@ -10,6 +10,27 @@ describe('Testing tokens on uniswap page', () => {
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.gte', 25)
|
||||
})
|
||||
|
||||
it('should keep the same configuration when reloaded: ETH global, OP local', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
|
||||
cy.reload()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
|
||||
})
|
||||
|
||||
it('should have the correct network configuration when reloaded: OP global, Polygon local', () => {
|
||||
cy.get(getTestSelector('chain-selector')).last().click()
|
||||
cy.get(getTestSelector('chain-selector-option-optimism')).click()
|
||||
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-polygon')).click()
|
||||
cy.reload()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Polygon')
|
||||
// With no wallet connected, reloading the page resets the global network.
|
||||
cy.get(getTestSelector('chain-selector')).last().should('contain', 'Ethereum')
|
||||
})
|
||||
|
||||
it('should load go to ethereum token and return to token list page', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelector('token-table-row-Ether')).click()
|
||||
@@ -17,4 +38,18 @@ describe('Testing tokens on uniswap page', () => {
|
||||
cy.get(getTestSelector('token-details-return-button')).click()
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.gte', 25)
|
||||
})
|
||||
|
||||
it('should go to native token on ethereum and render description', () => {
|
||||
cy.visit('/tokens/ethereum/NATIVE')
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('Ethereum is a smart contract platform that enables developers').should('exist')
|
||||
cy.contains('Etherscan').should('exist')
|
||||
})
|
||||
|
||||
it('should go to native token on polygon and render description and links', () => {
|
||||
cy.visit('/tokens/polygon/NATIVE')
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('Wrapped Matic on Polygon').should('exist')
|
||||
cy.contains('Block Explorer').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,8 @@ describe(
|
||||
},
|
||||
() => {
|
||||
it('loads swap page', () => {
|
||||
// We *must* wait in order to space out the retry attempts.
|
||||
// TODO: We *must* wait in order to space out the retry attempts. Find a better way to do this.
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(ONE_MINUTE)
|
||||
.visit('/', {
|
||||
retryOnStatusCodeFailure: true,
|
||||
|
||||
@@ -20,6 +20,8 @@ declare global {
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
selectedWallet?: string
|
||||
noWallet?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +40,12 @@ Cypress.Commands.overwrite(
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
win.localStorage.clear()
|
||||
win.localStorage.setItem('redux_localstorage_simple_user', '{"selectedWallet":"INJECTED"}')
|
||||
|
||||
const userState = {
|
||||
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined,
|
||||
fiatOnrampDismissed: true,
|
||||
}
|
||||
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState))
|
||||
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce(
|
||||
@@ -78,8 +85,7 @@ beforeEach(() => {
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.on('uncaught:exception', (_err, _runnable) => {
|
||||
// returning false here prevents Cypress from
|
||||
// failing the test
|
||||
Cypress.on('uncaught:exception', () => {
|
||||
// returning false here prevents Cypress from failing the test
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Utility to match GraphQL mutation based on the query name
|
||||
export const hasQuery = (req: any, queryName: string) => {
|
||||
const { body } = req
|
||||
return body.hasOwnProperty('query') && body.query.includes(queryName)
|
||||
return Object.prototype.hasOwnProperty.call(body, 'query') && body.query.includes(queryName)
|
||||
}
|
||||
|
||||
// Alias query if queryName matches
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* eslint-env node */
|
||||
|
||||
require('dotenv').config({ path: '.env.production' })
|
||||
|
||||
const { exec } = require('child_process')
|
||||
const dataConfig = require('./relay.config')
|
||||
const thegraphConfig = require('./relay_thegraph.config')
|
||||
/* eslint-enable */
|
||||
const dataConfig = require('./graphql.config')
|
||||
const thegraphConfig = require('./graphql_thegraph.config')
|
||||
|
||||
function fetchSchema(url, outputFile) {
|
||||
exec(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
src: './src',
|
||||
language: 'typescript',
|
||||
@@ -1,5 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const defaultConfig = require('./relay.config')
|
||||
/* eslint-env node */
|
||||
|
||||
const defaultConfig = require('./graphql.config')
|
||||
|
||||
module.exports = {
|
||||
src: defaultConfig.src,
|
||||
35
package.json
@@ -8,10 +8,10 @@
|
||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"relay": "relay-compiler relay.config.js",
|
||||
"relay-thegraph": "relay-compiler relay_thegraph.config.js",
|
||||
"graphql:fetch": "node fetch-schema.js",
|
||||
"graphql:generate": "yarn relay && yarn relay-thegraph",
|
||||
"graphql:generate:data": "graphql-codegen --config apollo-codegen.ts",
|
||||
"graphql:generate:thegraph": "graphql-codegen --config apollo-codegen_thegraph.ts",
|
||||
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
|
||||
"prei18n:extract": "node prei18n-extract.js",
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
@@ -90,39 +90,35 @@
|
||||
"@types/ua-parser-js": "^0.7.35",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4",
|
||||
"@typescript-eslint/parser": "^4",
|
||||
"@uniswap/eslint-config": "^1.1.1",
|
||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
"babel-plugin-relay": "^14.1.0",
|
||||
"cypress": "^10.3.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-better-styled-components": "^1.1.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"ms.macro": "^2.0.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"react-scripts": "^4.0.3",
|
||||
"relay-compiler": "^14.1.0",
|
||||
"serve": "^11.3.2",
|
||||
"ts-transform-graphql-tag": "^0.2.1",
|
||||
"typechain": "^5.0.0",
|
||||
"typescript": "^4.4.3",
|
||||
"yarn-deduplicate": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.2",
|
||||
"@coinbase/wallet-sdk": "^3.3.0",
|
||||
"@fontsource/ibm-plex-mono": "^4.5.1",
|
||||
"@fontsource/inter": "^4.5.1",
|
||||
"@graphql-codegen/cli": "^2.15.0",
|
||||
"@graphql-codegen/client-preset": "^1.2.1",
|
||||
"@graphql-codegen/typescript": "^2.8.3",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.8",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@graphql-codegen/typescript-resolvers": "^2.7.8",
|
||||
"@lingui/core": "^3.14.0",
|
||||
"@lingui/macro": "^3.14.0",
|
||||
"@lingui/react": "^3.14.0",
|
||||
@@ -135,11 +131,10 @@
|
||||
"@react-hook/window-scroll": "^1.3.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@sentry/react": "7.20.1",
|
||||
"@types/react-relay": "^13.0.2",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "1.2.0",
|
||||
"@uniswap/analytics-events": "1.3.1",
|
||||
"@uniswap/conedison": "^1.1.0",
|
||||
"@uniswap/analytics-events": "^2.1.0",
|
||||
"@uniswap/conedison": "^1.1.1",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
@@ -215,8 +210,6 @@
|
||||
"react-popper": "^2.2.3",
|
||||
"react-query": "^3.39.1",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-relay": "^14.1.0",
|
||||
"react-relay-network-modern": "^6.2.1",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-spring": "^9.5.5",
|
||||
"react-table": "^7.8.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
/* eslint-env node */
|
||||
|
||||
const { exec } = require('child_process')
|
||||
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -97,13 +97,13 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #212429;
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background-color: #f7f8fa;
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
21
src/assets/svg/fiat_mask.svg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
224
src/components/About/AboutFooter.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ExternalLink } from 'theme'
|
||||
|
||||
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
|
||||
import darkUnicornImgSrc from './images/unicornEmbossDark.png'
|
||||
import lightUnicornImgSrc from './images/unicornEmbossLight.png'
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 48px;
|
||||
max-width: 1440px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`
|
||||
|
||||
const LogoSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const LogoSectionLeft = styled(LogoSection)`
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
|
||||
const LogoSectionBottom = styled(LogoSection)`
|
||||
display: flex;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLogo = styled.img`
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
|
||||
const SocialLinks = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 20px 0 0 0;
|
||||
`
|
||||
|
||||
const SocialLink = styled.a`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const FooterLinks = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const LinkGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 200px;
|
||||
margin: 20px 0 0 0;
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const LinkGroupTitle = styled.span`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
`
|
||||
|
||||
const ExternalTextLink = styled(ExternalLink)`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextLink = styled(Link)`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const Copyright = styled.span`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
margin: 1rem 0 0 0;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
|
||||
const LogoSectionContent = () => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<>
|
||||
<StyledLogo src={isDarkMode ? darkUnicornImgSrc : lightUnicornImgSrc} alt="Uniswap Logo" />
|
||||
<SocialLinks>
|
||||
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
|
||||
<DiscordIcon size={32} />
|
||||
</SocialLink>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.TWITTER_LINK}
|
||||
>
|
||||
<SocialLink href="https://twitter.com/uniswap" target="_blank" rel="noopener noreferrer">
|
||||
<TwitterIcon size={32} />
|
||||
</SocialLink>
|
||||
</TraceEvent>
|
||||
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon size={32} />
|
||||
</SocialLink>
|
||||
</SocialLinks>
|
||||
<Copyright>© {new Date().getFullYear()} Uniswap Labs</Copyright>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const AboutFooter = () => {
|
||||
return (
|
||||
<Footer>
|
||||
<LogoSectionLeft>
|
||||
<LogoSectionContent />
|
||||
</LogoSectionLeft>
|
||||
|
||||
<FooterLinks>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>App</LinkGroupTitle>
|
||||
<TextLink to="/swap">Swap</TextLink>
|
||||
<TextLink to="/tokens">Tokens</TextLink>
|
||||
<TextLink to="/nfts">NFTs</TextLink>
|
||||
<TextLink to="/pool">Pools</TextLink>
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>Protocol</LinkGroupTitle>
|
||||
<ExternalTextLink href="https://uniswap.org/community" target="_blank" rel="noopener noreferrer">
|
||||
Community
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/governance" target="_blank" rel="noopener noreferrer">
|
||||
Governance
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/developers" target="_blank" rel="noopener noreferrer">
|
||||
Developers
|
||||
</ExternalTextLink>
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>Company</LinkGroupTitle>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.CAREERS_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs" target="_blank" rel="noopener noreferrer">
|
||||
Careers
|
||||
</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.BLOG_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://uniswap.org/blog" target="_blank" rel="noopener noreferrer">
|
||||
Blog
|
||||
</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>Get Help</LinkGroupTitle>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.SUPPORT_LINK}
|
||||
>
|
||||
<ExternalTextLink
|
||||
href="https://support.uniswap.org/hc/en-us/requests/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Contact Us
|
||||
</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.SUPPORT_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://support.uniswap.org/hc/en-us" target="_blank" rel="noopener noreferrer">
|
||||
Help Center
|
||||
</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
</LinkGroup>
|
||||
</FooterLinks>
|
||||
|
||||
<LogoSectionBottom>
|
||||
<LogoSectionContent />
|
||||
</LogoSectionBottom>
|
||||
</Footer>
|
||||
)
|
||||
}
|
||||
150
src/components/About/Card.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
export enum CardType {
|
||||
Primary = 'Primary',
|
||||
Secondary = 'Secondary',
|
||||
}
|
||||
|
||||
const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string; type: CardType }>`
|
||||
display: flex;
|
||||
background: ${({ isDarkMode, backgroundImgSrc, type, theme }) =>
|
||||
isDarkMode
|
||||
? `${type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
|
||||
backgroundImgSrc ? ` url(${backgroundImgSrc})` : ''
|
||||
}`
|
||||
: `${type === CardType.Primary ? 'white' : theme.backgroundModule} url(${backgroundImgSrc})`};
|
||||
background-size: auto 100%;
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: border-box;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
padding: 24px;
|
||||
height: 212px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid ${({ theme, type }) => (type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
|
||||
box-shadow: 0px 10px 24px 0px rgba(51, 53, 72, 0.04);
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
|
||||
}
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
height: ${({ backgroundImgSrc }) => (backgroundImgSrc ? 360 : 260)}px;
|
||||
}
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
padding: 32px;
|
||||
}
|
||||
`
|
||||
|
||||
const TitleRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const CardTitle = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 600;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const getCardDescriptionColor = (type: CardType, theme: DefaultTheme) => {
|
||||
switch (type) {
|
||||
case CardType.Secondary:
|
||||
return theme.textSecondary
|
||||
default:
|
||||
return theme.textPrimary
|
||||
}
|
||||
}
|
||||
|
||||
const CardDescription = styled.div<{ type: CardType }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme, type }) => getCardDescriptionColor(type, theme)};
|
||||
padding: 0 40px 0 0;
|
||||
max-width: 480px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
max-width: 480px;
|
||||
}
|
||||
`
|
||||
|
||||
const CardCTA = styled(CardDescription)`
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
font-weight: 500;
|
||||
margin: 24px 0 0;
|
||||
cursor: pointer;
|
||||
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const Card = ({
|
||||
type = CardType.Primary,
|
||||
title,
|
||||
description,
|
||||
cta,
|
||||
to,
|
||||
external,
|
||||
backgroundImgSrc,
|
||||
icon,
|
||||
elementName,
|
||||
}: {
|
||||
type?: CardType
|
||||
title: string
|
||||
description: string
|
||||
cta?: string
|
||||
to: string
|
||||
external?: boolean
|
||||
backgroundImgSrc?: string
|
||||
icon?: React.ReactNode
|
||||
elementName?: string
|
||||
}) => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<TraceEvent events={[BrowserEvent.onClick]} name={SharedEventName.ELEMENT_CLICKED} element={elementName}>
|
||||
<StyledCard
|
||||
type={type}
|
||||
as={external ? 'a' : Link}
|
||||
to={external ? undefined : to}
|
||||
href={external ? to : undefined}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopenener noreferrer' : undefined}
|
||||
isDarkMode={isDarkMode}
|
||||
backgroundImgSrc={backgroundImgSrc}
|
||||
>
|
||||
<TitleRow>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{icon}
|
||||
</TitleRow>
|
||||
<CardDescription type={type}>
|
||||
{description}
|
||||
<CardCTA type={type}>{cta}</CardCTA>
|
||||
</CardDescription>
|
||||
</StyledCard>
|
||||
</TraceEvent>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
26
src/components/About/Icons.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ReactComponent as DiscordI } from './images/discord.svg'
|
||||
import { ReactComponent as GithubI } from './images/github.svg'
|
||||
import { ReactComponent as TwitterI } from './images/twitter-safe.svg'
|
||||
|
||||
export const DiscordIcon = styled(DiscordI)<{ size?: number; fill?: string }>`
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
fill: ${({ fill }) => fill ?? '#98A1C0'};
|
||||
opacity: 1;
|
||||
`
|
||||
|
||||
export const TwitterIcon = styled(TwitterI)<{ size?: number; fill?: string }>`
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
fill: ${({ fill }) => fill ?? '#98A1C0'};
|
||||
opacity: 1;
|
||||
`
|
||||
|
||||
export const GithubIcon = styled(GithubI)<{ size?: number; fill?: string }>`
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
fill: ${({ fill }) => fill ?? '#98A1C0'};
|
||||
opacity: 1;
|
||||
`
|
||||
106
src/components/About/ProtocolBanner.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { ButtonEmpty } from 'components/Button'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
import meshSrc from './images/Mesh.png'
|
||||
|
||||
const DARK_MODE_GRADIENT = 'radial-gradient(101.8% 4091.31% at 0% 0%, #4673FA 0%, #9646FA 100%)'
|
||||
|
||||
const Banner = styled.div<{ isDarkMode: boolean }>`
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
border-radius: 32px;
|
||||
max-width: 1440px;
|
||||
margin: 80px 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px 48px;
|
||||
|
||||
box-shadow: 0px 10px 24px rgba(51, 53, 72, 0.04);
|
||||
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? `url(${meshSrc}), ${DARK_MODE_GRADIENT}`
|
||||
: `url(${meshSrc}), linear-gradient(93.06deg, #FF00C7 2.66%, #FF9FFB 98.99%);`};
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
height: 140px;
|
||||
flex-direction: row;
|
||||
}
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
color: white;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const HeaderText = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const DescriptionText = styled.div`
|
||||
margin: 10px 10px 0 0;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
`
|
||||
|
||||
const BannerButtonContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
width: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const BannerButton = styled(ButtonEmpty)`
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
`
|
||||
|
||||
const ProtocolBanner = () => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<Banner isDarkMode={isDarkMode}>
|
||||
<TextContainer>
|
||||
<HeaderText>Powered by the Uniswap Protocol</HeaderText>
|
||||
<DescriptionText>
|
||||
The leading decentralized crypto trading protocol, governed by a global community.
|
||||
</DescriptionText>
|
||||
</TextContainer>
|
||||
<BannerButtonContainer>
|
||||
<BannerButton width="200px" as="a" href="https://uniswap.org" rel="noopener noreferrer" target="_blank">
|
||||
Learn more
|
||||
</BannerButton>
|
||||
</BannerButtonContainer>
|
||||
</Banner>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProtocolBanner
|
||||
71
src/components/About/constants.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { DollarSign, Terminal } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { lightTheme } from 'theme/colors'
|
||||
|
||||
import darkArrowImgSrc from './images/aboutArrowDark.png'
|
||||
import lightArrowImgSrc from './images/aboutArrowLight.png'
|
||||
import darkDollarImgSrc from './images/aboutDollarDark.png'
|
||||
import darkTerminalImgSrc from './images/aboutTerminalDark.png'
|
||||
import nftCardImgSrc from './images/nftCard.png'
|
||||
import swapCardImgSrc from './images/swapCard.png'
|
||||
|
||||
export const MAIN_CARDS = [
|
||||
{
|
||||
to: '/swap',
|
||||
title: 'Swap tokens',
|
||||
description: 'Buy, sell, and explore tokens on Ethereum, Polygon, Optimism, and more.',
|
||||
cta: 'Trade Tokens',
|
||||
darkBackgroundImgSrc: swapCardImgSrc,
|
||||
lightBackgroundImgSrc: swapCardImgSrc,
|
||||
elementName: InterfaceElementName.ABOUT_PAGE_SWAP_CARD,
|
||||
},
|
||||
{
|
||||
to: '/nfts',
|
||||
title: 'Trade NFTs',
|
||||
description: 'Buy and sell NFTs across marketplaces to find more listings at better prices.',
|
||||
cta: 'Explore NFTs',
|
||||
darkBackgroundImgSrc: nftCardImgSrc,
|
||||
lightBackgroundImgSrc: nftCardImgSrc,
|
||||
elementName: InterfaceElementName.ABOUT_PAGE_NFTS_CARD,
|
||||
},
|
||||
]
|
||||
|
||||
const StyledCardLogo = styled.img`
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
max-height: 48px;
|
||||
max-width: 48px;
|
||||
`
|
||||
|
||||
export const MORE_CARDS = [
|
||||
{
|
||||
to: 'https://support.uniswap.org/hc/en-us/articles/11306574799117-How-to-use-Moon-Pay-on-the-Uniswap-web-app-',
|
||||
external: true,
|
||||
title: 'Buy crypto',
|
||||
description: 'Buy crypto with your credit card or bank account at the best rates.',
|
||||
lightIcon: <DollarSign color={lightTheme.textTertiary} size={48} />,
|
||||
darkIcon: <StyledCardLogo src={darkDollarImgSrc} alt="Earn" />,
|
||||
cta: 'Buy now',
|
||||
elementName: InterfaceElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
|
||||
},
|
||||
{
|
||||
to: '/pool',
|
||||
title: 'Earn',
|
||||
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
|
||||
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,
|
||||
darkIcon: <StyledCardLogo src={darkArrowImgSrc} alt="Analytics" />,
|
||||
cta: 'Provide liquidity',
|
||||
elementName: InterfaceElementName.ABOUT_PAGE_EARN_CARD,
|
||||
},
|
||||
{
|
||||
to: 'https://docs.uniswap.org',
|
||||
external: true,
|
||||
title: 'Build dApps',
|
||||
description: 'Build apps and tools on the largest DeFi protocol on Ethereum.',
|
||||
lightIcon: <Terminal color={lightTheme.textTertiary} size={48} />,
|
||||
darkIcon: <StyledCardLogo src={darkTerminalImgSrc} alt="Developers" />,
|
||||
cta: 'Developer docs',
|
||||
elementName: InterfaceElementName.ABOUT_PAGE_DEV_DOCS_CARD,
|
||||
},
|
||||
]
|
||||
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 379 KiB |
BIN
src/components/About/images/Mesh.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/components/About/images/aboutArrowDark.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/components/About/images/aboutArrowLight.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
src/components/About/images/aboutDollarDark.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/components/About/images/aboutTerminalDark.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
3
src/components/About/images/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 71 55" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/components/About/images/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 822 B |
BIN
src/components/About/images/nftCard.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
src/components/About/images/swapCard.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
3
src/components/About/images/twitter-safe.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 32 32" role="img" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.2746 5.92398C30.7719 6.14694 30.2551 6.33512 29.727 6.4879C30.3522 5.7808 30.8289 4.9488 31.1199 4.03835C31.1851 3.83427 31.1175 3.61089 30.9498 3.47742C30.7822 3.34385 30.5495 3.32785 30.365 3.43716C29.2434 4.10235 28.0334 4.58039 26.7647 4.85993C25.4866 3.6111 23.7508 2.90039 21.9563 2.90039C18.1684 2.90039 15.0867 5.98199 15.0867 9.76975C15.0867 10.0681 15.1056 10.3647 15.143 10.6573C10.4426 10.2446 6.07276 7.9343 3.07198 4.25337C2.96504 4.12217 2.80029 4.05146 2.63162 4.06498C2.46285 4.0782 2.31121 4.17337 2.22595 4.31964C1.61733 5.36398 1.29557 6.5584 1.29557 7.77368C1.29557 9.4289 1.88654 10.9994 2.93046 12.2265C2.61304 12.1166 2.30502 11.9792 2.01103 11.816C1.8532 11.7282 1.66058 11.7295 1.50378 11.8194C1.34687 11.9093 1.2485 12.0747 1.24437 12.2554C1.24365 12.2859 1.24365 12.3163 1.24365 12.3472C1.24365 14.8179 2.5734 17.0423 4.60644 18.2547C4.43178 18.2373 4.25722 18.212 4.0838 18.1788C3.90502 18.1447 3.72117 18.2073 3.6006 18.3437C3.47983 18.4799 3.43988 18.6699 3.49552 18.8433C4.24804 21.1927 6.18548 22.9208 8.52767 23.4477C6.58507 24.6644 4.36355 25.3017 2.03147 25.3017C1.54486 25.3017 1.05547 25.2731 0.5765 25.2165C0.338565 25.1882 0.111055 25.3287 0.0300229 25.5549C-0.0510093 25.7813 0.0348745 26.0337 0.2373 26.1634C3.23322 28.0844 6.69738 29.0997 10.2551 29.0997C17.249 29.0997 21.6242 25.8016 24.063 23.0349C27.104 19.585 28.8481 15.0186 28.8481 10.5067C28.8481 10.3182 28.8452 10.1278 28.8394 9.93812C30.0392 9.03417 31.0722 7.94018 31.9128 6.68279C32.0404 6.49182 32.0266 6.23943 31.8787 6.06364C31.731 5.88774 31.4848 5.83087 31.2746 5.92398Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/components/About/images/unicornEmbossDark.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/components/About/images/unicornEmbossLight.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -14,18 +14,15 @@ import {
|
||||
CollectFeesTransactionInfo,
|
||||
CreateV3PoolTransactionInfo,
|
||||
DelegateTransactionInfo,
|
||||
DepositLiquidityStakingTransactionInfo,
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
ExecuteTransactionInfo,
|
||||
MigrateV2LiquidityToV3TransactionInfo,
|
||||
QueueTransactionInfo,
|
||||
RemoveLiquidityV3TransactionInfo,
|
||||
SubmitProposalTransactionInfo,
|
||||
TransactionInfo,
|
||||
TransactionType,
|
||||
VoteTransactionInfo,
|
||||
WithdrawLiquidityStakingTransactionInfo,
|
||||
WrapTransactionInfo,
|
||||
} from '../../state/transactions/types'
|
||||
|
||||
@@ -83,7 +80,7 @@ function ClaimSummary({ info: { recipient, uniAmountRaw } }: { info: ClaimTransa
|
||||
)
|
||||
}
|
||||
|
||||
function SubmitProposalTransactionSummary(_: { info: SubmitProposalTransactionInfo }) {
|
||||
function SubmitProposalTransactionSummary() {
|
||||
return <Trans>Submit new proposal</Trans>
|
||||
}
|
||||
|
||||
@@ -175,13 +172,13 @@ function WrapSummary({ info: { chainId, currencyAmountRaw, unwrapped } }: { info
|
||||
}
|
||||
}
|
||||
|
||||
function DepositLiquidityStakingSummary(_: { info: DepositLiquidityStakingTransactionInfo }) {
|
||||
function DepositLiquidityStakingSummary() {
|
||||
// not worth rendering the tokens since you can should no longer deposit liquidity in the staking contracts
|
||||
// todo: deprecate and delete the code paths that allow this, show user more information
|
||||
return <Trans>Deposit liquidity</Trans>
|
||||
}
|
||||
|
||||
function WithdrawLiquidityStakingSummary(_: { info: WithdrawLiquidityStakingTransactionInfo }) {
|
||||
function WithdrawLiquidityStakingSummary() {
|
||||
return <Trans>Withdraw deposited liquidity</Trans>
|
||||
}
|
||||
|
||||
@@ -319,10 +316,10 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
|
||||
return <ClaimSummary info={info} />
|
||||
|
||||
case TransactionType.DEPOSIT_LIQUIDITY_STAKING:
|
||||
return <DepositLiquidityStakingSummary info={info} />
|
||||
return <DepositLiquidityStakingSummary />
|
||||
|
||||
case TransactionType.WITHDRAW_LIQUIDITY_STAKING:
|
||||
return <WithdrawLiquidityStakingSummary info={info} />
|
||||
return <WithdrawLiquidityStakingSummary />
|
||||
|
||||
case TransactionType.SWAP:
|
||||
return <SwapSummary info={info} />
|
||||
@@ -358,6 +355,6 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
|
||||
return <ExecuteSummary info={info} />
|
||||
|
||||
case TransactionType.SUBMIT_PROPOSAL:
|
||||
return <SubmitProposalTransactionSummary info={info} />
|
||||
return <SubmitProposalTransactionSummary />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,26 +39,32 @@ const getCurrency = ({ info, chainId }: { info: TransactionInfo; chainId: number
|
||||
switch (info.type) {
|
||||
case TransactionType.ADD_LIQUIDITY_V3_POOL:
|
||||
case TransactionType.REMOVE_LIQUIDITY_V3:
|
||||
case TransactionType.CREATE_V3_POOL:
|
||||
case TransactionType.CREATE_V3_POOL: {
|
||||
const { baseCurrencyId, quoteCurrencyId } = info
|
||||
return { currencyId0: baseCurrencyId, currencyId1: quoteCurrencyId }
|
||||
case TransactionType.SWAP:
|
||||
}
|
||||
case TransactionType.SWAP: {
|
||||
const { inputCurrencyId, outputCurrencyId } = info
|
||||
return { currencyId0: inputCurrencyId, currencyId1: outputCurrencyId }
|
||||
case TransactionType.WRAP:
|
||||
}
|
||||
case TransactionType.WRAP: {
|
||||
const { unwrapped } = info
|
||||
const native = info.chainId ? nativeOnChain(info.chainId) : undefined
|
||||
const base = 'ETH'
|
||||
const wrappedCurrency = native?.wrapped.address ?? 'WETH'
|
||||
return { currencyId0: unwrapped ? wrappedCurrency : base, currencyId1: unwrapped ? base : wrappedCurrency }
|
||||
case TransactionType.COLLECT_FEES:
|
||||
}
|
||||
case TransactionType.COLLECT_FEES: {
|
||||
const { currencyId0, currencyId1 } = info
|
||||
return { currencyId0, currencyId1 }
|
||||
case TransactionType.APPROVAL:
|
||||
}
|
||||
case TransactionType.APPROVAL: {
|
||||
return { currencyId0: info.tokenAddress, currencyId1: undefined }
|
||||
case TransactionType.CLAIM:
|
||||
}
|
||||
case TransactionType.CLAIM: {
|
||||
const uniAddress = chainId ? UNI_ADDRESS[chainId] : undefined
|
||||
return { currencyId0: uniAddress, currencyId1: undefined }
|
||||
}
|
||||
default:
|
||||
return { currencyId0: undefined, currencyId1: undefined }
|
||||
}
|
||||
|
||||
13
src/components/Button/LoadingButtonSpinner.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SpinnerSVG } from 'theme'
|
||||
|
||||
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
|
||||
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
opacity="0.1"
|
||||
d="M18.8334 10.0003C18.8334 14.6027 15.1025 18.3337 10.5001 18.3337C5.89771 18.3337 2.16675 14.6027 2.16675 10.0003C2.16675 5.39795 5.89771 1.66699 10.5001 1.66699C15.1025 1.66699 18.8334 5.39795 18.8334 10.0003ZM4.66675 10.0003C4.66675 13.222 7.27842 15.8337 10.5001 15.8337C13.7217 15.8337 16.3334 13.222 16.3334 10.0003C16.3334 6.77867 13.7217 4.16699 10.5001 4.16699C7.27842 4.16699 4.66675 6.77867 4.66675 10.0003Z"
|
||||
/>
|
||||
<path d="M17.5834 10.0003C18.2738 10.0003 18.843 9.4376 18.7398 8.755C18.6392 8.0891 18.458 7.43633 18.1991 6.8113C17.7803 5.80025 17.1665 4.88159 16.3926 4.10777C15.6188 3.33395 14.7002 2.72012 13.6891 2.30133C13.0641 2.04243 12.4113 1.86121 11.7454 1.76057C11.0628 1.6574 10.5001 2.22664 10.5001 2.91699C10.5001 3.60735 11.066 4.15361 11.7405 4.30041C12.0789 4.37406 12.4109 4.47786 12.7324 4.61103C13.4401 4.90418 14.0832 5.33386 14.6249 5.87554C15.1665 6.41721 15.5962 7.06027 15.8894 7.76801C16.0225 8.08949 16.1264 8.42147 16.2 8.75986C16.3468 9.43443 16.8931 10.0003 17.5834 10.0003Z" />
|
||||
</SpinnerSVG>
|
||||
)
|
||||
|
||||
export default ButtonLoadingSpinner
|
||||
@@ -5,16 +5,31 @@ import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
|
||||
|
||||
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
|
||||
|
||||
export const BaseButton = styled(RebassButton)<
|
||||
{
|
||||
padding?: string
|
||||
width?: string
|
||||
$borderRadius?: string
|
||||
altDisabledStyle?: boolean
|
||||
} & ButtonProps
|
||||
>`
|
||||
const ButtonOverlay = styled.div`
|
||||
background-color: transparent;
|
||||
bottom: 0;
|
||||
border-radius: inherit;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: 150ms ease background-color;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
type BaseButtonProps = {
|
||||
padding?: string
|
||||
width?: string
|
||||
$borderRadius?: string
|
||||
altDisabledStyle?: boolean
|
||||
} & ButtonProps
|
||||
|
||||
export const BaseButton = styled(RebassButton)<BaseButtonProps>`
|
||||
padding: ${({ padding }) => padding ?? '16px'};
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
font-weight: 500;
|
||||
@@ -86,7 +101,7 @@ export const SmallButtonPrimary = styled(ButtonPrimary)`
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
export const ButtonLight = styled(BaseButton)`
|
||||
const BaseButtonLight = styled(BaseButton)`
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
font-size: 20px;
|
||||
@@ -103,6 +118,19 @@ export const ButtonLight = styled(BaseButton)`
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
||||
}
|
||||
|
||||
:hover {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
}
|
||||
|
||||
:active {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||
}
|
||||
}
|
||||
|
||||
:disabled {
|
||||
opacity: 0.4;
|
||||
:hover {
|
||||
@@ -369,18 +397,6 @@ export function ButtonRadioChecked({ active = false, children, ...rest }: { acti
|
||||
}
|
||||
}
|
||||
|
||||
const ButtonOverlay = styled.div`
|
||||
background-color: transparent;
|
||||
bottom: 0;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: 150ms ease background-color;
|
||||
width: 100%;
|
||||
`
|
||||
export enum ButtonSize {
|
||||
small,
|
||||
medium,
|
||||
@@ -395,7 +411,7 @@ export enum ButtonEmphasis {
|
||||
warning,
|
||||
destructive,
|
||||
}
|
||||
interface BaseButtonProps {
|
||||
interface BaseThemeButtonProps {
|
||||
size: ButtonSize
|
||||
emphasis: ButtonEmphasis
|
||||
}
|
||||
@@ -474,7 +490,7 @@ function pickThemeButtonTextColor({ theme, emphasis }: { theme: DefaultTheme; em
|
||||
}
|
||||
}
|
||||
|
||||
const BaseThemeButton = styled.button<BaseButtonProps>`
|
||||
const BaseThemeButton = styled.button<BaseThemeButtonProps>`
|
||||
align-items: center;
|
||||
background-color: ${pickThemeButtonBackgroundColor};
|
||||
border-radius: 16px;
|
||||
@@ -491,16 +507,13 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
||||
padding: ${pickThemeButtonPadding};
|
||||
position: relative;
|
||||
transition: 150ms ease opacity;
|
||||
user-select: none;
|
||||
|
||||
:active {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||
}
|
||||
}
|
||||
:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
:focus {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||
@@ -511,9 +524,20 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
}
|
||||
:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
:disabled:active,
|
||||
:disabled:focus,
|
||||
:disabled:hover {
|
||||
${ButtonOverlay} {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseButtonProps {}
|
||||
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseThemeButtonProps {}
|
||||
|
||||
export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
||||
return (
|
||||
@@ -523,3 +547,12 @@ export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
||||
</BaseThemeButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const ButtonLight = ({ children, ...rest }: BaseButtonProps) => {
|
||||
return (
|
||||
<BaseButtonLight {...rest}>
|
||||
<ButtonOverlay />
|
||||
{children}
|
||||
</BaseButtonLight>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import { curveCardinal, scaleLinear } from 'd3'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { PricePoint } from 'graphql/data/util'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { memo } from 'react'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
@@ -21,18 +20,10 @@ interface SparklineChartProps {
|
||||
height: number
|
||||
tokenData: TopToken
|
||||
pricePercentChange: number | undefined | null
|
||||
timePeriod: TimePeriod
|
||||
sparklineMap: SparklineMap
|
||||
}
|
||||
|
||||
function _SparklineChart({
|
||||
width,
|
||||
height,
|
||||
tokenData,
|
||||
pricePercentChange,
|
||||
timePeriod,
|
||||
sparklineMap,
|
||||
}: SparklineChartProps) {
|
||||
function _SparklineChart({ width, height, tokenData, pricePercentChange, sparklineMap }: SparklineChartProps) {
|
||||
const theme = useTheme()
|
||||
// for sparkline
|
||||
const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@@ -332,8 +332,8 @@ export default function SwapCurrencyInputPanel({
|
||||
{showMaxButton && selectedCurrencyBalance ? (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||
name={SwapEventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||
element={InterfaceElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||
>
|
||||
<StyledBalanceMax onClick={onMax}>
|
||||
<Trans>Max</Trans>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@@ -315,8 +315,8 @@ export default function CurrencyInputPanel({
|
||||
{showMaxButton && selectedCurrencyBalance ? (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||
name={SwapEventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||
element={InterfaceElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||
>
|
||||
<StyledBalanceMax onClick={onMax}>
|
||||
<Trans>MAX</Trans>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useIsMobile } from 'nft/hooks'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
import { Copy } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
|
||||
import { CopyToClipboard, ExternalLink, ThemedText } from '../../theme'
|
||||
import { Column } from '../Column'
|
||||
@@ -85,6 +86,7 @@ const CodeTitle = styled.div`
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
word-break: break-word;
|
||||
`
|
||||
|
||||
@@ -92,49 +94,87 @@ const Fallback = ({ error, eventId }: { error: Error; eventId: string | null })
|
||||
const [isExpanded, setExpanded] = useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const errorId = eventId || 'unknown-error'
|
||||
|
||||
// @todo: ThemedText components should be responsive by default
|
||||
const [Title, Description] = isMobile
|
||||
? [ThemedText.HeadlineSmall, ThemedText.BodySmall]
|
||||
: [ThemedText.HeadlineLarge, ThemedText.BodySecondary]
|
||||
|
||||
const showErrorId = isSentryEnabled() && eventId
|
||||
|
||||
const showMoreButton = (
|
||||
<ShowMoreButton onClick={() => setExpanded((s) => !s)}>
|
||||
<ThemedText.Link color="textSecondary">
|
||||
<Trans>{isExpanded ? 'Show less' : 'Show more'}</Trans>
|
||||
</ThemedText.Link>
|
||||
<ShowMoreIcon $isExpanded={isExpanded} secondaryWidth="20" secondaryHeight="20" />
|
||||
</ShowMoreButton>
|
||||
)
|
||||
|
||||
const errorDetails = error.stack || error.message
|
||||
|
||||
return (
|
||||
<FallbackWrapper>
|
||||
<BodyWrapper>
|
||||
<Column gap="xl">
|
||||
<Column gap="sm">
|
||||
<Title textAlign="center">
|
||||
<Trans>Something went wrong</Trans>
|
||||
</Title>
|
||||
<Description textAlign="center" color="textSecondary">
|
||||
<Trans>
|
||||
Sorry, an error occured while processing your request. If you request support, be sure to provide your
|
||||
error ID.
|
||||
</Trans>
|
||||
</Description>
|
||||
</Column>
|
||||
<CodeBlockWrapper>
|
||||
<CodeTitle>
|
||||
<ThemedText.SubHeader fontWeight={500}>Error ID: {errorId}</ThemedText.SubHeader>
|
||||
<CopyToClipboard toCopy={errorId}>
|
||||
<CopyIcon />
|
||||
</CopyToClipboard>
|
||||
</CodeTitle>
|
||||
<Separator />
|
||||
{isExpanded && (
|
||||
<>
|
||||
<Code>{error.stack}</Code>
|
||||
{showErrorId ? (
|
||||
<>
|
||||
<Column gap="sm">
|
||||
<Title textAlign="center">
|
||||
<Trans>Something went wrong</Trans>
|
||||
</Title>
|
||||
<Description textAlign="center" color="textSecondary">
|
||||
<Trans>
|
||||
Sorry, an error occured while processing your request. If you request support, be sure to provide
|
||||
your error ID.
|
||||
</Trans>
|
||||
</Description>
|
||||
</Column>
|
||||
<CodeBlockWrapper>
|
||||
<CodeTitle>
|
||||
<ThemedText.SubHeader fontWeight={500}>
|
||||
<Trans>Error ID: {eventId}</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<CopyToClipboard toCopy={eventId}>
|
||||
<CopyIcon />
|
||||
</CopyToClipboard>
|
||||
</CodeTitle>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<ShowMoreButton onClick={() => setExpanded((s) => !s)}>
|
||||
<ThemedText.Link color="textSecondary">
|
||||
<Trans>{isExpanded ? 'Show less' : 'Show more'}</Trans>
|
||||
</ThemedText.Link>
|
||||
<ShowMoreIcon $isExpanded={isExpanded} secondaryWidth="20" secondaryHeight="20" />
|
||||
</ShowMoreButton>
|
||||
</CodeBlockWrapper>
|
||||
{isExpanded && (
|
||||
<>
|
||||
<Code>{errorDetails}</Code>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
{showMoreButton}
|
||||
</CodeBlockWrapper>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Column gap="sm">
|
||||
<Title textAlign="center">
|
||||
<Trans>Something went wrong</Trans>
|
||||
</Title>
|
||||
<Description textAlign="center" color="textSecondary">
|
||||
<Trans>
|
||||
Sorry, an error occured while processing your request. If you request support, be sure to copy the
|
||||
details of this error.
|
||||
</Trans>
|
||||
</Description>
|
||||
</Column>
|
||||
<CodeBlockWrapper>
|
||||
<CodeTitle>
|
||||
<ThemedText.SubHeader fontWeight={500}>Error details</ThemedText.SubHeader>
|
||||
<CopyToClipboard toCopy={errorDetails}>
|
||||
<CopyIcon />
|
||||
</CopyToClipboard>
|
||||
</CodeTitle>
|
||||
<Separator />
|
||||
<Code>{errorDetails.split('\n').slice(0, isExpanded ? undefined : 4)}</Code>
|
||||
<Separator />
|
||||
{showMoreButton}
|
||||
</CodeBlockWrapper>
|
||||
</>
|
||||
)}
|
||||
<StretchedRow>
|
||||
<SmallButtonPrimary onClick={() => window.location.reload()}>
|
||||
<Trans>Reload the app</Trans>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
@@ -163,7 +164,7 @@ function FeatureFlagGroup({ name, children }: PropsWithChildren<{ name: string }
|
||||
)
|
||||
}
|
||||
|
||||
function FeatureFlagOption({ variant, featureFlag, value, label }: FeatureFlagProps) {
|
||||
function FeatureFlagOption({ variant, featureFlag, label }: FeatureFlagProps) {
|
||||
const updateFlag = useUpdateFlag()
|
||||
const [count, setCount] = useState(0)
|
||||
const featureFlags = useAtomValue(featureFlagSettings)
|
||||
@@ -208,6 +209,12 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.permit2}
|
||||
label="Permit 2 / Universal Router"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useFiatOnrampFlag()}
|
||||
featureFlag={FeatureFlag.fiatOnramp}
|
||||
label="Fiat on-ramp"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
149
src/components/FiatOnrampAnnouncement/index.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import fiatMaskUrl from 'assets/svg/fiat_mask.svg'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useToggleWalletDropdown } from 'state/application/hooks'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useFiatOnrampAck } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
const Arrow = styled.div`
|
||||
top: -4px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
width: 16px;
|
||||
|
||||
::before {
|
||||
background: hsl(315.75, 93%, 83%);
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
box-sizing: border-box;
|
||||
content: '';
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
width: 16px;
|
||||
}
|
||||
`
|
||||
const ArrowWrapper = styled.div`
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 90%;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
min-height: 92px;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
|
||||
right: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled(X)`
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
`
|
||||
const Wrapper = styled.button`
|
||||
background: radial-gradient(105% 250% at 100% 5%, hsla(318, 95%, 85%) 1%, hsla(331, 80%, 75%, 0.1) 84%),
|
||||
linear-gradient(180deg, hsla(296, 92%, 67%, 0.5) 0%, hsla(313, 96%, 60%, 0.5) 130%);
|
||||
background-color: hsla(297, 93%, 68%, 1);
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-align: start;
|
||||
max-width: 320px;
|
||||
min-height: 92px;
|
||||
width: 100%;
|
||||
|
||||
:before {
|
||||
background-image: url(${fiatMaskUrl});
|
||||
background-repeat: no-repeat;
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: -154px; // roughly width of fiat mask image
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
const Header = styled(ThemedText.SubHeader)`
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 12px 12px 4px;
|
||||
position: relative;
|
||||
`
|
||||
const Body = styled(ThemedText.BodySmall)`
|
||||
color: white;
|
||||
margin: 0 12px 12px 12px !important;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const ANNOUNCEMENT_RENDERED = 'FiatOnrampAnnouncement-rendered'
|
||||
const ANNOUNCEMENT_DISMISSED = 'FiatOnrampAnnouncement-dismissed'
|
||||
|
||||
const MAX_RENDER_COUNT = 3
|
||||
export function FiatOnrampAnnouncement() {
|
||||
const { account } = useWeb3React()
|
||||
const [acks, acknowledge] = useFiatOnrampAck()
|
||||
useEffect(() => {
|
||||
if (!sessionStorage.getItem(ANNOUNCEMENT_RENDERED)) {
|
||||
acknowledge({ renderCount: acks?.renderCount + 1 })
|
||||
sessionStorage.setItem(ANNOUNCEMENT_RENDERED, 'true')
|
||||
}
|
||||
}, [acknowledge, acks])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
localStorage.setItem(ANNOUNCEMENT_DISMISSED, 'true')
|
||||
}, [])
|
||||
|
||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||
const handleClick = useCallback(() => {
|
||||
sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_BANNER_CLICKED)
|
||||
toggleWalletDropdown()
|
||||
acknowledge({ user: true })
|
||||
}, [acknowledge, toggleWalletDropdown])
|
||||
|
||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||
const openModal = useAppSelector((state) => state.application.openModal)
|
||||
|
||||
if (
|
||||
!account ||
|
||||
acks?.user ||
|
||||
fiatOnrampFlag === BaseVariant.Control ||
|
||||
localStorage.getItem(ANNOUNCEMENT_DISMISSED) ||
|
||||
acks?.renderCount >= MAX_RENDER_COUNT ||
|
||||
isMobile ||
|
||||
openModal !== null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ArrowWrapper>
|
||||
<Arrow />
|
||||
<CloseIcon onClick={handleClose} data-testid="FiatOnrampAnnouncement-close" />
|
||||
<Wrapper onClick={handleClick}>
|
||||
<Header>
|
||||
<Trans>Buy crypto</Trans>
|
||||
</Header>
|
||||
<Body>
|
||||
<Trans>Get tokens at the best prices in web3 on Uniswap, powered by Moonpay.</Trans>
|
||||
</Body>
|
||||
</Wrapper>
|
||||
</ArrowWrapper>
|
||||
)
|
||||
}
|
||||
143
src/components/FiatOnrampModal/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { CustomLightSpinner, ThemedText } from 'theme'
|
||||
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
import Modal from '../Modal'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.white};
|
||||
border-radius: 20px;
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 0;
|
||||
min-height: 720px;
|
||||
min-width: 375px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ErrorText = styled(ThemedText.BodyPrimary)`
|
||||
color: ${({ theme }) => theme.accentFailure};
|
||||
margin: auto !important;
|
||||
text-align: center;
|
||||
width: 90%;
|
||||
`
|
||||
const StyledIframe = styled.iframe`
|
||||
background-color: ${({ theme }) => theme.white};
|
||||
border-radius: 12px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: calc(100% - 16px);
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: calc(100% - 16px);
|
||||
`
|
||||
const StyledSpinner = styled(CustomLightSpinner)`
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
`
|
||||
|
||||
const MOONPAY_SUPPORTED_CURRENCY_CODES = [
|
||||
'eth',
|
||||
'eth_arbitrum',
|
||||
'eth_optimism',
|
||||
'eth_polygon',
|
||||
'weth',
|
||||
'wbtc',
|
||||
'matic_polygon',
|
||||
'polygon',
|
||||
'usdc_arbitrum',
|
||||
'usdc_optimism',
|
||||
'usdc_polygon',
|
||||
]
|
||||
|
||||
export default function FiatOnrampModal() {
|
||||
const { account } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
const closeModal = useCloseModal()
|
||||
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
|
||||
|
||||
const [signedIframeUrl, setSignedIframeUrl] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchSignedIframeUrl = useCallback(async () => {
|
||||
if (!account) {
|
||||
setError('Please connect an account before making a purchase.')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const signedIframeUrlFetchEndpoint = process.env.REACT_APP_MOONPAY_LINK as string
|
||||
const res = await fetch(signedIframeUrlFetchEndpoint, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
colorCode: theme.accentAction,
|
||||
defaultCurrencyCode: 'eth',
|
||||
redirectUrl: 'https://app.uniswap.org/#/swap',
|
||||
walletAddresses: JSON.stringify(
|
||||
MOONPAY_SUPPORTED_CURRENCY_CODES.reduce(
|
||||
(acc, currencyCode) => ({
|
||||
...acc,
|
||||
[currencyCode]: account,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
),
|
||||
}),
|
||||
})
|
||||
const { url } = await res.json()
|
||||
setSignedIframeUrl(url)
|
||||
} catch (e) {
|
||||
console.log('there was an error fetching the link', e)
|
||||
setError(e.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [account, theme.accentAction])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSignedIframeUrl()
|
||||
}, [fetchSignedIframeUrl])
|
||||
|
||||
return (
|
||||
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} maxHeight={720}>
|
||||
<Wrapper data-testid="fiat-onramp-modal">
|
||||
{error ? (
|
||||
<>
|
||||
<ThemedText.MediumHeader>
|
||||
<Trans>Moonpay Fiat On-ramp iframe</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
<ErrorText>
|
||||
<Trans>something went wrong!</Trans>
|
||||
<br />
|
||||
{error}
|
||||
</ErrorText>
|
||||
</>
|
||||
) : loading ? (
|
||||
<StyledSpinner src={Circle} alt="loading spinner" size="90px" />
|
||||
) : (
|
||||
<StyledIframe src={signedIframeUrl ?? ''} frameBorder="0" title="fiat-onramp-iframe" />
|
||||
)}
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
|
||||
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>`
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: ${Z_INDEX.modalBackdrop};
|
||||
background-color: transparent;
|
||||
@@ -17,7 +17,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
|
||||
overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
|
||||
justify-content: center;
|
||||
|
||||
background-color: ${({ theme }) => theme.backgroundScrim};
|
||||
@@ -89,7 +89,7 @@ interface ModalProps {
|
||||
maxWidth?: number
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
scrollOverlay?: boolean
|
||||
$scrollOverlay?: boolean
|
||||
hideBorder?: boolean
|
||||
isBottomSheet?: boolean
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export default function Modal({
|
||||
initialFocusRef,
|
||||
children,
|
||||
onSwipe = onDismiss,
|
||||
scrollOverlay,
|
||||
$scrollOverlay,
|
||||
isBottomSheet = isMobile,
|
||||
hideBorder = false,
|
||||
}: ModalProps) {
|
||||
@@ -136,7 +136,7 @@ export default function Modal({
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
unstable_lockFocusAcrossFrames={false}
|
||||
scrollOverlay={scrollOverlay}
|
||||
$scrollOverlay={$scrollOverlay}
|
||||
>
|
||||
<StyledDialogContent
|
||||
{...(isMobile
|
||||
@@ -149,7 +149,7 @@ export default function Modal({
|
||||
$minHeight={minHeight}
|
||||
$maxHeight={maxHeight}
|
||||
$isBottomSheet={isBottomSheet}
|
||||
$scrollOverlay={scrollOverlay}
|
||||
$scrollOverlay={$scrollOverlay}
|
||||
$hideBorder={hideBorder}
|
||||
$maxWidth={maxWidth}
|
||||
>
|
||||
|
||||
@@ -93,6 +93,7 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
|
||||
gap="8"
|
||||
className={styles.ChainSelector}
|
||||
background={isOpen ? 'accentActiveSoft' : 'none'}
|
||||
data-testid="chain-selector"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{!isSupported ? (
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function ChainSelectorRow({
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Container onClick={() => onSelectChain(targetChain)}>
|
||||
<Container onClick={() => onSelectChain(targetChain)} data-testid={`chain-selector-option-${label.toLowerCase()}`}>
|
||||
<Logo src={logoUrl} alt={label} />
|
||||
<Label>{label}</Label>
|
||||
{isPending && <ApproveText>Approve in wallet</ApproveText>}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { body, bodySmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { ReactNode, useReducer, useRef } from 'react'
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||
|
||||
import { useToggleModal } from '../../state/application/hooks'
|
||||
@@ -50,8 +51,13 @@ const PrimaryMenuRow = ({
|
||||
)
|
||||
}
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
|
||||
return <Box className={`${styles.PrimaryText} ${body}`}>{children}</Box>
|
||||
return <StyledBox className={`${styles.PrimaryText} ${body}`}>{children}</StyledBox>
|
||||
}
|
||||
|
||||
PrimaryMenuRow.Text = PrimaryMenuRowText
|
||||
@@ -115,7 +121,6 @@ export const MenuDropdown = () => {
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ const baseNavDropdown = style([
|
||||
borderWidth: '1px',
|
||||
paddingBottom: '8',
|
||||
paddingTop: '8',
|
||||
zIndex: '2',
|
||||
}),
|
||||
{
|
||||
boxShadow: '0px 4px 12px 0px #00000026',
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { Box, BoxProps } from 'nft/components/Box'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import * as styles from './NavDropdown.css'
|
||||
|
||||
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
|
||||
const isMobile = useIsMobile()
|
||||
return <Box ref={ref} className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown} {...props} />
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
style={{ zIndex: Z_INDEX.modal }}
|
||||
className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
NavDropdown.displayName = 'NavDropdown'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName, SectionName } from '@uniswap/analytics-events'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
|
||||
import clsx from 'clsx'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
@@ -109,7 +109,7 @@ export const SearchBar = () => {
|
||||
|
||||
const isMobileOrTablet = isMobile || isTablet
|
||||
|
||||
const trace = useTrace({ section: SectionName.NAVBAR_SEARCH })
|
||||
const trace = useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH })
|
||||
|
||||
const navbarSearchEventProperties = {
|
||||
navbar_search_input_text: debouncedSearchValue,
|
||||
@@ -146,7 +146,7 @@ export const SearchBar = () => {
|
||||
}, [handleKeyPress, inputRef])
|
||||
|
||||
return (
|
||||
<Trace section={SectionName.NAVBAR_SEARCH}>
|
||||
<Trace section={InterfaceSectionName.NAVBAR_SEARCH}>
|
||||
<Box
|
||||
position={{ sm: 'fixed', md: 'absolute', xl: 'relative' }}
|
||||
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
|
||||
@@ -178,8 +178,8 @@ export const SearchBar = () => {
|
||||
</Box>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onFocus]}
|
||||
name={EventName.NAVBAR_SEARCH_SELECTED}
|
||||
element={ElementName.NAVBAR_SEARCH_INPUT}
|
||||
name={InterfaceEventName.NAVBAR_SEARCH_SELECTED}
|
||||
element={InterfaceElementName.NAVBAR_SEARCH_INPUT}
|
||||
properties={{ ...trace }}
|
||||
>
|
||||
<Trans
|
||||
@@ -192,7 +192,9 @@ export const SearchBar = () => {
|
||||
!isOpen && toggleOpen()
|
||||
setSearchValue(event.target.value)
|
||||
}}
|
||||
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
|
||||
onBlur={() =>
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)
|
||||
}
|
||||
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
|
||||
value={searchValue}
|
||||
ref={inputRef}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useTrace } from '@uniswap/analytics'
|
||||
import { NavBarSearchTypes, SectionName } from '@uniswap/analytics-events'
|
||||
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
@@ -202,7 +202,7 @@ export const SearchBarDropdown = ({
|
||||
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) ||
|
||||
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
|
||||
|
||||
const trace = JSON.stringify(useTrace({ section: SectionName.NAVBAR_SEARCH }))
|
||||
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
|
||||
|
||||
useEffect(() => {
|
||||
const eventProperties = { total_suggestions: totalSuggestions, query_text: queryText, ...JSON.parse(trace) }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { EventName } from '@uniswap/analytics-events'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import clsx from 'clsx'
|
||||
@@ -71,7 +71,7 @@ export const CollectionRow = ({
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(collection)
|
||||
toggleOpen()
|
||||
sendAnalyticsEvent(EventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, collection, toggleOpen, eventProperties])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -157,7 +157,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(token)
|
||||
toggleOpen()
|
||||
sendAnalyticsEvent(EventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, toggleOpen, token, eventProperties])
|
||||
|
||||
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
|
||||
|
||||
@@ -90,9 +90,13 @@ const Navbar = () => {
|
||||
<UniIcon
|
||||
width="48"
|
||||
height="48"
|
||||
data-testid="uniswap-logo"
|
||||
className={styles.logo}
|
||||
onClick={() => {
|
||||
navigate('/')
|
||||
navigate({
|
||||
pathname: '/',
|
||||
search: '?intro=true',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function AddRemoveTabs({
|
||||
// detect if back should redirect to v3 or v2 pool page
|
||||
const poolLink = location.pathname.includes('add/v2')
|
||||
? '/pool/v2'
|
||||
: '/pool' + (!!positionID ? `/${positionID.toString()}` : '')
|
||||
: '/pool' + (positionID ? `/${positionID.toString()}` : '')
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RowFixed } from 'components/Row'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import useGasPrice from 'hooks/useGasPrice'
|
||||
import { useIsLandingPage } from 'hooks/useIsLandingPage'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import useMachineTimeMs from 'hooks/useMachineTime'
|
||||
import JSBI from 'jsbi'
|
||||
@@ -120,6 +121,7 @@ export default function Polling() {
|
||||
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
|
||||
const blockTime = useCurrentBlockTimestamp()
|
||||
const isNftPage = useIsNftPage()
|
||||
const isLandingPage = useIsLandingPage()
|
||||
|
||||
const ethGasPrice = useGasPrice()
|
||||
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
|
||||
@@ -154,7 +156,7 @@ export default function Polling() {
|
||||
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
|
||||
}, [blockNumber, chainId])
|
||||
|
||||
if (isNftPage) {
|
||||
if (isNftPage || isLandingPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
const Wrapper = styled(AutoColumn)`
|
||||
margin-right: 8px;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const Grouping = styled(AutoColumn)`
|
||||
width: fit-content;
|
||||
padding: 4px;
|
||||
/* background-color: ${({ theme }) => theme.backgroundInteractive}; */
|
||||
border-radius: 16px;
|
||||
`
|
||||
|
||||
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: ${({ theme, confirmed, disabled }) =>
|
||||
disabled ? theme.deprecated_bg3 : confirmed ? theme.accentSuccess : theme.accentAction};
|
||||
border-radius: 50%;
|
||||
color: ${({ theme, disabled }) => (disabled ? theme.textTertiary : theme.textPrimary)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 8px;
|
||||
font-size: 16px;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
const CircleRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
interface ProgressCirclesProps {
|
||||
steps: boolean[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on array of steps, create a step counter of circles.
|
||||
* A circle can be enabled, disabled, or confirmed. States are derived
|
||||
* from previous step.
|
||||
*
|
||||
* An extra circle is added to represent the ability to swap, add, or remove.
|
||||
* This step will never be marked as complete (because no 'txn done' state in body ui).
|
||||
*
|
||||
* @param steps array of booleans where true means step is complete
|
||||
*/
|
||||
export default function ProgressCircles({ steps, disabled = false, ...rest }: ProgressCirclesProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Wrapper justify="center" {...rest}>
|
||||
<Grouping>
|
||||
{steps.map((step, i) => {
|
||||
return (
|
||||
<CircleRow key={i}>
|
||||
<Circle confirmed={step} disabled={disabled || (!steps[i - 1] && i !== 0)}>
|
||||
{step ? '✓' : i + 1 + '.'}
|
||||
</Circle>
|
||||
<ThemedText.DeprecatedMain color={theme.deprecated_text4}>|</ThemedText.DeprecatedMain>
|
||||
</CircleRow>
|
||||
)
|
||||
})}
|
||||
<Circle disabled={disabled || !steps[steps.length - 1]}>{steps.length + 1 + '.'}</Circle>
|
||||
</Grouping>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
@@ -70,9 +70,9 @@ export default function CommonBases({
|
||||
return (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
|
||||
name={EventName.TOKEN_SELECTED}
|
||||
name={InterfaceEventName.TOKEN_SELECTED}
|
||||
properties={formatAnalyticsEventProperties(currency, searchQuery, isAddressSearch)}
|
||||
element={ElementName.COMMON_BASES_CURRENCY_BUTTON}
|
||||
element={InterfaceElementName.COMMON_BASES_CURRENCY_BUTTON}
|
||||
key={currencyId(currency)}
|
||||
>
|
||||
<BaseWrapper
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
@@ -131,9 +131,9 @@ export function CurrencyRow({
|
||||
return (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
|
||||
name={EventName.TOKEN_SELECTED}
|
||||
name={InterfaceEventName.TOKEN_SELECTED}
|
||||
properties={{ is_imported_by_user: customAdded, ...eventProperties }}
|
||||
element={ElementName.TOKEN_SELECTOR_ROW}
|
||||
element={InterfaceElementName.TOKEN_SELECTOR_ROW}
|
||||
>
|
||||
<MenuItem
|
||||
tabIndex={0}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { EventName, ModalName } from '@uniswap/analytics-events'
|
||||
import { InterfaceEventName, InterfaceModalName } from '@uniswap/analytics-events'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
@@ -181,7 +181,11 @@ export function CurrencySearch({
|
||||
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<Trace name={EventName.TOKEN_SELECTOR_OPENED} modal={ModalName.TOKEN_SELECTOR} shouldLogImpression>
|
||||
<Trace
|
||||
name={InterfaceEventName.TOKEN_SELECTOR_OPENED}
|
||||
modal={InterfaceModalName.TOKEN_SELECTOR}
|
||||
shouldLogImpression
|
||||
>
|
||||
<PaddedColumn gap="16px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
|
||||
@@ -51,7 +51,7 @@ const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string; isInit
|
||||
${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)}
|
||||
ease-in;
|
||||
background: ${({ theme, bgColor, isActive }) =>
|
||||
isActive ? bgColor ?? theme.accentAction : !!bgColor ? theme.deprecated_bg4 : theme.textTertiary};
|
||||
isActive ? bgColor ?? theme.accentAction : bgColor ? theme.deprecated_bg4 : theme.textTertiary};
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
:hover {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { useTokenWarningColor } from 'hooks/useTokenWarningColor'
|
||||
import { useTokenWarningColor, useTokenWarningTextColor } from 'hooks/useTokenWarningColor'
|
||||
import { ReactNode } from 'react'
|
||||
import { AlertTriangle, Slash } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const Label = styled.div<{ color: string }>`
|
||||
const Label = styled.div<{ color: string; backgroundColor: string }>`
|
||||
padding: 4px 4px;
|
||||
font-size: 12px;
|
||||
background-color: ${({ color }) => color + '1F'};
|
||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||
border-radius: 8px;
|
||||
color: ${({ color }) => color};
|
||||
display: inline-flex;
|
||||
@@ -28,7 +28,7 @@ type TokenWarningLabelProps = {
|
||||
}
|
||||
export default function TokenSafetyLabel({ level, canProceed, children }: TokenWarningLabelProps) {
|
||||
return (
|
||||
<Label color={useTokenWarningColor(level)}>
|
||||
<Label color={useTokenWarningTextColor(level)} backgroundColor={useTokenWarningColor(level)}>
|
||||
<Title marginRight="5px">{children}</Title>
|
||||
{canProceed ? <AlertTriangle strokeWidth={2.5} size="14px" /> : <Slash strokeWidth={2.5} size="14px" />}
|
||||
</Label>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
|
||||
import { useTokenWarningColor } from 'hooks/useTokenWarningColor'
|
||||
import { useTokenWarningColor, useTokenWarningTextColor } from 'hooks/useTokenWarningColor'
|
||||
import { AlertTriangle, Slash } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink } from 'theme'
|
||||
|
||||
const Label = styled.div<{ color: string }>`
|
||||
const Label = styled.div<{ color: string; backgroundColor: string }>`
|
||||
width: 100%;
|
||||
padding: 12px 20px 16px;
|
||||
background-color: ${({ color }) => color + '1F'};
|
||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||
border-radius: 16px;
|
||||
color: ${({ color }) => color};
|
||||
`
|
||||
@@ -39,17 +39,18 @@ const StyledLink = styled(ExternalLink)`
|
||||
font-weight: 700;
|
||||
`
|
||||
|
||||
type TokenWarningMessageProps = {
|
||||
type TokenSafetyMessageProps = {
|
||||
warning: Warning
|
||||
tokenAddress: string
|
||||
}
|
||||
|
||||
export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarningMessageProps) {
|
||||
const color = useTokenWarningColor(warning.level)
|
||||
export default function TokenSafetyMessage({ warning, tokenAddress }: TokenSafetyMessageProps) {
|
||||
const backgroundColor = useTokenWarningColor(warning.level)
|
||||
const textColor = useTokenWarningTextColor(warning.level)
|
||||
const { heading, description } = getWarningCopy(warning)
|
||||
|
||||
return (
|
||||
<Label color={color}>
|
||||
<Label color={textColor} backgroundColor={backgroundColor}>
|
||||
<TitleRow>
|
||||
{warning.canProceed ? <AlertTriangle size="16px" /> : <Slash size="16px" />}
|
||||
<Title marginLeft="7px">{warning.message}</Title>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { darken } from 'polished'
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -65,19 +67,22 @@ const ResourcesContainer = styled.div`
|
||||
|
||||
type AboutSectionProps = {
|
||||
address: string
|
||||
chainId: SupportedChainId
|
||||
description?: string | null | undefined
|
||||
homepageUrl?: string | null | undefined
|
||||
twitterName?: string | null | undefined
|
||||
}
|
||||
|
||||
export function AboutSection({ address, description, homepageUrl, twitterName }: AboutSectionProps) {
|
||||
export function AboutSection({ address, chainId, description, homepageUrl, twitterName }: AboutSectionProps) {
|
||||
const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(true)
|
||||
const shouldTruncate = !!description && description.length > TRUNCATE_CHARACTER_COUNT
|
||||
|
||||
const tokenDescription = shouldTruncate && isDescriptionTruncated ? truncateDescription(description) : description
|
||||
|
||||
const baseExplorerUrl = getChainInfo(chainId).explorer
|
||||
|
||||
return (
|
||||
<AboutContainer>
|
||||
<AboutContainer data-testid="token-details-about-section">
|
||||
<AboutHeader>
|
||||
<Trans>About</Trans>
|
||||
</AboutHeader>
|
||||
@@ -99,7 +104,11 @@ export function AboutSection({ address, description, homepageUrl, twitterName }:
|
||||
<Trans>Links</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ResourcesContainer>
|
||||
<Resource name="Etherscan" link={`https://etherscan.io/address/${address}`} />
|
||||
<Resource
|
||||
data-testid="token-details-about-section-explorer-link"
|
||||
name={chainId === SupportedChainId.MAINNET ? 'Etherscan' : 'Block Explorer'}
|
||||
link={`${baseExplorerUrl}${address === 'NATIVE' ? '' : 'address/' + address}`}
|
||||
/>
|
||||
<Resource name="More analytics" link={`https://info.uniswap.org/#/tokens/${address}`} />
|
||||
{homepageUrl && <Resource name="Website" link={homepageUrl} />}
|
||||
{twitterName && <Resource name="Twitter" link={`https://twitter.com/${twitterName}`} />}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { Currency, SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const BalancesCard = styled.div`
|
||||
box-shadow: ${({ theme }) => theme.shallowShadow};
|
||||
@@ -14,9 +17,7 @@ const BalancesCard = styled.div`
|
||||
border-radius: 16px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
height: fit-content;
|
||||
line-height: 16px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -35,33 +36,65 @@ const BalanceRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 20px;
|
||||
justify-content: space-between;
|
||||
line-height: 28px;
|
||||
margin-top: 12px;
|
||||
margin-top: 20px;
|
||||
`
|
||||
const BalanceItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const BalanceContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const BalanceAmountsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const StyledNetworkLabel = styled.div`
|
||||
color: ${({ color }) => color};
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
`
|
||||
|
||||
export default function BalanceSummary({ token }: { token: Currency }) {
|
||||
const { account } = useWeb3React()
|
||||
const { account, chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
const { label, color } = getChainInfo(isSupportedChain(chainId) ? chainId : SupportedChainId.MAINNET)
|
||||
const balance = useCurrencyBalance(account, token)
|
||||
const formattedBalance = formatCurrencyAmount(balance, NumberType.TokenNonTx)
|
||||
const formattedUsdValue = formatCurrencyAmount(useStablecoinValue(balance), NumberType.FiatTokenStats)
|
||||
|
||||
if (!account || !balance) return null
|
||||
if (!account || !balance) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<BalancesCard>
|
||||
<BalanceSection>
|
||||
<Trans>Your balance</Trans>
|
||||
<ThemedText.SubHeaderSmall color={theme.textPrimary}>
|
||||
<Trans>Your balance on {label}</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<BalanceRow>
|
||||
<BalanceItem>
|
||||
<CurrencyLogo currency={token} />
|
||||
{formattedBalance} {token.symbol}
|
||||
</BalanceItem>
|
||||
<BalanceItem>{formattedUsdValue}</BalanceItem>
|
||||
<CurrencyLogo currency={token} size="2rem" />
|
||||
<BalanceContainer>
|
||||
<BalanceAmountsContainer>
|
||||
<BalanceItem>
|
||||
<ThemedText.SubHeader>
|
||||
{formattedBalance} {token.symbol}
|
||||
</ThemedText.SubHeader>
|
||||
</BalanceItem>
|
||||
<BalanceItem>
|
||||
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
|
||||
</BalanceItem>
|
||||
</BalanceAmountsContainer>
|
||||
<StyledNetworkLabel color={color}>{label}</StyledNetworkLabel>
|
||||
</BalanceContainer>
|
||||
</BalanceRow>
|
||||
</BalanceSection>
|
||||
</BalancesCard>
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import { ChartContainer, LoadingChart } from 'components/Tokens/TokenDetails/Skeleton'
|
||||
import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
|
||||
import { TokenPriceQuery } from 'graphql/data/TokenPrice'
|
||||
import { isPricePoint, PricePoint } from 'graphql/data/util'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { pageTimePeriodAtom } from 'pages/TokenDetails'
|
||||
import { startTransition, Suspense, useMemo } from 'react'
|
||||
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
|
||||
|
||||
import { PriceChart } from './PriceChart'
|
||||
import TimePeriodSelector from './TimeSelector'
|
||||
|
||||
function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPriceQuery>): PricePoint[] | undefined {
|
||||
const queryData = usePreloadedQuery(tokenPriceQuery, priceQueryReference)
|
||||
|
||||
function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
|
||||
// Appends the current price to the end of the priceHistory array
|
||||
const priceHistory = useMemo(() => {
|
||||
const market = queryData.tokens?.[0]?.market
|
||||
const market = tokenPriceData.tokens?.[0]?.market
|
||||
const priceHistory = market?.priceHistory?.filter(isPricePoint)
|
||||
const currentPrice = market?.price?.value
|
||||
if (Array.isArray(priceHistory) && currentPrice !== undefined) {
|
||||
@@ -24,39 +21,39 @@ function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPr
|
||||
return [...priceHistory, { timestamp, value: currentPrice }]
|
||||
}
|
||||
return priceHistory
|
||||
}, [queryData])
|
||||
}, [tokenPriceData])
|
||||
|
||||
return priceHistory
|
||||
}
|
||||
export default function ChartSection({
|
||||
priceQueryReference,
|
||||
refetchTokenPrices,
|
||||
tokenPriceQuery,
|
||||
onChangeTimePeriod,
|
||||
}: {
|
||||
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined
|
||||
refetchTokenPrices: RefetchPricesFunction
|
||||
tokenPriceQuery?: TokenPriceQuery
|
||||
onChangeTimePeriod: OnChangeTimePeriod
|
||||
}) {
|
||||
if (!priceQueryReference) {
|
||||
if (!tokenPriceQuery) {
|
||||
return <LoadingChart />
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingChart />}>
|
||||
<ChartContainer>
|
||||
<Chart priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} />
|
||||
<Chart tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
|
||||
</ChartContainer>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export type RefetchPricesFunction = (t: TimePeriod) => void
|
||||
export type OnChangeTimePeriod = (t: TimePeriod) => void
|
||||
function Chart({
|
||||
priceQueryReference,
|
||||
refetchTokenPrices,
|
||||
tokenPriceQuery,
|
||||
onChangeTimePeriod,
|
||||
}: {
|
||||
priceQueryReference: PreloadedQuery<TokenPriceQuery>
|
||||
refetchTokenPrices: RefetchPricesFunction
|
||||
tokenPriceQuery: TokenPriceQuery
|
||||
onChangeTimePeriod: OnChangeTimePeriod
|
||||
}) {
|
||||
const prices = usePreloadedTokenPriceQuery(priceQueryReference)
|
||||
const prices = usePriceHistory(tokenPriceQuery)
|
||||
// Initializes time period to global & maintain separate time period for subsequent changes
|
||||
const timePeriod = useAtomValue(pageTimePeriodAtom)
|
||||
|
||||
@@ -68,7 +65,7 @@ function Chart({
|
||||
<TimePeriodSelector
|
||||
currentTimePeriod={timePeriod}
|
||||
onTimeChange={(t: TimePeriod) => {
|
||||
startTransition(() => refetchTokenPrices(t))
|
||||
startTransition(() => onChangeTimePeriod(t))
|
||||
}}
|
||||
/>
|
||||
</ChartContainer>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { Info } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const InfoTipContainer = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
cursor: help;
|
||||
`
|
||||
|
||||
const InfoTipBody = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
`
|
||||
|
||||
const InfoTipWrapper = styled.div`
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function InfoTip({ text }: { text: ReactNode; size?: number }) {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
return (
|
||||
<InfoTipWrapper>
|
||||
<Tooltip text={<InfoTipBody>{text}</InfoTipBody>} show={show} placement="right">
|
||||
<InfoTipContainer onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Info size={14} />
|
||||
</InfoTipContainer>
|
||||
</Tooltip>
|
||||
</InfoTipWrapper>
|
||||
)
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const TokenDetailsLayout = styled.div`
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.sm}px) {
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
padding: 0 16px 52px;
|
||||
}
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||
gap: 40px;
|
||||
@@ -222,7 +222,7 @@ export default function TokenDetailsSkeleton() {
|
||||
const { chainName } = useParams<{ chainName?: string }>()
|
||||
return (
|
||||
<LeftPanel>
|
||||
<BreadcrumbNavLink to={{ chainName } ? `/tokens/${chainName}` : `/explore`}>
|
||||
<BreadcrumbNavLink to={chainName ? `/tokens/${chainName}` : `/explore`}>
|
||||
<ArrowLeft size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
@@ -7,16 +8,12 @@ import { textFadeIn } from 'theme/styles'
|
||||
|
||||
import { TokenSortMethod } from '../state'
|
||||
import { HEADER_DESCRIPTIONS } from '../TokenTable/TokenRow'
|
||||
import InfoTip from './InfoTip'
|
||||
|
||||
export const StatWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
min-width: 168px;
|
||||
flex: 1;
|
||||
gap: 4px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
const TokenStatsSection = styled.div`
|
||||
@@ -32,12 +29,9 @@ export const StatPair = styled.div`
|
||||
const Header = styled(ThemedText.MediumHeader)`
|
||||
font-size: 28px !important;
|
||||
`
|
||||
const StatTitle = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
`
|
||||
const StatPrice = styled.span`
|
||||
|
||||
const StatPrice = styled.div`
|
||||
margin-top: 4px;
|
||||
font-size: 28px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
@@ -51,24 +45,10 @@ export const StatsWrapper = styled.div`
|
||||
|
||||
type NumericStat = number | undefined | null
|
||||
|
||||
function Stat({
|
||||
value,
|
||||
title,
|
||||
description,
|
||||
isPrice = false,
|
||||
}: {
|
||||
value: NumericStat
|
||||
title: ReactNode
|
||||
description?: ReactNode
|
||||
isPrice?: boolean
|
||||
}) {
|
||||
function Stat({ value, title, description }: { value: NumericStat; title: ReactNode; description?: ReactNode }) {
|
||||
return (
|
||||
<StatWrapper>
|
||||
<StatTitle>
|
||||
{title}
|
||||
{description && <InfoTip text={description}></InfoTip>}
|
||||
</StatTitle>
|
||||
|
||||
<MouseoverTooltip text={description}>{title}</MouseoverTooltip>
|
||||
<StatPrice>{formatNumber(value, NumberType.FiatTokenStats)}</StatPrice>
|
||||
</StatWrapper>
|
||||
)
|
||||
@@ -106,8 +86,8 @@ export default function StatsSection(props: StatsSectionProps) {
|
||||
/>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} isPrice={true} />
|
||||
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} isPrice={true} />
|
||||
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} />
|
||||
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} />
|
||||
</StatPair>
|
||||
</TokenStatsSection>
|
||||
</StatsWrapper>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { PageName } from '@uniswap/analytics-events'
|
||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
@@ -27,21 +27,20 @@ import Widget from 'components/Widget'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { TokenPriceQuery } from 'graphql/data/__generated__/TokenPriceQuery.graphql'
|
||||
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
|
||||
import { QueryToken, tokenQuery } from 'graphql/data/Token'
|
||||
import { QueryToken } from 'graphql/data/Token'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
|
||||
import { useCallback, useMemo, useState, useTransition } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isAddress } from 'utils'
|
||||
|
||||
import { RefetchPricesFunction } from './ChartSection'
|
||||
import { OnChangeTimePeriod } from './ChartSection'
|
||||
import InvalidTokenDetails from './InvalidTokenDetails'
|
||||
|
||||
const TokenSymbol = styled.span`
|
||||
@@ -75,7 +74,7 @@ function useRelevantToken(
|
||||
const queryToken = useMemo(() => {
|
||||
if (!address) return undefined
|
||||
if (address === NATIVE_CHAIN_ID) return nativeOnChain(pageChainId)
|
||||
if (tokenQueryData) return new QueryToken(tokenQueryData)
|
||||
if (tokenQueryData) return new QueryToken(address, tokenQueryData)
|
||||
return undefined
|
||||
}, [pageChainId, address, tokenQueryData])
|
||||
// fetches on-chain token if query data is missing and page chain matches global chain (else fetch won't work)
|
||||
@@ -91,16 +90,16 @@ function useRelevantToken(
|
||||
type TokenDetailsProps = {
|
||||
urlAddress: string | undefined
|
||||
chain: Chain
|
||||
tokenQueryReference: PreloadedQuery<TokenQuery>
|
||||
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined
|
||||
refetchTokenPrices: RefetchPricesFunction
|
||||
tokenQuery: TokenQuery
|
||||
tokenPriceQuery: TokenPriceQuery | undefined
|
||||
onChangeTimePeriod: OnChangeTimePeriod
|
||||
}
|
||||
export default function TokenDetails({
|
||||
urlAddress,
|
||||
chain,
|
||||
tokenQueryReference,
|
||||
priceQueryReference,
|
||||
refetchTokenPrices,
|
||||
tokenQuery,
|
||||
tokenPriceQuery,
|
||||
onChangeTimePeriod,
|
||||
}: TokenDetailsProps) {
|
||||
if (!urlAddress) {
|
||||
throw new Error('Invalid token details route: tokenAddress param is undefined')
|
||||
@@ -112,7 +111,7 @@ export default function TokenDetails({
|
||||
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
|
||||
const tokenQueryData = usePreloadedQuery(tokenQuery, tokenQueryReference).tokens?.[0]
|
||||
const tokenQueryData = tokenQuery.tokens?.[0]
|
||||
const crossChainMap = useMemo(
|
||||
() =>
|
||||
tokenQueryData?.project?.tokens.reduce((map, current) => {
|
||||
@@ -177,7 +176,7 @@ export default function TokenDetails({
|
||||
}
|
||||
return (
|
||||
<Trace
|
||||
page={PageName.TOKEN_DETAILS_PAGE}
|
||||
page={InterfacePageName.TOKEN_DETAILS_PAGE}
|
||||
properties={{ tokenAddress: address, tokenName: token?.name }}
|
||||
shouldLogImpression
|
||||
>
|
||||
@@ -200,25 +199,22 @@ export default function TokenDetails({
|
||||
<ShareButton currency={token} />
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartSection priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} />
|
||||
<ChartSection tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
|
||||
<StatsSection
|
||||
TVL={tokenQueryData?.market?.totalValueLocked?.value}
|
||||
volume24H={tokenQueryData?.market?.volume24H?.value}
|
||||
priceHigh52W={tokenQueryData?.market?.priceHigh52W?.value}
|
||||
priceLow52W={tokenQueryData?.market?.priceLow52W?.value}
|
||||
/>
|
||||
{!token.isNative && (
|
||||
<>
|
||||
<Hr />
|
||||
<AboutSection
|
||||
address={address}
|
||||
description={tokenQueryData?.project?.description}
|
||||
homepageUrl={tokenQueryData?.project?.homepageUrl}
|
||||
twitterName={tokenQueryData?.project?.twitterName}
|
||||
/>
|
||||
<AddressSection address={address} />
|
||||
</>
|
||||
)}
|
||||
<Hr />
|
||||
<AboutSection
|
||||
address={address}
|
||||
chainId={pageChainId}
|
||||
description={tokenQueryData?.project?.description}
|
||||
homepageUrl={tokenQueryData?.project?.homepageUrl}
|
||||
twitterName={tokenQueryData?.project?.twitterName}
|
||||
/>
|
||||
{!token.isNative && <AddressSection address={address} />}
|
||||
</LeftPanel>
|
||||
) : (
|
||||
<TokenDetailsSkeleton />
|
||||
|
||||
@@ -103,7 +103,12 @@ export default function NetworkFilter() {
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<NetworkFilterOption onClick={toggleMenu} aria-label="networkFilter" active={open}>
|
||||
<NetworkFilterOption
|
||||
onClick={toggleMenu}
|
||||
aria-label="networkFilter"
|
||||
active={open}
|
||||
data-testid="tokens-network-filter-selected"
|
||||
>
|
||||
<StyledMenuContent>
|
||||
<NetworkLabel>
|
||||
<Logo src={chainInfo?.logoUrl} /> {chainInfo?.label}
|
||||
@@ -125,6 +130,7 @@ export default function NetworkFilter() {
|
||||
return (
|
||||
<InternalLinkMenuItem
|
||||
key={network}
|
||||
data-testid={`tokens-network-filter-option-${network.toLowerCase()}`}
|
||||
onClick={() => {
|
||||
navigate(`/tokens/${network.toLowerCase()}`)
|
||||
toggleMenu()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import searchIcon from 'assets/svg/search.svg'
|
||||
import xIcon from 'assets/svg/x.svg'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
@@ -80,8 +80,8 @@ export default function SearchBar() {
|
||||
render={({ translation }) => (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onFocus]}
|
||||
name={EventName.EXPLORE_SEARCH_SELECTED}
|
||||
element={ElementName.EXPLORE_SEARCH_INPUT}
|
||||
name={InterfaceEventName.EXPLORE_SEARCH_SELECTED}
|
||||
element={InterfaceElementName.EXPLORE_SEARCH_INPUT}
|
||||
>
|
||||
<SearchInput
|
||||
type="search"
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { EventName } from '@uniswap/analytics-events'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { formatNumber, formatUSDPrice, NumberType } from '@uniswap/conedison/format'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
import { CSSProperties, ReactNode } from 'react'
|
||||
import { ArrowDown, ArrowUp } from 'react-feather'
|
||||
import { ArrowDown, ArrowUp, Info } from 'react-feather'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
import { ClickableStyle } from 'theme'
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
TokenSortMethod,
|
||||
useSetSortMethod,
|
||||
} from '../state'
|
||||
import InfoTip from '../TokenDetails/InfoTip'
|
||||
import { ArrowCell, DeltaText, formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
|
||||
|
||||
const Cell = styled.div`
|
||||
@@ -200,7 +200,6 @@ const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
|
||||
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'unset')};
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
@@ -297,6 +296,13 @@ export const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const InfoIconContainer = styled.div`
|
||||
margin-left: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: help;
|
||||
`
|
||||
|
||||
export const HEADER_DESCRIPTIONS: Record<TokenSortMethod, ReactNode | undefined> = {
|
||||
[TokenSortMethod.PRICE]: undefined,
|
||||
[TokenSortMethod.PERCENT_CHANGE]: undefined,
|
||||
@@ -333,7 +339,13 @@ function HeaderCell({
|
||||
</>
|
||||
)}
|
||||
{category}
|
||||
{description && <InfoTip text={description}></InfoTip>}
|
||||
{description && (
|
||||
<MouseoverTooltip text={description} placement="right">
|
||||
<InfoIconContainer>
|
||||
<Info size={14} />
|
||||
</InfoIconContainer>
|
||||
</MouseoverTooltip>
|
||||
)}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
@@ -459,8 +471,10 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
return (
|
||||
<div ref={ref} data-testid={`token-table-row-${tokenName}`}>
|
||||
<StyledLink
|
||||
to={getTokenDetailsURL(token.address, token.chain)}
|
||||
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
|
||||
to={getTokenDetailsURL(token.address ?? '', token.chain)}
|
||||
onClick={() =>
|
||||
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
|
||||
}
|
||||
>
|
||||
<TokenRow
|
||||
header={false}
|
||||
@@ -512,7 +526,6 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
height={height}
|
||||
tokenData={token}
|
||||
pricePercentChange={token.market?.pricePercentChange?.value}
|
||||
timePeriod={timePeriod}
|
||||
sparklineMap={props.sparklineMap}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ const LoadingRows = ({ rowCount }: { rowCount: number }) => (
|
||||
</>
|
||||
)
|
||||
|
||||
export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
@@ -75,14 +75,15 @@ export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenTable({ setRowCount }: { setRowCount: (c: number) => void }) {
|
||||
export default function TokenTable() {
|
||||
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
|
||||
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
|
||||
const { tokens, sparklines } = useTopTokens(chainName)
|
||||
setRowCount(tokens?.length ?? PAGE_SIZE)
|
||||
const { tokens, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
|
||||
/* loading and error state */
|
||||
if (!tokens) {
|
||||
if (loadingTokens) {
|
||||
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||
} else if (!tokens) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
|
||||
@@ -4,4 +4,4 @@ export const LARGE_MEDIA_BREAKPOINT = '840px'
|
||||
export const MEDIUM_MEDIA_BREAKPOINT = '720px'
|
||||
export const SMALL_MEDIA_BREAKPOINT = '540px'
|
||||
export const MOBILE_MEDIA_BREAKPOINT = '420px'
|
||||
export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'
|
||||
// export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'
|
||||
|
||||
@@ -8,7 +8,11 @@ export const TooltipContainer = styled.div`
|
||||
max-width: 256px;
|
||||
cursor: default;
|
||||
padding: 0.6rem 1rem;
|
||||
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
word-break: break-word;
|
||||
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
@@ -56,7 +60,7 @@ function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps)
|
||||
/** Standard text tooltip. */
|
||||
export function MouseoverTooltip({ text, disableHover, children, timeout, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const open = useCallback(() => text && setShow(true), [text, setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import FiatOnrampModal from 'components/FiatOnrampModal'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import NftExploreBanner from 'nft/components/nftExploreBanner/NftExploreBanner'
|
||||
import { lazy } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
|
||||
@@ -17,21 +18,18 @@ export default function TopLevelModals() {
|
||||
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
|
||||
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
|
||||
const { account } = useWeb3React()
|
||||
const location = useLocation()
|
||||
const pageShowsNftPromoBanner =
|
||||
location.pathname.startsWith('/swap') ||
|
||||
location.pathname.startsWith('/tokens') ||
|
||||
location.pathname.startsWith('/pool')
|
||||
useAccountRiskCheck(account)
|
||||
const open = Boolean(blockedAccountModalOpen && account)
|
||||
const accountBlocked = Boolean(blockedAccountModalOpen && account)
|
||||
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
|
||||
<ConnectedAccountBlocked account={account} isOpen={open} />
|
||||
<ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
|
||||
<Bag />
|
||||
<TransactionCompleteModal />
|
||||
<AirdropModal />
|
||||
{pageShowsNftPromoBanner && <NftExploreBanner />}
|
||||
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ export default function TransactionConfirmationModal({
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
|
||||
<Modal isOpen={isOpen} $scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
|
||||
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
|
||||
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
|
||||
) : attemptingTxn ? (
|
||||
|
||||
@@ -1,33 +1,71 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import ms from 'ms.macro'
|
||||
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Copy, ExternalLink, Power } from 'react-feather'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { useCurrencyBalanceString } from 'state/connection/hooks'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { useFiatOnrampAck } from 'state/user/hooks'
|
||||
import { updateSelectedWallet } from 'state/user/reducer'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import styled, { css, keyframes } from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
|
||||
import { shortenAddress } from '../../nft/utils/address'
|
||||
import { useCloseModal, useToggleModal } from '../../state/application/hooks'
|
||||
import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import { ButtonEmphasis, ButtonSize, ThemeButton } from '../Button'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import IconButton, { IconHoverText } from './IconButton'
|
||||
|
||||
const BuyCryptoButtonBorderKeyframes = keyframes`
|
||||
0% {
|
||||
border-color: transparent;
|
||||
}
|
||||
33% {
|
||||
border-color: hsla(225, 95%, 63%, 1);
|
||||
}
|
||||
66% {
|
||||
border-color: hsla(267, 95%, 63%, 1);
|
||||
}
|
||||
100% {
|
||||
border-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
const BuyCryptoButton = styled(ThemeButton)<{ $animateBorder: boolean }>`
|
||||
border-color: transparent;
|
||||
border-radius: 12px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
height: 40px;
|
||||
margin-top: 12px;
|
||||
animation-direction: alternate;
|
||||
animation-duration: ${({ theme }) => theme.transition.duration.slow};
|
||||
animation-fill-mode: none;
|
||||
animation-iteration-count: 2;
|
||||
animation-name: ${BuyCryptoButtonBorderKeyframes};
|
||||
animation-play-state: ${({ $animateBorder }) => ($animateBorder ? 'running' : 'paused')};
|
||||
animation-timing-function: ${({ theme }) => theme.transition.timing.inOut};
|
||||
`
|
||||
const WalletButton = styled(ThemeButton)`
|
||||
border-radius: 12px;
|
||||
padding-top: 10px;
|
||||
@@ -75,7 +113,20 @@ const USDText = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const FiatOnrampNotAvailableText = styled(ThemedText.Caption)`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
margin-left: 6px;
|
||||
width: 14px;
|
||||
`
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
@@ -108,7 +159,14 @@ const AccountContainer = styled(ThemedText.BodySmall)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-top: 2.5px;
|
||||
`
|
||||
|
||||
const StyledInfoIcon = styled(Info)`
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
flex: 1 1 auto;
|
||||
`
|
||||
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
|
||||
fill: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
const BalanceWrapper = styled.div`
|
||||
padding: 16px 0;
|
||||
`
|
||||
@@ -136,8 +194,7 @@ const AuthenticatedHeader = () => {
|
||||
explorer,
|
||||
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
|
||||
const navigate = useNavigate()
|
||||
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
|
||||
|
||||
const closeModal = useCloseModal()
|
||||
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
|
||||
const resetSellAssets = useSellAsset((state) => state.reset)
|
||||
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
|
||||
@@ -147,7 +204,7 @@ const AuthenticatedHeader = () => {
|
||||
const isUnclaimed = useUserHasAvailableClaim(account)
|
||||
const connectionType = getConnection(connector).type
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined) || 0
|
||||
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined)
|
||||
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
|
||||
const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM)
|
||||
const disconnect = useCallback(() => {
|
||||
@@ -159,18 +216,68 @@ const AuthenticatedHeader = () => {
|
||||
}, [connector, dispatch])
|
||||
|
||||
const amountUSD = useMemo(() => {
|
||||
if (!nativeCurrencyPrice || !balanceString) return undefined
|
||||
const price = parseFloat(nativeCurrencyPrice.toFixed(5))
|
||||
const balance = parseFloat(balanceString || '0')
|
||||
const balance = parseFloat(balanceString)
|
||||
return price * balance
|
||||
}, [balanceString, nativeCurrencyPrice])
|
||||
|
||||
const navigateToProfile = () => {
|
||||
const navigateToProfile = useCallback(() => {
|
||||
resetSellAssets()
|
||||
setSellPageState(ProfilePageStateType.VIEWING)
|
||||
clearCollectionFilters()
|
||||
navigate('/nfts/profile')
|
||||
closeModal()
|
||||
}
|
||||
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
|
||||
|
||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||
// animate the border of the buy crypto button when a user navigates here from the feature announcement
|
||||
// can be removed when components/FiatOnrampAnnouncment.tsx is no longer used
|
||||
const [acknowledgements, acknowledge] = useFiatOnrampAck()
|
||||
const animateBuyCryptoButtonBorder = acknowledgements?.user && !acknowledgements.system
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
let timeoutId = 0
|
||||
if (animateBuyCryptoButtonBorder) {
|
||||
timeoutId = setTimeout(() => {
|
||||
if (stale) return
|
||||
acknowledge({ system: true })
|
||||
}, ms`2 seconds`) as unknown as number
|
||||
// as unknown as number is necessary so it's not incorrectly typed as a NodeJS.Timeout
|
||||
}
|
||||
return () => {
|
||||
stale = true
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}, [acknowledge, animateBuyCryptoButtonBorder])
|
||||
|
||||
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
|
||||
const openFoRModalWithAnalytics = useCallback(() => {
|
||||
sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_WIDGET_OPENED)
|
||||
openFiatOnrampModal()
|
||||
}, [openFiatOnrampModal])
|
||||
|
||||
const [shouldCheck, setShouldCheck] = useState(false)
|
||||
const {
|
||||
available: fiatOnrampAvailable,
|
||||
availabilityChecked: fiatOnrampAvailabilityChecked,
|
||||
error,
|
||||
loading: fiatOnrampAvailabilityLoading,
|
||||
} = useFiatOnrampAvailability(shouldCheck, openFoRModalWithAnalytics)
|
||||
|
||||
const handleBuyCryptoClick = useCallback(() => {
|
||||
if (!fiatOnrampAvailabilityChecked) {
|
||||
setShouldCheck(true)
|
||||
} else if (fiatOnrampAvailable) {
|
||||
openFoRModalWithAnalytics()
|
||||
}
|
||||
}, [fiatOnrampAvailabilityChecked, fiatOnrampAvailable, openFoRModalWithAnalytics])
|
||||
const disableBuyCryptoButton = Boolean(
|
||||
error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading
|
||||
)
|
||||
const [showFiatOnrampUnavailableTooltip, setShow] = useState<boolean>(false)
|
||||
const openFiatOnrampUnavailableTooltip = useCallback(() => setShow(true), [setShow])
|
||||
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
return (
|
||||
<AuthenticatedHeaderWrapper>
|
||||
@@ -192,7 +299,7 @@ const AuthenticatedHeader = () => {
|
||||
<IconButton onClick={copy} Icon={Copy}>
|
||||
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
||||
</IconButton>
|
||||
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLink}>
|
||||
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLinkIcon}>
|
||||
<Trans>Explore</Trans>
|
||||
</IconButton>
|
||||
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
|
||||
@@ -205,7 +312,7 @@ const AuthenticatedHeader = () => {
|
||||
<Text fontSize={36} fontWeight={400}>
|
||||
{balanceString} {nativeCurrencySymbol}
|
||||
</Text>
|
||||
<USDText>${amountUSD.toFixed(2)} USD</USDText>
|
||||
{amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
|
||||
</BalanceWrapper>
|
||||
<ProfileButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
@@ -215,6 +322,44 @@ const AuthenticatedHeader = () => {
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</ProfileButton>
|
||||
{fiatOnrampFlag === BaseVariant.Enabled && (
|
||||
<>
|
||||
<BuyCryptoButton
|
||||
$animateBorder={animateBuyCryptoButtonBorder}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
onClick={handleBuyCryptoClick}
|
||||
disabled={disableBuyCryptoButton}
|
||||
>
|
||||
{error ? (
|
||||
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||
) : (
|
||||
<>
|
||||
{fiatOnrampAvailabilityLoading ? <StyledLoadingButtonSpinner /> : <CreditCard />}{' '}
|
||||
<Trans>Buy crypto</Trans>
|
||||
</>
|
||||
)}
|
||||
</BuyCryptoButton>
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
<Tooltip
|
||||
show={showFiatOnrampUnavailableTooltip}
|
||||
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
|
||||
>
|
||||
<FiatOnrampAvailabilityExternalLink
|
||||
onMouseEnter={openFiatOnrampUnavailableTooltip}
|
||||
onMouseLeave={closeFiatOnrampUnavailableTooltip}
|
||||
style={{ color: 'inherit' }}
|
||||
href="https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-"
|
||||
>
|
||||
<StyledInfoIcon />
|
||||
</FiatOnrampAvailabilityExternalLink>
|
||||
</Tooltip>
|
||||
</FiatOnrampNotAvailableText>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isUnclaimed && (
|
||||
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
|
||||
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import React from 'react'
|
||||
import { Check } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -116,9 +116,9 @@ export default function Option({
|
||||
const content = (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.WALLET_SELECTED}
|
||||
name={InterfaceEventName.WALLET_SELECTED}
|
||||
properties={{ wallet_type: header }}
|
||||
element={ElementName.WALLET_TYPE_OPTION}
|
||||
element={InterfaceElementName.WALLET_TYPE_OPTION}
|
||||
>
|
||||
<OptionCardClickable
|
||||
id={id}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import * as connectionUtils from 'connection/utils'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
|
||||
import { nativeOnChain } from '../../constants/tokens'
|
||||
import { render, screen } from '../../test-utils'
|
||||
@@ -20,7 +18,7 @@ jest.mock('utils/userAgent', () => ({
|
||||
|
||||
jest.mock('.../../state/application/hooks', () => {
|
||||
return {
|
||||
useModalIsOpen: (_modal: ApplicationModal) => true,
|
||||
useModalIsOpen: () => true,
|
||||
useToggleWalletModal: () => {
|
||||
return
|
||||
},
|
||||
@@ -29,7 +27,7 @@ jest.mock('.../../state/application/hooks', () => {
|
||||
|
||||
jest.mock('hooks/useStablecoinPrice', () => {
|
||||
return {
|
||||
useStablecoinValue: (_currencyAmount: CurrencyAmount<Currency> | undefined | null) => {
|
||||
useStablecoinValue: () => {
|
||||
return
|
||||
},
|
||||
}
|
||||
@@ -38,10 +36,10 @@ jest.mock('hooks/useStablecoinPrice', () => {
|
||||
jest.mock('lib/hooks/useCurrencyBalance', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: (account?: string, currency?: Currency) => {
|
||||
default: () => {
|
||||
return
|
||||
},
|
||||
useTokenBalance: (account?: string, token?: Token) => {
|
||||
useTokenBalance: () => {
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, user } from '@uniswap/analytics'
|
||||
import { CustomUserProperties, EventName, WalletConnectionResult } from '@uniswap/analytics-events'
|
||||
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
@@ -125,7 +125,7 @@ const sendAnalyticsEventAndUserInfo = (
|
||||
chainId: number | undefined,
|
||||
isReconnect: boolean
|
||||
) => {
|
||||
sendAnalyticsEvent(EventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
result: WalletConnectionResult.SUCCEEDED,
|
||||
wallet_address: account,
|
||||
wallet_type: walletType,
|
||||
@@ -236,7 +236,7 @@ export default function WalletModal({
|
||||
console.debug(`web3-react connection error: ${error}`)
|
||||
dispatch(updateConnectionError({ connectionType, error: error.message }))
|
||||
|
||||
sendAnalyticsEvent(EventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
result: WalletConnectionResult.FAILED,
|
||||
wallet_type: getConnectionName(connectionType, getIsMetaMask()),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { Connection } from 'connection'
|
||||
import { ConnectionType, setMetMaskErrorHandler } from 'connection'
|
||||
import { getConnectionName } from 'connection/utils'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
@@ -8,8 +9,19 @@ import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/tra
|
||||
import useEagerlyConnect from 'hooks/useEagerlyConnect'
|
||||
import useOrderedConnections from 'hooks/useOrderedConnections'
|
||||
import { ReactNode, useEffect, useMemo } from 'react'
|
||||
import { updateConnectionError } from 'state/connection/reducer'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Set metamask error handler for metamask disconnection warning modal.
|
||||
useEffect(() => {
|
||||
setMetMaskErrorHandler((error: Error) =>
|
||||
dispatch(updateConnectionError({ connectionType: ConnectionType.INJECTED, error: error.message }))
|
||||
)
|
||||
}, [dispatch])
|
||||
|
||||
useEagerlyConnect()
|
||||
const connections = useOrderedConnections()
|
||||
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
|
||||
|
||||
97
src/components/Web3Status/MetamaskConnectionError.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Modal from 'components/Modal'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CloseIcon, ThemedText } from 'theme'
|
||||
|
||||
import { useModalIsOpen, useToggleMetamaskConnectionErrorModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding: 32px 32px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const ShortColumn = styled(AutoColumn)`
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const InfoText = styled(Text)`
|
||||
padding: 0 12px 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const StyledButton = styled(ButtonPrimary)`
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const WarningIcon = styled(AlertTriangle)`
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 28px;
|
||||
stroke-width: 1px;
|
||||
margin-right: 4px;
|
||||
color: ${({ theme }) => theme.accentCritical};
|
||||
`
|
||||
|
||||
const onReconnect = () => window.location.reload()
|
||||
|
||||
const header = 'Wallet disconnected'
|
||||
const description = 'A Metamask error caused your wallet to disconnect. Reload the page to reconnect.'
|
||||
|
||||
export default function MetamaskConnectionError() {
|
||||
const modalOpen = useModalIsOpen(ApplicationModal.METAMASK_CONNECTION_ERROR)
|
||||
const toggleModal = useToggleMetamaskConnectionErrorModal()
|
||||
|
||||
return (
|
||||
<Modal isOpen={modalOpen} onDismiss={toggleModal} minHeight={false} maxHeight={90}>
|
||||
<Wrapper>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<div />
|
||||
<CloseIcon onClick={toggleModal} />
|
||||
</RowBetween>
|
||||
<Container>
|
||||
<AutoColumn>
|
||||
<LogoContainer>
|
||||
<WarningIcon />
|
||||
</LogoContainer>
|
||||
</AutoColumn>
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
<ThemedText.HeadlineSmall marginBottom="8px">{header}</ThemedText.HeadlineSmall>
|
||||
<ThemedText.BodySmall>{description}</ThemedText.BodySmall>
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<StyledButton onClick={onReconnect}>
|
||||
<Trans>Reload</Trans>
|
||||
</StyledButton>
|
||||
</Container>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import WalletDropdown from 'components/WalletDropdown'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { getConnection, getIsMetaMask } from 'connection/utils'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
@@ -21,6 +22,7 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import {
|
||||
useCloseModal,
|
||||
useModalIsOpen,
|
||||
useToggleMetamaskConnectionErrorModal,
|
||||
useToggleWalletDropdown,
|
||||
useToggleWalletModal,
|
||||
} from '../../state/application/hooks'
|
||||
@@ -33,6 +35,7 @@ import StatusIcon from '../Identicon/StatusIcon'
|
||||
import Loader from '../Loader'
|
||||
import { RowBetween } from '../Row'
|
||||
import WalletModal from '../WalletModal'
|
||||
import MetamaskConnectionError from './MetamaskConnectionError'
|
||||
|
||||
// https://stackoverflow.com/a/31617326
|
||||
const FULL_BORDER_RADIUS = 9999
|
||||
@@ -205,11 +208,21 @@ function Web3StatusInner() {
|
||||
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
|
||||
const theme = useTheme()
|
||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||
const handleWalletDropdownClick = useCallback(() => {
|
||||
sendAnalyticsEvent(InterfaceEventName.ACCOUNT_DROPDOWN_BUTTON_CLICKED)
|
||||
toggleWalletDropdown()
|
||||
}, [toggleWalletDropdown])
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const toggleMetamaskConnectionErrorModal = useToggleMetamaskConnectionErrorModal()
|
||||
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
useEffect(() => {
|
||||
if (getIsMetaMask() && error) {
|
||||
toggleMetamaskConnectionErrorModal()
|
||||
}
|
||||
}, [error, toggleMetamaskConnectionErrorModal])
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
@@ -221,13 +234,12 @@ function Web3StatusInner() {
|
||||
const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash)
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
const toggleWallet = toggleWalletDropdown
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWallet}>
|
||||
<Web3StatusError onClick={handleWalletDropdownClick}>
|
||||
<NetworkIcon />
|
||||
<Text>
|
||||
<Trans>Error</Trans>
|
||||
@@ -243,7 +255,7 @@ function Web3StatusInner() {
|
||||
return (
|
||||
<Web3StatusConnected
|
||||
data-testid="web3-status-connected"
|
||||
onClick={toggleWallet}
|
||||
onClick={handleWalletDropdownClick}
|
||||
pending={hasPendingTransactions}
|
||||
isClaimAvailable={isClaimAvailable}
|
||||
>
|
||||
@@ -272,16 +284,16 @@ function Web3StatusInner() {
|
||||
return (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: validSwapQuote }}
|
||||
element={ElementName.CONNECT_WALLET_BUTTON}
|
||||
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<Web3StatusConnectWrapper faded={!account}>
|
||||
<StyledConnectButton data-testid="navbar-connect-wallet" onClick={toggleWalletModal}>
|
||||
<Trans>Connect</Trans>
|
||||
</StyledConnectButton>
|
||||
<VerticalDivider />
|
||||
<ChevronWrapper onClick={toggleWalletDropdown} data-testid="navbar-toggle-dropdown">
|
||||
<ChevronWrapper onClick={handleWalletDropdownClick} data-testid="navbar-toggle-dropdown">
|
||||
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
|
||||
</ChevronWrapper>
|
||||
</Web3StatusConnectWrapper>
|
||||
@@ -296,7 +308,7 @@ export default function Web3Status() {
|
||||
const allTransactions = useAllTransactions()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const walletRef = useRef<HTMLDivElement>(null)
|
||||
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
|
||||
const closeModal = useCloseModal()
|
||||
const isOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
|
||||
useOnClickOutside(ref, isOpen ? closeModal : undefined, [walletRef])
|
||||
@@ -312,7 +324,9 @@ export default function Web3Status() {
|
||||
return (
|
||||
<span ref={ref}>
|
||||
<Web3StatusInner />
|
||||
<FiatOnrampAnnouncement />
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
<MetamaskConnectionError />
|
||||
<Portal>
|
||||
<span ref={walletRef}>
|
||||
<WalletDropdown />
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { EventName, SectionName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
|
||||
import {
|
||||
InterfaceEventName,
|
||||
InterfaceSectionName,
|
||||
SwapEventName,
|
||||
SwapPriceUpdateUserResponse,
|
||||
} from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import {
|
||||
@@ -58,7 +63,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
[connector]
|
||||
)
|
||||
|
||||
const trace = useTrace({ section: SectionName.WIDGET })
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
const [initialQuoteDate, setInitialQuoteDate] = useState<Date>()
|
||||
const onInitialSwapQuote = useCallback(
|
||||
(trade: Trade<Currency, Currency, TradeType>) => {
|
||||
@@ -72,7 +77,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.SWAP_QUOTE_RECEIVED, eventProperties)
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, eventProperties)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
@@ -85,10 +90,10 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
token_address: getTokenAddress(input),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
}, [inputs.value.INPUT, trace])
|
||||
const onExpandSwapDetails = useCallback(() => {
|
||||
sendAnalyticsEvent(EventName.SWAP_DETAILS_EXPANDED, { ...trace })
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_DETAILS_EXPANDED, { ...trace })
|
||||
}, [trace])
|
||||
const onSwapPriceUpdateAck = useCallback(
|
||||
(stale: Trade<Currency, Currency, TradeType>, update: Trade<Currency, Currency, TradeType>) => {
|
||||
@@ -100,7 +105,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
price_update_basis_points: getPriceUpdateBasisPoints(stale.executionPrice, update.executionPrice),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED, eventProperties)
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED, eventProperties)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
@@ -127,7 +132,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
swap_quote_block_number: undefined,
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(EventName.SWAP_SUBMITTED_BUTTON_CLICKED, eventProperties)
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED, eventProperties)
|
||||
},
|
||||
[initialQuoteDate, trace]
|
||||
)
|
||||
|
||||