Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ffc8a0bdf | ||
|
|
5ec9cdc5c4 | ||
|
|
4d85775d90 | ||
|
|
c1c59ca692 | ||
|
|
f29d97413e | ||
|
|
a078d94a38 | ||
|
|
c9c3329bc3 | ||
|
|
13d0b70fa8 | ||
|
|
b852e4e64a | ||
|
|
55bd3555be | ||
|
|
972a65066c | ||
|
|
39a212f762 | ||
|
|
c362f4fe39 | ||
|
|
271ef580e1 | ||
|
|
81ced4cb8b | ||
|
|
ab214a8133 | ||
|
|
1b2d86ae3a | ||
|
|
40cac44e07 | ||
|
|
4e6d28cff4 | ||
|
|
709fad0804 | ||
|
|
573f4c873a | ||
|
|
d300db669f | ||
|
|
fb8217ddea | ||
|
|
7b9a23d920 | ||
|
|
120ad935fa | ||
|
|
4eaf16b624 | ||
|
|
857e2915ab | ||
|
|
7410c81b42 | ||
|
|
fb05439d32 | ||
|
|
fb7eade70b | ||
|
|
bd2b2c487a | ||
|
|
2f004ed1d9 | ||
|
|
db257c73f2 | ||
|
|
7c37b9d00e | ||
|
|
7688c527f0 | ||
|
|
06dd41a9cd | ||
|
|
850fec40a9 | ||
|
|
3c7eabc3d8 |
19
.eslintrc.js
19
.eslintrc.js
@@ -4,4 +4,23 @@ require('@uniswap/eslint-config/load')
|
||||
|
||||
module.exports = {
|
||||
extends: '@uniswap/eslint-config/react',
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'zustand',
|
||||
importNames: ['default'],
|
||||
message: 'Default import from zustand is deprecated. Import `{ create }` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
59
.github/pull_request_template.md
vendored
59
.github/pull_request_template.md
vendored
@@ -1,24 +1,45 @@
|
||||
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
|
||||
<!-- Your PR title must follow conventional commits: https://github.com/Uniswap/interface#pr-title -->
|
||||
|
||||
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
|
||||
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
|
||||
- docs: Documentation only changes
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- perf: A code change that improves performance
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- test: Adding missing tests or correcting existing tests
|
||||
## Description
|
||||
<!-- Summary of change, including motivation and context. -->
|
||||
<!-- Use verb-driven language: "Fixes XYZ" instead of "This change fixes XYZ" -->
|
||||
|
||||
Example commit messages:
|
||||
|
||||
- feat: adds support for gnosis safe wallet
|
||||
- fix: removes a polling memory leak
|
||||
- chore: bumps redux version
|
||||
<!-- Delete inapplicable lines: -->
|
||||
_JIRA ticket:_
|
||||
_Slack thread:_
|
||||
_Relevant docs:_
|
||||
|
||||
Other things to note:
|
||||
|
||||
- Please describe the change using verb statements (ex: Removes X from Y)
|
||||
- PRs with multiple changes should use a list of verb statements
|
||||
- Add any relevant unit / integration tests
|
||||
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
|
||||
<!-- Delete this section if your change does not affect UI. -->
|
||||
## Screen capture
|
||||
|
||||
| Before | After (Desktop) | After (Mobile) |
|
||||
| ------------ |---------------- | -------------- |
|
||||
| paste_before | past_after | paste_after |
|
||||
|
||||
|
||||
## Test plan
|
||||
|
||||
<!-- Delete this section if your change is not a bug fix. -->
|
||||
### Reproducing the error
|
||||
|
||||
<!-- Include steps to reproduce the bug. -->
|
||||
1.
|
||||
|
||||
### QA (ie manual testing)
|
||||
|
||||
<!-- Include steps to test the change, ensuring no regression. -->
|
||||
- [ ] N/A
|
||||
|
||||
|
||||
#### Devices
|
||||
<!-- If applicable, include different devices and screen sizes that may be affected, and how you've tested them. -->
|
||||
|
||||
|
||||
### Automated testing
|
||||
|
||||
<!-- If N/A, check and note so it is obvious to your reviewers and does not show up as an incomplete task. -->
|
||||
<!-- eg - [x] Unit test N/A -->
|
||||
- [ ] Unit test
|
||||
- [ ] Integration/E2E test
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -42,6 +42,8 @@ jobs:
|
||||
needs: tag
|
||||
if: ${{ needs.tag.outputs.new_tag != null }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
|
||||
27
README.md
27
README.md
@@ -38,6 +38,33 @@ You can block an entire list of tokens by passing in a tokenlist like [here](./s
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
#### PR Title
|
||||
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
|
||||
|
||||
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
|
||||
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
|
||||
- docs: Documentation only changes
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- perf: A code change that improves performance
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- test: Adding missing tests or correcting existing tests
|
||||
|
||||
Example commit messages:
|
||||
|
||||
- feat: adds support for gnosis safe wallet
|
||||
- fix: removes a polling memory leak
|
||||
- chore: bumps redux version
|
||||
|
||||
Other things to note:
|
||||
|
||||
- Please describe the change using verb statements (ex: Removes X from Y)
|
||||
- PRs with multiple changes should use a list of verb statements
|
||||
- Add any relevant unit / integration tests
|
||||
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
|
||||
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
@@ -14,9 +14,8 @@ coverage:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 5%
|
||||
threshold: 1%
|
||||
if_ci_failed: error
|
||||
patch:
|
||||
default:
|
||||
enabled: no
|
||||
if_not_found: success
|
||||
target: 80%
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-env node */
|
||||
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const { DefinePlugin } = require('webpack')
|
||||
|
||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
||||
videoUploadOnPasses: false,
|
||||
defaultCommandTimeout: 24000, // 2x average block time
|
||||
chromeWebSecurity: false,
|
||||
retries: { runMode: 2 },
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
codeCoverageTask(on, config)
|
||||
|
||||
@@ -26,7 +26,15 @@ describe('Landing Page', () => {
|
||||
|
||||
it('allows navigation to pool', () => {
|
||||
cy.viewport(2000, 1600)
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('pool-nav-link')).first().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
|
||||
it('allows navigation to pool on mobile', () => {
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('pool-nav-link')).last().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -53,4 +53,11 @@ describe('Testing nfts', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
||||
})
|
||||
|
||||
it('should close the sidebar when navigating to NFT details', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-nfts')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('Token explore', () => {
|
||||
|
||||
it('should load token leaderboard', () => {
|
||||
cy.visit('/tokens/ethereum')
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.eq', 100)
|
||||
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.greaterThan', 0)
|
||||
// check sorted svg icon is present in volume cell, since tokens are sorted by volume by default
|
||||
cy.get(getTestSelector('header-row')).find(getTestSelector('volume-cell')).find('svg').should('exist')
|
||||
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
|
||||
|
||||
@@ -77,4 +77,12 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
|
||||
})
|
||||
|
||||
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
|
||||
visit(true)
|
||||
cy.viewport('iphone-6')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.root().click(15, 40)
|
||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
11
package.json
11
package.json
@@ -37,8 +37,12 @@
|
||||
"src/lib/utils/**/*.ts*",
|
||||
"src/pages/**/*.ts*",
|
||||
"src/state/**/*.ts*",
|
||||
"src/tracing/**/*.ts*",
|
||||
"src/utils/**/*.ts*"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".snap"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 4,
|
||||
@@ -66,6 +70,7 @@
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@typechain/ethers-v5": "^7.0.0",
|
||||
"@types/array.prototype.flat": "^1.2.1",
|
||||
"@types/array.prototype.flatmap": "^1.2.2",
|
||||
@@ -132,11 +137,11 @@
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@sentry/react": "^7.40.0",
|
||||
"@sentry/tracing": "^7.40.0",
|
||||
"@sentry/react": "^7.45.0",
|
||||
"@sentry/tracing": "^7.45.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "^1.3.1",
|
||||
"@uniswap/analytics-events": "^2.8.0",
|
||||
"@uniswap/analytics-events": "^2.9.0",
|
||||
"@uniswap/conedison": "^1.4.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
|
||||
@@ -30,13 +30,13 @@ import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import { useToggleWalletDrawer } from '.'
|
||||
import { useToggleAccountDrawer } from '.'
|
||||
import IconButton, { IconHoverText } from './IconButton'
|
||||
import MiniPortfolio from './MiniPortfolio'
|
||||
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
|
||||
|
||||
const AuthenticatedHeaderWrapper = styled.div`
|
||||
padding: 14px 12px 16px 16px;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -184,7 +184,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
dispatch(updateSelectedWallet({ wallet: undefined }))
|
||||
}, [connector, dispatch])
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
const navigateToProfile = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
@@ -287,6 +287,16 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
<LoadingBubble height="16px" width="100px" margin="4px 0 20px 0" />
|
||||
</Column>
|
||||
)}
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<HeaderButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
onClick={navigateToProfile}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</HeaderButton>
|
||||
)}
|
||||
<HeaderButton
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
@@ -306,16 +316,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
</>
|
||||
)}
|
||||
</HeaderButton>
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<HeaderButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
onClick={navigateToProfile}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</HeaderButton>
|
||||
)}
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
|
||||
import { TransactionStatus, useTransactionListQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { PollingInterval } from 'graphql/data/util'
|
||||
@@ -98,10 +98,10 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
const lastFetchedAtom = atom<number | undefined>(0)
|
||||
|
||||
export function ActivityTab({ account }: { account: string }) {
|
||||
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
|
||||
|
||||
const localMap = useLocalActivities()
|
||||
const localMap = useLocalActivities(account)
|
||||
|
||||
const { data, loading, refetch } = useTransactionListQuery({
|
||||
variables: { account },
|
||||
@@ -0,0 +1,209 @@
|
||||
// jest unit tests for the parseLocalActivity function
|
||||
|
||||
import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core'
|
||||
import { DAI as MockDAI, USDC_MAINNET as MockUSDC_MAINNET } from 'constants/tokens'
|
||||
import { TokenAddressMap } from 'state/lists/hooks'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import {
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
TransactionDetails,
|
||||
TransactionType,
|
||||
} from 'state/transactions/types'
|
||||
import { renderHook } from 'test-utils'
|
||||
|
||||
import { parseLocalActivity, useLocalActivities } from './parseLocal'
|
||||
|
||||
const oneUSDCRaw = '1000000'
|
||||
const oneDAIRaw = '1000000000000000000'
|
||||
|
||||
function mockSwapInfo(
|
||||
type: MockTradeType,
|
||||
inputCurrency: Token,
|
||||
inputCurrencyAmountRaw: string,
|
||||
outputCurrency: Token,
|
||||
outputCurrencyAmountRaw: string
|
||||
): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo {
|
||||
if (type === MockTradeType.EXACT_INPUT) {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
inputCurrencyAmountRaw,
|
||||
outputCurrencyId: outputCurrency.address,
|
||||
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
|
||||
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: MockTradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
outputCurrencyId: outputCurrency.address,
|
||||
outputCurrencyAmountRaw,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockAccount1 = '0x000000000000000000000000000000000000000001'
|
||||
const mockAccount2 = '0x000000000000000000000000000000000000000002'
|
||||
const mockChainId = SupportedChainId.MAINNET
|
||||
|
||||
jest.mock('../../../../state/transactions/hooks', () => {
|
||||
return {
|
||||
useMultichainTransactions: () => {
|
||||
return [
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x123',
|
||||
from: mockAccount1,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x456',
|
||||
from: mockAccount2,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x789',
|
||||
from: mockAccount2,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function mockTokenAddressMap(...tokens: WrappedTokenInfo[]): TokenAddressMap {
|
||||
return {
|
||||
[SupportedChainId.MAINNET]: Object.fromEntries(tokens.map((token) => [token.address, { token }])),
|
||||
}
|
||||
}
|
||||
|
||||
describe('parseLocalActivity', () => {
|
||||
it('returns swap activity fields with known tokens, exact input', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = mockTokenAddressMap(MockUSDC_MAINNET as WrappedTokenInfo, MockDAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns swap activity fields with known tokens, exact output', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(MockTradeType.EXACT_OUTPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = mockTokenAddressMap(MockUSDC_MAINNET as WrappedTokenInfo, MockDAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: MockTradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
expectedInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
maximumInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
outputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns swap activity fields with unknown tokens', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = {} as TokenAddressMap
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [undefined, undefined],
|
||||
descriptor: 'Unknown for Unknown',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('only returns activity for the current account', () => {
|
||||
const account1Activites = renderHook(() => useLocalActivities(mockAccount1)).result.current
|
||||
const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current
|
||||
|
||||
expect(Object.values(account1Activites)).toHaveLength(1)
|
||||
expect(Object.values(account2Activites)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,6 @@ import { t } from '@lingui/macro'
|
||||
import { formatCurrencyAmount } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { nativeOnChain } from '@uniswap/smart-order-router'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TransactionPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useMemo } from 'react'
|
||||
@@ -26,20 +25,22 @@ import {
|
||||
import { getActivityTitle } from '../constants'
|
||||
import { Activity, ActivityMap } from './types'
|
||||
|
||||
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: TokenAddressMap) {
|
||||
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId][currencyId].token
|
||||
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: TokenAddressMap): Currency | undefined {
|
||||
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]?.token
|
||||
}
|
||||
|
||||
function buildCurrencyDescriptor(
|
||||
currencyA: Currency,
|
||||
currencyA: Currency | undefined,
|
||||
amtA: string,
|
||||
currencyB: Currency,
|
||||
currencyB: Currency | undefined,
|
||||
amtB: string,
|
||||
delimiter = t`for`
|
||||
) {
|
||||
const formattedA = formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyA, amtA))
|
||||
const formattedB = formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyB, amtB))
|
||||
return `${formattedA} ${currencyA.symbol} ${delimiter} ${formattedB} ${currencyB.symbol}`
|
||||
const formattedA = currencyA ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyA, amtA)) : t`Unknown`
|
||||
const symbolA = currencyA?.symbol ?? ''
|
||||
const formattedB = currencyB ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyB, amtB)) : t`Unknown`
|
||||
const symbolB = currencyB?.symbol ?? ''
|
||||
return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function parseSwap(
|
||||
@@ -79,7 +80,7 @@ function parseApproval(
|
||||
): Partial<Activity> {
|
||||
// TODO: Add 'amount' approved to ApproveTransactionInfo so we can distinguish between revoke and approve
|
||||
const currency = getCurrency(approval.tokenAddress, chainId, tokens)
|
||||
const descriptor = t`${currency.symbol ?? currency.name}`
|
||||
const descriptor = currency?.symbol ?? currency?.name ?? t`Unknown`
|
||||
return {
|
||||
descriptor,
|
||||
currencies: [currency],
|
||||
@@ -120,8 +121,10 @@ function parseMigrateCreateV3(
|
||||
tokens: TokenAddressMap
|
||||
): Partial<Activity> {
|
||||
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const baseSymbol = baseCurrency?.symbol ?? t`Unknown`
|
||||
const quoteCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const descriptor = t`${baseCurrency.symbol} and ${quoteCurrency.symbol}`
|
||||
const quoteSymbol = quoteCurrency?.symbol ?? t`Unknown`
|
||||
const descriptor = t`${baseSymbol} and ${quoteSymbol}`
|
||||
|
||||
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
|
||||
}
|
||||
@@ -131,71 +134,69 @@ export function parseLocalActivity(
|
||||
chainId: SupportedChainId,
|
||||
tokens: TokenAddressMap
|
||||
): Activity | undefined {
|
||||
const status = !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
try {
|
||||
const status = !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
|
||||
const receipt: TransactionPartsFragment | undefined = details.receipt
|
||||
? {
|
||||
id: details.receipt.transactionHash,
|
||||
...details.receipt,
|
||||
...details,
|
||||
status,
|
||||
}
|
||||
: undefined
|
||||
const receipt: TransactionPartsFragment | undefined = details.receipt
|
||||
? {
|
||||
id: details.receipt.transactionHash,
|
||||
...details.receipt,
|
||||
...details,
|
||||
status,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const defaultFields = {
|
||||
hash: details.hash,
|
||||
chainId,
|
||||
title: getActivityTitle(details.info.type, status),
|
||||
status,
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
receipt,
|
||||
const defaultFields = {
|
||||
hash: details.hash,
|
||||
chainId,
|
||||
title: getActivityTitle(details.info.type, status),
|
||||
status,
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
receipt,
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
const info = details.info
|
||||
if (info.type === TransactionType.SWAP) {
|
||||
additionalFields = parseSwap(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.APPROVAL) {
|
||||
additionalFields = parseApproval(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.WRAP) {
|
||||
additionalFields = parseWrap(info, chainId, status)
|
||||
} else if (
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
|
||||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
|
||||
) {
|
||||
additionalFields = parseLP(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.COLLECT_FEES) {
|
||||
additionalFields = parseCollectFees(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
|
||||
return { ...defaultFields, ...additionalFields }
|
||||
} catch (error) {
|
||||
console.debug(`Failed to parse transaction ${details.hash}`, error)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
const info = details.info
|
||||
if (info.type === TransactionType.SWAP) {
|
||||
additionalFields = parseSwap(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.APPROVAL) {
|
||||
additionalFields = parseApproval(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.WRAP) {
|
||||
additionalFields = parseWrap(info, chainId, status)
|
||||
} else if (
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
|
||||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
|
||||
) {
|
||||
additionalFields = parseLP(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.COLLECT_FEES) {
|
||||
additionalFields = parseCollectFees(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
|
||||
return { ...defaultFields, ...additionalFields }
|
||||
}
|
||||
|
||||
export function useLocalActivities(): ActivityMap | undefined {
|
||||
export function useLocalActivities(account: string): ActivityMap {
|
||||
const allTransactions = useMultichainTransactions()
|
||||
const { chainId } = useWeb3React()
|
||||
const tokens = useCombinedActiveList()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
chainId
|
||||
? allTransactions.reduce((acc: { [hash: string]: Activity }, [transaction, chainId]) => {
|
||||
try {
|
||||
const localActivity = parseLocalActivity(transaction, chainId, tokens)
|
||||
if (localActivity) acc[localActivity.hash] = localActivity
|
||||
} catch (error) {
|
||||
console.error('Failed to parse local activity', transaction)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
: undefined,
|
||||
[allTransactions, chainId, tokens]
|
||||
)
|
||||
return useMemo(() => {
|
||||
const activityByHash: ActivityMap = {}
|
||||
for (const [transaction, chainId] of allTransactions) {
|
||||
if (transaction.from !== account) continue
|
||||
|
||||
activityByHash[transaction.hash] = parseLocalActivity(transaction, chainId, tokens)
|
||||
}
|
||||
return activityByHash
|
||||
}, [account, allTransactions, tokens])
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export type Activity = {
|
||||
title: string
|
||||
descriptor?: string
|
||||
logos?: Array<string | undefined>
|
||||
currencies?: Array<Currency>
|
||||
currencies?: Array<Currency | undefined>
|
||||
otherAccount?: string
|
||||
receipt?: Receipt
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { NftCard } from 'nft/components/card'
|
||||
import { detailsHref } from 'nft/components/card/utils'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { WalletAsset } from 'nft/types'
|
||||
import { floorFormatter } from 'nft/utils'
|
||||
@@ -45,13 +46,13 @@ export function NFT({
|
||||
mediaShouldBePlaying: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}) {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const navigate = useNavigate()
|
||||
const trace = useTrace()
|
||||
|
||||
const navigateToNFTDetails = () => {
|
||||
navigate(`/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}`)
|
||||
toggleWalletDrawer()
|
||||
navigate(detailsHref(asset))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -62,10 +63,7 @@ export function NFT({
|
||||
display={{ disabledInfo: true }}
|
||||
isSelected={false}
|
||||
isDisabled={false}
|
||||
selectAsset={navigateToNFTDetails}
|
||||
unselectAsset={() => {
|
||||
/* */
|
||||
}}
|
||||
onCardClick={navigateToNFTDetails}
|
||||
sendAnalyticsEvent={() =>
|
||||
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
|
||||
element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM,
|
||||
@@ -77,6 +75,7 @@ export function NFT({
|
||||
}
|
||||
mediaShouldBePlaying={mediaShouldBePlaying}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
testId="mini-portfolio-nft"
|
||||
/>
|
||||
<NFTDetails asset={asset} />
|
||||
</NFTContainer>
|
||||
@@ -5,13 +5,22 @@ import { useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useToggleWalletDrawer } from '..'
|
||||
import { useAccountDrawer } from '..'
|
||||
import { DEFAULT_NFT_QUERY_AMOUNT } from './constants'
|
||||
import { NFT } from './NFT'
|
||||
|
||||
export default function NFTs({ account }: { account: string }) {
|
||||
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(account, [], [], DEFAULT_NFT_QUERY_AMOUNT)
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(
|
||||
account,
|
||||
[],
|
||||
[],
|
||||
DEFAULT_NFT_QUERY_AMOUNT,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
!walletDrawerOpen
|
||||
)
|
||||
|
||||
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import Pools from '.'
|
||||
import useMultiChainPositions from './useMultiChainPositions'
|
||||
|
||||
jest.mock('./useMultiChainPositions')
|
||||
const mockUseMultiChainPositions = useMultiChainPositions as jest.MockedFunction<typeof useMultiChainPositions>
|
||||
|
||||
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
|
||||
|
||||
const pool = new Pool(
|
||||
USDC_MAINNET,
|
||||
WETH9[SupportedChainId.MAINNET],
|
||||
FeeAmount.MEDIUM,
|
||||
'1851127709498178402383049949138810',
|
||||
'7076437181775065414',
|
||||
201189
|
||||
)
|
||||
|
||||
const position = new Position({
|
||||
pool,
|
||||
liquidity: 1341008833950736,
|
||||
tickLower: 200040,
|
||||
tickUpper: 202560,
|
||||
})
|
||||
const details = {
|
||||
nonce: BigNumber.from('0'),
|
||||
tokenId: BigNumber.from('0'),
|
||||
operator: '0x0',
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -100,
|
||||
tickUpper: 100,
|
||||
liquidity: BigNumber.from('9000'),
|
||||
feeGrowthInside0LastX128: BigNumber.from('0'),
|
||||
feeGrowthInside1LastX128: BigNumber.from('0'),
|
||||
tokensOwed0: BigNumber.from('0'),
|
||||
tokensOwed1: BigNumber.from('0'),
|
||||
}
|
||||
const useMultiChainPositionsReturnValue = {
|
||||
positions: [
|
||||
{
|
||||
owner,
|
||||
chainId: SupportedChainId.MAINNET,
|
||||
position,
|
||||
pool,
|
||||
details,
|
||||
inRange: true,
|
||||
closed: false,
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseMultiChainPositions.mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
})
|
||||
test('Pools should render LP positions', () => {
|
||||
const props = { account: owner }
|
||||
const { container } = render(<Pools {...props} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
@@ -4,9 +4,9 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { Position } from '@uniswap/v3-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
import { useCallback, useMemo, useReducer } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -33,7 +33,7 @@ export default function Pools({ account }: { account: string }) {
|
||||
return [openPositions, closedPositions]
|
||||
}, [positions])
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
if (!positions || loading) {
|
||||
return <PortfolioSkeleton />
|
||||
@@ -93,7 +93,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
|
||||
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const { chainId: walletChainId, connector } = useWeb3React()
|
||||
const onClick = useCallback(async () => {
|
||||
if (walletChainId !== chainId) await switchChain(connector, chainId)
|
||||
@@ -111,15 +111,9 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
|
||||
[chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol]
|
||||
)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() =>
|
||||
[pool.token0.name, pool.token0.symbol, pool.token1.name, pool.token1.symbol].some((testString) =>
|
||||
hasURL(testString)
|
||||
),
|
||||
[pool]
|
||||
)
|
||||
const shouldHidePosition = hasURL(pool.token0.symbol) || hasURL(pool.token1.symbol)
|
||||
|
||||
if (containsURL) {
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { DAI_ARBITRUM } from '@uniswap/smart-order-router'
|
||||
import { DAI, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import { PortfolioLogo } from './PortfolioLogo'
|
||||
|
||||
describe('PortfolioLogo', () => {
|
||||
it('renders without L2 icon', () => {
|
||||
const { container } = render(<PortfolioLogo chainId={SupportedChainId.MAINNET} currencies={[DAI, USDC_MAINNET]} />)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with L2 icon', () => {
|
||||
const { container } = render(
|
||||
<PortfolioLogo chainId={SupportedChainId.ARBITRUM_ONE} currencies={[DAI_ARBITRUM, USDC_ARBITRUM]} />
|
||||
)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,7 @@ import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import React from 'react'
|
||||
import { Loader } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
const UnknownContract = styled(UnknownStatus)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
@@ -39,7 +39,7 @@ const DoubleLogoContainer = styled.div`
|
||||
type MultiLogoProps = {
|
||||
chainId: SupportedChainId
|
||||
accountAddress?: string
|
||||
currencies?: Currency[]
|
||||
currencies?: Array<Currency | undefined>
|
||||
images?: (string | undefined)[]
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
@@ -57,34 +57,28 @@ const ENSAvatarImg = styled.img`
|
||||
width: 40px;
|
||||
`
|
||||
|
||||
const StyledChainLogo = styled.img<{ isSquare: boolean }>`
|
||||
height: ${({ isSquare }) => (isSquare ? '16px' : '14px')};
|
||||
width: ${({ isSquare }) => (isSquare ? '16px' : '14px')};
|
||||
margin-top: ${({ isSquare }) => (isSquare ? '0px' : '1px')};
|
||||
margin-left: ${({ isSquare }) => (isSquare ? '0px' : '1px')};
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 68%;
|
||||
const StyledChainLogo = styled.img`
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
`
|
||||
|
||||
const ChainLogoSquareBackground = styled.div`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border-radius: 4px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
const SquareChainLogo = styled.img`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const L2LogoContainer = styled.div<{ $backgroundColor?: string }>`
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
border-radius: 2px;
|
||||
height: 16px;
|
||||
left: 60%;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
left: 60%;
|
||||
`
|
||||
|
||||
const SquareBackgroundForNonSquareLogo = styled.div`
|
||||
height: 16px;
|
||||
outline: 2px solid ${({ theme }) => theme.backgroundSurface};
|
||||
width: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: ${({ theme }) => theme.textPrimary};
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 68%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
/**
|
||||
@@ -101,6 +95,7 @@ export function PortfolioLogo({
|
||||
const { squareLogoUrl, logoUrl } = getChainInfo(chainId)
|
||||
const chainLogo = squareLogoUrl ?? logoUrl
|
||||
const { avatar, loading } = useENSAvatar(accountAddress, false)
|
||||
const theme = useTheme()
|
||||
|
||||
const [src, nextSrc] = useTokenLogoSource(currencies?.[0]?.wrapped.address, chainId, currencies?.[0]?.isNative)
|
||||
const [src2, nextSrc2] = useTokenLogoSource(currencies?.[1]?.wrapped.address, chainId, currencies?.[1]?.isNative)
|
||||
@@ -147,13 +142,15 @@ export function PortfolioLogo({
|
||||
}
|
||||
|
||||
const L2Logo =
|
||||
chainId === SupportedChainId.MAINNET ? null : (
|
||||
<div>
|
||||
{chainLogo && <ChainLogoSquareBackground />}
|
||||
{!squareLogoUrl && logoUrl && <SquareBackgroundForNonSquareLogo />}
|
||||
{chainLogo && <StyledChainLogo isSquare={!!squareLogoUrl} src={chainLogo} alt="chainLogo" />}
|
||||
</div>
|
||||
)
|
||||
chainId !== SupportedChainId.MAINNET && chainLogo ? (
|
||||
<L2LogoContainer $backgroundColor={squareLogoUrl ? theme.backgroundSurface : theme.textPrimary}>
|
||||
{squareLogoUrl ? (
|
||||
<SquareChainLogo src={chainLogo} alt="chainLogo" />
|
||||
) : (
|
||||
<StyledChainLogo src={chainLogo} alt="chainLogo" />
|
||||
)}
|
||||
</L2LogoContainer>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<StyledLogoParentContainer>
|
||||
@@ -12,7 +12,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
|
||||
import { useToggleWalletDrawer } from '..'
|
||||
import { useToggleAccountDrawer } from '..'
|
||||
import { PortfolioArrow } from '../AuthenticatedHeader'
|
||||
import { hideSmallBalancesAtom } from '../SmallBalanceToggle'
|
||||
import { ExpandoRow } from './ExpandoRow'
|
||||
@@ -26,7 +26,7 @@ function meetsThreshold(tokenBalance: TokenBalance, hideSmallBalances: boolean)
|
||||
}
|
||||
|
||||
export default function Tokens({ account }: { account: string }) {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
|
||||
const [showHiddenTokens, setShowHiddenTokens] = useState(false)
|
||||
|
||||
@@ -96,7 +96,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const navigateToTokenDetails = useCallback(async () => {
|
||||
navigate(getTokenDetailsURL(token))
|
||||
toggleWalletDrawer()
|
||||
@@ -0,0 +1,164 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PortfolioLogo renders with L2 icon 1`] = `
|
||||
.c3 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(n) {
|
||||
width: 19px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(1) {
|
||||
border-radius: 20px 0 0 20px;
|
||||
object-position: 0 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(2) {
|
||||
border-radius: 0 20px 20px 0;
|
||||
object-position: 100% 0;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
background-color: #0D111C;
|
||||
border-radius: 2px;
|
||||
height: 16px;
|
||||
left: 60%;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
width: 16px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1/logo.png"
|
||||
/>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<img
|
||||
alt="chainLogo"
|
||||
class="c5"
|
||||
src="arbitrum_logo.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PortfolioLogo renders without L2 icon 1`] = `
|
||||
.c3 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(n) {
|
||||
width: 19px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(1) {
|
||||
border-radius: 20px 0 0 20px;
|
||||
object-position: 0 0;
|
||||
}
|
||||
|
||||
.c1 .c2:nth-child(2) {
|
||||
border-radius: 0 20px 20px 0;
|
||||
object-position: 100% 0;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
|
||||
/>
|
||||
<img
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -55,6 +55,7 @@ const PageWrapper = styled.div`
|
||||
|
||||
interface Page {
|
||||
title: React.ReactNode
|
||||
key: string
|
||||
component: ({ account }: { account: string }) => JSX.Element
|
||||
loggingElementName: string
|
||||
}
|
||||
@@ -62,13 +63,25 @@ interface Page {
|
||||
const Pages: Array<Page> = [
|
||||
{
|
||||
title: <Trans>Tokens</Trans>,
|
||||
key: 'tokens',
|
||||
component: Tokens,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_TOKENS_TAB,
|
||||
},
|
||||
{ title: <Trans>NFTs</Trans>, component: NFTs, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB },
|
||||
{ title: <Trans>Pools</Trans>, component: Pools, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB },
|
||||
{
|
||||
title: <Trans>NFTs</Trans>,
|
||||
key: 'nfts',
|
||||
component: NFTs,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB,
|
||||
},
|
||||
{
|
||||
title: <Trans>Pools</Trans>,
|
||||
key: 'pools',
|
||||
component: Pools,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB,
|
||||
},
|
||||
{
|
||||
title: <Trans>Activity</Trans>,
|
||||
key: 'activity',
|
||||
component: ActivityTab,
|
||||
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_TAB,
|
||||
},
|
||||
@@ -83,7 +96,7 @@ function MiniPortfolio({ account }: { account: string }) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Nav>
|
||||
{Pages.map(({ title, loggingElementName }, index) => {
|
||||
{Pages.map(({ title, loggingElementName, key }, index) => {
|
||||
if (shouldDisableNFTRoutes && loggingElementName.includes('nft')) return null
|
||||
return (
|
||||
<TraceEvent
|
||||
@@ -93,6 +106,7 @@ function MiniPortfolio({ account }: { account: string }) {
|
||||
key={index}
|
||||
>
|
||||
<NavItem
|
||||
data-testid={`mini-portfolio-nav-${key}`}
|
||||
onClick={() => setCurrentPage(index)}
|
||||
active={currentPage === index}
|
||||
key={`Mini Portfolio page ${index}`}
|
||||
@@ -5,7 +5,7 @@ import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 're
|
||||
import { useAllTransactions } from 'state/transactions/hooks'
|
||||
import { TransactionDetails } from 'state/transactions/types'
|
||||
|
||||
import { useWalletDrawer } from '.'
|
||||
import { useAccountDrawer } from '.'
|
||||
|
||||
const isTxPending = (tx: TransactionDetails) => !tx.receipt
|
||||
function wasPending(previousTxs: { [hash: string]: TransactionDetails | undefined }, current: TransactionDetails) {
|
||||
@@ -39,7 +39,7 @@ function useHasUpdatedTx() {
|
||||
export default function PrefetchBalancesWrapper({ children }: PropsWithChildren) {
|
||||
const { account } = useWeb3React()
|
||||
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
|
||||
const [drawerOpen] = useWalletDrawer()
|
||||
const [drawerOpen] = useAccountDrawer()
|
||||
|
||||
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useState(true)
|
||||
const fetchBalances = useCallback(() => {
|
||||
@@ -18,18 +18,18 @@ const DRAWER_MARGIN = '8px'
|
||||
const DRAWER_OFFSET = '10px'
|
||||
const DRAWER_TOP_MARGIN_MOBILE_WEB = '72px'
|
||||
|
||||
const walletDrawerOpenAtom = atom(false)
|
||||
const accountDrawerOpenAtom = atom(false)
|
||||
|
||||
export function useToggleWalletDrawer() {
|
||||
const updateWalletDrawerOpen = useUpdateAtom(walletDrawerOpenAtom)
|
||||
export function useToggleAccountDrawer() {
|
||||
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
|
||||
return useCallback(() => {
|
||||
updateWalletDrawerOpen((open) => !open)
|
||||
}, [updateWalletDrawerOpen])
|
||||
updateAccountDrawerOpen((open) => !open)
|
||||
}, [updateAccountDrawerOpen])
|
||||
}
|
||||
|
||||
export function useWalletDrawer(): [boolean, () => void] {
|
||||
const walletDrawerOpen = useAtomValue(walletDrawerOpenAtom)
|
||||
return [walletDrawerOpen, useToggleWalletDrawer()]
|
||||
export function useAccountDrawer(): [boolean, () => void] {
|
||||
const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom)
|
||||
return [accountDrawerOpen, useToggleAccountDrawer()]
|
||||
}
|
||||
|
||||
const ScrimBackground = styled.div<{ open: boolean }>`
|
||||
@@ -63,7 +63,7 @@ const Scrim = ({ onClick, open }: { onClick: () => void; open: boolean }) => {
|
||||
return <ScrimBackground onClick={onClick} open={open} />
|
||||
}
|
||||
|
||||
const WalletDropdownScrollWrapper = styled.div`
|
||||
const AccountDrawerScrollWrapper = styled.div`
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
@@ -76,32 +76,45 @@ const WalletDropdownScrollWrapper = styled.div`
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
right: ${DRAWER_MARGIN};
|
||||
top: ${DRAWER_MARGIN};
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH)};
|
||||
z-index: ${Z_INDEX.fixed};
|
||||
|
||||
overflow: hidden;
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
`
|
||||
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
const AccountDrawerWrapper = styled.div<{ open: boolean }>`
|
||||
margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH)};
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
z-index: ${Z_INDEX.modal};
|
||||
top: unset;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: ${({ open }) => (open ? 0 : `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))`)};
|
||||
position: absolute;
|
||||
margin-right: 0;
|
||||
top: ${({ open }) => (open ? `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))` : 0)};
|
||||
|
||||
width: 100%;
|
||||
height: calc(100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB});
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
box-shadow: unset;
|
||||
transition: top ${({ theme }) => theme.transition.duration.medium};
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1440px) {
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH_XL)};
|
||||
margin-right: ${({ open }) => (open ? 0 : `-${DRAWER_WIDTH_XL}`)};
|
||||
width: ${DRAWER_WIDTH_XL};
|
||||
}
|
||||
|
||||
@@ -112,8 +125,7 @@ const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
transition: right ${({ theme }) => theme.transition.duration.medium},
|
||||
bottom ${({ theme }) => theme.transition.duration.medium};
|
||||
transition: margin-right ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
|
||||
const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
|
||||
@@ -123,30 +135,24 @@ const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
|
||||
const CloseDrawer = styled.div`
|
||||
${ClickableStyle}
|
||||
cursor: pointer;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
position: fixed;
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH} - ${DRAWER_OFFSET});
|
||||
top: 4px;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
height: 100%;
|
||||
// When the drawer is not hovered, the icon should be 18px from the edge of the sidebar.
|
||||
padding: 24px calc(18px + ${DRAWER_OFFSET}) 24px 14px;
|
||||
border-radius: 20px 0 0 20px;
|
||||
transition: ${({ theme }) =>
|
||||
`${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color, ${theme.transition.duration.medium} ${theme.transition.timing.ease} margin`};
|
||||
&:hover {
|
||||
margin: 0 -4px 0 0;
|
||||
z-index: -1;
|
||||
margin: 0 -8px 0 0;
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 1440px) {
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH_XL} - ${DRAWER_OFFSET});
|
||||
}
|
||||
`
|
||||
|
||||
function WalletDropdown() {
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
function AccountDrawer() {
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!walletDrawerOpen) {
|
||||
@@ -187,7 +193,7 @@ function WalletDropdown() {
|
||||
}, [walletDrawerOpen, toggleWalletDrawer])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
{walletDrawerOpen && (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@@ -200,14 +206,14 @@ function WalletDropdown() {
|
||||
</TraceEvent>
|
||||
)}
|
||||
<Scrim onClick={toggleWalletDrawer} open={walletDrawerOpen} />
|
||||
<WalletDropdownWrapper open={walletDrawerOpen}>
|
||||
<AccountDrawerWrapper open={walletDrawerOpen}>
|
||||
{/* id used for child InfiniteScrolls to reference when it has reached the bottom of the component */}
|
||||
<WalletDropdownScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
|
||||
<AccountDrawerScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
|
||||
<DefaultMenu />
|
||||
</WalletDropdownScrollWrapper>
|
||||
</WalletDropdownWrapper>
|
||||
</>
|
||||
</AccountDrawerScrollWrapper>
|
||||
</AccountDrawerWrapper>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletDropdown
|
||||
export default AccountDrawer
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { DownloadButton, LearnMoreButton } from 'components/AccountDrawer/DownloadButton'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Row, { RowBetween } from 'components/Row'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import { DownloadButton, LearnMoreButton } from 'components/WalletDropdown/DownloadButton'
|
||||
import { X } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useHideUniswapWalletBanner } from 'state/user/hooks'
|
||||
@@ -66,7 +66,7 @@ const StyledXButton = styled(X)`
|
||||
|
||||
export default function UniswapWalletBanner() {
|
||||
const [hideUniswapWalletBanner, toggleHideUniswapWalletBanner] = useHideUniswapWalletBanner()
|
||||
const [walletDrawerOpen] = useWalletDrawer()
|
||||
const [walletDrawerOpen] = useAccountDrawer()
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
|
||||
import { ChevronUpIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
import { Copy } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
|
||||
@@ -220,18 +217,14 @@ const updateServiceWorkerInBackground = async () => {
|
||||
}
|
||||
|
||||
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
|
||||
const { pathname } = useLocation()
|
||||
return (
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, eventId }) => <Fallback error={error} eventId={eventId} />}
|
||||
beforeCapture={(scope) => {
|
||||
scope.setLevel('fatal')
|
||||
}}
|
||||
onError={(error) => {
|
||||
onError={() => {
|
||||
updateServiceWorkerInBackground()
|
||||
if (pathname === '/swap') {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
24
src/components/Identicon/StatusIcon.test.tsx
Normal file
24
src/components/Identicon/StatusIcon.test.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getConnections } from 'connection'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import StatusIcon from './StatusIcon'
|
||||
|
||||
jest.mock('../../hooks/useSocksBalance', () => ({
|
||||
useHasSocks: () => true,
|
||||
}))
|
||||
|
||||
describe('StatusIcon', () => {
|
||||
it('renders children in correct order, with no account and with socks', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with no account and showMiniIcons=false', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -60,7 +60,7 @@ const Socks = () => {
|
||||
const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => {
|
||||
return (
|
||||
<MiniIconContainer side={side}>
|
||||
<MiniImg src={connection.icon} alt={`${connection.name} icon`} />
|
||||
<MiniImg src={connection.getIcon?.()} alt={`${connection.getName()} icon`} />
|
||||
</MiniIconContainer>
|
||||
)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ const MainWalletIcon = ({ connection, size }: { connection: Connection; size: nu
|
||||
|
||||
if (!account) {
|
||||
return null
|
||||
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.name === 'MetaMask')) {
|
||||
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.getName() === 'MetaMask')) {
|
||||
return <Identicon size={size} />
|
||||
} else {
|
||||
return <Unicon address={account} size={size} />
|
||||
@@ -90,10 +90,10 @@ export default function StatusIcon({
|
||||
const hasSocks = useHasSocks()
|
||||
|
||||
return (
|
||||
<IconWrapper size={size}>
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
<IconWrapper size={size} data-testid="StatusIconRoot">
|
||||
<MainWalletIcon connection={connection} size={size} />
|
||||
{showMiniIcons && <MiniWalletIcon connection={connection} side="right" />}
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
129
src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap
Normal file
129
src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap
Normal file
@@ -0,0 +1,129 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusIcon renders children in correct order, with no account and with socks 1`] = `
|
||||
.c0 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-flow: column nowrap;
|
||||
-ms-flex-flow: column nowrap;
|
||||
flex-flow: column nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c0 > img,
|
||||
.c0 span {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (max-width:960px) {
|
||||
.c0 {
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c1 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c3 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-testid="StatusIconRoot"
|
||||
size="16"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
alt="MetaMask icon"
|
||||
class="c2"
|
||||
src="metamask.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
<img
|
||||
class="c2"
|
||||
src="socks.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { parseLocalActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
|
||||
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
|
||||
import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow'
|
||||
import Column from 'components/Column'
|
||||
import { parseLocalActivity } from 'components/WalletDropdown/MiniPortfolio/Activity/parseLocal'
|
||||
import { PortfolioLogo } from 'components/WalletDropdown/MiniPortfolio/PortfolioLogo'
|
||||
import PortfolioRow from 'components/WalletDropdown/MiniPortfolio/PortfolioRow'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useCombinedActiveList } from 'state/lists/hooks'
|
||||
import { useTransaction } from 'state/transactions/hooks'
|
||||
|
||||
@@ -1,38 +1,91 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { render, screen } from 'test-utils'
|
||||
import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { usePool } from 'hooks/usePools'
|
||||
import { PoolState } from 'hooks/usePools'
|
||||
import { render } from 'test-utils'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('hooks/Tokens', () => {
|
||||
const originalModule = jest.requireActual('hooks/Tokens')
|
||||
const uniSDK = jest.requireActual('@uniswap/sdk-core')
|
||||
jest.mock('utils/unwrappedToken')
|
||||
const mockUnwrappedToken = unwrappedToken as jest.MockedFunction<typeof unwrappedToken>
|
||||
|
||||
jest.mock('hooks/usePools')
|
||||
const mockUsePool = usePool as jest.MockedFunction<typeof usePool>
|
||||
|
||||
jest.mock('hooks/Tokens')
|
||||
const mockUseToken = useToken as jest.MockedFunction<typeof useToken>
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
jest.mock('components/DoubleLogo', () => () => <div />)
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
useToken: jest.fn(
|
||||
() =>
|
||||
new uniSDK.Token(
|
||||
1,
|
||||
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
8,
|
||||
'https://www.example.com',
|
||||
'example.com coin'
|
||||
)
|
||||
),
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
chainId: 1,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when the name contains a url', () => {
|
||||
const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
|
||||
|
||||
beforeEach(() => {
|
||||
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
|
||||
mockUseToken.mockImplementation((tokenAddress?: string | null | undefined) => {
|
||||
if (!tokenAddress) return null
|
||||
if (tokenAddress === susToken0.address) return susToken0
|
||||
return new Token(1, tokenAddress, 8, 'symbol', 'name')
|
||||
})
|
||||
mockUsePool.mockReturnValue([
|
||||
PoolState.EXISTS,
|
||||
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
|
||||
])
|
||||
mockUnwrappedToken.mockReturnValue(susToken0)
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token0 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
||||
token0: susToken0Address,
|
||||
token1: USDC_MAINNET.address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
render(<PositionListItem {...positionDetails} />)
|
||||
screen.debug()
|
||||
expect(screen.queryByText('.com', { exact: false })).toBe(null)
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token1 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: susToken0Address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should render a position', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
@@ -203,12 +203,9 @@ export default function PositionListItem({
|
||||
|
||||
const removed = liquidity?.eq(0)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() => [token0?.name, token0?.symbol, token1?.name, token1?.symbol].some((testString) => hasURL(testString)),
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
|
||||
)
|
||||
const shouldHidePosition = hasURL(token0?.symbol) || hasURL(token1?.symbol)
|
||||
|
||||
if (containsURL) {
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
color: #98A1C0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
margin-left: 4px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
color: #98A1C0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(auto,1fr) auto minmax(0,72px);
|
||||
grid-gap: 16px;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
background-color: #98A1C014;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
<div
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 168px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
>
|
||||
USD//C
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`renders loading rows when isLoading is true 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
-webkit-animation: fAQEyV 1.5s infinite;
|
||||
animation: fAQEyV 1.5s infinite;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient( to left,#F5F6FC 25%,#E8ECFB 50%,#F5F6FC 75% );
|
||||
background-size: 400%;
|
||||
border-radius: 12px;
|
||||
height: 2.4em;
|
||||
will-change: background-position;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
grid-column-gap: 0.5em;
|
||||
grid-template-columns: repeat(12,1fr);
|
||||
max-width: 960px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 1) {
|
||||
grid-column: 1 / 8;
|
||||
height: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 2) {
|
||||
grid-column: 12;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 3) {
|
||||
grid-column: 1 / 4;
|
||||
height: 0.75em;
|
||||
}
|
||||
|
||||
<div
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 560px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import * as mockJSBI from 'jsbi'
|
||||
@@ -25,11 +26,11 @@ jest.mock(
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
account: '123',
|
||||
isActive: true,
|
||||
}),
|
||||
...web3React,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,37 +43,38 @@ jest.mock('../../../state/connection/hooks', () => {
|
||||
})
|
||||
|
||||
it('renders loading rows when isLoading is true', () => {
|
||||
const { asFragment } = render(
|
||||
const component = render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={true}
|
||||
searchQuery=""
|
||||
isAddressSearch=""
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(component.findByTestId('loading-rows')).toBeTruthy()
|
||||
expect(screen.queryByText('Wrapped BTC')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('DAI')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('USDC')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders currency rows correctly when currencies list is non-empty', () => {
|
||||
const { asFragment } = render(
|
||||
render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[DAI, USDC_MAINNET, WBTC]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={false}
|
||||
searchQuery=""
|
||||
isAddressSearch=""
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText('Wrapped BTC')).toBeInTheDocument()
|
||||
expect(screen.getByText('DAI')).toBeInTheDocument()
|
||||
expect(screen.getByText('USDC')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
|
||||
import Row, { RowFixed } from '../../Row'
|
||||
import { MouseoverTooltip } from '../../Tooltip'
|
||||
import { LoadingRows, MenuItem } from '../styleds'
|
||||
import * as styles from './index.css'
|
||||
import { scrollbarStyle } from './index.css'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency.isToken ? currency.address : 'ETHER'
|
||||
@@ -65,6 +65,10 @@ const WarningContainer = styled.div`
|
||||
margin-left: 0.3em;
|
||||
`
|
||||
|
||||
const ListWrapper = styled.div`
|
||||
padding-right: 0.25rem;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount<Currency> }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
@@ -212,7 +216,7 @@ export const formatAnalyticsEventProperties = (
|
||||
})
|
||||
|
||||
const LoadingRow = () => (
|
||||
<LoadingRows>
|
||||
<LoadingRows data-testid="loading-rows">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
@@ -290,10 +294,10 @@ export default function CurrencyList({
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ paddingRight: '4px' }}>
|
||||
<ListWrapper>
|
||||
{isLoading ? (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
className={scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
@@ -305,7 +309,7 @@ export default function CurrencyList({
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
className={scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
@@ -317,6 +321,6 @@ export default function CurrencyList({
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
</ListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
// TODO(WEB-3163): migrate noops throughout web to a shared util file.
|
||||
const noop = () => null
|
||||
|
||||
export const TooltipContainer = styled.div`
|
||||
max-width: 256px;
|
||||
cursor: default;
|
||||
padding: 0.6rem 1rem;
|
||||
pointer-events: auto;
|
||||
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 400;
|
||||
@@ -25,7 +29,6 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
text: ReactNode
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
noOp?: () => void
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
timeout?: number
|
||||
}
|
||||
@@ -33,17 +36,19 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
|
||||
content: ReactNode
|
||||
onOpen?: () => void
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
// whether to wrap the content in a `TooltipContainer`
|
||||
wrap?: boolean
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
}
|
||||
|
||||
export default function Tooltip({ text, open, close, noOp, disableHover, ...rest }: TooltipProps) {
|
||||
export default function Tooltip({ text, open, close, disableHover, ...rest }: TooltipProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
text && (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noOp : open} onMouseLeave={disableHover ? noOp : close}>
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
{text}
|
||||
</TooltipContainer>
|
||||
)
|
||||
@@ -53,15 +58,28 @@ export default function Tooltip({ text, open, close, noOp, disableHover, ...rest
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
|
||||
return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
|
||||
function TooltipContent({ content, wrap = false, open, close, disableHover, ...rest }: TooltipContentProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
wrap ? (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
{content}
|
||||
</TooltipContainer>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Standard text tooltip. */
|
||||
export function MouseoverTooltip({ text, disableHover, children, timeout, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => text && setShow(true), [text, setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
const open = () => text && setShow(true)
|
||||
const close = () => setShow(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (show && timeout) {
|
||||
@@ -76,18 +94,16 @@ export function MouseoverTooltip({ text, disableHover, children, timeout, ...res
|
||||
return
|
||||
}, [timeout, show])
|
||||
|
||||
const noOp = () => null
|
||||
return (
|
||||
<Tooltip
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
noOp={noOp}
|
||||
disableHover={disableHover}
|
||||
show={show}
|
||||
text={disableHover ? null : text}
|
||||
>
|
||||
<div onMouseEnter={disableHover ? noOp : open} onMouseLeave={disableHover || timeout ? noOp : close}>
|
||||
<div onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover || timeout ? noop : close}>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -103,18 +119,23 @@ export function MouseoverTooltipContent({
|
||||
...rest
|
||||
}: Omit<TooltipContentProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => {
|
||||
const open = () => {
|
||||
setShow(true)
|
||||
openCallback?.()
|
||||
}, [openCallback])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
}
|
||||
const close = () => {
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipContent {...rest} show={!disableHover && show} content={disableHover ? null : content}>
|
||||
<div
|
||||
style={{ display: 'inline-block', lineHeight: 0, padding: '0.25rem' }}
|
||||
onMouseEnter={open}
|
||||
onMouseLeave={close}
|
||||
>
|
||||
<TooltipContent
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
show={!disableHover && show}
|
||||
content={disableHover ? null : content}
|
||||
>
|
||||
<div onMouseEnter={open} onMouseLeave={close}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import UniwalletModal from 'components/AccountDrawer/UniwalletModal'
|
||||
import UniswapWalletBanner from 'components/Banner/UniswapWalletBanner'
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import FiatOnrampModal from 'components/FiatOnrampModal'
|
||||
import UniwalletModal from 'components/WalletDropdown/UniwalletModal'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { lazy } from 'react'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function Option({ connection, pendingConnectionType, activate }:
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.WALLET_SELECTED}
|
||||
properties={{ wallet_type: connection.name }}
|
||||
properties={{ wallet_type: connection.getName() }}
|
||||
element={InterfaceElementName.WALLET_TYPE_OPTION}
|
||||
>
|
||||
<OptionCardClickable
|
||||
@@ -83,9 +83,9 @@ export default function Option({ connection, pendingConnectionType, activate }:
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<IconWrapper>
|
||||
<img src={connection.icon} alt="Icon" />
|
||||
<img src={connection.getIcon?.()} alt="Icon" />
|
||||
</IconWrapper>
|
||||
<HeaderText>{connection.name}</HeaderText>
|
||||
<HeaderText>{connection.getName()}</HeaderText>
|
||||
{connection.isNew && <NewBadge />}
|
||||
</OptionCardLeft>
|
||||
{isPending && <Loader />}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { sendAnalyticsEvent, user } from '@uniswap/analytics'
|
||||
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
|
||||
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import IconButton from 'components/AccountDrawer/IconButton'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import IconButton from 'components/WalletDropdown/IconButton'
|
||||
import { Connection, ConnectionType, networkConnection, useConnections } from 'connection'
|
||||
import { Connection, ConnectionType, getConnections, networkConnection } from 'connection'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { ErrorCode } from 'connection/utils'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
@@ -84,14 +84,14 @@ function didUserReject(connection: Connection, error: any): boolean {
|
||||
export default function WalletModal({ openSettings }: { openSettings: () => void }) {
|
||||
const dispatch = useAppDispatch()
|
||||
const { connector, account, chainId, provider } = useWeb3React()
|
||||
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
|
||||
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
|
||||
const [lastActiveWalletAddress, setLastActiveWalletAddress] = useState<string | undefined>(account)
|
||||
const [pendingConnection, setPendingConnection] = useState<Connection | undefined>()
|
||||
const [pendingError, setPendingError] = useState<any>()
|
||||
|
||||
const connections = useConnections()
|
||||
const connections = getConnections()
|
||||
const getConnection = useGetConnection()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,7 +116,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
|
||||
useEffect(() => {
|
||||
if (account && account !== lastActiveWalletAddress) {
|
||||
const walletName = getConnection(connector).name
|
||||
const walletName = getConnection(connector).getName()
|
||||
const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined
|
||||
const isReconnect =
|
||||
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletName).length > 0
|
||||
@@ -141,6 +141,9 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
|
||||
const tryActivation = useCallback(
|
||||
async (connection: Connection) => {
|
||||
// Skips wallet connection if the connection should override the default behavior, i.e. install metamask or launch coinbase app
|
||||
if (connection.overrideActivate?.()) return
|
||||
|
||||
// log selected wallet
|
||||
sendEvent({
|
||||
category: 'Wallet',
|
||||
@@ -165,7 +168,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
|
||||
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
result: WalletConnectionResult.FAILED,
|
||||
wallet_type: connection.name,
|
||||
wallet_type: connection.getName(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -190,11 +193,11 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
<OptionGrid data-testid="option-grid">
|
||||
{connections.map((connection) =>
|
||||
// Hides Uniswap Wallet if mgtm is disabled
|
||||
connection.shouldDisplay && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
|
||||
connection.shouldDisplay() && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
|
||||
<Option
|
||||
key={connection.name}
|
||||
key={connection.getName()}
|
||||
connection={connection}
|
||||
activate={connection.overrideActivate ?? (() => tryActivation(connection))}
|
||||
activate={() => tryActivation(connection)}
|
||||
pendingConnectionType={pendingConnection?.type}
|
||||
/>
|
||||
) : null
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
const connections = useOrderedConnections()
|
||||
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
|
||||
|
||||
const key = useMemo(() => connections.map((connection) => connection.name).join('-'), [connections])
|
||||
const key = useMemo(() => connections.map((connection) => connection.getName()).join('-'), [connections])
|
||||
|
||||
return (
|
||||
<Web3ReactProvider connectors={connectors} key={key}>
|
||||
|
||||
@@ -2,19 +2,17 @@ import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import PortfolioDrawer, { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import WalletDropdown, { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import PrefetchBalancesWrapper from 'components/WalletDropdown/PrefetchBalancesWrapper'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { colors } from 'theme/colors'
|
||||
import { flexRowNoWrap } from 'theme/styles'
|
||||
@@ -153,16 +151,11 @@ function Web3StatusInner() {
|
||||
const { account, connector, chainId, ENSName } = useWeb3React()
|
||||
const getConnection = useGetConnection()
|
||||
const connection = getConnection(connector)
|
||||
const {
|
||||
trade: { state: tradeState, trade },
|
||||
inputError: swapInputError,
|
||||
} = useDerivedSwapInfo()
|
||||
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
|
||||
const [, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [, toggleAccountDrawer] = useAccountDrawer()
|
||||
const handleWalletDropdownClick = useCallback(() => {
|
||||
sendAnalyticsEvent(InterfaceEventName.ACCOUNT_DROPDOWN_BUTTON_CLICKED)
|
||||
toggleWalletDrawer()
|
||||
}, [toggleWalletDrawer])
|
||||
toggleAccountDrawer()
|
||||
}, [toggleAccountDrawer])
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
@@ -223,7 +216,6 @@ function Web3StatusInner() {
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: validSwapQuote }}
|
||||
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<Web3StatusConnectWrapper
|
||||
@@ -241,13 +233,12 @@ function Web3StatusInner() {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export default function Web3Status() {
|
||||
return (
|
||||
<PrefetchBalancesWrapper>
|
||||
<Web3StatusInner />
|
||||
<Portal>
|
||||
<WalletDropdown />
|
||||
<PortfolioDrawer />
|
||||
</Portal>
|
||||
</PrefetchBalancesWrapper>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SwapWidgetSkeleton,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
@@ -67,7 +67,7 @@ export default function Widget({
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
|
||||
111
src/components/swap/SwapBuyFiatButton.test.tsx
Normal file
111
src/components/swap/SwapBuyFiatButton.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { fireEvent, render, screen } from 'test-utils'
|
||||
|
||||
import { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
|
||||
import SwapBuyFiatButton, { MOONPAY_REGION_AVAILABILITY_ARTICLE } from './SwapBuyFiatButton'
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('../../state/application/hooks')
|
||||
const mockUseFiatOnrampAvailability = useFiatOnrampAvailability as jest.MockedFunction<typeof useFiatOnrampAvailability>
|
||||
const mockUseOpenModal = useOpenModal as jest.MockedFunction<typeof useOpenModal>
|
||||
|
||||
jest.mock('components/AccountDrawer')
|
||||
const mockuseAccountDrawer = useAccountDrawer as jest.MockedFunction<typeof useAccountDrawer>
|
||||
|
||||
const mockUseFiatOnRampsUnavailable = (shouldCheck: boolean) => {
|
||||
return {
|
||||
available: false,
|
||||
availabilityChecked: shouldCheck,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
const mockUseFiatOnRampsAvailable = (shouldCheck: boolean) => {
|
||||
if (shouldCheck) {
|
||||
return {
|
||||
available: true,
|
||||
availabilityChecked: true,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
available: false,
|
||||
availabilityChecked: false,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
describe('SwapBuyFiatButton.tsx', () => {
|
||||
let toggleWalletDrawer: jest.Mock<any, any>
|
||||
let useOpenModal: jest.Mock<any, any>
|
||||
|
||||
beforeAll(() => {
|
||||
toggleWalletDrawer = jest.fn()
|
||||
useOpenModal = jest.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
;(useWeb3React as jest.Mock).mockReturnValue({
|
||||
account: undefined,
|
||||
isActive: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('matches base snapshot', () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
const { asFragment } = render(<SwapBuyFiatButton />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fiat on ramps available in region, account unconnected', async () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
mockUseOpenModal.mockImplementation(() => useOpenModal)
|
||||
render(<SwapBuyFiatButton />)
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
expect(toggleWalletDrawer).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fiat on ramps available in region, account connected', async () => {
|
||||
;(useWeb3React as jest.Mock).mockReturnValue({
|
||||
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
|
||||
isActive: true,
|
||||
})
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
mockUseOpenModal.mockImplementation(() => useOpenModal)
|
||||
render(<SwapBuyFiatButton />)
|
||||
expect(screen.getByTestId('buy-fiat-flow-incomplete-indicator')).toBeInTheDocument()
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
expect(toggleWalletDrawer).toHaveBeenCalledTimes(0)
|
||||
expect(useOpenModal).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('buy-fiat-flow-incomplete-indicator')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fiat on ramps unavailable in region', async () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
render(<SwapBuyFiatButton />)
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
fireEvent.mouseOver(screen.getByTestId('buy-fiat-button'))
|
||||
expect(await screen.findByTestId('fiat-on-ramp-unavailable-tooltip')).toBeInTheDocument()
|
||||
expect(await screen.findByText(/Learn more/i)).toHaveAttribute('href', MOONPAY_REGION_AVAILABILITY_ARTICLE)
|
||||
expect(await screen.findByTestId('buy-fiat-button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
146
src/components/swap/SwapBuyFiatButton.tsx
Normal file
146
src/components/swap/SwapBuyFiatButton.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { ButtonText } from 'components/Button'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useBuyFiatFlowCompleted } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink } from 'theme'
|
||||
|
||||
import { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
|
||||
const Dot = styled.div`
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: ${({ theme }) => theme.accentActive};
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
export const MOONPAY_REGION_AVAILABILITY_ARTICLE =
|
||||
'https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-'
|
||||
|
||||
enum BuyFiatFlowState {
|
||||
// Default initial state. User is not actively trying to buy fiat.
|
||||
INACTIVE,
|
||||
// Buy fiat flow is active and region availability has been checked.
|
||||
ACTIVE_CHECKING_REGION,
|
||||
// Buy fiat flow is active, feature is available in user's region & needs wallet connection.
|
||||
ACTIVE_NEEDS_ACCOUNT,
|
||||
}
|
||||
|
||||
const StyledTextButton = styled(ButtonText)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
gap: 4px;
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function SwapBuyFiatButton() {
|
||||
const { account } = useWeb3React()
|
||||
const openFiatOnRampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
|
||||
const [buyFiatFlowCompleted, setBuyFiatFlowCompleted] = useBuyFiatFlowCompleted()
|
||||
const [checkFiatRegionAvailability, setCheckFiatRegionAvailability] = useState(false)
|
||||
const {
|
||||
available: fiatOnrampAvailable,
|
||||
availabilityChecked: fiatOnrampAvailabilityChecked,
|
||||
loading: fiatOnrampAvailabilityLoading,
|
||||
} = useFiatOnrampAvailability(checkFiatRegionAvailability)
|
||||
const [buyFiatFlowState, setBuyFiatFlowState] = useState(BuyFiatFlowState.INACTIVE)
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
|
||||
/*
|
||||
* Depending on the current state of the buy fiat flow the user is in (buyFiatFlowState),
|
||||
* the desired behavior of clicking the 'Buy' button is different.
|
||||
* 1) Initially upon first click, need to check the availability of the feature in the user's
|
||||
* region, and continue the flow.
|
||||
* 2) If the feature is available in the user's region, need to connect a wallet, and continue
|
||||
* the flow.
|
||||
* 3) If the feature is available and a wallet account is connected, show fiat on ramp modal.
|
||||
* 4) If the feature is unavailable, show feature unavailable tooltip.
|
||||
*/
|
||||
const handleBuyCrypto = useCallback(() => {
|
||||
if (!fiatOnrampAvailabilityChecked) {
|
||||
setCheckFiatRegionAvailability(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.ACTIVE_CHECKING_REGION)
|
||||
} else if (fiatOnrampAvailable && !account && !walletDrawerOpen) {
|
||||
toggleWalletDrawer()
|
||||
setBuyFiatFlowState(BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
} else if (fiatOnrampAvailable && account) {
|
||||
openFiatOnRampModal()
|
||||
setBuyFiatFlowCompleted(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.INACTIVE)
|
||||
} else if (!fiatOnrampAvailable) {
|
||||
setBuyFiatFlowCompleted(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.INACTIVE)
|
||||
}
|
||||
}, [
|
||||
fiatOnrampAvailabilityChecked,
|
||||
fiatOnrampAvailable,
|
||||
account,
|
||||
walletDrawerOpen,
|
||||
toggleWalletDrawer,
|
||||
openFiatOnRampModal,
|
||||
setBuyFiatFlowCompleted,
|
||||
])
|
||||
|
||||
// Continue buy fiat flow automatically when requisite state changes have occured.
|
||||
useEffect(() => {
|
||||
if (
|
||||
(buyFiatFlowState === BuyFiatFlowState.ACTIVE_CHECKING_REGION && fiatOnrampAvailabilityChecked) ||
|
||||
(account && buyFiatFlowState === BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
) {
|
||||
handleBuyCrypto()
|
||||
}
|
||||
}, [account, handleBuyCrypto, buyFiatFlowState, fiatOnrampAvailabilityChecked])
|
||||
|
||||
const buyCryptoButtonDisabled =
|
||||
(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) ||
|
||||
fiatOnrampAvailabilityLoading ||
|
||||
// When wallet drawer is open AND user is in the connect wallet step of the buy fiat flow, disable buy fiat button.
|
||||
(walletDrawerOpen && buyFiatFlowState === BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
|
||||
const fiatOnRampsUnavailableTooltipDisabled =
|
||||
!fiatOnrampAvailabilityChecked || (fiatOnrampAvailabilityChecked && fiatOnrampAvailable)
|
||||
|
||||
return (
|
||||
<MouseoverTooltipContent
|
||||
wrap
|
||||
content={
|
||||
<div data-testid="fiat-on-ramp-unavailable-tooltip">
|
||||
<Trans>Crypto purchases are not available in your region. </Trans>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.FIAT_ON_RAMP_LEARN_MORE_LINK}
|
||||
>
|
||||
<ExternalLink href={MOONPAY_REGION_AVAILABILITY_ARTICLE} style={{ paddingLeft: '4px' }}>
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</TraceEvent>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
disableHover={fiatOnRampsUnavailableTooltipDisabled}
|
||||
>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.FIAT_ON_RAMP_BUY_BUTTON}
|
||||
properties={{ account_connected: !!account }}
|
||||
>
|
||||
<StyledTextButton onClick={handleBuyCrypto} disabled={buyCryptoButtonDisabled} data-testid="buy-fiat-button">
|
||||
<Trans>Buy</Trans>
|
||||
{!buyFiatFlowCompleted && <Dot data-testid="buy-fiat-flow-incomplete-indicator" />}
|
||||
</StyledTextButton>
|
||||
</TraceEvent>
|
||||
</MouseoverTooltipContent>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SettingsTab from '../Settings'
|
||||
import SwapBuyFiatButton from './SwapBuyFiatButton'
|
||||
|
||||
const StyledSwapHeader = styled.div`
|
||||
padding: 8px 12px;
|
||||
@@ -13,14 +15,27 @@ const StyledSwapHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function SwapHeader({ allowedSlippage }: { allowedSlippage: Percent }) {
|
||||
const fiatOnRampButtonEnabled = useFiatOnRampButtonEnabled()
|
||||
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBlack fontWeight={500} fontSize={16} style={{ marginRight: '8px' }}>
|
||||
<RowFixed style={{ gap: '8px' }}>
|
||||
<TextHeader className={subhead}>
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</TextHeader>
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<SettingsTab placeholderSlippage={allowedSlippage} />
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: inherit;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
color: white;
|
||||
background-color: primary;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
color: #0D111C;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-flex-wrap: nowrap;
|
||||
-ms-flex-wrap: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
will-change: transform;
|
||||
-webkit-transition: -webkit-transform 450ms ease;
|
||||
-webkit-transition: transform 450ms ease;
|
||||
transition: transform 450ms ease;
|
||||
-webkit-transform: perspective(1px) translateZ(0);
|
||||
-ms-transform: perspective(1px) translateZ(0);
|
||||
transform: perspective(1px) translateZ(0);
|
||||
}
|
||||
|
||||
.c2:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c2 > * {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c2 > a {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 0;
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
background: none;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c3:focus {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.c3:active {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c3:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: #4C82FB;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
color: #7780A0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c4:focus {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c4:active {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="c1 c2 c3 c4"
|
||||
data-testid="buy-fiat-button"
|
||||
>
|
||||
Buy
|
||||
<div
|
||||
class="c5"
|
||||
data-testid="buy-fiat-flow-incomplete-indicator"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,111 +1,145 @@
|
||||
// eslint-disable-next-line jest/no-export
|
||||
export {}
|
||||
import { ConnectionType, getConnections, useGetConnection } from 'connection'
|
||||
import { renderHook } from 'test-utils'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('Non-injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
const UserAgentMock = jest.requireMock('utils/userAgent')
|
||||
jest.mock('utils/userAgent', () => ({
|
||||
isMobile: false,
|
||||
}))
|
||||
|
||||
it('MetaMask Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
describe('connection utility/metadata tests', () => {
|
||||
const createWalletEnvironment = (ethereum: Window['window']['ethereum'], isMobile = false) => {
|
||||
UserAgentMock.isMobile = isMobile
|
||||
global.window.ethereum = ethereum
|
||||
|
||||
it('Coinbase Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
const displayed = getConnections().filter((c) => c.shouldDisplay())
|
||||
const getConnection = renderHook(() => useGetConnection()).result.current
|
||||
const injected = getConnection(ConnectionType.INJECTED)
|
||||
const coinbase = getConnection(ConnectionType.COINBASE_WALLET)
|
||||
const uniswap = getConnection(ConnectionType.UNIWALLET)
|
||||
const walletconnect = getConnection(ConnectionType.WALLET_CONNECT)
|
||||
|
||||
it('Coinbase and MetaMask Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: true }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
return { displayed, injected, coinbase, uniswap, walletconnect }
|
||||
}
|
||||
|
||||
it('Generic Injected Desktop', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: false }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
|
||||
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
|
||||
})
|
||||
it('Non-injected Desktop', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment(undefined)
|
||||
|
||||
it('Generic Injected Mobile Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
|
||||
})
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
it('MetaMask Mobile Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
|
||||
})
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Coinbase Mobile Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
it('MetaMask-Injected Desktop', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true })
|
||||
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.coinbaseWalletConnection.overrideActivate).toBeUndefined()
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
|
||||
})
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
it('mWeb Browser', async () => {
|
||||
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
|
||||
jest.mock('utils/userAgent', () => ({ isMobile: true }))
|
||||
const connection = await import('connection')
|
||||
expect(connection.darkInjectedConnection.shouldDisplay).toBe(false)
|
||||
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.coinbaseWalletConnection.overrideActivate).toBeDefined()
|
||||
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
|
||||
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(3)
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Coinbase-Injected Desktop', async () => {
|
||||
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true })
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Coinbase and MetaMask Injected Desktop', async () => {
|
||||
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true, isMetaMask: true })
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Generic Injected Desktop', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isTrustWallet: true })
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Generic Browser Wallet that injects as MetaMask', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isRabby: true, isMetaMask: true })
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('Generic Wallet Browser with delayed injection', async () => {
|
||||
const { injected } = createWalletEnvironment(undefined)
|
||||
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
createWalletEnvironment({ isTrustWallet: true })
|
||||
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
})
|
||||
|
||||
const UNKNOWN_INJECTOR = { isRandomWallet: true } as Window['window']['ethereum']
|
||||
it('Generic Unknown Injected Wallet Browser', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment(UNKNOWN_INJECTOR, true)
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
// Ensures we provide multiple connection options if in an unknown injected browser
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
it('MetaMask Mobile Browser', async () => {
|
||||
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true }, true)
|
||||
|
||||
expect(displayed.includes(injected)).toBe(true)
|
||||
expect(injected.getName()).toBe('MetaMask')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
expect(displayed.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('Coinbase Mobile Browser', async () => {
|
||||
const { displayed, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true }, true)
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
// Expect coinbase option to not override activation in a the cb mobile browser
|
||||
expect(coinbase.overrideActivate?.()).toBeFalsy()
|
||||
expect(displayed.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('Uninjected mWeb Browser', async () => {
|
||||
const { displayed, injected, coinbase, walletconnect } = createWalletEnvironment(undefined, true)
|
||||
|
||||
expect(displayed.includes(coinbase)).toBe(true)
|
||||
expect(displayed.includes(walletconnect)).toBe(true)
|
||||
// Don't show injected connection on plain mWeb browser
|
||||
expect(displayed.includes(injected)).toBe(false)
|
||||
// Expect coinbase option to launch coinbase app in a regular mobile browser
|
||||
expect(coinbase.overrideActivate?.()).toBeTruthy()
|
||||
|
||||
expect(displayed.length).toEqual(3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,17 +9,15 @@ import GNOSIS_ICON_URL from 'assets/images/gnosis.png'
|
||||
import METAMASK_ICON_URL from 'assets/images/metamask.svg'
|
||||
import UNIWALLET_ICON_URL from 'assets/images/uniwallet.svg'
|
||||
import WALLET_CONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg'
|
||||
import INJECTED_DARK_ICON_URL from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON_URL from 'assets/svg/browser-wallet-light.svg'
|
||||
import UNISWAP_LOGO_URL from 'assets/svg/logo.svg'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useCallback } from 'react'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
|
||||
|
||||
import { RPC_URLS } from '../constants/networks'
|
||||
import { RPC_PROVIDERS } from '../constants/providers'
|
||||
import { isCoinbaseWallet, isInjected, isMetaMaskWallet } from './utils'
|
||||
import { getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
|
||||
import { UniwalletConnect, WalletConnectPopup } from './WalletConnect'
|
||||
|
||||
export enum ConnectionType {
|
||||
@@ -32,13 +30,14 @@ export enum ConnectionType {
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
name: string
|
||||
getName(): string
|
||||
connector: Connector
|
||||
hooks: Web3ReactHooks
|
||||
type: ConnectionType
|
||||
icon?: string
|
||||
shouldDisplay?: boolean
|
||||
overrideActivate?: () => void
|
||||
// TODO(WEB-3130): add darkmode check for icons
|
||||
getIcon?(): string
|
||||
shouldDisplay(): boolean
|
||||
overrideActivate?: () => boolean
|
||||
isNew?: boolean
|
||||
}
|
||||
|
||||
@@ -50,73 +49,72 @@ const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
|
||||
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
|
||||
)
|
||||
export const networkConnection: Connection = {
|
||||
name: 'Network',
|
||||
getName: () => 'Network',
|
||||
connector: web3Network,
|
||||
hooks: web3NetworkHooks,
|
||||
type: ConnectionType.NETWORK,
|
||||
shouldDisplay: false,
|
||||
shouldDisplay: () => false,
|
||||
}
|
||||
|
||||
const isCoinbaseWalletBrowser = isMobile && isCoinbaseWallet
|
||||
const isMetaMaskBrowser = isMobile && isMetaMaskWallet
|
||||
const getIsInjectedMobileBrowser = isCoinbaseWalletBrowser || isMetaMaskBrowser
|
||||
const getIsCoinbaseWalletBrowser = () => isMobile && getIsCoinbaseWallet()
|
||||
const getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet()
|
||||
const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser()
|
||||
|
||||
const getShouldAdvertiseMetaMask = !isMetaMaskWallet && !isMobile && (!isInjected || isCoinbaseWallet)
|
||||
const isGenericInjector = isInjected && !isMetaMaskWallet && !isCoinbaseWallet
|
||||
const getShouldAdvertiseMetaMask = () =>
|
||||
!getIsMetaMaskWallet() && !isMobile && (!getIsInjected() || getIsCoinbaseWallet())
|
||||
const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() && !getIsCoinbaseWallet()
|
||||
|
||||
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
|
||||
const baseInjectedConnection: Omit<Connection, 'icon'> = {
|
||||
name: isGenericInjector ? 'Browser Wallet' : 'MetaMask',
|
||||
const injectedConnection: Connection = {
|
||||
// TODO(WEB-3131) re-add "Install MetaMask" string when no injector is present
|
||||
getName: () => (getIsGenericInjector() ? 'Browser Wallet' : 'MetaMask'),
|
||||
connector: web3Injected,
|
||||
hooks: web3InjectedHooks,
|
||||
type: ConnectionType.INJECTED,
|
||||
shouldDisplay: isMetaMaskWallet || getShouldAdvertiseMetaMask || isGenericInjector,
|
||||
getIcon: () => (getIsGenericInjector() ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL),
|
||||
shouldDisplay: () => getIsMetaMaskWallet() || getShouldAdvertiseMetaMask() || getIsGenericInjector(),
|
||||
// If on non-injected, non-mobile browser, prompt user to install Metamask
|
||||
overrideActivate: getShouldAdvertiseMetaMask ? () => window.open('https://metamask.io/', 'inst_metamask') : undefined,
|
||||
}
|
||||
|
||||
export const darkInjectedConnection: Connection = {
|
||||
...baseInjectedConnection,
|
||||
icon: isGenericInjector ? INJECTED_DARK_ICON_URL : METAMASK_ICON_URL,
|
||||
}
|
||||
|
||||
export const lightInjectedConnection: Connection = {
|
||||
...baseInjectedConnection,
|
||||
icon: isGenericInjector ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL,
|
||||
overrideActivate: () => {
|
||||
if (getShouldAdvertiseMetaMask()) {
|
||||
window.open('https://metamask.io/', 'inst_metamask')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
|
||||
export const gnosisSafeConnection: Connection = {
|
||||
name: 'Gnosis Safe',
|
||||
getName: () => 'Gnosis Safe',
|
||||
connector: web3GnosisSafe,
|
||||
hooks: web3GnosisSafeHooks,
|
||||
type: ConnectionType.GNOSIS_SAFE,
|
||||
icon: GNOSIS_ICON_URL,
|
||||
shouldDisplay: false,
|
||||
getIcon: () => GNOSIS_ICON_URL,
|
||||
shouldDisplay: () => false,
|
||||
}
|
||||
|
||||
const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnectPopup>(
|
||||
(actions) => new WalletConnectPopup({ actions, onError })
|
||||
)
|
||||
export const walletConnectConnection: Connection = {
|
||||
name: 'WalletConnect',
|
||||
getName: () => 'WalletConnect',
|
||||
connector: web3WalletConnect,
|
||||
hooks: web3WalletConnectHooks,
|
||||
type: ConnectionType.WALLET_CONNECT,
|
||||
icon: WALLET_CONNECT_ICON_URL,
|
||||
shouldDisplay: !getIsInjectedMobileBrowser,
|
||||
getIcon: () => WALLET_CONNECT_ICON_URL,
|
||||
shouldDisplay: () => !getIsInjectedMobileBrowser(),
|
||||
}
|
||||
|
||||
const [web3UniwalletConnect, web3UniwalletConnectHooks] = initializeConnector<UniwalletConnect>(
|
||||
(actions) => new UniwalletConnect({ actions, onError })
|
||||
)
|
||||
export const uniwalletConnectConnection: Connection = {
|
||||
name: 'Uniswap Wallet',
|
||||
getName: () => 'Uniswap Wallet',
|
||||
connector: web3UniwalletConnect,
|
||||
hooks: web3UniwalletConnectHooks,
|
||||
type: ConnectionType.UNIWALLET,
|
||||
icon: UNIWALLET_ICON_URL,
|
||||
shouldDisplay: Boolean(!getIsInjectedMobileBrowser && !isNonIOSPhone),
|
||||
getIcon: () => UNIWALLET_ICON_URL,
|
||||
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
|
||||
isNew: true,
|
||||
}
|
||||
|
||||
@@ -134,24 +132,28 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba
|
||||
})
|
||||
)
|
||||
|
||||
export const coinbaseWalletConnection: Connection = {
|
||||
name: 'Coinbase Wallet',
|
||||
const coinbaseWalletConnection: Connection = {
|
||||
getName: () => 'Coinbase Wallet',
|
||||
connector: web3CoinbaseWallet,
|
||||
hooks: web3CoinbaseWalletHooks,
|
||||
type: ConnectionType.COINBASE_WALLET,
|
||||
icon: COINBASE_ICON_URL,
|
||||
shouldDisplay: Boolean((isMobile && !getIsInjectedMobileBrowser) || !isMobile || isCoinbaseWalletBrowser),
|
||||
getIcon: () => COINBASE_ICON_URL,
|
||||
shouldDisplay: () =>
|
||||
Boolean((isMobile && !getIsInjectedMobileBrowser()) || !isMobile || getIsCoinbaseWalletBrowser()),
|
||||
// If on a mobile browser that isn't the coinbase wallet browser, deeplink to the coinbase wallet app
|
||||
overrideActivate:
|
||||
isMobile && !getIsInjectedMobileBrowser
|
||||
? () => window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
|
||||
: undefined,
|
||||
overrideActivate: () => {
|
||||
if (isMobile && !getIsInjectedMobileBrowser()) {
|
||||
window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export function getConnections(isDarkMode: boolean) {
|
||||
export function getConnections() {
|
||||
return [
|
||||
uniwalletConnectConnection,
|
||||
isDarkMode ? darkInjectedConnection : lightInjectedConnection,
|
||||
injectedConnection,
|
||||
walletConnectConnection,
|
||||
coinbaseWalletConnection,
|
||||
gnosisSafeConnection,
|
||||
@@ -159,38 +161,29 @@ export function getConnections(isDarkMode: boolean) {
|
||||
]
|
||||
}
|
||||
|
||||
export function useConnections() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return getConnections(isDarkMode)
|
||||
}
|
||||
|
||||
export function useGetConnection() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return useCallback(
|
||||
(c: Connector | ConnectionType) => {
|
||||
if (c instanceof Connector) {
|
||||
const connection = getConnections(isDarkMode).find((connection) => connection.connector === c)
|
||||
if (!connection) {
|
||||
throw Error('unsupported connector')
|
||||
}
|
||||
return connection
|
||||
} else {
|
||||
switch (c) {
|
||||
case ConnectionType.INJECTED:
|
||||
return isDarkMode ? darkInjectedConnection : lightInjectedConnection
|
||||
case ConnectionType.COINBASE_WALLET:
|
||||
return coinbaseWalletConnection
|
||||
case ConnectionType.WALLET_CONNECT:
|
||||
return walletConnectConnection
|
||||
case ConnectionType.UNIWALLET:
|
||||
return uniwalletConnectConnection
|
||||
case ConnectionType.NETWORK:
|
||||
return networkConnection
|
||||
case ConnectionType.GNOSIS_SAFE:
|
||||
return gnosisSafeConnection
|
||||
}
|
||||
return useCallback((c: Connector | ConnectionType) => {
|
||||
if (c instanceof Connector) {
|
||||
const connection = getConnections().find((connection) => connection.connector === c)
|
||||
if (!connection) {
|
||||
throw Error('unsupported connector')
|
||||
}
|
||||
},
|
||||
[isDarkMode]
|
||||
)
|
||||
return connection
|
||||
} else {
|
||||
switch (c) {
|
||||
case ConnectionType.INJECTED:
|
||||
return injectedConnection
|
||||
case ConnectionType.COINBASE_WALLET:
|
||||
return coinbaseWalletConnection
|
||||
case ConnectionType.WALLET_CONNECT:
|
||||
return walletConnectConnection
|
||||
case ConnectionType.UNIWALLET:
|
||||
return uniwalletConnectConnection
|
||||
case ConnectionType.NETWORK:
|
||||
return networkConnection
|
||||
case ConnectionType.GNOSIS_SAFE:
|
||||
return gnosisSafeConnection
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
export const isInjected = Boolean(window.ethereum)
|
||||
export const getIsInjected = () => Boolean(window.ethereum)
|
||||
|
||||
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
|
||||
// This variable should be true only when using the MetaMask extension
|
||||
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
|
||||
type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet' | 'isLedgerConnect'
|
||||
const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet', 'isLedgerConnect']
|
||||
export const isMetaMaskWallet = Boolean(
|
||||
window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag])
|
||||
)
|
||||
export const getIsMetaMaskWallet = () =>
|
||||
Boolean(window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag]))
|
||||
|
||||
export const isCoinbaseWallet = Boolean(window.ethereum?.isCoinbaseWallet)
|
||||
export const getIsCoinbaseWallet = () => Boolean(window.ethereum?.isCoinbaseWallet)
|
||||
|
||||
// https://eips.ethereum.org/EIPS/eip-1193#provider-errors
|
||||
export enum ErrorCode {
|
||||
|
||||
@@ -489,7 +489,7 @@ class ExtendedEther extends Ether {
|
||||
public get wrapped(): Token {
|
||||
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
if (wrapped) return wrapped
|
||||
throw new Error('Unsupported chain ID')
|
||||
throw new Error(`Unsupported chain ID: ${this.chainId}`)
|
||||
}
|
||||
|
||||
private static _cachedExtendedEther: { [chainId: number]: NativeCurrency } = {}
|
||||
|
||||
@@ -5,6 +5,7 @@ export enum FeatureFlag {
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
|
||||
swapWidget = 'swap_widget_replacement_enabled',
|
||||
statsigDummy = 'web_dummy_gate_amplitude_id',
|
||||
nftGraphql = 'nft_graphql_migration',
|
||||
|
||||
9
src/featureFlags/flags/fiatOnRampButton.ts
Normal file
9
src/featureFlags/flags/fiatOnRampButton.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
function useFiatOnRampButtonFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.fiatOnRampButtonOnSwap)
|
||||
}
|
||||
|
||||
export function useFiatOnRampButtonEnabled(): boolean {
|
||||
return useFiatOnRampButtonFlag() === BaseVariant.Enabled
|
||||
}
|
||||
@@ -39,33 +39,18 @@ gql`
|
||||
node {
|
||||
id
|
||||
name
|
||||
ownerAddress
|
||||
image {
|
||||
url
|
||||
}
|
||||
smallImage {
|
||||
url
|
||||
}
|
||||
originalImage {
|
||||
url
|
||||
}
|
||||
tokenId
|
||||
description
|
||||
animationUrl
|
||||
suspiciousFlag
|
||||
collection {
|
||||
name
|
||||
isVerified
|
||||
image {
|
||||
url
|
||||
}
|
||||
creator {
|
||||
address
|
||||
profileImage {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
}
|
||||
nftContracts {
|
||||
address
|
||||
standard
|
||||
@@ -98,11 +83,8 @@ gql`
|
||||
}
|
||||
}
|
||||
rarities {
|
||||
provider
|
||||
rank
|
||||
score
|
||||
}
|
||||
metadataUrl
|
||||
}
|
||||
cursor
|
||||
}
|
||||
|
||||
@@ -122,7 +122,8 @@ export function useNftBalance(
|
||||
first?: number,
|
||||
after?: string,
|
||||
last?: number,
|
||||
before?: string
|
||||
before?: string,
|
||||
skip = false
|
||||
) {
|
||||
const { data, loading, fetchMore } = useNftBalanceQuery({
|
||||
variables: {
|
||||
@@ -140,6 +141,7 @@ export function useNftBalance(
|
||||
last,
|
||||
before,
|
||||
},
|
||||
skip,
|
||||
})
|
||||
|
||||
const hasNext = data?.nftBalances?.pageInfo?.hasNextPage
|
||||
|
||||
@@ -18,7 +18,9 @@ describe('fetchTokenList', () => {
|
||||
fetch.mockOnceIf(url, () => {
|
||||
throw new Error()
|
||||
})
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`failed to fetch list: ${url}`)
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -33,9 +35,63 @@ describe('fetchTokenList', () => {
|
||||
expect(resolver).toHaveBeenCalledWith(url)
|
||||
})
|
||||
|
||||
it('throws an error when the ENS resolver throws', async () => {
|
||||
const url = 'example.eth'
|
||||
const error = new Error('ENS resolver error')
|
||||
resolver.mockRejectedValue(error)
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`failed to resolve ENS name: ${url}`)
|
||||
expect(resolver).toHaveBeenCalledWith(url)
|
||||
})
|
||||
|
||||
it('fetches and validates a list from an ENS address', async () => {
|
||||
jest.mock('../../utils/contenthashToUri', () =>
|
||||
jest.fn().mockImplementation(() => 'ipfs://QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1')
|
||||
)
|
||||
const url = 'example.eth'
|
||||
const contenthash = '0xe3010170122013e051d1cfff20606de36845d4fe28deb9861a319a5bc8596fa4e610e8803918'
|
||||
const translatedUri = 'https://cloudflare-ipfs.com/ipfs/QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1/'
|
||||
resolver.mockResolvedValue(contenthash)
|
||||
fetch.mockOnceIf(translatedUri, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
})
|
||||
|
||||
it('throws for an unrecognized list URL protocol', async () => {
|
||||
const url = 'unknown://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve(''))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`Unrecognized list URL protocol.`)
|
||||
})
|
||||
|
||||
it('logs a debug statement if the response is not successful', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve({ status: 404 }))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fetches and validates the default token list', async () => {
|
||||
fetch.mockOnceIf(DEFAULT_TOKEN_LIST, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(DEFAULT_TOKEN_LIST, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('throws for a list with invalid json response', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve('invalid json'))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses cached value the second time', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,11 @@ export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.o
|
||||
|
||||
const listCache = new Map<string, TokenList>()
|
||||
|
||||
/** Fetches and validates a token list. */
|
||||
/**
|
||||
* Fetches and validates a token list.
|
||||
* For a given token list URL, we try to fetch the list from all the possible HTTP URLs.
|
||||
* For example, IPFS URLs can be fetched through multiple gateways.
|
||||
*/
|
||||
export default async function fetchTokenList(
|
||||
listUrl: string,
|
||||
resolveENSContentHash: (ensName: string) => Promise<string>,
|
||||
@@ -43,31 +47,38 @@ export default async function fetchTokenList(
|
||||
urls = uriToHttp(listUrl)
|
||||
}
|
||||
|
||||
if (urls.length === 0) {
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
}
|
||||
|
||||
// Try each of the derived URLs until one succeeds.
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i]
|
||||
const isLast = i === urls.length - 1
|
||||
let response
|
||||
try {
|
||||
response = await fetch(url, { credentials: 'omit' })
|
||||
} catch (error) {
|
||||
const message = `failed to fetch list: ${listUrl}`
|
||||
console.debug(message, error)
|
||||
if (isLast) throw new Error(message)
|
||||
console.debug(`failed to fetch list: ${listUrl} (${url})`, error)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `failed to fetch list: ${listUrl}`
|
||||
console.debug(message, response.statusText)
|
||||
if (isLast) throw new Error(message)
|
||||
console.debug(`failed to fetch list ${listUrl} (${url})`, response.statusText)
|
||||
continue
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
const list = skipValidation ? json : await validateTokenList(json)
|
||||
listCache?.set(listUrl, list)
|
||||
return list
|
||||
try {
|
||||
// The content of the result is sometimes invalid even with a 200 status code.
|
||||
// A response can be invalid if it's not a valid JSON or if it doesn't match the TokenList schema.
|
||||
const json = await response.json()
|
||||
const list = skipValidation ? json : await validateTokenList(json)
|
||||
listCache?.set(listUrl, list)
|
||||
return list
|
||||
} catch (error) {
|
||||
console.debug(`failed to parse and validate list response: ${listUrl} (${url})`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
throw new Error(`No valid token list found at any URLs derived from ${listUrl}.`)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { i18n } from '@lingui/core'
|
||||
import { I18nProvider } from '@lingui/react'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
|
||||
import {
|
||||
af,
|
||||
@@ -83,9 +82,8 @@ export async function dynamicActivate(locale: SupportedLocale) {
|
||||
const catalog = await import(`locales/${locale}.js`)
|
||||
// Bundlers will either export it as default or as a named export named default.
|
||||
i18n.load(locale, catalog.messages || catalog.default.messages)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Sentry.captureException(new Error(`Unable to load locale (${locale})`))
|
||||
} catch (error: unknown) {
|
||||
console.error(new Error(`Unable to load locale (${locale}): ${error}`))
|
||||
}
|
||||
i18n.activate(locale)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
@@ -12,7 +13,6 @@ import Row from 'components/Row'
|
||||
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
@@ -287,7 +287,7 @@ const PENDING_BAG_STATUSES = [
|
||||
]
|
||||
|
||||
export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) => {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const theme = useTheme()
|
||||
const { account, chainId, connector } = useWeb3React()
|
||||
const connected = Boolean(account && chainId)
|
||||
|
||||
@@ -5,11 +5,11 @@ import Bag from './Bag'
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
|
||||
isActive: true,
|
||||
}),
|
||||
...web3React,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
24
src/nft/components/card/MarketplaceContainer.test.tsx
Normal file
24
src/nft/components/card/MarketplaceContainer.test.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Markets } from 'nft/types'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import { MarketplaceContainer } from './icons'
|
||||
|
||||
describe('MarketplaceContainer', () => {
|
||||
it('should render with list price', () => {
|
||||
const result = render(<MarketplaceContainer isSelected={false} listedPrice="10" />)
|
||||
expect(result.queryByText('10 ETH')).toBeTruthy()
|
||||
expect(result.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render null without list price or marketplace', () => {
|
||||
const result = render(<MarketplaceContainer isSelected={false} listedPrice="10" hidePrice={true} />)
|
||||
expect(result.queryByText('10 ETH')).toBeFalsy()
|
||||
expect(result.container.children.length).toEqual(0)
|
||||
expect(result.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render with marketplace', () => {
|
||||
const result = render(<MarketplaceContainer isSelected={false} marketplace={Markets.Opensea} />)
|
||||
expect(result.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,144 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MarketplaceContainer should render null without list price or marketplace 1`] = `<div />`;
|
||||
|
||||
exports[`MarketplaceContainer should render with list price 1`] = `
|
||||
.c1 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
height: 32px;
|
||||
width: auto;
|
||||
padding: 0px 8px;
|
||||
background: rgba(93,103,133,0.24);
|
||||
color: #F5F6FC;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
border-radius: 32px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
gap: 6px;
|
||||
color: #F5F6FC;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
text-shadow: 1px 1px 3px rgba(51,53,72,0.54);
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c1 c2 c3"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="20"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"
|
||||
/>
|
||||
<line
|
||||
x1="7"
|
||||
x2="7.01"
|
||||
y1="7"
|
||||
y2="7"
|
||||
/>
|
||||
</svg>
|
||||
10
|
||||
ETH
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`MarketplaceContainer should render with marketplace 1`] = `
|
||||
.c0 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0px;
|
||||
background: rgba(93,103,133,0.24);
|
||||
color: #F5F6FC;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
border-radius: 32px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M24 16.06V14.806C24 14.69 23.888 14.606 23.778 14.638L17.856 16.35C17.824 16.358 17.796 16.376 17.774 16.4C17.1396 17.1008 16.6005 17.571 16.4578 17.6955L16.448 17.704C16.08 18.016 15.624 18.186 15.144 18.186H14.093C13.4894 18.186 13 17.6966 13 17.093C13 16.4894 13.4894 16 14.093 16H14.704C14.748 16 14.79 15.984 14.822 15.956L15.042 15.754C15.136 15.668 15.248 15.564 15.382 15.43C15.3933 15.4187 15.4047 15.4073 15.4163 15.3958C15.4868 15.3256 15.5621 15.2505 15.636 15.168C15.724 15.082 15.81 14.986 15.89 14.892C16.024 14.748 16.152 14.598 16.286 14.44C16.382 14.336 16.47 14.218 16.556 14.1C16.652 13.988 16.746 13.862 16.834 13.742C16.8666 13.6941 16.9013 13.6457 16.9367 13.5963C16.9708 13.5486 17.0057 13.5 17.04 13.45C17.104 13.354 17.168 13.252 17.222 13.156C17.39 12.896 17.532 12.618 17.652 12.34C17.707 12.2211 17.751 12.096 17.7937 11.9743C17.7992 11.9588 17.8046 11.9434 17.81 11.928C17.858 11.786 17.898 11.652 17.928 11.51C18 11.176 18.016 10.844 17.984 10.512C17.9764 10.4136 17.9688 10.317 17.9477 10.2255C17.9454 10.2152 17.944 10.2046 17.944 10.194C17.936 10.126 17.92 10.05 17.898 9.98001C17.826 9.65601 17.714 9.332 17.572 9.014C17.524 8.89599 17.468 8.77599 17.414 8.66598C17.286 8.42802 17.152 8.19001 17 7.96C16.9695 7.91136 16.9357 7.86209 16.902 7.81289C16.8762 7.77511 16.8503 7.73737 16.826 7.70002C16.7297 7.5514 16.6213 7.40815 16.5163 7.26916C16.4926 7.2379 16.4692 7.20686 16.446 7.17602C16.384 7.09458 16.3161 7.01314 16.2477 6.93116C16.2103 6.88629 16.1728 6.84127 16.136 6.79599C16.032 6.66998 15.93 6.54998 15.826 6.43201C15.454 6.01201 15.064 5.63198 14.716 5.30802C14.652 5.24399 14.582 5.18001 14.51 5.11798C14.24 4.87201 13.994 4.65801 13.788 4.49198C13.726 4.44425 13.6703 4.39722 13.6185 4.35345C13.5835 4.32387 13.5503 4.29579 13.518 4.26998C13.4545 4.22272 13.3996 4.18086 13.3537 4.14585C13.3258 4.12459 13.3012 4.10585 13.28 4.08998C13.264 4.078 13.246 4.06999 13.228 4.06398C13.0932 4.02615 13 3.90323 13 3.76322V2.11201C13 1.80401 12.876 1.52802 12.678 1.326C12.48 1.12398 12.204 1 11.9 1C11.292 1 10.8 1.498 10.8 2.11201V3.2656C10.8 3.32504 10.7432 3.36806 10.686 3.35198L10.376 3.26399L10.102 3.18821C10.1004 3.18775 10.0987 3.18716 10.097 3.18657C10.0934 3.18529 10.0898 3.18401 10.086 3.18401C10.082 3.18401 10.078 3.18348 10.0742 3.18244L7.93999 2.60399C7.84603 2.578 7.766 2.68 7.81402 2.766L8.15602 3.39801C8.17546 3.44665 8.2001 3.4953 8.22543 3.54529C8.24175 3.57751 8.25835 3.61028 8.27403 3.64398C8.33001 3.75602 8.38603 3.87399 8.44002 3.992C8.48799 4.09599 8.53601 4.198 8.59203 4.30999C8.61561 4.36275 8.63965 4.41614 8.66403 4.4703C8.75336 4.6687 8.8473 4.87732 8.94 5.10202L8.94079 5.10389C9.02051 5.29326 9.10024 5.48265 9.17001 5.68C9.36199 6.178 9.54402 6.70999 9.70201 7.25601C9.7413 7.37805 9.7727 7.49617 9.80452 7.61587C9.81806 7.66682 9.83168 7.71806 9.84601 7.77001L9.86799 7.866C9.93201 8.12001 9.98799 8.372 10.028 8.62601C10.06 8.8 10.09 8.96598 10.106 9.134L10.106 9.13407C10.13 9.32404 10.154 9.51401 10.162 9.70398C10.178 9.87801 10.186 10.06 10.186 10.234C10.186 10.678 10.146 11.106 10.052 11.51C10.0462 11.5316 10.0403 11.5534 10.0344 11.5755C10.008 11.6739 9.98068 11.776 9.94802 11.874C9.91838 11.9792 9.87997 12.0844 9.84008 12.1937C9.82613 12.2319 9.812 12.2706 9.798 12.31C9.7957 12.3162 9.7934 12.3224 9.7911 12.3286C9.76138 12.4087 9.73114 12.4902 9.694 12.57C9.49601 13.046 9.24999 13.52 8.99602 13.964C8.624 14.622 8.25002 15.2 7.988 15.572C7.97207 15.5959 7.95652 15.6186 7.94154 15.6405C7.92269 15.6681 7.90474 15.6944 7.88803 15.72C7.80601 15.836 7.89002 16 8.032 16H9.707C10.3106 16 10.8 16.4894 10.8 17.093C10.8 17.6966 10.3106 18.186 9.707 18.186H8.00003C7.24802 18.186 6.55203 17.76 6.21599 17.078C6.042 16.736 5.974 16.36 6.01402 15.992C6.02401 15.882 5.94199 15.778 5.82999 15.778H0.17403C0.0779956 15.778 0 15.856 0 15.952V16.068C0 19.676 2.914 22.6 6.51002 22.6H16.656C18.5579 22.6 19.6378 20.8669 20.6993 19.1634C20.9951 18.6886 21.2896 18.216 21.6 17.784C22.158 17.008 23.5 16.392 23.892 16.224C23.956 16.196 24 16.132 24 16.06ZM1.51195 13.202L1.42794 13.334C1.35397 13.448 1.43594 13.6 1.57593 13.6H6.78395C6.84196 13.6 6.89594 13.572 6.92796 13.524C6.99596 13.4201 7.05994 13.312 7.11795 13.202C7.56797 12.446 7.96794 11.628 8.11394 11.024C8.45594 9.55604 7.72595 7.19805 6.87994 5.30201C6.82396 5.17604 6.64993 5.16401 6.57596 5.28004L1.51195 13.202Z"
|
||||
fill="white"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -186,22 +186,20 @@ const Container = ({
|
||||
isSelected,
|
||||
isDisabled,
|
||||
detailsHref,
|
||||
doNotLinkToDetails = false,
|
||||
testId,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
isSelected: boolean
|
||||
isDisabled: boolean
|
||||
detailsHref: string
|
||||
doNotLinkToDetails: boolean
|
||||
detailsHref?: string
|
||||
testId?: string
|
||||
children: ReactNode
|
||||
onClick?: (e: React.MouseEvent) => void
|
||||
}) => {
|
||||
return (
|
||||
<CardContainer isSelected={isSelected} isDisabled={isDisabled} testId={testId} onClick={onClick}>
|
||||
<StyledLink to={doNotLinkToDetails ? '' : detailsHref}>{children}</StyledLink>
|
||||
{detailsHref ? <StyledLink to={detailsHref}>{children}</StyledLink> : children}
|
||||
</CardContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,11 +40,13 @@ export const MarketplaceContainer = ({
|
||||
marketplace,
|
||||
tokenType,
|
||||
listedPrice,
|
||||
hidePrice,
|
||||
}: {
|
||||
isSelected: boolean
|
||||
marketplace?: Markets
|
||||
tokenType?: NftStandard
|
||||
listedPrice?: string
|
||||
hidePrice?: boolean
|
||||
}) => {
|
||||
if (isSelected) {
|
||||
if (!marketplace) {
|
||||
@@ -62,7 +64,7 @@ export const MarketplaceContainer = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (listedPrice) {
|
||||
if (listedPrice && !hidePrice) {
|
||||
return (
|
||||
<StyledMarketplaceContainer isText={true}>
|
||||
<ListPriceRowContainer>
|
||||
|
||||
@@ -13,11 +13,11 @@ interface NftCardProps {
|
||||
display: NftCardDisplayProps
|
||||
isSelected: boolean
|
||||
isDisabled: boolean
|
||||
selectAsset: () => void
|
||||
unselectAsset: () => void
|
||||
onClick?: () => void
|
||||
selectAsset?: () => void
|
||||
unselectAsset?: () => void
|
||||
onButtonClick?: () => void
|
||||
onCardClick?: () => void
|
||||
sendAnalyticsEvent?: () => void
|
||||
doNotLinkToDetails?: boolean
|
||||
mediaShouldBePlaying: boolean
|
||||
uniformAspectRatio?: UniformAspectRatio
|
||||
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
|
||||
@@ -38,6 +38,12 @@ export interface NftCardDisplayProps {
|
||||
disabledInfo?: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* NftCard is a component that displays an NFT asset.
|
||||
*
|
||||
* By default, clicking on the card will navigate to the details page.
|
||||
* If you wish to override this behavior, pass a value for the onCardClick prop.
|
||||
*/
|
||||
export const NftCard = ({
|
||||
asset,
|
||||
display,
|
||||
@@ -45,9 +51,9 @@ export const NftCard = ({
|
||||
selectAsset,
|
||||
unselectAsset,
|
||||
isDisabled,
|
||||
onClick,
|
||||
onButtonClick,
|
||||
onCardClick,
|
||||
sendAnalyticsEvent,
|
||||
doNotLinkToDetails = false,
|
||||
mediaShouldBePlaying,
|
||||
uniformAspectRatio = UniformAspectRatios.square,
|
||||
setUniformAspectRatio,
|
||||
@@ -57,7 +63,13 @@ export const NftCard = ({
|
||||
testId,
|
||||
hideDetails = false,
|
||||
}: NftCardProps) => {
|
||||
const clickActionButton = useSelectAsset(selectAsset, unselectAsset, isSelected, isDisabled, onClick)
|
||||
const clickActionButton = useSelectAsset({
|
||||
selectAsset,
|
||||
unselectAsset,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
onClick: onButtonClick,
|
||||
})
|
||||
const { bagExpanded, setBagExpanded } = useBag(
|
||||
(state) => ({
|
||||
bagExpanded: state.bagExpanded,
|
||||
@@ -77,16 +89,17 @@ export const NftCard = ({
|
||||
<Card.Container
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
detailsHref={detailsHref(asset)}
|
||||
doNotLinkToDetails={doNotLinkToDetails}
|
||||
detailsHref={onCardClick ? undefined : detailsHref(asset)}
|
||||
testId={testId}
|
||||
onClick={() => {
|
||||
if (bagExpanded) setBagExpanded({ bagExpanded: false })
|
||||
onCardClick?.()
|
||||
sendAnalyticsEvent?.()
|
||||
}}
|
||||
>
|
||||
<MediaContainer isDisabled={isDisabled}>
|
||||
<MarketplaceContainer
|
||||
hidePrice={hideDetails}
|
||||
isSelected={isSelected}
|
||||
marketplace={marketplace}
|
||||
tokenType={tokenType}
|
||||
|
||||
@@ -96,13 +96,19 @@ export function getNftDisplayComponent(
|
||||
}
|
||||
}
|
||||
|
||||
export function useSelectAsset(
|
||||
selectAsset: () => void,
|
||||
unselectAsset: () => void,
|
||||
isSelected: boolean,
|
||||
isDisabled: boolean,
|
||||
export function useSelectAsset({
|
||||
selectAsset,
|
||||
unselectAsset,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
onClick,
|
||||
}: {
|
||||
selectAsset?: () => void
|
||||
unselectAsset?: () => void
|
||||
isSelected: boolean
|
||||
isDisabled: boolean
|
||||
onClick?: () => void
|
||||
) {
|
||||
}) {
|
||||
return useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -117,7 +123,7 @@ export function useSelectAsset(
|
||||
return
|
||||
}
|
||||
|
||||
return isSelected ? unselectAsset() : selectAsset()
|
||||
return isSelected ? unselectAsset?.() : selectAsset?.()
|
||||
},
|
||||
[selectAsset, isDisabled, onClick, unselectAsset, isSelected]
|
||||
)
|
||||
|
||||
@@ -3,10 +3,12 @@ import { useTrace } from '@uniswap/analytics'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { NFTEventName } from '@uniswap/analytics-events'
|
||||
import { NftCard, NftCardDisplayProps } from 'nft/components/card'
|
||||
import { detailsHref } from 'nft/components/card/utils'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
|
||||
import { WalletAsset } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
interface ViewMyNftsAssetProps {
|
||||
asset: WalletAsset
|
||||
@@ -27,6 +29,7 @@ export const ViewMyNftsAsset = ({
|
||||
const cartExpanded = useBag((state) => state.bagExpanded)
|
||||
const toggleCart = useBag((state) => state.toggleBag)
|
||||
const isMobile = useIsMobile()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const isSelected = useMemo(() => {
|
||||
return sellAssets.some(
|
||||
@@ -35,7 +38,7 @@ export const ViewMyNftsAsset = ({
|
||||
}, [asset, sellAssets])
|
||||
|
||||
const trace = useTrace()
|
||||
const onCardClick = () => handleSelect(isSelected)
|
||||
const toggleSelect = () => handleSelect(isSelected)
|
||||
|
||||
const handleSelect = (removeAsset: boolean) => {
|
||||
if (removeAsset) {
|
||||
@@ -79,11 +82,13 @@ export const ViewMyNftsAsset = ({
|
||||
isDisabled={Boolean(isDisabled)}
|
||||
selectAsset={() => handleSelect(false)}
|
||||
unselectAsset={() => handleSelect(true)}
|
||||
onClick={onCardClick}
|
||||
onButtonClick={toggleSelect}
|
||||
onCardClick={() => {
|
||||
if (!hideDetails) navigate(detailsHref(asset))
|
||||
}}
|
||||
mediaShouldBePlaying={mediaShouldBePlaying}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
testId="nft-profile-asset"
|
||||
doNotLinkToDetails={hideDetails}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
interface BagState {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NftAssetSortableField } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
export enum SortBy {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools, persist } from 'zustand/middleware'
|
||||
|
||||
interface State {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
interface State {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
interface NFTClaim {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
export type MarketplaceOption = { name: string; icon: string }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CollectionRow, ListingRow, ListingStatus } from 'nft/types'
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
interface NFTListState {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import { OpenSeaAsset } from '../types'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
interface PriceRangeProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import { ProfilePageStateType } from '../types'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import { GenieAsset } from '../types'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import { ListingMarket, WalletAsset } from '../types'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ContractReceipt } from '@ethersproject/contracts'
|
||||
import type { JsonRpcSigner } from '@ethersproject/providers'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { NFTEventName } from '@uniswap/analytics-events'
|
||||
import create from 'zustand'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import ERC721 from '../../abis/erc721.json'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user