Compare commits

..

83 Commits

Author SHA1 Message Date
Zach Pomerantz
a538bf0b69 build: target ESNext for dev (#6379)
* build: add caching to eslint

* build: add caching to jest

* build: add caching to tsc

* build: add caching to actions

* fix: upgrade upload-artifact to v3

* build: update craco eslint cacheLocation

* build: target ESNext for dev

* merge again
2023-04-17 14:26:00 -07:00
Zach Pomerantz
135cb8fb34 fix: translate "Get started" on landing page (#6371)
* fix: translate "Get started" on landing page

* lint: html
2023-04-17 14:03:38 -07:00
cartcrom
bf50582d38 test: local activity tests (#6341)
* refactor: move MP files into subfolders

* refactor: consolidate MP subfolder file-naming scheme

* test: add tests for parseLocal.ts

* refactor: update existing parseLocal tests
2023-04-17 13:03:43 -07:00
lynn
110e23d6eb fix: fix padding for transaction toast notifications (#6373)
* padding fixes, special casing for txn

* add drop shadow, change width to 348px, remove debug code

* opacity animation

* address comments

* one more change

* respond to tina comments

* name change

* add $ to padding
2023-04-17 14:39:38 -04:00
Zach Pomerantz
7ab6a17b42 build: utilize the node_modules/.cache (#6364)
* build: add caching to eslint

* build: add caching to jest

* build: add caching to tsc

* build: add caching to actions

* fix: upgrade upload-artifact to v3

* build: update craco eslint cacheLocation

* fix: pr nits
2023-04-17 09:28:00 -07:00
lynn
7ad13c96a8 fix: fix slow yarn start (#6369)
init
2023-04-14 11:45:33 -05:00
Vignesh Mohankumar
4e99cc4d93 build: send url without hash to sentry (#6352)
* build: send url without hash to sentry

* comments

* Update src/tracing/index.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/tracing/index.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* add test

* move files around
2023-04-13 17:49:29 -04:00
Vignesh Mohankumar
6d29815f59 fix: ignore error caused by Cloudflare HTML response (#6356)
* fix: ignore error caused by Cloudflare HTML response

* add test

* Update src/tracing/errors.test.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/tracing/errors.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/tracing/errors.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* multiline
2023-04-13 17:48:50 -04:00
Zach Pomerantz
4888fe23df test: add jest.asMock (#6310)
* test: add jest.asMock

* test: use mocked instead

* test: split test-utils to prevent interaction

* test: whoops missed one

* Merge but actually this time
2023-04-13 12:44:06 -07:00
Zach Pomerantz
ef9ecd9ce2 build: optimize build by splitting out typechecking and linting (#6323)
* build: move typecheck/lint out of build

* build: add typecheck to test action

* build: fix lint to use gitignore

* fix: correctly lint/check

* fix: simplify lint

* build: back out eslint array-ification

* test(lint): add comment RE config/typings

* build: clarify craco webpack plugin mods

* build: simplify craco webpack with functional methods

* build: rm unused IgnorePlugin

* test(lint): order imports
2023-04-13 12:43:47 -07:00
Jack Short
f5d0804c46 feat: creating feature flag for details v2 page (#6359)
* feat: creating feature flag for details v2 page

* eslint ignore

* moving details v2 under trace
2023-04-13 15:43:14 -04:00
Vignesh Mohankumar
0bac257254 fix: handle invalid chainId on position page (#6338)
* fix: handle invalid chainId on position page

* fix

* add test

* Revert "add test"

This reverts commit d18742aa50.

* pr comments

* rename

* fix
2023-04-13 15:40:31 -04:00
lynn
a77752ab83 fix: re-implement dark mode for connector icons (#6329)
* init

* init

* Update src/connection/index.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/components/Identicon/StatusIcon.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/components/Identicon/StatusIcon.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* address comments

* unit test + remove _url in names

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-13 11:17:48 -04:00
cartcrom
bf31ca4f06 feat: updated banner (#6355)
* feat: updated banner

* fix: linted

* feat: add download link to overflow menu

* feat: hover animation

* fix: update landing screen hide logic

* feat: added descriptor comment for stopPropogation

* fix: translations & responsiveness of button text

* fix: icon sizing / padding + word casing

* fix: Learn more casing
2023-04-12 22:00:05 -04:00
Charles Bachmeier
ed8afbd851 fix: App crashing when changing sorting or toggling usd price on trneding NFTs table (#6354)
* fix app breaking when changing sorting or toggling usd price

* compare floor change correctly

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-04-12 15:49:04 -07:00
Vignesh Mohankumar
47b6a7c4d5 build: add chainId as tag in sentry ErrorBoundary (#6345)
* build: add chainId to sentry tags

* set it in web3provider

* set before sending
2023-04-12 14:43:37 -04:00
cartcrom
086fc65457 feat: remove /wallet route (#6350)
* feat: replace internal microsite routes with links to external site

* feat: use updated analytics events from events repo

* fix: remove unnused empty wallet page
2023-04-12 13:55:53 -04:00
cartcrom
7df53f30a0 feat: uniwallet banner (#6344)
* feat: update wallet banner display logic

* fix: linted

* fix: learn more link
2023-04-12 12:03:00 -04:00
Jack Short
66497a0108 fix: last sale nan (#6337)
* fix: last sale nan

* correct formatted price

* responding to comments

* refactoring for readability
2023-04-12 11:45:48 -04:00
cartcrom
e0eb701bc0 feat: log connection activation/errors (#6333)
* feat: log connection activation/errors in console.debug

* fix: remove now-redundant comment
2023-04-12 10:36:01 -04:00
cartcrom
36cb0668a3 fix: use updated wallet links/language (#6343)
fix: use updated links/language
2023-04-11 18:36:34 -04:00
eddie
810f42136e fix: auto-dismiss MP bottom sheet after Buy Crypto click (#6298) 2023-04-11 14:46:28 -07:00
Vignesh Mohankumar
07b7d7f268 test: add test for position page states (#6342) 2023-04-11 17:10:38 -04:00
Charles Bachmeier
39b5bb37cd feat: hide looksrare until we can support their sdk (#6339)
* hide looksrare until we can support their sdk

* remove commented code

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-04-11 13:13:55 -07:00
cartcrom
feed63b1b3 refactor: reorganize mini-portfolio file names (#6336)
* refactor: move MP files into subfolders

* refactor: consolidate MP subfolder file-naming scheme
2023-04-11 15:26:27 -04:00
Vignesh Mohankumar
ee56382956 fix: ignores "user rejected transaction" errors (#6330)
* ignore user rejected errors

* test

* fix

* use util
2023-04-11 14:28:30 -04:00
lynn
64e396d9e0 fix: update empty state svg icons (#6326)
* update icons

* add snapshot test for empty states

* update failing snapshot
2023-04-11 13:37:05 -04:00
Tina
2ffc8a0bdf chore: Remove SWAP_TRANSACTION_COMPLETED event (#6328)
remove swap completed event
2023-04-10 17:27:02 -07:00
eddie
5ec9cdc5c4 fix: redo MP drawer layout changes with mobile fixed (#6280)
* fix: redo MP drawer layout changes with mobile fixed

* fix: mobile fix and another test

* fix: comments
2023-04-10 12:53:58 -07:00
eddie
4d85775d90 fix: update CurrencyList unit tests (#6321)
* fix: testing snapshot updates

* fix: remove inline style and update snapshots tests
2023-04-10 12:18:54 -07:00
eddie
c1c59ca692 refactor: rename WalletDropdown to AccountDrawer (#6313)
* feat: rename WalletDropdown to Portfolio

* fix: update after rebase

* feat: rename from Portfolio to AccountDrawer

* fix: fix test
2023-04-10 11:26:05 -07:00
Zach Pomerantz
f29d97413e build: use env node for craco lint (#6311) 2023-04-10 10:05:22 -07:00
Zach Pomerantz
a078d94a38 chore: update pr template (#6314) 2023-04-10 10:05:06 -07:00
lynn
c9c3329bc3 fix: switch back buttons in mini portfolio (#6327)
switch back header
2023-04-10 12:57:31 -04:00
eddie
13d0b70fa8 fix: remove "Received Swap Quote" field from Connect Wallet event (#6316) 2023-04-10 09:18:07 -07:00
lynn
b852e4e64a feat: adding analytics for fiat on ramp buy button feature (#6272)
* init

* testing if it works

* wip

* tooltip still not working correctly

* modal still not triggered after initial buy click

* remove invalid import

* region check fixed

* add disabled buy button treatment

* simplify and fix toggle twice bug

* no more state mgmt bugs finally

* rename vars for clarity and add todos

* add feature flag, remove toast

* keep wallet drawer open upon repeated buy clicks

* remove from feature flag modal for now

* unused vars

* first round respond to tina comments

* respond to tina padding comments, fix padding in response to cal feedback

* last round tina comments

* init pending element names being added to analytics events repo

* update event names

* add tooltip delay requested by fred and cal

* middle of revisions, fiat buy flow readability wip

* hook logic refactor done + added basic unit test

* rename enum and add todo for unit tests

* mouseover tooltip disable properly

* fix mouseover tooltip not working, ensure dot working as expected, rename buyFiatClicked to buyFiatFlowCompleted

* change developer doc comment

* respond comments

* update snapshot test

* lint fix

* remove unnecessary changes
2023-04-07 13:02:07 -04:00
lynn
55bd3555be feat: Web 2996 add fiat on ramp buy flow to swap modal on the interface (#6240)
* init

* testing if it works

* wip

* tooltip still not working correctly

* modal still not triggered after initial buy click

* remove invalid import

* region check fixed

* add disabled buy button treatment

* simplify and fix toggle twice bug

* no more state mgmt bugs finally

* rename vars for clarity and add todos

* add feature flag, remove toast

* keep wallet drawer open upon repeated buy clicks

* remove from feature flag modal for now

* unused vars

* first round respond to tina comments

* respond to tina padding comments, fix padding in response to cal feedback

* last round tina comments

* add tooltip delay requested by fred and cal

* middle of revisions, fiat buy flow readability wip

* hook logic refactor done + added basic unit test

* rename enum and add todo for unit tests

* mouseover tooltip disable properly

* fix mouseover tooltip not working, ensure dot working as expected, rename buyFiatClicked to buyFiatFlowCompleted

* change developer doc comment

* respond comments

* update snapshot test

* comments

* small changes + unit tests

* dedup

* remove enzyme

* Remove unecessary line

* simplify

* more cleanup

* add missing await

* more comments

* more comment responses

* more comment responses

* delay show fixes and respond to comments

* fix logic for show

* remove tooltip delay, unit test changes

* Update src/components/Popover/index.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* remove delay on tooltip

* missed one

* Update src/components/swap/SwapBuyFiatButton.test.tsx

Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>

* comments

* .

* lint error

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
2023-04-06 17:06:50 -04:00
Zach Pomerantz
972a65066c fix: do not double-report i18n exception (#6308) 2023-04-06 13:18:01 -07:00
eddie
39a212f762 fix: catch json parse error in fetchTokenList (#6278)
* fix: catch json parse error in fetchTokenList

* fix: refactor fetchTokenList and add more tests

* fix: import in test

* fix: comments and names

* fix: comment format

* fix: comment formatting
2023-04-06 11:15:39 -07:00
Jack Short
c362f4fe39 chore: removing unnecessary data in assets query (#6301)
* chore: removing unnecessary data in assets query

* no smallimageurl

* disabling nft drawer loading initially on nft pages

* removed too much

* renaming

* fix missing rank
2023-04-06 14:04:16 -04:00
eddie
271ef580e1 fix: make token list version bump error quieter (#6271)
* fix: use console.debug for expected transient error

* fix: add tests

* fix: name and lints
2023-04-06 10:26:56 -07:00
Zach Pomerantz
81ced4cb8b test(e2e): configure cypress test retries for CI (#6305)
test: configure retries for cypress CI
2023-04-06 09:30:07 -07:00
cartcrom
ab214a8133 fix: only use local txs for current account (#6284)
* fix: only use local txs for current account

* refactor: remove unecessary try/catch

* fix: add back try/catch
2023-04-06 11:43:39 -04:00
eddie
1b2d86ae3a feat: remove amplitude swap error logging (#6306) 2023-04-05 16:09:07 -07:00
Zach Pomerantz
40cac44e07 docs: use comments for pull_request_template (#6304)
* docs: use comments for pull_request_template

* docs: update
2023-04-05 09:12:38 -07:00
eddie
4e6d28cff4 feat: Update pull_request_template.md (#6302)
Update pull_request_template.md

add notes about testing mobile layouts to the test plan prompts
2023-04-04 14:57:47 -07:00
eddie
709fad0804 test: add unit test coverage to some redux state files (#6285) 2023-04-04 09:46:24 -07:00
Zach Pomerantz
573f4c873a fix: omit failed eth_blockNumber calls from sentry (#6267)
* build: upgrade sentry

* fix: omit failed eth_blockNumber calls from sentry

* test: beforeSend

* fix: bring to parity with #6281

* docs: type filterKnownErrors to beforeSend
2023-04-03 15:12:33 -07:00
eddie
d300db669f fix: z index issue with socks icon (#6295) 2023-04-03 15:12:22 -07:00
eddie
fb8217ddea fix: dont block trade when price impact is favorable (#6261)
* fix: dont block trade when price impact is favorable

* fix: add comment
2023-04-03 14:13:27 -07:00
Jordan Frankfurt
7b9a23d920 feat: reduce severity of phishing filter to allow url token names (#6282)
* feat: reduce severity of phishing filter to allow url token names

* tests

* remove unused var from test

* test rendering mini portfolio pools list

* update owner

* update variable names to match cmcewen's suggestions

* checkStringForURL -> hasURL
2023-03-31 12:59:02 -05:00
Connor McEwen
120ad935fa revert: "fix: mini portfolio layout fixes" (#6279)
Revert "fix: mini portfolio layout fixes (#6260)"

This reverts commit fb05439d32.
2023-03-30 18:47:42 -04:00
cartcrom
4eaf16b624 fix: injection detection bug (#6276)
* fix: use functions to check injection status rather than static vars

* fix: unnused field

* fix: don't prompt mm install for generics

* fix: generic injector function

* fix: display name for MM on cb browser

* fix: re-add ios mobile check for uniswap wallet

* fix: reword comment

* fix: refactor delayed-injection test

* feat: added comments

* fix: revert to minimal changes

* fix: update tests
2023-03-30 17:50:20 -04:00
Connor McEwen
857e2915ab fix: put environment in the wrong place (#6277)
* fix: put environment in the wrong place

* move to proper step
2023-03-30 16:54:45 -04:00
Connor McEwen
7410c81b42 chore: update workflow release env (#6275) 2023-03-30 15:43:18 -04:00
eddie
fb05439d32 fix: mini portfolio layout fixes (#6260)
* fix: mini portfolio layout fixes

* feat: refactor MP drawer CSS
2023-03-30 12:07:02 -07:00
eddie
fb7eade70b fix: l2 icon borders in MP (#6254)
* fix: l2 icon borders in MP

* fix: bool logic

* fix: comments and add test

* fix: change variable name

* fix: split l2 icon into two components
2023-03-30 11:59:46 -07:00
eddie
bd2b2c487a fix: close MP drawer on nft nav (#6251)
* fix: close MP drawer on nft nav

* fix: make callbacks optional, rename props

* fix: improve card API

* fix: add e2e test
2023-03-29 15:08:30 -07:00
eddie
2f004ed1d9 fix: remove deprecated default imports from zustand (#6270)
* fix: replace default imports from zustand

* fix: add eslint rule

* fix: typo

o
2023-03-29 13:25:30 -07:00
Zach Pomerantz
db257c73f2 fix: improve chain id error (#6266)
* build: upgrade sentry

* fix: improve chain id error
2023-03-29 12:38:33 -07:00
Zach Pomerantz
7c37b9d00e build: upgrade sentry (#6264)
* build: upgrade sentry

* chore: comment nits
2023-03-29 11:05:13 -07:00
Connor McEwen
7688c527f0 chore: update codecov yaml (#6262)
chore: set patch coverage to 80%, lower tolerance threshold, remove unused keys
2023-03-29 11:31:27 -04:00
eddie
06dd41a9cd fix: getCurrency crash when token not found (#6263)
* fix: getCurrency crash when token not found

* fix: comments and add test

* fix: remove extra whitespace in unit test

* fix: make e2e test pass
2023-03-28 18:44:48 -07:00
eddie
850fec40a9 fix: remove price tag from nft cards in the side drawer (#6252)
* fix: remove price tag from nft cards in the side drawer

* fix: decouple price from display logic

* fix: missing file chaneg

* fix: add tests
2023-03-28 09:26:32 -07:00
yyip-dev
3c7eabc3d8 docs: Update PR template (#6235)
* Update PR template

* Add screen capture section and simplify descriptions

* Link out to PR title conventions & remove monitoring section
2023-03-28 11:31:11 -04:00
Jordan Frankfurt
048607080c feat: add query param to disable the nft sections of the app (#6225)
* feat: add queryparam to disable the nft sections of the app

* fix

* include mini portfolio

* add comments explaining nft disable atom usage and suggesting future work

* add subtitle exception to landing page and correct the bool

* update comment

* comment syntax nits
2023-03-27 20:54:39 -05:00
eddie
a0f20c54d8 feat: implement new designs for tx notifs (#6232)
* feat: re-add transaction activity popups

* feat: implement new designs for tx notifs

* fix: address comments

* fix: remove color from alert icon

* fix: nits

* fix: remove null check

* fix: fix
2023-03-27 13:56:52 -07:00
Zoltan Arvai
da79abbc0d feat: adds Ledger Connect to non-MetaMask wallet list (#6257)
feat: add Ledger Connect to non-MetaMask wallet list
2023-03-27 16:16:30 -04:00
Vignesh Mohankumar
5ac08e1142 fix: link to lowercase chain name from position page (#6195)
* fix: link to lowercase chain name from position page

* fix: use existing const
2023-03-27 14:41:03 -04:00
eddie
d330eea375 feat: re-add transaction activity popups (#6223) 2023-03-27 10:07:41 -07:00
eddie
35dace7bfe feat: cypress test coverage reporting (#6212)
* wip

* fix: only instrument thru babel in test and dev

* fix: remove unused deps

* fix: yarn dedup

* fix: remove nyc_output dir from git

* fix: remove comment from index.html
2023-03-27 09:48:02 -07:00
Vignesh Mohankumar
8ce8e17f62 fix: Revert to previous Nav menu (#6258)
* fix: revert nav

* lint

* fix

* fix
2023-03-27 12:32:01 -04:00
Vignesh Mohankumar
eb105b6ec7 fix: align /pools CTAs to the top (#6253)
* fix: align /pools CTAs to the top

* connor comments

* Revert "connor comments"

This reverts commit e8f25e8fa1.
2023-03-24 18:33:28 -04:00
eddie
267e7de2b6 feat: remove unicon tooltip entirely (#6250) 2023-03-24 13:11:21 -07:00
lynn
ab9f2af054 fix: modify mini portfolio pools row event properties (#6248)
* init

* use memo
2023-03-24 15:49:38 -04:00
Connor McEwen
67b405dd42 chore: update codeowners (#6247) 2023-03-24 12:18:07 -04:00
lynn
281dbf4305 feat: remove tax service toast, modal, and menu item (#6244)
* init

* fix
2023-03-24 11:50:07 -04:00
lynn
369f2d7dfa fix: update tax copy again (#6241)
init
2023-03-23 20:29:37 -04:00
Jordan Frankfurt
803c749b13 fix: remove backend name check from badge flag (#6236)
* fix: remove backend name check from badge flag

* drop undefined check

* drop bool cast
2023-03-23 17:01:09 -05:00
lynn
8818dadf24 fix: update tax services copy (#6239)
init
2023-03-23 17:39:22 -04:00
Jordan Frankfurt
d179fc6b84 fix: don't show the add liquidity warning when no position currently … (#6238)
fix: don't show the add liquidity warning when no position currently exists
2023-03-23 16:18:35 -05:00
Jordan Frankfurt
18ec675c52 feat: warn when adding liquidity to an unowned LP position (#6219)
* feat: warn when adding liquidity to an unowned LP position

* update boolean name

* use a better address equivalence check
2023-03-23 12:09:22 -05:00
lynn
1a79bac893 feat: add additional mini portfolio events (#6233)
* add events

* update nft logging

* fix lint

* mikes comments
2023-03-23 12:34:59 -04:00
192 changed files with 4789 additions and 2460 deletions

View File

@@ -4,4 +4,31 @@ require('@uniswap/eslint-config/load')
module.exports = {
extends: '@uniswap/eslint-config/react',
overrides: [
{
// Configuration/typings typically export objects/definitions that are used outside of the transpiled package
// (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely.
files: ['**/*.config.*', '**/*.d.ts'],
rules: {
'import/no-unused-modules': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'zustand',
importNames: ['default'],
message: 'Default import from zustand is deprecated. Import `{ create }` instead.',
},
],
},
],
},
},
],
}

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
@uniswap/web
@uniswap/web-reviewers

View File

@@ -1,4 +1,6 @@
name: Setup
description: checkout repo, setup node, and install node_modules
runs:
using: composite
steps:
@@ -10,12 +12,14 @@ runs:
registry-url: https://registry.npmjs.org
cache: yarn
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
- uses: actions/cache@v3
id: install-cache
with:
path: node_modules/
path: |
node_modules
!node_modules/.cache
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
- if: steps.install-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --ignore-scripts
shell: bash

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,9 @@
name: Test
# Many build steps have their own caches, so each job has its own cache to improve subsequent build times.
# Build tools are configured to cache cache to node_modules/.cache, so this is cached independently of node_modules.
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
on:
push:
branches:
@@ -14,7 +18,27 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: eslint-cache
with:
path: node_modules/.cache
key: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-
- run: yarn lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: tsc-cache
with:
path: node_modules/.cache
key: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn typecheck
deps-tests:
runs-on: ubuntu-latest
@@ -28,6 +52,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: jest-cache
with:
path: node_modules/.cache
key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn test
- uses: codecov/codecov-action@v3
@@ -41,9 +71,15 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: build-cache
with:
path: node_modules/.cache
key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn build
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: build
path: build
@@ -55,31 +91,15 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: build
path: build
- run: yarn test:size
cypress-build:
runs-on: ubuntu-latest
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: cypress-cache
with:
path: /root/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
- if: steps.cypress-cache.outputs.cache-hit != 'true'
run: |
yarn cypress install
yarn cypress info
cypress-test-matrix:
needs: [build, cypress-build]
needs: [build]
runs-on: ubuntu-latest
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
strategy:
@@ -89,17 +109,20 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/download-artifact@v2
with:
name: build
path: build
- uses: actions/cache@v3
id: cypress-cache
with:
path: /root/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
- if: steps.cypress-cache.outputs.cache-hit != 'true'
run: yarn cypress install
key: ${{ runner.os }}-cypress
- run: |
yarn cypress install
yarn cypress info
- uses: actions/download-artifact@v3
with:
name: build
path: build
- uses: cypress-io/github-action@v4
with:
@@ -113,6 +136,12 @@ jobs:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
cypress-tests:
if: ${{ always() }}

4
.gitignore vendored
View File

@@ -29,6 +29,10 @@ schema.graphql
.env.test.local
.env.production.local
instrumented
.nyc_output
.nyc_output/**/*
/.netlify
npm-debug.log*

7
.nycrc Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"all": true,
"report-dir": "coverage",
"soureMap": false,
"instrument": false
}

View File

@@ -38,6 +38,33 @@ You can block an entire list of tokens by passing in a tokenlist like [here](./s
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
#### PR Title
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
- docs: Documentation only changes
- feat: A new feature
- fix: A bug fix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- test: Adding missing tests or correcting existing tests
Example commit messages:
- feat: adds support for gnosis safe wallet
- fix: removes a polling memory leak
- chore: bumps redux version
Other things to note:
- Please describe the change using verb statements (ex: Removes X from Y)
- PRs with multiple changes should use a list of verb statements
- Add any relevant unit / integration tests
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
## Accessing Uniswap V2
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.

21
codecov.yml Normal file
View File

@@ -0,0 +1,21 @@
ignore:
- "**/generated/**/*"
- "**/generated/*"
- "**/cypress/**/*"
- "cypress/**/*"
- "**/instrumented/**/*"
- "**/styles/**/*"
- "styles/**/*"
- "**/constants/**/*"
- "constants/**/*"
coverage:
status:
project:
default:
target: auto
threshold: 1%
if_ci_failed: error
patch:
default:
target: 80%

View File

@@ -1,17 +1,44 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
const { execSync } = require('child_process')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { DefinePlugin } = require('webpack')
const commitHash = require('child_process').execSync('git rev-parse HEAD')
const commitHash = execSync('git rev-parse HEAD').toString().trim()
const isProduction = process.env.NODE_ENV === 'production'
// Linting and type checking are only necessary as part of development and testing.
// Omit them from production builds, as they slow down the feedback loop.
const shouldLintOrTypeCheck = !isProduction
module.exports = {
babel: {
plugins: ['@vanilla-extract/babel-plugin'],
env: {
test: {
plugins: ['istanbul'],
},
development: {
plugins: ['istanbul'],
},
},
},
eslint: {
enable: shouldLintOrTypeCheck,
pluginOptions(eslintConfig) {
return Object.assign(eslintConfig, {
cache: true,
cacheLocation: 'node_modules/.cache/eslint/',
})
},
},
typescript: {
enableTypeChecking: shouldLintOrTypeCheck,
},
jest: {
configure(jestConfig) {
return Object.assign({}, jestConfig, {
return Object.assign(jestConfig, {
cacheDirectory: 'node_modules/.cache/jest',
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
moduleNameMapper: {
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
@@ -21,25 +48,29 @@ module.exports = {
},
},
webpack: {
plugins: [
new VanillaExtractPlugin({ identifiers: 'short' }),
new DefinePlugin({
'process.env.REACT_APP_GIT_COMMIT_HASH': JSON.stringify(commitHash.toString()),
}),
],
plugins: [new VanillaExtractPlugin({ identifiers: 'short' })],
configure: (webpackConfig) => {
const instanceOfMiniCssExtractPlugin = webpackConfig.plugins.find(
(plugin) => plugin instanceof MiniCssExtractPlugin
)
if (instanceOfMiniCssExtractPlugin !== undefined) instanceOfMiniCssExtractPlugin.options.ignoreOrder = true
webpackConfig.plugins = webpackConfig.plugins.map((plugin) => {
// Extend process.env with dynamic values (eg commit hash).
// This will make dynamic values available to JavaScript only, not to interpolated HTML (ie index.html).
if (plugin instanceof DefinePlugin) {
Object.assign(plugin.definitions['process.env'], {
REACT_APP_GIT_COMMIT_HASH: JSON.stringify(commitHash),
})
}
// We're currently on Webpack 4.x that doesn't support the `exports` field in package.json.
// CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings.
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
if (plugin instanceof MiniCssExtractPlugin) {
plugin.options.ignoreOrder = true
}
return plugin
})
// We're currently on Webpack 4.x which doesn't support the `exports` field in package.json.
// Instead, we need to manually map the import path to the correct exports path (eg dist or build folder).
// See https://github.com/webpack/webpack/issues/9509.
//
// In case you need to add more modules, make sure to remap them to the correct path.
//
// Map @uniswap/conedison to its dist folder.
// This is required because conedison uses * to redirect all imports to its dist.
webpackConfig.resolve.alias['@uniswap/conedison'] = '@uniswap/conedison/dist'
return webpackConfig

View File

@@ -1,3 +1,4 @@
import codeCoverageTask from '@cypress/code-coverage/task'
import { defineConfig } from 'cypress'
export default defineConfig({
@@ -5,8 +6,10 @@ export default defineConfig({
videoUploadOnPasses: false,
defaultCommandTimeout: 24000, // 2x average block time
chromeWebSecurity: false,
retries: { runMode: 2 },
e2e: {
setupNodeEvents(on, config) {
codeCoverageTask(on, config)
return {
...config,
// Only enable Chrome.

View File

@@ -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')
})
})

View File

@@ -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')
})
})

View File

@@ -0,0 +1,11 @@
describe('Position', () => {
it('shows an valid state on a supported network', () => {
cy.visit('/pools/1')
cy.contains('UNI / ETH')
})
it('shows an invalid state on a supported network', () => {
cy.visit('/pools/788893')
cy.contains('To view a position, you must be connected to the network it belongs to.')
})
})

View File

@@ -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')

View File

@@ -77,4 +77,20 @@ describe('Wallet Dropdown', () => {
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
})
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
visit(false)
cy.viewport('iphone-6')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-buy-crypto')).click()
cy.contains('Buy crypto').should('not.be.visible')
})
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
visit(true)
cy.viewport('iphone-6')
cy.get(getTestSelector('web3-status-connected')).click()
cy.root().click(15, 40)
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
})
})

View File

@@ -8,6 +8,7 @@
// Import commands.ts using ES2015 syntax:
import { injected } from './ethereum'
import assert = require('assert')
import '@cypress/code-coverage/support'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'

View File

@@ -17,16 +17,17 @@
"i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"prepare": "yarn contracts:compile && yarn graphql:fetch && yarn graphql:generate && yarn i18n:compile",
"postinstall": "patch-package",
"start": "craco start",
"build": "craco build",
"serve": "serve build -l 3000",
"deduplicate": "yarn-deduplicate --strategy=highest",
"lint": "yarn eslint .",
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
"typecheck": "tsc --noEmit",
"test": "craco test --coverage",
"test:size": "node scripts/test-size.js",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e",
"postinstall": "patch-package"
"deduplicate": "yarn-deduplicate --strategy=highest"
},
"jest": {
"collectCoverageFrom": [
@@ -37,8 +38,12 @@
"src/lib/utils/**/*.ts*",
"src/pages/**/*.ts*",
"src/state/**/*.ts*",
"src/tracing/**/*.ts*",
"src/utils/**/*.ts*"
],
"coveragePathIgnorePatterns": [
".snap"
],
"coverageThreshold": {
"global": {
"branches": 4,
@@ -66,6 +71,7 @@
"@lingui/cli": "^3.9.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1",
"@testing-library/user-event": "^14.4.3",
"@typechain/ethers-v5": "^7.0.0",
"@types/array.prototype.flat": "^1.2.1",
"@types/array.prototype.flatmap": "^1.2.2",
@@ -94,7 +100,7 @@
"@uniswap/eslint-config": "^1.1.1",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/webpack-plugin": "^2.1.11",
"cypress": "^10.3.1",
"cypress": "10.3.1",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"jest-fetch-mock": "^3.0.3",
@@ -113,6 +119,7 @@
"dependencies": {
"@apollo/client": "^3.7.2",
"@coinbase/wallet-sdk": "^3.6.4",
"@cypress/code-coverage": "^3.10.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@graphql-codegen/cli": "^2.15.0",
@@ -131,11 +138,11 @@
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "^7.40.0",
"@sentry/tracing": "^7.40.0",
"@sentry/react": "^7.45.0",
"@sentry/tracing": "^7.45.0",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "^1.3.1",
"@uniswap/analytics-events": "^2.7.0",
"@uniswap/analytics-events": "^2.10.0",
"@uniswap/conedison": "^1.4.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
@@ -178,6 +185,7 @@
"@web3-react/walletconnect": "8.1.2-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-istanbul": "^6.1.1",
"cids": "^1.0.0",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.2.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 990 KiB

View File

@@ -12,12 +12,14 @@ import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import Tooltip from 'components/Tooltip'
import { useGetConnection } from 'connection'
import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useAtomValue } from 'jotai/utils'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types'
import { useCallback, useState } from 'react'
import { ArrowDownRight, ArrowUpRight, Copy, CreditCard, IconProps, Info, Power, Settings } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled, { useTheme } from 'styled-components/macro'
@@ -28,13 +30,13 @@ import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import StatusIcon from '../Identicon/StatusIcon'
import { useToggleWalletDrawer } from '.'
import { useToggleAccountDrawer } from '.'
import IconButton, { IconHoverText } from './IconButton'
import MiniPortfolio from './MiniPortfolio'
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
const AuthenticatedHeaderWrapper = styled.div`
padding: 14px 12px 16px 16px;
padding: 20px 16px;
display: flex;
flex-direction: column;
flex: 1;
@@ -166,6 +168,8 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
const isUnclaimed = useUserHasAvailableClaim(account)
const getConnection = useGetConnection()
@@ -180,7 +184,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
dispatch(updateSelectedWallet({ wallet: undefined }))
}, [connector, dispatch])
const toggleWalletDrawer = useToggleWalletDrawer()
const toggleWalletDrawer = useToggleAccountDrawer()
const navigateToProfile = useCallback(() => {
toggleWalletDrawer()
@@ -193,9 +197,10 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
const openFoRModalWithAnalytics = useCallback(() => {
toggleWalletDrawer()
sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_WIDGET_OPENED)
openFiatOnrampModal()
}, [openFiatOnrampModal])
}, [openFiatOnrampModal, toggleWalletDrawer])
const [shouldCheck, setShouldCheck] = useState(false)
const {
@@ -283,11 +288,22 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
<LoadingBubble height="16px" width="100px" margin="4px 0 20px 0" />
</Column>
)}
{!shouldDisableNFTRoutes && (
<HeaderButton
data-testid="nft-view-self-nfts"
onClick={navigateToProfile}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
>
<Trans>View and sell NFTs</Trans>
</HeaderButton>
)}
<HeaderButton
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
data-testid="wallet-buy-crypto"
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
@@ -302,14 +318,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
</>
)}
</HeaderButton>
<HeaderButton
data-testid="nft-view-self-nfts"
onClick={navigateToProfile}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
>
<Trans>View and sell NFTs</Trans>
</HeaderButton>
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
<FiatOnrampNotAvailableText marginTop="8px">
<Trans>Not available in your region</Trans>

View File

@@ -1,7 +1,6 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
import { InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
import { PropsWithChildren, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { isIOS } from 'utils/userAgent'
@@ -33,22 +32,38 @@ function BaseButton({ onClick, branded, children }: PropsWithChildren<{ onClick?
)
}
export const APP_STORE_LINK = 'https://apps.apple.com/us/app/uniswap-wallet-defi-nfts/id6443944476'
const APP_STORE_LINK = 'https://apps.apple.com/us/app/uniswap-wallet/id6443944476'
const MICROSITE_LINK = 'https://wallet.uniswap.org/'
const openAppStore = () => {
window.open(APP_STORE_LINK, /* target = */ 'uniswap_wallet_appstore')
}
export const openWalletMicrosite = () => {
sendAnalyticsEvent(InterfaceEventName.UNISWAP_WALLET_MICROSITE_OPENED)
window.open(MICROSITE_LINK, /* target = */ 'uniswap_wallet_microsite')
}
export function openDownloadApp(element: InterfaceElementName) {
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { element })
if (isIOS) openAppStore()
else openWalletMicrosite()
}
// Launches App Store if on an iOS device, else navigates to Uniswap Wallet microsite
export function DownloadButton({ onClick, text = 'Download' }: { onClick?: () => void; text?: string }) {
const navigate = useNavigate()
const micrositeEnabled = useMGTMMicrositeEnabled()
export function DownloadButton({
onClick,
text = 'Download',
element,
}: {
onClick?: () => void
text?: string
element: InterfaceElementName
}) {
const onButtonClick = useCallback(() => {
// handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad
onClick?.()
if (isIOS || !micrositeEnabled) {
sendAnalyticsEvent('Uniswap wallet download clicked')
window.open(APP_STORE_LINK)
} else navigate('/wallet')
}, [onClick, micrositeEnabled, navigate])
openDownloadApp(element)
}, [element, onClick])
return (
<BaseButton branded onClick={onButtonClick}>
@@ -56,8 +71,3 @@ export function DownloadButton({ onClick, text = 'Download' }: { onClick?: () =>
</BaseButton>
)
}
export function LearnMoreButton() {
const navigate = useNavigate()
return <BaseButton onClick={() => navigate('/wallet')}>Learn More</BaseButton>
}

View File

@@ -0,0 +1,71 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow from '../PortfolioRow'
import { useTimeSince } from './parseRemote'
import { Activity } from './types'
const ActivityRowDescriptor = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
${EllipsisStyle}
`
const StyledTimestamp = styled(ThemedText.Caption)`
color: ${({ theme }) => theme.textSecondary};
font-variant: small;
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
`
export function ActivityRow({
activity: { chainId, status, title, descriptor, logos, otherAccount, currencies, timestamp, hash },
}: {
activity: Activity
}) {
const { ENSName } = useENSName(otherAccount)
const timeSince = useTimeSince(timestamp)
const explorerUrl = getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_ROW}
properties={{ hash, chain_id: chainId, explorer_url: explorerUrl }}
>
<PortfolioRow
left={
<Column>
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
descriptor={
<ActivityRowDescriptor color="textSecondary">
{descriptor}
{ENSName ?? otherAccount}
</ActivityRowDescriptor>
}
right={
status === TransactionStatus.Pending ? (
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
</TraceEvent>
)
}

View File

@@ -1,24 +1,20 @@
import { t } from '@lingui/macro'
import { useAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import { LoadingBubble } from 'components/Tokens/loading'
import { useWalletDrawer } from 'components/WalletDropdown'
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
import { TransactionStatus, useTransactionListQuery } from 'graphql/data/__generated__/types-and-hooks'
import { PollingInterval } from 'graphql/data/util'
import useENSName from 'hooks/useENSName'
import { atom, useAtom } from 'jotai'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useEffect, useMemo } from 'react'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { ThemedText } from 'theme'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { ActivityRow } from './ActivityRow'
import { useLocalActivities } from './parseLocal'
import { parseRemoteActivities, useTimeSince } from './parseRemote'
import { parseRemoteActivities } from './parseRemote'
import { Activity, ActivityMap } from './types'
interface ActivityGroup {
@@ -101,11 +97,11 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
const lastFetchedAtom = atom<number | undefined>(0)
export default function ActivityTab({ account }: { account: string }) {
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
export function ActivityTab({ account }: { account: string }) {
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
const localMap = useLocalActivities()
const localMap = useLocalActivities(account)
const { data, loading, refetch } = useTransactionListQuery({
variables: { account },
@@ -158,49 +154,3 @@ export default function ActivityTab({ account }: { account: string }) {
)
}
}
const StyledDescriptor = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
${EllipsisStyle}
`
const StyledTimestamp = styled(ThemedText.Caption)`
color: ${({ theme }) => theme.textSecondary};
font-variant: small;
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
`
function ActivityRow({ activity }: { activity: Activity }) {
const { chainId, status, title, descriptor, logos, otherAccount, currencies } = activity
const { ENSName } = useENSName(otherAccount)
const explorerUrl = getExplorerLink(activity.chainId, activity.hash, ExplorerDataType.TRANSACTION)
const timeSince = useTimeSince(activity.timestamp)
return (
<PortfolioRow
left={
<Column>
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
descriptor={
<StyledDescriptor color="textSecondary">
{descriptor}
{ENSName ?? otherAccount}
</StyledDescriptor>
}
right={
status === TransactionStatus.Pending ? (
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
)
}

View File

@@ -0,0 +1,512 @@
import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core'
import { PERMIT2_ADDRESS } from '@uniswap/universal-router-sdk'
import { DAI as MockDAI, nativeOnChain, USDC_MAINNET as MockUSDC_MAINNET } from 'constants/tokens'
import { TransactionStatus as MockTxStatus } from 'graphql/data/__generated__/types-and-hooks'
import { TokenAddressMap } from 'state/lists/hooks'
import {
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
TransactionDetails,
TransactionInfo,
TransactionType as MockTxType,
} from 'state/transactions/types'
import { renderHook } from 'test-utils/render'
import { parseLocalActivity, useLocalActivities } from './parseLocal'
function mockSwapInfo(
type: MockTradeType,
inputCurrency: Token,
inputCurrencyAmountRaw: string,
outputCurrency: Token,
outputCurrencyAmountRaw: string
): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo {
if (type === MockTradeType.EXACT_INPUT) {
return {
type: MockTxType.SWAP,
tradeType: MockTradeType.EXACT_INPUT,
inputCurrencyId: inputCurrency.address,
inputCurrencyAmountRaw,
outputCurrencyId: outputCurrency.address,
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
}
} else {
return {
type: MockTxType.SWAP,
tradeType: MockTradeType.EXACT_OUTPUT,
inputCurrencyId: inputCurrency.address,
expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw,
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
outputCurrencyId: outputCurrency.address,
outputCurrencyAmountRaw,
}
}
}
const mockAccount1 = '0x000000000000000000000000000000000000000001'
const mockAccount2 = '0x000000000000000000000000000000000000000002'
const mockChainId = SupportedChainId.MAINNET
const mockSpenderAddress = PERMIT2_ADDRESS[mockChainId]
const mockCurrencyAmountRaw = '1000000000000000000'
const mockCurrencyAmountRawUSDC = '1000000'
function mockHash(id: string, status: MockTxStatus = MockTxStatus.Confirmed) {
return id + status
}
function mockCommonFields(id: string, account = mockAccount2, status: MockTxStatus) {
const hash = mockHash(id, status)
return {
hash,
from: account,
receipt:
status === MockTxStatus.Pending
? undefined
: {
transactionHash: hash,
status: status === MockTxStatus.Confirmed ? 1 : 0,
},
addedTime: 0,
}
}
function mockMultiStatus(info: TransactionInfo, id: string): [TransactionDetails, number][] {
// Mocks a transaction with multiple statuses
return [
[
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Pending) } as unknown as TransactionDetails,
mockChainId,
],
[
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Confirmed) } as unknown as TransactionDetails,
mockChainId,
],
[
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Failed) } as unknown as TransactionDetails,
mockChainId,
],
]
}
const mockTokenAddressMap: TokenAddressMap = {
[mockChainId]: {
[MockDAI.address]: { token: MockDAI },
[MockUSDC_MAINNET.address]: { token: MockUSDC_MAINNET },
} as TokenAddressMap[number],
}
jest.mock('../../../../state/lists/hooks', () => ({
useCombinedActiveList: () => mockTokenAddressMap,
}))
jest.mock('../../../../state/transactions/hooks', () => {
return {
useMultichainTransactions: (): [TransactionDetails, number][] => {
return [
[
{
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
...mockCommonFields('0x123', mockAccount1, MockTxStatus.Confirmed),
} as TransactionDetails,
mockChainId,
],
...mockMultiStatus(
mockSwapInfo(
MockTradeType.EXACT_OUTPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
'0xswap_exact_input'
),
...mockMultiStatus(
mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
'0xswap_exact_output'
),
...mockMultiStatus(
{
type: MockTxType.APPROVAL,
tokenAddress: MockDAI.address,
spender: mockSpenderAddress,
},
'0xapproval'
),
...mockMultiStatus(
{
type: MockTxType.WRAP,
unwrapped: false,
currencyAmountRaw: mockCurrencyAmountRaw,
chainId: mockChainId,
},
'0xwrap'
),
...mockMultiStatus(
{
type: MockTxType.WRAP,
unwrapped: true,
currencyAmountRaw: mockCurrencyAmountRaw,
chainId: mockChainId,
},
'0xunwrap'
),
...mockMultiStatus(
{
type: MockTxType.ADD_LIQUIDITY_V3_POOL,
createPool: false,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
feeAmount: 500,
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
},
'0xadd_liquidity_v3'
),
...mockMultiStatus(
{
type: MockTxType.REMOVE_LIQUIDITY_V3,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
},
'0xremove_liquidity_v3'
),
...mockMultiStatus(
{
type: MockTxType.ADD_LIQUIDITY_V2_POOL,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
},
'0xadd_liquidity_v2'
),
...mockMultiStatus(
{
type: MockTxType.COLLECT_FEES,
currencyId0: MockUSDC_MAINNET.address,
currencyId1: MockDAI.address,
expectedCurrencyOwed0: mockCurrencyAmountRawUSDC,
expectedCurrencyOwed1: mockCurrencyAmountRaw,
},
'0xcollect_fees'
),
...mockMultiStatus(
{
type: MockTxType.MIGRATE_LIQUIDITY_V3,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
isFork: false,
},
'0xmigrate_v3_liquidity'
),
]
},
}
})
describe('parseLocalActivity', () => {
it('returns swap activity fields with known tokens, exact input', () => {
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
receipt: {
transactionHash: '0x123',
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toEqual({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
hash: undefined,
receipt: {
id: '0x123',
info: {
type: 1,
tradeType: MockTradeType.EXACT_INPUT,
inputCurrencyId: MockUSDC_MAINNET.address,
inputCurrencyAmountRaw: mockCurrencyAmountRawUSDC,
outputCurrencyId: MockDAI.address,
expectedOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
},
receipt: { status: 1, transactionHash: '0x123' },
status: 'CONFIRMED',
transactionHash: '0x123',
},
status: 'CONFIRMED',
timestamp: NaN,
title: 'Swapped',
})
})
it('returns swap activity fields with known tokens, exact output', () => {
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_OUTPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
receipt: {
transactionHash: '0x123',
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
status: 'CONFIRMED',
title: 'Swapped',
})
})
it('returns swap activity fields with unknown tokens', () => {
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
receipt: {
transactionHash: '0x123',
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
const tokens = {} as TokenAddressMap
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({
chainId: 1,
currencies: [undefined, undefined],
descriptor: 'Unknown for Unknown',
status: 'CONFIRMED',
title: 'Swapped',
})
})
it('only returns activity for the current account', () => {
const account1Activites = renderHook(() => useLocalActivities(mockAccount1)).result.current
const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current
expect(Object.values(account1Activites)).toHaveLength(1)
expect(Object.values(account2Activites)).toHaveLength(30)
})
it('Properly uses correct tense of activity title based on tx status', () => {
const activities = renderHook(() => useLocalActivities(mockAccount2)).result.current
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Pending)]?.title).toEqual('Swapping')
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Confirmed)]?.title).toEqual('Swapped')
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Failed)]?.title).toEqual('Swap failed')
})
it('Adapts Swap exact input to Activity type', () => {
const hash = mockHash('0xswap_exact_input')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Swapped',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Swap exact output to Activity type', () => {
const hash = mockHash('0xswap_exact_output')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Swapped',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Approval to Activity type', () => {
const hash = mockHash('0xapproval')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockDAI],
title: 'Approved',
descriptor: MockDAI.symbol,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Wrap to Activity type', () => {
const hash = mockHash('0xwrap')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
const native = nativeOnChain(mockChainId)
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [native, native.wrapped],
title: 'Wrapped',
descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Unwrap to Activity type', () => {
const hash = mockHash('0xunwrap')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
const native = nativeOnChain(mockChainId)
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [native.wrapped, native],
title: 'Unwrapped',
descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts AddLiquidityV3 to Activity type', () => {
const hash = mockHash('0xadd_liquidity_v3')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Added liquidity',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts RemoveLiquidityV3 to Activity type', () => {
const hash = mockHash('0xremove_liquidity_v3')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Removed liquidity',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts RemoveLiquidityV2 to Activity type', () => {
const hash = mockHash('0xadd_liquidity_v2')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Added V2 liquidity',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts CollectFees to Activity type', () => {
const hash = mockHash('0xcollect_fees')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Collected fees',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts MigrateLiquidityV3 to Activity type', () => {
const hash = mockHash('0xmigrate_v3_liquidity')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Migrated liquidity',
descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
})

View File

@@ -2,7 +2,6 @@ import { t } from '@lingui/macro'
import { formatCurrencyAmount } from '@uniswap/conedison/format'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { nativeOnChain } from '@uniswap/smart-order-router'
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import { TransactionPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { useMemo } from 'react'
@@ -26,20 +25,22 @@ import {
import { getActivityTitle } from '../constants'
import { Activity, ActivityMap } from './types'
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: TokenAddressMap) {
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId][currencyId].token
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: TokenAddressMap): Currency | undefined {
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]?.token
}
function buildCurrencyDescriptor(
currencyA: Currency,
currencyA: Currency | undefined,
amtA: string,
currencyB: Currency,
currencyB: Currency | undefined,
amtB: string,
delimiter = t`for`
) {
const formattedA = formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyA, amtA))
const formattedB = formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyB, amtB))
return `${formattedA} ${currencyA.symbol} ${delimiter} ${formattedB} ${currencyB.symbol}`
const formattedA = currencyA ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyA, amtA)) : t`Unknown`
const symbolA = currencyA?.symbol ?? ''
const formattedB = currencyB ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyB, amtB)) : t`Unknown`
const symbolB = currencyB?.symbol ?? ''
return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ')
}
function parseSwap(
@@ -79,7 +80,7 @@ function parseApproval(
): Partial<Activity> {
// TODO: Add 'amount' approved to ApproveTransactionInfo so we can distinguish between revoke and approve
const currency = getCurrency(approval.tokenAddress, chainId, tokens)
const descriptor = t`${currency.symbol ?? currency.name}`
const descriptor = currency?.symbol ?? currency?.name ?? t`Unknown`
return {
descriptor,
currencies: [currency],
@@ -120,82 +121,82 @@ function parseMigrateCreateV3(
tokens: TokenAddressMap
): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const quoteCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const descriptor = t`${baseCurrency.symbol} and ${quoteCurrency.symbol}`
const baseSymbol = baseCurrency?.symbol ?? t`Unknown`
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
const quoteSymbol = quoteCurrency?.symbol ?? t`Unknown`
const descriptor = t`${baseSymbol} and ${quoteSymbol}`
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
function parseLocalActivity(
export function parseLocalActivity(
details: TransactionDetails,
chainId: SupportedChainId,
tokens: TokenAddressMap
): Activity | undefined {
const status = !details.receipt
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
try {
const status = !details.receipt
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
const receipt: TransactionPartsFragment | undefined = details.receipt
? {
id: details.receipt.transactionHash,
...details.receipt,
...details,
status,
}
: undefined
const receipt: TransactionPartsFragment | undefined = details.receipt
? {
id: details.receipt.transactionHash,
...details.receipt,
...details,
status,
}
: undefined
const defaultFields = {
hash: details.hash,
chainId,
title: getActivityTitle(details.info.type, status),
status,
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
receipt,
const defaultFields = {
hash: details.hash,
chainId,
title: getActivityTitle(details.info.type, status),
status,
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
receipt,
}
let additionalFields: Partial<Activity> = {}
const info = details.info
if (info.type === TransactionType.SWAP) {
additionalFields = parseSwap(info, chainId, tokens)
} else if (info.type === TransactionType.APPROVAL) {
additionalFields = parseApproval(info, chainId, tokens)
} else if (info.type === TransactionType.WRAP) {
additionalFields = parseWrap(info, chainId, status)
} else if (
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
) {
additionalFields = parseLP(info, chainId, tokens)
} else if (info.type === TransactionType.COLLECT_FEES) {
additionalFields = parseCollectFees(info, chainId, tokens)
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
}
return { ...defaultFields, ...additionalFields }
} catch (error) {
console.debug(`Failed to parse transaction ${details.hash}`, error)
return undefined
}
let additionalFields: Partial<Activity> = {}
const info = details.info
if (info.type === TransactionType.SWAP) {
additionalFields = parseSwap(info, chainId, tokens)
} else if (info.type === TransactionType.APPROVAL) {
additionalFields = parseApproval(info, chainId, tokens)
} else if (info.type === TransactionType.WRAP) {
additionalFields = parseWrap(info, chainId, status)
} else if (
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
) {
additionalFields = parseLP(info, chainId, tokens)
} else if (info.type === TransactionType.COLLECT_FEES) {
additionalFields = parseCollectFees(info, chainId, tokens)
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
}
return { ...defaultFields, ...additionalFields }
}
export function useLocalActivities(): ActivityMap | undefined {
export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions()
const { chainId } = useWeb3React()
const tokens = useCombinedActiveList()
return useMemo(
() =>
chainId
? allTransactions.reduce((acc: { [hash: string]: Activity }, [transaction, chainId]) => {
try {
const localActivity = parseLocalActivity(transaction, chainId, tokens)
if (localActivity) acc[localActivity.hash] = localActivity
} catch (error) {
console.error('Failed to parse local activity', transaction)
}
return acc
}, {})
: undefined,
[allTransactions, chainId, tokens]
)
return useMemo(() => {
const activityByHash: ActivityMap = {}
for (const [transaction, chainId] of allTransactions) {
if (transaction.from !== account) continue
activityByHash[transaction.hash] = parseLocalActivity(transaction, chainId, tokens)
}
return activityByHash
}, [account, allTransactions, tokens])
}

View File

@@ -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
}

View File

@@ -1,8 +1,11 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
import Row from 'components/Row'
import { useToggleWalletDrawer } from 'components/WalletDropdown'
import { Box } from 'nft/components/Box'
import { NftCard } from 'nft/components/card'
import { detailsHref } from 'nft/components/card/utils'
import { VerifiedIcon } from 'nft/components/icons'
import { WalletAsset } from 'nft/types'
import { floorFormatter } from 'nft/utils'
@@ -43,12 +46,13 @@ export function NFT({
mediaShouldBePlaying: boolean
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
}) {
const toggleWalletDrawer = useToggleWalletDrawer()
const toggleWalletDrawer = useToggleAccountDrawer()
const navigate = useNavigate()
const trace = useTrace()
const navigateToNFTDetails = () => {
navigate(`/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}`)
toggleWalletDrawer()
navigate(detailsHref(asset))
}
return (
@@ -59,12 +63,19 @@ export function NFT({
display={{ disabledInfo: true }}
isSelected={false}
isDisabled={false}
selectAsset={navigateToNFTDetails}
unselectAsset={() => {
/* */
}}
onCardClick={navigateToNFTDetails}
sendAnalyticsEvent={() =>
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM,
collection_name: asset.collection?.name,
collection_address: asset.collection?.address,
token_id: asset.tokenId,
...trace,
})
}
mediaShouldBePlaying={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
testId="mini-portfolio-nft"
/>
<NFTDetails asset={asset} />
</NFTContainer>

View File

@@ -5,13 +5,22 @@ import { useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import styled from 'styled-components/macro'
import { useToggleWalletDrawer } from '..'
import { DEFAULT_NFT_QUERY_AMOUNT } from './constants'
import { NFT } from './NFT'
import { useAccountDrawer } from '../..'
import { DEFAULT_NFT_QUERY_AMOUNT } from '../constants'
import { NFT } from './NFTItem'
export default function NFTs({ account }: { account: string }) {
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(account, [], [], DEFAULT_NFT_QUERY_AMOUNT)
const toggleWalletDrawer = useToggleWalletDrawer()
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(
account,
[],
[],
DEFAULT_NFT_QUERY_AMOUNT,
undefined,
undefined,
undefined,
!walletDrawerOpen
)
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()

View File

@@ -0,0 +1,67 @@
import { BigNumber } from '@ethersproject/bignumber'
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
import { USDC_MAINNET } from 'constants/tokens'
import { mocked } from 'test-utils/mocked'
import { render } from 'test-utils/render'
import Pools from '.'
import useMultiChainPositions from './useMultiChainPositions'
jest.mock('./useMultiChainPositions')
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
const pool = new Pool(
USDC_MAINNET,
WETH9[SupportedChainId.MAINNET],
FeeAmount.MEDIUM,
'1851127709498178402383049949138810',
'7076437181775065414',
201189
)
const position = new Position({
pool,
liquidity: 1341008833950736,
tickLower: 200040,
tickUpper: 202560,
})
const details = {
nonce: BigNumber.from('0'),
tokenId: BigNumber.from('0'),
operator: '0x0',
token0: USDC_MAINNET.address,
token1: WETH9[SupportedChainId.MAINNET].address,
fee: FeeAmount.MEDIUM,
tickLower: -100,
tickUpper: 100,
liquidity: BigNumber.from('9000'),
feeGrowthInside0LastX128: BigNumber.from('0'),
feeGrowthInside1LastX128: BigNumber.from('0'),
tokensOwed0: BigNumber.from('0'),
tokensOwed1: BigNumber.from('0'),
}
const useMultiChainPositionsReturnValue = {
positions: [
{
owner,
chainId: SupportedChainId.MAINNET,
position,
pool,
details,
inRange: true,
closed: false,
},
],
loading: false,
}
beforeEach(() => {
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
})
test('Pools should render LP positions', () => {
const props = { account: owner }
const { container } = render(<Pools {...props} />)
expect(container).not.toBeEmptyDOMElement()
})

View File

@@ -1,10 +1,12 @@
import { t } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { formatNumber, NumberType } from '@uniswap/conedison/format'
import { Position } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { useToggleWalletDrawer } from 'components/WalletDropdown'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useCallback, useMemo, useReducer } from 'react'
import { useNavigate } from 'react-router-dom'
@@ -31,7 +33,7 @@ export default function Pools({ account }: { account: string }) {
return [openPositions, closedPositions]
}, [positions])
const toggleWalletDrawer = useToggleWalletDrawer()
const toggleWalletDrawer = useToggleAccountDrawer()
if (!positions || loading) {
return <PortfolioSkeleton />
@@ -91,64 +93,75 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
const navigate = useNavigate()
const toggleWalletDrawer = useToggleWalletDrawer()
const toggleWalletDrawer = useToggleAccountDrawer()
const { chainId: walletChainId, connector } = useWeb3React()
const onClick = useCallback(async () => {
if (walletChainId !== chainId) await switchChain(connector, chainId)
toggleWalletDrawer()
navigate('/pool/' + details.tokenId)
}, [walletChainId, chainId, connector, toggleWalletDrawer, navigate, details.tokenId])
const containsURL = useMemo(
() =>
[pool.token0.name, pool.token0.symbol, pool.token1.name, pool.token1.symbol].some((testString) =>
hasURL(testString)
),
[pool]
const analyticsEventProperties = useMemo(
() => ({
chain_id: chainId,
pool_token_0_symbol: pool.token0.symbol,
pool_token_1_symbol: pool.token1.symbol,
pool_token_0_address: pool.token0.address,
pool_token_1_address: pool.token1.address,
}),
[chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol]
)
if (containsURL) {
const shouldHidePosition = hasURL(pool.token0.symbol) || hasURL(pool.token1.symbol)
if (shouldHidePosition) {
return null
}
return (
<PortfolioRow
onClick={onClick}
left={<PortfolioLogo chainId={chainId} currencies={[pool.token0, pool.token1]} />}
title={
<Row>
<ThemedText.SubHeader fontWeight={500}>
{pool.token0.symbol} / {pool.token1?.symbol}
</ThemedText.SubHeader>
</Row>
}
descriptor={<ThemedText.Caption>{`${pool.fee / 10000}%`}</ThemedText.Caption>}
right={
<>
<MouseoverTooltip
placement="left"
text={
<div style={{ padding: '4px 0px' }}>
<ThemedText.Caption>{`${formatNumber(
liquidityValue,
NumberType.PortfolioBalance
)} (liquidity) + ${formatNumber(feeValue, NumberType.PortfolioBalance)} (fees)`}</ThemedText.Caption>
</div>
}
>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_POOLS_ROW}
properties={analyticsEventProperties}
>
<PortfolioRow
onClick={onClick}
left={<PortfolioLogo chainId={chainId} currencies={[pool.token0, pool.token1]} />}
title={
<Row>
<ThemedText.SubHeader fontWeight={500}>
{formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)}
{pool.token0.symbol} / {pool.token1?.symbol}
</ThemedText.SubHeader>
</MouseoverTooltip>
<Row justify="flex-end">
<ThemedText.Caption color="textSecondary">
{closed ? t`Closed` : inRange ? t`In range` : t`Out of range`}
</ThemedText.Caption>
<ActiveDot closed={closed} outOfRange={!inRange} />
</Row>
</>
}
/>
}
descriptor={<ThemedText.Caption>{`${pool.fee / 10000}%`}</ThemedText.Caption>}
right={
<>
<MouseoverTooltip
placement="left"
text={
<div style={{ padding: '4px 0px' }}>
<ThemedText.Caption>{`${formatNumber(
liquidityValue,
NumberType.PortfolioBalance
)} (liquidity) + ${formatNumber(feeValue, NumberType.PortfolioBalance)} (fees)`}</ThemedText.Caption>
</div>
}
>
<ThemedText.SubHeader fontWeight={500}>
{formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)}
</ThemedText.SubHeader>
</MouseoverTooltip>
<Row justify="flex-end">
<ThemedText.Caption color="textSecondary">
{closed ? t`Closed` : inRange ? t`In range` : t`Out of range`}
</ThemedText.Caption>
<ActiveDot closed={closed} outOfRange={!inRange} />
</Row>
</>
}
/>
</TraceEvent>
)
}

View File

@@ -0,0 +1,20 @@
import { SupportedChainId } from '@uniswap/sdk-core'
import { DAI_ARBITRUM } from '@uniswap/smart-order-router'
import { DAI, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
import { render } from 'test-utils/render'
import { PortfolioLogo } from './PortfolioLogo'
describe('PortfolioLogo', () => {
it('renders without L2 icon', () => {
const { container } = render(<PortfolioLogo chainId={SupportedChainId.MAINNET} currencies={[DAI, USDC_MAINNET]} />)
expect(container).toMatchSnapshot()
})
it('renders with L2 icon', () => {
const { container } = render(
<PortfolioLogo chainId={SupportedChainId.ARBITRUM_ONE} currencies={[DAI_ARBITRUM, USDC_ARBITRUM]} />
)
expect(container).toMatchSnapshot()
})
})

View File

@@ -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>

View File

@@ -1,10 +1,9 @@
import Column, { AutoColumn } from 'components/Column'
import Row from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import { useMemo } from 'react'
import styled, { css, keyframes } from 'styled-components/macro'
const RowWrapper = styled(Row)<{ onClick?: any }>`
export const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>`
gap: 12px;
height: 68px;
padding: 0 16px;
@@ -14,7 +13,6 @@ const RowWrapper = styled(Row)<{ onClick?: any }>`
${({ onClick }) => onClick && 'cursor: pointer'};
&:hover {
background: ${({ theme }) => theme.hoverDefault};
cursor: pointer;
}
`
@@ -28,39 +26,30 @@ export default function PortfolioRow({
title,
descriptor,
right,
setIsHover,
onClick,
}: {
left: React.ReactNode
title: React.ReactNode
descriptor?: React.ReactNode
right: React.ReactNode
right?: React.ReactNode
setIsHover?: (b: boolean) => void
onClick?: () => void
}) {
const onHover = useMemo(
() =>
setIsHover && {
onMouseEnter: () => setIsHover?.(true),
onMouseLeave: () => setIsHover?.(false),
},
[setIsHover]
)
return (
<RowWrapper {...onHover} onClick={onClick}>
<PortfolioRowWrapper onClick={onClick}>
{left}
<AutoColumn grow>
{title}
{descriptor}
</AutoColumn>
<EndColumn>{right}</EndColumn>
</RowWrapper>
{right && <EndColumn>{right}</EndColumn>}
</PortfolioRowWrapper>
)
}
function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) {
return (
<RowWrapper>
<PortfolioRowWrapper>
<LoadingBubble height="40px" width="40px" round />
<AutoColumn grow gap="4px">
<LoadingBubble height="16px" width="60px" delay="300ms" />
@@ -76,7 +65,7 @@ function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) {
</>
)}
</EndColumn>
</RowWrapper>
</PortfolioRowWrapper>
)
}

View File

@@ -1,3 +1,5 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { formatNumber, NumberType } from '@uniswap/conedison/format'
import Row from 'components/Row'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
@@ -10,12 +12,12 @@ import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { useToggleWalletDrawer } from '..'
import { PortfolioArrow } from '../AuthenticatedHeader'
import { hideSmallBalancesAtom } from '../SmallBalanceToggle'
import { ExpandoRow } from './ExpandoRow'
import { PortfolioLogo } from './PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from './PortfolioRow'
import { useToggleAccountDrawer } from '../..'
import { PortfolioArrow } from '../../AuthenticatedHeader'
import { hideSmallBalancesAtom } from '../../SmallBalanceToggle'
import { ExpandoRow } from '../ExpandoRow'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
@@ -24,7 +26,7 @@ function meetsThreshold(tokenBalance: TokenBalance, hideSmallBalances: boolean)
}
export default function Tokens({ account }: { account: string }) {
const toggleWalletDrawer = useToggleWalletDrawer()
const toggleWalletDrawer = useToggleAccountDrawer()
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
const [showHiddenTokens, setShowHiddenTokens] = useState(false)
@@ -94,7 +96,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
const navigate = useNavigate()
const toggleWalletDrawer = useToggleWalletDrawer()
const toggleWalletDrawer = useToggleAccountDrawer()
const navigateToTokenDetails = useCallback(async () => {
navigate(getTokenDetailsURL(token))
toggleWalletDrawer()
@@ -102,28 +104,35 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
const currency = gqlToCurrency(token)
return (
<PortfolioRow
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
title={<ThemedText.SubHeader fontWeight={500}>{token?.name}</ThemedText.SubHeader>}
descriptor={
<TokenBalanceText>
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol}
</TokenBalanceText>
}
onClick={navigateToTokenDetails}
right={
denominatedValue && (
<>
<ThemedText.SubHeader fontWeight={500}>
{formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)}
</ThemedText.SubHeader>
<Row justify="flex-end">
<PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} />
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
</Row>
</>
)
}
/>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_TOKEN_ROW}
properties={{ chain_id: currency.chainId, token_name: token?.name, address: token?.address }}
>
<PortfolioRow
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
title={<ThemedText.SubHeader fontWeight={500}>{token?.name}</ThemedText.SubHeader>}
descriptor={
<TokenBalanceText>
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol}
</TokenBalanceText>
}
onClick={navigateToTokenDetails}
right={
denominatedValue && (
<>
<ThemedText.SubHeader fontWeight={500}>
{formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)}
</ThemedText.SubHeader>
<Row justify="flex-end">
<PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} />
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
</Row>
</>
)
}
/>
</TraceEvent>
)
}

View File

@@ -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>
`;

View File

@@ -5,13 +5,16 @@ import Column from 'components/Column'
import { AutoRow } from 'components/Row'
import { useMiniPortfolioEnabled } from 'featureFlags/flags/miniPortfolio'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useAtomValue } from 'jotai/utils'
import { useState } from 'react'
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import Activity from './Activity'
import { ActivityTab } from './Activity'
import NFTs from './NFTs'
import Pools from './Pools'
import { PortfolioRowWrapper } from './PortfolioRow'
import Tokens from './Tokens'
const Wrapper = styled(Column)`
@@ -20,6 +23,12 @@ const Wrapper = styled(Column)`
flex-direction: column;
height: 100%;
gap: 12px;
${PortfolioRowWrapper} {
&:hover {
background: ${({ theme }) => theme.hoverDefault};
}
}
`
const Nav = styled(AutoRow)`
@@ -46,46 +55,67 @@ const PageWrapper = styled.div`
interface Page {
title: React.ReactNode
key: string
component: ({ account }: { account: string }) => JSX.Element
loggingElementName?: string
loggingElementName: string
}
const Pages: Array<Page> = [
{
title: <Trans>Tokens</Trans>,
key: 'tokens',
component: Tokens,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_TOKENS_TAB,
},
{ title: <Trans>NFTs</Trans>, component: NFTs, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB },
{ title: <Trans>Pools</Trans>, component: Pools, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB },
{ title: <Trans>Activity</Trans>, component: Activity },
{
title: <Trans>NFTs</Trans>,
key: 'nfts',
component: NFTs,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB,
},
{
title: <Trans>Pools</Trans>,
key: 'pools',
component: Pools,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB,
},
{
title: <Trans>Activity</Trans>,
key: 'activity',
component: ActivityTab,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_TAB,
},
]
function MiniPortfolio({ account }: { account: string }) {
const isNftPage = useIsNftPage()
const [currentPage, setCurrentPage] = useState(isNftPage ? 1 : 0)
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
const Page = Pages[currentPage].component
return (
<Wrapper>
<Nav>
{Pages.map(({ title }, index) => (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.NAVBAR_CLICKED}
element={Pages[index].loggingElementName}
shouldLogImpression={!!Pages[index].loggingElementName}
key={index}
>
<NavItem
onClick={() => setCurrentPage(index)}
active={currentPage === index}
key={`Mini Portfolio page ${index}`}
{Pages.map(({ title, loggingElementName, key }, index) => {
if (shouldDisableNFTRoutes && loggingElementName.includes('nft')) return null
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.NAVBAR_CLICKED}
element={loggingElementName}
key={index}
>
{title}
</NavItem>
</TraceEvent>
))}
<NavItem
data-testid={`mini-portfolio-nav-${key}`}
onClick={() => setCurrentPage(index)}
active={currentPage === index}
key={`Mini Portfolio page ${index}`}
>
{title}
</NavItem>
</TraceEvent>
)
})}
</Nav>
<PageWrapper>
<Page account={account} />

View File

@@ -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(() => {

View File

@@ -3,7 +3,7 @@ import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/loca
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
import { Check } from 'react-feather'
import { Link, useLocation } from 'react-router-dom'
import { Link } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import { ClickableStyle, ThemedText } from 'theme'
import ThemeToggle from 'theme/components/ThemeToggle'
@@ -56,16 +56,13 @@ const BalanceToggleContainer = styled.div`
export default function SettingsMenu({ onClose }: { onClose: () => void }) {
const activeLocale = useActiveLocale()
const { pathname } = useLocation()
const isWalletPage = pathname.includes('/wallet')
return (
<SlideOutMenu title={<Trans>Settings</Trans>} onClose={onClose}>
<SectionTitle>
<Trans>Preferences</Trans>
</SectionTitle>
<ThemeToggleContainer>
<ThemeToggle disabled={isWalletPage} />
<ThemeToggle />
</ThemeToggleContainer>
<BalanceToggleContainer>
<SmallBalanceToggle />

View File

@@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { WalletConnect } from '@web3-react/walletconnect'
import Column, { AutoColumn } from 'components/Column'
@@ -95,7 +96,7 @@ export default function UniwalletModal() {
)}
</QRCodeWrapper>
<Divider />
<InfoSection onClose={onClose} />
<InfoSection />
</UniwalletConnectWrapper>
</Modal>
)
@@ -108,7 +109,7 @@ const InfoSectionWrapper = styled(RowBetween)`
gap: 20px;
`
function InfoSection({ onClose }: { onClose: () => void }) {
function InfoSection() {
return (
<InfoSectionWrapper>
<AutoColumn gap="4px">
@@ -117,12 +118,12 @@ function InfoSection({ onClose }: { onClose: () => void }) {
</ThemedText.SubHeaderSmall>
<ThemedText.Caption color="textSecondary">
<Trans>
Download in the App Store to safely store and send tokens and NFTs, swap tokens, and connect to crypto apps.
Download in the App Store to safely store your tokens and NFTs, swap tokens, and connect to crypto apps.
</Trans>
</ThemedText.Caption>
</AutoColumn>
<Column>
<DownloadButton onClick={onClose} />
<DownloadButton element={InterfaceElementName.UNISWAP_WALLET_MODAL_DOWNLOAD_BUTTON} />
</Column>
</InfoSectionWrapper>
)

View File

@@ -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

View File

@@ -1,111 +1,147 @@
import { Trans } from '@lingui/macro'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { openDownloadApp, openWalletMicrosite } from 'components/AccountDrawer/DownloadButton'
import { BaseButton } from 'components/Button'
import { AutoColumn } from 'components/Column'
import Row, { RowBetween } from 'components/Row'
import { useWalletDrawer } from 'components/WalletDropdown'
import { DownloadButton, LearnMoreButton } from 'components/WalletDropdown/DownloadButton'
import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
import { useScreenSize } from 'hooks/useScreenSize'
import { X } from 'react-feather'
import { useLocation } from 'react-router-dom'
import { useHideUniswapWalletBanner } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { isIOS } from 'utils/userAgent'
import bannerImageDark from '../../assets/images/uniswapWalletBannerDark.png'
import bannerImageLight from '../../assets/images/uniswapWalletBannerLight.png'
import { ReactComponent as AppleLogo } from '../../assets/svg/apple_logo.svg'
import walletBannerPhoneImageSrc from '../../assets/svg/wallet_banner_phone_image.svg'
const PopupContainer = styled.div<{ show: boolean }>`
display: flex;
flex-direction: column;
justify-content: space-between;
${({ show }) => !show && 'display: none'};
box-shadow: ${({ theme }) =>
theme.darkMode
? '0px -16px 24px rgba(0, 0, 0, 0.4), 0px -8px 12px rgba(0, 0, 0, 0.4), 0px -4px 8px rgba(0, 0, 0, 0.32)'
: '0px -12px 20px rgba(51, 53, 72, 0.04), 0px -6px 12px rgba(51, 53, 72, 0.02), 0px -4px 8px rgba(51, 53, 72, 0.04)'};
background-image: ${({ theme }) => (theme.darkMode ? `url(${bannerImageDark})` : `url(${bannerImageLight})`)};
background: url(${walletBannerPhoneImageSrc});
background-repeat: no-repeat;
background-size: cover;
background-position: bottom -1px right 15px;
background-size: 166px;
cursor: pointer;
:hover {
background-size: 170px;
}
transition: background-size ${({ theme }) => theme.transition.duration.medium}
${({ theme }) => theme.transition.timing.inOut};
background-color: ${({ theme }) => theme.promotional};
color: ${({ theme }) => theme.textPrimary};
position: fixed;
z-index: ${Z_INDEX.sticky};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.slow} opacity ${timing.in}`};
width: 100%;
bottom: 56px;
height: 20%;
`
const InnerContainer = styled.div`
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
justify-content: space-between;
height: 100%;
padding: 24px 16px;
padding: 24px 16px 16px;
border-radius: 20px;
bottom: 20px;
right: 20px;
width: 390px;
height: 164px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.deepShadow};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
bottom: 62px;
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: unset;
right: 10px;
left: 10px;
}
user-select: none;
`
const ButtonRow = styled(Row)`
gap: 16px;
`
const StyledXButton = styled(X)`
color: ${({ theme }) => theme.textSecondary};
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
cursor: pointer;
position: absolute;
top: 21px;
right: 17px;
color: ${({ theme }) => theme.white};
${OpacityHoverState};
`
const BannerButton = styled(BaseButton)`
height: 40px;
border-radius: 16px;
padding: 10px;
${OpacityHoverState};
`
export default function UniswapWalletBanner() {
const [hideUniswapWalletBanner, toggleHideUniswapWalletBanner] = useHideUniswapWalletBanner()
const [walletDrawerOpen] = useWalletDrawer()
const mgtmEnabled = useMgtmEnabled()
const location = useLocation()
const isLandingScreen = location.search === '?intro=true' || location.pathname === '/'
const theme = useTheme()
const shouldDisplay = Boolean(mgtmEnabled && !hideUniswapWalletBanner && !isLandingScreen)
const { pathname } = useLocation()
// hardcodeToFalse hardcodes the banner to never display, temporarily:
const hardcodeToFalse = false
const shouldDisplay = Boolean(
!walletDrawerOpen && !hideUniswapWalletBanner && isIOS && !pathname.startsWith('/wallet') && hardcodeToFalse
)
const screenSize = useScreenSize()
return (
<PopupContainer show={shouldDisplay}>
<InnerContainer>
<AutoColumn gap="8px">
<RowBetween>
<ThemedText.SubHeader>
<Trans>Get the power of Uniswap in your pocket</Trans>
</ThemedText.SubHeader>
<StyledXButton
data-testid="uniswap-wallet-banner"
color={theme.textSecondary}
size={20}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toggleHideUniswapWalletBanner()
}}
/>
</RowBetween>
<ThemedText.BodySmall>
<Trans>Download in the App Store today.</Trans>{' '}
</ThemedText.BodySmall>
</AutoColumn>
<StyledXButton
data-testid="uniswap-wallet-banner"
size={20}
onClick={(e) => {
// prevent click from bubbling to UI on the page underneath, i.e. clicking a token row
e.preventDefault()
e.stopPropagation()
toggleHideUniswapWalletBanner()
}}
/>
<ButtonRow>
<LearnMoreButton />
<DownloadButton onClick={() => toggleHideUniswapWalletBanner()} />
</ButtonRow>
</InnerContainer>
<AutoColumn gap="8px">
<ThemedText.HeadlineMedium fontSize="24px" lineHeight="28px" color="white" maxWidth="60%">
<Trans>Uniswap in your pocket</Trans>
</ThemedText.HeadlineMedium>
</AutoColumn>
<ButtonRow>
{isIOS ? (
<>
<BannerButton
backgroundColor="white"
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON)}
>
<AppleLogo width={14} height={14} />
<ThemedText.LabelSmall color="black" marginLeft="5px">
{!screenSize['xs'] ? <Trans>Download</Trans> : <Trans>Download app</Trans>}
</ThemedText.LabelSmall>
</BannerButton>
<BannerButton backgroundColor="black" onClick={openWalletMicrosite}>
<ThemedText.LabelSmall color="white">
<Trans>Learn more</Trans>
</ThemedText.LabelSmall>
</BannerButton>
</>
) : (
<BannerButton backgroundColor="white" width="125px" onClick={openWalletMicrosite}>
<ThemedText.LabelSmall color="black">
<Trans>Learn more</Trans>
</ThemedText.LabelSmall>
</BannerButton>
)}
</ButtonRow>
</PopupContainer>
)
}

View File

@@ -1,13 +1,11 @@
import { Trans } from '@lingui/macro'
import * as Sentry from '@sentry/react'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { SwapEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
import { ChevronUpIcon } from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useState } from 'react'
import { Copy } from 'react-feather'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import { isSentryEnabled } from 'utils/env'
@@ -220,18 +218,16 @@ const updateServiceWorkerInBackground = async () => {
}
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
const { pathname } = useLocation()
const { chainId } = useWeb3React()
return (
<Sentry.ErrorBoundary
fallback={({ error, eventId }) => <Fallback error={error} eventId={eventId} />}
beforeCapture={(scope) => {
scope.setLevel('fatal')
scope.setTag('chain_id', chainId)
}}
onError={(error) => {
onError={() => {
updateServiceWorkerInBackground()
if (pathname === '/swap') {
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error })
}
}}
>
{children}

View File

@@ -1,10 +1,10 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useBaseFlag, useUpdateFlag } from 'featureFlags'
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { MgtmVariant, useMgtmFlag } from 'featureFlags/flags/mgtm'
import { useMiniPortfolioFlag } from 'featureFlags/flags/miniPortfolio'
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
import { TaxServiceVariant, useTaxServiceBannerFlag } from 'featureFlags/flags/taxServiceBanner'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
@@ -212,12 +212,6 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.mgtm}
label="Mobile Wallet go-to-market assets"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useBaseFlag(FeatureFlag.walletMicrosite)}
featureFlag={FeatureFlag.walletMicrosite}
label="Mobile Wallet microsite (requires mgtm to also be enabled)"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useMiniPortfolioFlag()}
@@ -243,10 +237,10 @@ export default function FeatureFlagModal() {
label="Migrate NFT read endpoints to GQL"
/>
<FeatureFlagOption
variant={TaxServiceVariant}
value={useTaxServiceBannerFlag()}
featureFlag={FeatureFlag.taxService}
label="Tax Service Banner"
variant={DetailsV2Variant}
value={useDetailsV2Flag()}
featureFlag={FeatureFlag.detailsV2}
label="Use the new details page for nfts"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption

View File

@@ -0,0 +1,24 @@
import { getConnections } from 'connection'
import { render } from 'test-utils/render'
import StatusIcon from './StatusIcon'
jest.mock('../../hooks/useSocksBalance', () => ({
useHasSocks: () => true,
}))
describe('StatusIcon', () => {
it('renders children in correct order, with no account and with socks', () => {
const supportedConnections = getConnections()
const injectedConnection = supportedConnections[1]
const component = render(<StatusIcon connection={injectedConnection} />)
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
})
it('renders with no account and showMiniIcons=false', () => {
const supportedConnections = getConnections()
const injectedConnection = supportedConnections[1]
const component = render(<StatusIcon connection={injectedConnection} showMiniIcons={false} />)
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
})
})

View File

@@ -1,12 +1,9 @@
import { useWeb3React } from '@web3-react/core'
import { MouseoverTooltip } from 'components/Tooltip'
import { Unicon } from 'components/Unicon'
import { Connection, ConnectionType } from 'connection'
import useENSAvatar from 'hooks/useENSAvatar'
import { useIsMobile } from 'nft/hooks'
import { PropsWithChildren } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { flexColumnNoWrap } from 'theme/styles'
import sockImg from '../../assets/svg/socks.svg'
@@ -62,88 +59,43 @@ const Socks = () => {
}
const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => {
const isDarkMode = useIsDarkMode()
return (
<MiniIconContainer side={side}>
<MiniImg src={connection.icon} alt={`${connection.name} icon`} />
<MiniImg src={connection.getIcon?.(isDarkMode)} alt={`${connection.getName()} icon`} />
</MiniIconContainer>
)
}
const Divider = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
margin: 12px 0;
`
function UniconTooltip({ children, enabled }: PropsWithChildren<{ enabled?: boolean }>) {
return (
<MouseoverTooltip
offsetY={8}
disableHover={!enabled}
text={
// TODO(cartcrom): add Learn More link when unicon microsite is polished
<>
<ThemedText.SubHeaderSmall color="textPrimary" paddingTop="4px">
This is your Unicon
</ThemedText.SubHeaderSmall>
<Divider />
<ThemedText.Caption paddingBottom="4px">
Unicons are avatars for your wallet, generated from your address.
</ThemedText.Caption>
</>
}
placement="bottom"
>
<div>{children}</div>
</MouseoverTooltip>
)
}
const MainWalletIcon = ({
connection,
size,
enableInfotips,
}: {
connection: Connection
size: number
enableInfotips?: boolean
}) => {
const MainWalletIcon = ({ connection, size }: { connection: Connection; size: number }) => {
const { account } = useWeb3React()
const { avatar } = useENSAvatar(account ?? undefined)
const isMobile = useIsMobile()
if (!account) {
return null
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.name === 'MetaMask')) {
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.getName() === 'MetaMask')) {
return <Identicon size={size} />
} else {
return isMobile ? (
<Unicon address={account} size={size} />
) : (
<UniconTooltip enabled={enableInfotips}>
<Unicon address={account} size={size} />
</UniconTooltip>
)
return <Unicon address={account} size={size} />
}
}
export default function StatusIcon({
connection,
size = 16,
enableInfotips,
showMiniIcons = true,
}: {
connection: Connection
size?: number
enableInfotips?: boolean
showMiniIcons?: boolean
}) {
const hasSocks = useHasSocks()
return (
<IconWrapper size={size}>
{hasSocks && showMiniIcons && <Socks />}
<MainWalletIcon connection={connection} size={size} enableInfotips={enableInfotips} />
<IconWrapper size={size} data-testid="StatusIconRoot">
<MainWalletIcon connection={connection} size={size} />
{showMiniIcons && <MiniWalletIcon connection={connection} side="right" />}
{hasSocks && showMiniIcons && <Socks />}
</IconWrapper>
)
}

View 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>
`;

View File

@@ -1,22 +1,30 @@
import { t, Trans } from '@lingui/macro'
import { ReactComponent as AppleLogo } from 'assets/svg/apple_logo.svg'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { openDownloadApp } from 'components/AccountDrawer/DownloadButton'
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
import { APP_STORE_LINK } from 'components/WalletDropdown/DownloadButton'
import NewBadge from 'components/WalletModal/NewBadge'
import { useMgtmEnabled, useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { BarChartIcon, EllipsisIcon, GovernanceIcon, PoolIcon } from 'nft/components/icons'
import {
BarChartIcon,
DiscordIconMenu,
EllipsisIcon,
GithubIconMenu,
GovernanceIcon,
PoolIcon,
TwitterIconMenu,
} from 'nft/components/icons'
import { body, bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { ReactNode, useReducer, useRef } from 'react'
import { DollarSign, HelpCircle, Shield, Terminal } from 'react-feather'
import { NavLink, NavLinkProps } from 'react-router-dom'
import { useToggleModal } from 'state/application/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
import { useToggleModal, useToggleTaxServiceModal } from '../../state/application/hooks'
import { ReactComponent as AppleLogo } from '../../assets/svg/apple_logo.svg'
import { ApplicationModal } from '../../state/application/reducer'
import * as styles from './MenuDropdown.css'
import { NavDropdown } from './NavDropdown'
@@ -27,30 +35,20 @@ const PrimaryMenuRow = ({
href,
close,
children,
onClick,
}: {
to?: NavLinkProps['to']
href?: string
close?: () => void
children: ReactNode
onClick?: () => void
}) => {
return (
<>
{to ? (
<NavLink to={to} className={styles.MenuRow} onClick={onClick}>
<NavLink to={to} className={styles.MenuRow}>
<Row onClick={close}>{children}</Row>
</NavLink>
) : (
<Row
as="a"
href={href}
target="_blank"
rel="noopener noreferrer"
className={styles.MenuRow}
onClick={onClick}
cursor="pointer"
>
<Row as="a" href={href} target="_blank" rel="noopener noreferrer" className={styles.MenuRow}>
{children}
</Row>
)}
@@ -97,6 +95,10 @@ const Separator = () => {
return <Box className={styles.Separator} />
}
const IconRow = ({ children }: { children: ReactNode }) => {
return <Row className={styles.IconRow}>{children}</Row>
}
const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
return (
<>
@@ -107,7 +109,7 @@ const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
rel={href ? 'noopener noreferrer' : undefined}
display="flex"
flexDirection="column"
color="textSecondary"
color="textPrimary"
background="none"
border="none"
justifyContent="center"
@@ -120,47 +122,21 @@ const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
)
}
const StyledAppleLogo = styled(AppleLogo)`
fill: ${({ theme }) => theme.textSecondary};
padding: 2px;
width: 24px;
height: 24px;
`
const BadgeWrapper = styled.div`
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
`
export const MenuDropdown = () => {
const theme = useTheme()
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
const toggleTaxServiceModal = useToggleTaxServiceModal()
const theme = useTheme()
const mgtmEnabled = useMgtmEnabled()
const micrositeEnabled = useMGTMMicrositeEnabled()
return (
<>
<Box position="relative" ref={ref}>
<NavIcon
isActive={isOpen}
onClick={toggleOpen}
label={isOpen ? t`Show resources` : t`Hide resources`}
activeBackground={isOpen}
>
<EllipsisIcon
viewBox="0 0 20 20"
width={24}
height={24}
color={isOpen ? theme.accentActive : theme.textSecondary}
/>
<NavIcon isActive={isOpen} onClick={toggleOpen} label={isOpen ? t`Show resources` : t`Hide resources`}>
<EllipsisIcon viewBox="0 0 20 20" width={24} height={24} />
</NavIcon>
{isOpen && (
@@ -170,99 +146,100 @@ export const MenuDropdown = () => {
<Box display={{ sm: 'none', lg: 'flex', xxl: 'none' }}>
<PrimaryMenuRow to="/pool" close={toggleOpen}>
<Icon>
<PoolIcon width={24} height={24} color={theme.textSecondary} />
<PoolIcon width={24} height={24} fill={theme.textPrimary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Pool</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
</Box>
<Box
display={mgtmEnabled ? 'flex' : 'none'}
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_MODAL_DOWNLOAD_BUTTON)}
>
<PrimaryMenuRow close={toggleOpen}>
<Icon>
<AppleLogo width="24px" height="24px" fill={theme.textPrimary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Download Uniswap Wallet</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
</Box>
<PrimaryMenuRow to="/vote" close={toggleOpen}>
<Icon>
<GovernanceIcon width={24} height={24} />
<GovernanceIcon width={24} height={24} color={theme.textPrimary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Governance</Trans>
<Trans>Vote in governance</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
<PrimaryMenuRow href="https://info.uniswap.org/#/">
<Icon>
<BarChartIcon width={24} height={24} />
<BarChartIcon width={24} height={24} color={theme.textPrimary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Token analytics</Trans>
<Trans>View more analytics</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
<PrimaryMenuRow href="https://help.uniswap.org/en/">
<Icon>
<HelpCircle color={theme.textSecondary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Help center</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
<PrimaryMenuRow href="https://docs.uniswap.org/">
<Icon>
<Terminal color={theme.textSecondary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Documentation</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
<PrimaryMenuRow
</Column>
<Separator />
<Box
display="flex"
flexDirection={{ sm: 'row', md: 'column' }}
flexWrap="wrap"
alignItems={{ sm: 'center', md: 'flex-start' }}
paddingX="8"
>
<SecondaryLinkedText href="https://help.uniswap.org/en/">
<Trans>Help center</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText href="https://docs.uniswap.org/">
<Trans>Documentation</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText href="https://uniswap.canny.io/feature-requests">
<Trans>Feedback</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText
onClick={() => {
toggleOpen()
togglePrivacyPolicy()
}}
>
<Icon>
<Shield color={theme.textSecondary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Legal & Privacy</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
<PrimaryMenuRow
onClick={() => {
toggleTaxServiceModal()
toggleOpen()
}}
>
<Icon>
<DollarSign size="24px" color={theme.textSecondary} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Save on Tax Services</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
{mgtmEnabled && (
<Box display={micrositeEnabled ? { xxl: 'flex', xxxl: 'none' } : 'flex'}>
<PrimaryMenuRow
to={micrositeEnabled ? '/wallet' : undefined}
href={micrositeEnabled ? undefined : APP_STORE_LINK}
close={toggleOpen}
>
<Icon>
<StyledAppleLogo />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Uniswap Wallet</Trans>
</PrimaryMenuRow.Text>
<BadgeWrapper>
<NewBadge />
</BadgeWrapper>
</PrimaryMenuRow>
</Box>
)}
<Trans>Legal & Privacy</Trans>
</SecondaryLinkedText>
{(isDevelopmentEnv() || isStagingEnv()) && (
<>
<Separator />
<SecondaryLinkedText onClick={openFeatureFlagsModal}>
<Trans>Feature Flags</Trans>
</SecondaryLinkedText>
</>
<SecondaryLinkedText onClick={openFeatureFlagsModal}>
<Trans>Feature Flags</Trans>
</SecondaryLinkedText>
)}
</Column>
</Box>
<IconRow>
<Icon href="https://discord.com/invite/FCfyBSbCU5">
<DiscordIconMenu
className={styles.hover}
width={24}
height={24}
color={themeVars.colors.textSecondary}
/>
</Icon>
<Icon href="https://twitter.com/Uniswap">
<TwitterIconMenu
className={styles.hover}
width={24}
height={24}
color={themeVars.colors.textSecondary}
/>
</Icon>
<Icon href="https://github.com/Uniswap">
<GithubIconMenu
className={styles.hover}
width={24}
height={24}
color={themeVars.colors.textSecondary}
/>
</Icon>
</IconRow>
</Column>
</NavDropdown>
)}

View File

@@ -9,7 +9,6 @@ import { HistoryDuration, SafetyLevel } from 'graphql/data/__generated__/types-a
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
import { SearchToken } from 'graphql/data/SearchTokens'
import useTrendingTokens from 'graphql/data/TrendingTokens'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
@@ -361,9 +360,7 @@ export const SearchBarDropdown = ({
searchHistory,
])
const showBNBComingSoonBadge = Boolean(
chainId !== undefined && chainId === SupportedChainId.BNB && !isLoading && !CHAIN_ID_TO_BACKEND_NAME[chainId]
)
const showBNBComingSoonBadge = chainId === SupportedChainId.BNB && !isLoading
return (
<Box className={styles.searchBarDropdownNft}>

View File

@@ -1,11 +1,10 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import NewBadge from 'components/WalletModal/NewBadge'
import Web3Status from 'components/Web3Status'
import { useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
import { chainIdToBackendName } from 'graphql/data/util'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useIsPoolsPage } from 'hooks/useIsPoolsPage'
import { useAtomValue } from 'jotai/utils'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { UniIcon } from 'nft/components/icons'
@@ -13,6 +12,7 @@ import { useProfilePageState } from 'nft/hooks'
import { ProfilePageStateType } from 'nft/types'
import { ReactNode } from 'react'
import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom'
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
import styled from 'styled-components/macro'
import { Bag } from './Bag'
@@ -58,7 +58,8 @@ export const PageTabs = () => {
const isPoolActive = useIsPoolsPage()
const isNftPage = useIsNftPage()
const micrositeEnabled = useMGTMMicrositeEnabled()
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
return (
<>
@@ -68,22 +69,16 @@ export const PageTabs = () => {
<MenuItem href={`/tokens/${chainName.toLowerCase()}`} isActive={pathname.startsWith('/tokens')}>
<Trans>Tokens</Trans>
</MenuItem>
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
<Trans>NFTs</Trans>
</MenuItem>
{!shouldDisableNFTRoutes && (
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
<Trans>NFTs</Trans>
</MenuItem>
)}
<Box display={{ sm: 'flex', lg: 'none', xxl: 'flex' }} width="full">
<MenuItem href="/pools" dataTestId="pool-nav-link" isActive={isPoolActive}>
<Trans>Pools</Trans>
</MenuItem>
</Box>
{micrositeEnabled && (
<Box display={{ sm: 'none', xxxl: 'flex' }}>
<MenuItem href="/wallet" isActive={pathname.startsWith('/wallet')}>
<Trans>Wallet</Trans>
<NewBadge />
</MenuItem>
</Box>
)}
<Box marginY={{ sm: '4', md: 'unset' }}>
<MenuDropdown />
</Box>

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { AlertTriangle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { ThemedText } from '../../theme'
import { AutoColumn } from '../Column'
@@ -12,26 +12,31 @@ const RowNoFlex = styled(AutoRow)`
flex-wrap: nowrap;
`
const ColumnContainer = styled(AutoColumn)`
margin: 0 12px;
`
export const PopupAlertTriangle = styled(AlertTriangleFilled)`
flex-shrink: 0;
width: 32px;
height: 32px;
`
export default function FailedNetworkSwitchPopup({ chainId }: { chainId: SupportedChainId }) {
const chainInfo = getChainInfo(chainId)
const theme = useTheme()
return (
<RowNoFlex>
<AutoColumn gap="sm">
<RowNoFlex style={{ alignItems: 'center' }}>
<div style={{ paddingRight: 13 }}>
<AlertTriangle color={theme.accentWarning} size={24} display="flex" />
</div>
<ThemedText.SubHeader>
<Trans>Failed to switch networks</Trans>
</ThemedText.SubHeader>
</RowNoFlex>
<RowNoFlex gap="12px">
<PopupAlertTriangle />
<ColumnContainer gap="sm">
<ThemedText.SubHeader color="textSecondary">
<Trans>Failed to switch networks</Trans>
</ThemedText.SubHeader>
<ThemedText.BodySmall>
<ThemedText.BodySmall color="textSecondary">
<Trans>To use Uniswap on {chainInfo.label}, switch the network in your wallets settings.</Trans>
</ThemedText.BodySmall>
</AutoColumn>
</ColumnContainer>
</RowNoFlex>
)
}

View File

@@ -1,50 +1,50 @@
import { useCallback, useEffect } from 'react'
import { useEffect } from 'react'
import { X } from 'react-feather'
import { animated } from 'react-spring'
import { useSpring } from 'react-spring'
import styled, { useTheme } from 'styled-components/macro'
import styled, { css, useTheme } from 'styled-components/macro'
import { useRemovePopup } from '../../state/application/hooks'
import { PopupContent } from '../../state/application/reducer'
import FailedNetworkSwitchPopup from './FailedNetworkSwitchPopup'
import TransactionPopup from './TransactionPopup'
const StyledClose = styled(X)`
const StyledClose = styled(X)<{ $padding: number }>`
position: absolute;
right: 20px;
top: 20px;
right: ${({ $padding }) => `${$padding}px`};
top: ${({ $padding }) => `${$padding}px`};
:hover {
cursor: pointer;
}
`
const Popup = styled.div`
const PopupCss = css<{ show: boolean }>`
display: inline-block;
width: 100%;
padding: 1em;
visibility: ${({ show }) => (show ? 'visible' : 'hidden')};
background-color: ${({ theme }) => theme.backgroundSurface};
position: relative;
border-radius: 10px;
padding: 20px;
padding-right: 35px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
overflow: hidden;
box-shadow: ${({ theme }) => theme.deepShadow};
transition: ${({ theme }) => `visibility ${theme.transition.duration.fast} ease-in-out`};
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
min-width: 290px;
&:not(:last-of-type) {
margin-right: 20px;
}
`}
`
const Fader = styled.div`
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
height: 2px;
background-color: ${({ theme }) => theme.deprecated_bg3};
min-width: 290px;
&:not(:last-of-type) {
margin-right: 20px;
}
`}
`
const AnimatedFader = animated(Fader)
const TransactionPopupContainer = styled.div`
${PopupCss}
padding: 2px 0px;
`
const FailedSwitchNetworkPopupContainer = styled.div<{ show: boolean }>`
${PopupCss}
padding: 20px 35px 20px 20px;
`
export default function PopupItem({
removeAfterMs,
@@ -56,36 +56,34 @@ export default function PopupItem({
popKey: string
}) {
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
const theme = useTheme()
useEffect(() => {
if (removeAfterMs === null) return undefined
const timeout = setTimeout(() => {
removeThisPopup()
removePopup(popKey)
}, removeAfterMs)
return () => {
clearTimeout(timeout)
}
}, [removeAfterMs, removeThisPopup])
}, [popKey, removeAfterMs, removePopup])
const theme = useTheme()
const faderStyle = useSpring({
from: { width: '100%' },
to: { width: '0%' },
config: { duration: removeAfterMs ?? undefined },
})
let popupContent
if ('failedSwitchNetwork' in content) {
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
if ('txn' in content) {
return (
<TransactionPopupContainer show={true}>
<StyledClose $padding={16} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
<TransactionPopup hash={content.txn.hash} />
</TransactionPopupContainer>
)
} else if ('failedSwitchNetwork' in content) {
return (
<FailedSwitchNetworkPopupContainer show={true}>
<StyledClose $padding={20} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
<FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
</FailedSwitchNetworkPopupContainer>
)
}
return popupContent ? (
<Popup>
<StyledClose color={theme.textSecondary} onClick={removeThisPopup} />
{popupContent}
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
</Popup>
) : null
return null
}

View File

@@ -0,0 +1,66 @@
import { useWeb3React } from '@web3-react/core'
import { parseLocalActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow'
import Column from 'components/Column'
import useENSName from 'hooks/useENSName'
import { useCombinedActiveList } from 'state/lists/hooks'
import { useTransaction } from 'state/transactions/hooks'
import { TransactionDetails } from 'state/transactions/types'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PopupAlertTriangle } from './FailedNetworkSwitchPopup'
const Descriptor = styled(ThemedText.BodySmall)`
${EllipsisStyle}
`
function TransactionPopupContent({ tx, chainId }: { tx: TransactionDetails; chainId: number }) {
const success = tx.receipt?.status === 1
const tokens = useCombinedActiveList()
const activity = parseLocalActivity(tx, chainId, tokens)
const { ENSName } = useENSName(activity?.otherAccount)
if (!activity) return null
const explorerUrl = getExplorerLink(chainId, tx.hash, ExplorerDataType.TRANSACTION)
return (
<PortfolioRow
left={
success ? (
<Column>
<PortfolioLogo
chainId={chainId}
currencies={activity.currencies}
images={activity.logos}
accountAddress={activity.otherAccount}
/>
</Column>
) : (
<PopupAlertTriangle />
)
}
title={<ThemedText.SubHeader fontWeight={500}>{activity.title}</ThemedText.SubHeader>}
descriptor={
<Descriptor color="textSecondary">
{activity.descriptor}
{ENSName ?? activity.otherAccount}
</Descriptor>
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
)
}
export default function TransactionPopup({ hash }: { hash: string }) {
const { chainId } = useWeb3React()
const tx = useTransaction(hash)
if (!chainId || !tx) return null
return <TransactionPopupContent tx={tx} chainId={chainId} />
}

View File

@@ -41,7 +41,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
position: fixed;
top: ${({ extraPadding }) => (extraPadding ? '72px' : '64px')};
right: 1rem;
max-width: 355px !important;
max-width: 348px !important;
width: 100%;
z-index: 3;

View File

@@ -1,38 +1,89 @@
import { BigNumber } from '@ethersproject/bignumber'
import { render, screen } from 'test-utils'
import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core'
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import { USDC_MAINNET } from 'constants/tokens'
import { useToken } from 'hooks/Tokens'
import { usePool } from 'hooks/usePools'
import { PoolState } from 'hooks/usePools'
import { mocked } from 'test-utils/mocked'
import { render } from 'test-utils/render'
import { unwrappedToken } from 'utils/unwrappedToken'
import PositionListItem from '.'
jest.mock('hooks/Tokens', () => {
const originalModule = jest.requireActual('hooks/Tokens')
const uniSDK = jest.requireActual('@uniswap/sdk-core')
jest.mock('utils/unwrappedToken')
jest.mock('hooks/usePools')
jest.mock('hooks/Tokens')
// eslint-disable-next-line react/display-name
jest.mock('components/DoubleLogo', () => () => <div />)
jest.mock('@web3-react/core', () => {
const web3React = jest.requireActual('@web3-react/core')
return {
__esModule: true,
...originalModule,
useToken: jest.fn(
() =>
new uniSDK.Token(
1,
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
8,
'https://www.example.com',
'example.com coin'
)
),
...web3React,
useWeb3React: () => ({
chainId: 1,
}),
}
})
test('PositionListItem should not render when the name contains a url', () => {
const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
beforeEach(() => {
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
mocked(useToken).mockImplementation((tokenAddress?: string | null | undefined) => {
if (!tokenAddress) return null
if (tokenAddress === susToken0.address) return susToken0
return new Token(1, tokenAddress, 8, 'symbol', 'name')
})
mocked(usePool).mockReturnValue([
PoolState.EXISTS,
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
])
mocked(unwrappedToken).mockReturnValue(susToken0)
})
test('PositionListItem should not render when token0 symbol contains a url', () => {
const positionDetails = {
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
token0: susToken0Address,
token1: USDC_MAINNET.address,
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
render(<PositionListItem {...positionDetails} />)
screen.debug()
expect(screen.queryByText('.com', { exact: false })).toBe(null)
const { container } = render(<PositionListItem {...positionDetails} />)
expect(container).toBeEmptyDOMElement()
})
test('PositionListItem should not render when token1 symbol contains a url', () => {
const positionDetails = {
token0: USDC_MAINNET.address,
token1: susToken0Address,
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
const { container } = render(<PositionListItem {...positionDetails} />)
expect(container).toBeEmptyDOMElement()
})
test('PositionListItem should render a position', () => {
const positionDetails = {
token0: USDC_MAINNET.address,
token1: WETH9[SupportedChainId.MAINNET].address,
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
const { container } = render(<PositionListItem {...positionDetails} />)
expect(container).not.toBeEmptyDOMElement()
})

View File

@@ -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
}

View File

@@ -3,7 +3,7 @@ import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
import { render } from 'test-utils'
import { render } from 'test-utils/render'
import RoutingDiagram from './RoutingDiagram'

View File

@@ -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>
`;

View File

@@ -1,7 +1,8 @@
import { screen } from '@testing-library/react'
import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core'
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
import * as mockJSBI from 'jsbi'
import { render } from 'test-utils'
import { render } from 'test-utils/render'
import CurrencyList from '.'
@@ -25,11 +26,11 @@ jest.mock(
jest.mock('@web3-react/core', () => {
const web3React = jest.requireActual('@web3-react/core')
return {
...web3React,
useWeb3React: () => ({
account: '123',
isActive: true,
}),
...web3React,
}
})
@@ -42,37 +43,38 @@ jest.mock('../../../state/connection/hooks', () => {
})
it('renders loading rows when isLoading is true', () => {
const { asFragment } = render(
const component = render(
<CurrencyList
height={10}
currencies={[]}
otherListTokens={[]}
selectedCurrency={null}
onCurrencySelect={noOp}
showImportView={noOp}
setImportToken={noOp}
isLoading={true}
searchQuery=""
isAddressSearch=""
/>
)
expect(asFragment()).toMatchSnapshot()
expect(component.findByTestId('loading-rows')).toBeTruthy()
expect(screen.queryByText('Wrapped BTC')).not.toBeInTheDocument()
expect(screen.queryByText('DAI')).not.toBeInTheDocument()
expect(screen.queryByText('USDC')).not.toBeInTheDocument()
})
it('renders currency rows correctly when currencies list is non-empty', () => {
const { asFragment } = render(
render(
<CurrencyList
height={10}
currencies={[DAI, USDC_MAINNET, WBTC]}
otherListTokens={[]}
selectedCurrency={null}
onCurrencySelect={noOp}
showImportView={noOp}
setImportToken={noOp}
isLoading={false}
searchQuery=""
isAddressSearch=""
/>
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText('Wrapped BTC')).toBeInTheDocument()
expect(screen.getByText('DAI')).toBeInTheDocument()
expect(screen.getByText('USDC')).toBeInTheDocument()
})

View File

@@ -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>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,185 +0,0 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button'
import { bodySmall, subhead } from 'nft/css/common.css'
import { useCallback, useState } from 'react'
import { X } from 'react-feather'
import { useModalIsOpen, useToggleTaxServiceModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { useTaxServiceDismissal } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { opacify } from 'theme/utils'
import { Z_INDEX } from 'theme/zIndex'
import TaxServiceModal from '.'
import CointrackerLogo from './CointrackerLogo.png'
import TokenTaxLogo from './TokenTaxLogo.png'
const PopupContainer = styled.div<{ show: boolean; isDarkMode: boolean }>`
box-shadow: ${({ theme }) => theme.deepShadow};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 13px;
cursor: pointer;
color: ${({ theme }) => theme.textPrimary};
display: ${({ show }) => (show ? 'flex' : 'none')};
flex-direction: column;
position: fixed;
right: clamp(0px, 1vw, 16px);
z-index: ${Z_INDEX.sticky};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.slow} opacity ${timing.in}`};
width: 320px;
height: 156px;
bottom: 50px;
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
border-style: solid none;
width: 100%;
border-radius: 0;
right: auto;
}
::before {
content: '';
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background-image: url(${CointrackerLogo}), url(${TokenTaxLogo});
background-size: 15%, 20%;
background-repeat: no-repeat;
background-position: top right 75px, bottom 5px right 7px;
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
background-size: 48px, 64px;
background-position: top right 75px, bottom 20px right 7px;
}
opacity: ${({ isDarkMode }) => (isDarkMode ? '0.9' : '0.25')};
}
`
const InnerContainer = styled.div<{ isDarkMode: boolean }>`
border-radius: 12px;
cursor: auto;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
gap: 8px;
padding: 16px;
background-color: ${({ isDarkMode, theme }) =>
isDarkMode ? opacify(10, theme.accentAction) : opacify(4, theme.accentAction)};
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
height: 100%;
width: 100%;
border-radius: 0;
}
`
const Button = styled(ThemeButton)`
margin-top: auto;
margin-right: auto;
padding: 8px 24px;
gap: 8px;
border-radius: 12px;
`
const TextContainer = styled.div`
user-select: none;
display: flex;
flex-direction: column;
width: 90%;
justify-content: center;
`
export const StyledXButton = styled(X)`
color: ${({ theme }) => theme.textPrimary};
cursor: pointer;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
`
const TAX_SERVICE_DISMISSED = 'TaxServiceToast-dismissed'
// TODO(lynnshaoyu): remove this count and change taxServiceDismissals in UserState to be a boolean
// flag instead after upgrading to redux-persist.
const MAX_RENDER_COUNT = 1
export default function TaxServiceBanner() {
const isDarkMode = useIsDarkMode()
const [dismissals, addTaxServiceDismissal] = useTaxServiceDismissal()
const modalOpen = useModalIsOpen(ApplicationModal.TAX_SERVICE)
const toggleTaxServiceModal = useToggleTaxServiceModal()
const sessionStorageTaxServiceDismissed = sessionStorage.getItem(TAX_SERVICE_DISMISSED)
if (!sessionStorageTaxServiceDismissed) {
sessionStorage.setItem(TAX_SERVICE_DISMISSED, 'false')
}
const [bannerOpen, setBannerOpen] = useState(
sessionStorageTaxServiceDismissed !== 'true' && (dismissals === undefined || dismissals < MAX_RENDER_COUNT)
)
const handleClose = useCallback(() => {
sessionStorage.setItem(TAX_SERVICE_DISMISSED, 'true')
setBannerOpen(false)
dismissals === undefined ? addTaxServiceDismissal(1) : addTaxServiceDismissal(dismissals + 1)
}, [addTaxServiceDismissal, dismissals])
const handleLearnMoreClick = useCallback(
(e: any) => {
e.preventDefault()
e.stopPropagation()
toggleTaxServiceModal()
},
[toggleTaxServiceModal]
)
return (
<PopupContainer show={bannerOpen} isDarkMode={isDarkMode}>
<InnerContainer isDarkMode={isDarkMode} tabIndex={0}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<TextContainer data-testid="tax-service-description">
<div className={subhead} style={{ paddingBottom: '12px' }}>
<Trans>Save on your crypto taxes</Trans>
</div>
<div className={bodySmall} style={{ paddingBottom: '12px' }}>
<Trans>Uniswap Labs can save you up to 20% on CoinTracker and TokenTax</Trans>{' '}
</div>
</TextContainer>
<StyledXButton size={20} onClick={handleClose} />
</div>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.TAX_SERVICE_BANNER_CTA_BUTTON}
>
<Button
size={ButtonSize.small}
emphasis={ButtonEmphasis.promotional}
onMouseDown={(e) => {
e.preventDefault()
}}
onClick={handleLearnMoreClick}
data-testid="learn-more-button"
>
<Trans>Learn more</Trans>
</Button>
</TraceEvent>
</InnerContainer>
<TaxServiceModal isOpen={modalOpen} onDismiss={toggleTaxServiceModal} />
</PopupContainer>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,17 +0,0 @@
import { render, screen } from '../../test-utils'
import TaxServiceModal from './'
import TaxServiceBanner from './TaxServiceBanner'
it('renders Tax Service Modal content', async () => {
render(<TaxServiceModal isOpen={true} onDismiss={() => null} />)
expect(screen.getByText('Save 10% on all plans')).toBeInTheDocument()
expect(screen.getByText('New and existing users save up to 20%')).toBeInTheDocument()
expect(screen.getAllByTestId('tax-service-option-button')).toHaveLength(2)
})
it('renders Tax Service Banner', async () => {
render(<TaxServiceBanner />)
expect(screen.getByText('Save on your crypto taxes')).toBeInTheDocument()
expect(screen.getAllByTestId('learn-more-button')).toHaveLength(1)
expect(screen.getByText('Uniswap Labs can save you up to 20% on CoinTracker and TokenTax')).toBeInTheDocument()
})

View File

@@ -1,141 +0,0 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { ButtonEmphasis } from 'components/Button'
import { ButtonSize, ThemeButton } from 'components/Button'
import { Box } from 'nft/components/Box'
import { bodySmall, subhead } from 'nft/css/common.css'
import { memo } from 'react'
import styled from 'styled-components/macro'
import Modal from '../Modal'
import CointrackerFullLogo from './CointrackerFullLogo.png'
import { StyledXButton } from './TaxServiceBanner'
import TokenTaxFullLogo from './TokenTaxFullLogo.png'
interface TaxServiceModalProps {
isOpen: boolean
onDismiss: () => void
}
interface TaxServiceOptionProps {
logo: any
description: string
url: string
}
const InnerContainer = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
overflow: hidden;
display: flex;
width: 420px;
height: 268px;
flex-direction: column;
position: relative;
gap: 20px;
padding: 16px;
`
const TaxOptionContainer = styled.div`
display: flex;
flex: 1;
gap: 16px;
justify-content: center;
`
const TaxOptionDescription = styled.div`
display: flex;
height: 100%;
justify-content: center;
user-select: none;
text-align: center;
`
const TaxOption = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.backgroundModule};
border-radius: 12px;
cursor: auto;
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
padding: 12px;
gap: 16px;
`
const StyledImageContainer = styled(Box)`
width: 75%;
height: 80%;
cursor: auto;
object-fit: contain;
`
const Button = styled(ThemeButton)`
cursor: pointer;
width: 100%;
margin-right: auto;
`
const TOKEN_TAX_URL = 'https://tokentax.co/uniswap?via=uniswap'
const COINTRACKER_URL = 'https://www.cointracker.io/partner/uniswap?utm_source=uniswap'
const TOKEN_TAX_DESCRIPTION = 'Save 10% on all plans'
const COINTRACKER_DESCRIPTION = 'New and existing users save up to 20%'
function TaxServiceOption({ description, logo, url }: TaxServiceOptionProps) {
return (
<TaxOption tabIndex={0}>
<StyledImageContainer as="img" src={logo} draggable={false} />
<TaxOptionDescription className={bodySmall}>{description}</TaxOptionDescription>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={
url.includes('tokentax')
? InterfaceElementName.TAX_SERVICE_TOKENTAX_BUTTON
: InterfaceElementName.TAX_SERVICE_COINTRACKER_BUTTON
}
>
<a href={url} target="_blank" rel="noreferrer" style={{ textDecoration: 'none' }}>
<Button
onMouseDown={(e) => {
e.preventDefault()
}}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
data-testid="tax-service-option-button"
>
Get started
</Button>
</a>
</TraceEvent>
</TaxOption>
)
}
export default memo(function TaxServiceModal({ isOpen, onDismiss }: TaxServiceModalProps) {
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} minHeight={false}>
<InnerContainer>
<div style={{ display: 'flex', justifyContent: 'space-between', userSelect: 'none' }}>
<div className={subhead}>
<Trans>Save on your crypto taxes</Trans>
</div>
<StyledXButton
size={20}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onDismiss()
}}
/>
</div>
<TaxOptionContainer>
<TaxServiceOption description={COINTRACKER_DESCRIPTION} logo={CointrackerFullLogo} url={COINTRACKER_URL} />
<TaxServiceOption description={TOKEN_TAX_DESCRIPTION} logo={TokenTaxFullLogo} url={TOKEN_TAX_URL} />
</TaxOptionContainer>
</InnerContainer>
</Modal>
)
})

View File

@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from 'test-utils'
import { fireEvent, render, screen } from 'test-utils/render'
import { ResizingTextArea, TextInput } from './'

View File

@@ -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>

View File

@@ -1,16 +1,11 @@
import { useWeb3React } from '@web3-react/core'
import UniwalletModal from 'components/AccountDrawer/UniwalletModal'
import UniswapWalletBanner from 'components/Banner/UniswapWalletBanner'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import FiatOnrampModal from 'components/FiatOnrampModal'
import TaxServiceBanner from 'components/TaxServiceModal/TaxServiceBanner'
import UniwalletModal from 'components/WalletDropdown/UniwalletModal'
import { useTaxServiceBannerEnabled } from 'featureFlags/flags/taxServiceBanner'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useIsPoolsPage } from 'hooks/useIsPoolsPage'
import { lazy } from 'react'
import { useLocation } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
@@ -25,13 +20,6 @@ export default function TopLevelModals() {
const { account } = useWeb3React()
useAccountRiskCheck(account)
const accountBlocked = Boolean(blockedAccountModalOpen && account)
const taxServiceEnabled = useTaxServiceBannerEnabled()
const { pathname } = useLocation()
const isNftPage = useIsNftPage()
const isPoolPage = useIsPoolsPage()
const isTaxModalServicePage = isNftPage || isPoolPage || pathname.startsWith('/swap')
return (
<>
@@ -43,7 +31,6 @@ export default function TopLevelModals() {
<TransactionCompleteModal />
<AirdropModal />
<FiatOnrampModal />
{taxServiceEnabled && isTaxModalServicePage && <TaxServiceBanner />}
</>
)
}

View File

@@ -3,6 +3,7 @@ import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap
import Loader from 'components/Icons/LoadingSpinner'
import { Connection, ConnectionType } from 'connection'
import styled from 'styled-components/macro'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
import NewBadge from './NewBadge'
@@ -68,11 +69,12 @@ type OptionProps = {
}
export default function Option({ connection, pendingConnectionType, activate }: OptionProps) {
const isPending = pendingConnectionType === connection.type
const isDarkMode = useIsDarkMode()
const content = (
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.WALLET_SELECTED}
properties={{ wallet_type: connection.name }}
properties={{ wallet_type: connection.getName() }}
element={InterfaceElementName.WALLET_TYPE_OPTION}
>
<OptionCardClickable
@@ -83,9 +85,9 @@ export default function Option({ connection, pendingConnectionType, activate }:
>
<OptionCardLeft>
<IconWrapper>
<img src={connection.icon} alt="Icon" />
<img src={connection.getIcon?.(isDarkMode)} alt="Icon" />
</IconWrapper>
<HeaderText>{connection.name}</HeaderText>
<HeaderText>{connection.getName()}</HeaderText>
{connection.isNew && <NewBadge />}
</OptionCardLeft>
{isPending && <Loader />}

View File

@@ -2,12 +2,12 @@ import { sendAnalyticsEvent, user } from '@uniswap/analytics'
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
import { useWeb3React } from '@web3-react/core'
import { useAccountDrawer } from 'components/AccountDrawer'
import IconButton from 'components/AccountDrawer/IconButton'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { useWalletDrawer } from 'components/WalletDropdown'
import IconButton from 'components/WalletDropdown/IconButton'
import { Connection, ConnectionType, networkConnection, useConnections } from 'connection'
import { Connection, ConnectionType, getConnections, networkConnection } from 'connection'
import { useGetConnection } from 'connection'
import { ErrorCode } from 'connection/utils'
import { isSupportedChain } from 'constants/chains'
@@ -84,14 +84,14 @@ function didUserReject(connection: Connection, error: any): boolean {
export default function WalletModal({ openSettings }: { openSettings: () => void }) {
const dispatch = useAppDispatch()
const { connector, account, chainId, provider } = useWeb3React()
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
const [lastActiveWalletAddress, setLastActiveWalletAddress] = useState<string | undefined>(account)
const [pendingConnection, setPendingConnection] = useState<Connection | undefined>()
const [pendingError, setPendingError] = useState<any>()
const connections = useConnections()
const connections = getConnections()
const getConnection = useGetConnection()
useEffect(() => {
@@ -116,7 +116,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
useEffect(() => {
if (account && account !== lastActiveWalletAddress) {
const walletName = getConnection(connector).name
const walletName = getConnection(connector).getName()
const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined
const isReconnect =
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletName).length > 0
@@ -141,6 +141,9 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
const tryActivation = useCallback(
async (connection: Connection) => {
// Skips wallet connection if the connection should override the default behavior, i.e. install metamask or launch coinbase app
if (connection.overrideActivate?.()) return
// log selected wallet
sendEvent({
category: 'Wallet',
@@ -153,19 +156,20 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
setPendingError(undefined)
await connection.connector.activate()
console.debug(`connection activated: ${connection.getName()}`)
dispatch(updateSelectedWallet({ wallet: connection.type }))
if (drawerOpenRef.current) toggleWalletDrawer()
} catch (error) {
console.debug(`web3-react connection error: ${JSON.stringify(error)}`)
// TODO(WEB-3162): re-add special treatment for already-pending injected errors
if (didUserReject(connection, error)) {
setPendingConnection(undefined)
} // Prevents showing error caused by MetaMask being prompted twice
else if (error?.code !== ErrorCode.MM_ALREADY_PENDING) {
console.debug(`web3-react connection error: ${error}`)
setPendingError(error.message)
} else {
setPendingError(error)
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.FAILED,
wallet_type: connection.name,
wallet_type: connection.getName(),
})
}
}
@@ -190,11 +194,11 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
<OptionGrid data-testid="option-grid">
{connections.map((connection) =>
// Hides Uniswap Wallet if mgtm is disabled
connection.shouldDisplay && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
connection.shouldDisplay() && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
<Option
key={connection.name}
key={connection.getName()}
connection={connection}
activate={connection.overrideActivate ?? (() => tryActivation(connection))}
activate={() => tryActivation(connection)}
pendingConnectionType={pendingConnection?.type}
/>
) : null

View File

@@ -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}>

View File

@@ -2,19 +2,17 @@ import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import PortfolioDrawer, { useAccountDrawer } from 'components/AccountDrawer'
import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWrapper'
import Loader from 'components/Icons/LoadingSpinner'
import { IconWrapper } from 'components/Identicon/StatusIcon'
import WalletDropdown, { useWalletDrawer } from 'components/WalletDropdown'
import PrefetchBalancesWrapper from 'components/WalletDropdown/PrefetchBalancesWrapper'
import { useGetConnection } from 'connection'
import { Portal } from 'nft/components/common/Portal'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { getIsValidSwapQuote } from 'pages/Swap'
import { darken } from 'polished'
import { useCallback, useMemo } from 'react'
import { AlertTriangle } from 'react-feather'
import { useAppSelector } from 'state/hooks'
import { useDerivedSwapInfo } from 'state/swap/hooks'
import styled from 'styled-components/macro'
import { colors } from 'theme/colors'
import { flexRowNoWrap } from 'theme/styles'
@@ -153,16 +151,11 @@ function Web3StatusInner() {
const { account, connector, chainId, ENSName } = useWeb3React()
const getConnection = useGetConnection()
const connection = getConnection(connector)
const {
trade: { state: tradeState, trade },
inputError: swapInputError,
} = useDerivedSwapInfo()
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
const [, toggleWalletDrawer] = useWalletDrawer()
const [, toggleAccountDrawer] = useAccountDrawer()
const handleWalletDropdownClick = useCallback(() => {
sendAnalyticsEvent(InterfaceEventName.ACCOUNT_DROPDOWN_BUTTON_CLICKED)
toggleWalletDrawer()
}, [toggleWalletDrawer])
toggleAccountDrawer()
}, [toggleAccountDrawer])
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
@@ -202,9 +195,7 @@ function Web3StatusInner() {
pending={hasPendingTransactions}
isClaimAvailable={isClaimAvailable}
>
{!hasPendingTransactions && (
<StatusIcon enableInfotips={true} size={24} connection={connection} showMiniIcons={false} />
)}
{!hasPendingTransactions && <StatusIcon size={24} connection={connection} showMiniIcons={false} />}
{hasPendingTransactions ? (
<RowBetween>
<Text>
@@ -225,7 +216,6 @@ function Web3StatusInner() {
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: validSwapQuote }}
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
>
<Web3StatusConnectWrapper
@@ -243,13 +233,12 @@ function Web3StatusInner() {
}
}
// eslint-disable-next-line import/no-unused-modules
export default function Web3Status() {
return (
<PrefetchBalancesWrapper>
<Web3StatusInner />
<Portal>
<WalletDropdown />
<PortfolioDrawer />
</Portal>
</PrefetchBalancesWrapper>
)

View File

@@ -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

View File

@@ -0,0 +1,46 @@
import { Trans } from '@lingui/macro'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
const ExplainerText = styled.div`
color: ${({ theme }) => theme.textSecondary};
`
const TitleRow = styled.div`
align-items: center;
display: flex;
flex-direction: row;
color: ${({ theme }) => theme.accentWarning};
margin-bottom: 8px;
`
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.accentWarningSoft};
border-radius: 16px;
margin-top: 12px;
max-width: 480px;
padding: 12px 20px;
width: 100%;
`
interface OwnershipWarningProps {
ownerAddress: string
}
const OwnershipWarning = ({ ownerAddress }: OwnershipWarningProps) => (
<Wrapper>
<TitleRow>
<AlertTriangle style={{ marginRight: '8px' }} />
<ThemedText.SubHeader color="accentWarning">
<Trans>Warning</Trans>
</ThemedText.SubHeader>
</TitleRow>
<ExplainerText>
<Trans>
You are not the owner of this LP position. You will not be able to withdraw the liquidity from this position
unless you own the following address: {ownerAddress}
</Trans>
</ExplainerText>
</Wrapper>
)
export default OwnershipWarning

View File

@@ -0,0 +1,111 @@
import userEvent from '@testing-library/user-event'
import { useWeb3React } from '@web3-react/core'
import { useAccountDrawer } from 'components/AccountDrawer'
import { fireEvent, render, screen } from 'test-utils/render'
import { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
import SwapBuyFiatButton, { MOONPAY_REGION_AVAILABILITY_ARTICLE } from './SwapBuyFiatButton'
jest.mock('@web3-react/core', () => {
const web3React = jest.requireActual('@web3-react/core')
return {
...web3React,
useWeb3React: jest.fn(),
}
})
jest.mock('../../state/application/hooks')
const mockUseFiatOnrampAvailability = useFiatOnrampAvailability as jest.MockedFunction<typeof useFiatOnrampAvailability>
const mockUseOpenModal = useOpenModal as jest.MockedFunction<typeof useOpenModal>
jest.mock('components/AccountDrawer')
const mockuseAccountDrawer = useAccountDrawer as jest.MockedFunction<typeof useAccountDrawer>
const mockUseFiatOnRampsUnavailable = (shouldCheck: boolean) => {
return {
available: false,
availabilityChecked: shouldCheck,
error: null,
loading: false,
}
}
const mockUseFiatOnRampsAvailable = (shouldCheck: boolean) => {
if (shouldCheck) {
return {
available: true,
availabilityChecked: true,
error: null,
loading: false,
}
}
return {
available: false,
availabilityChecked: false,
error: null,
loading: false,
}
}
describe('SwapBuyFiatButton.tsx', () => {
let toggleWalletDrawer: jest.Mock<any, any>
let useOpenModal: jest.Mock<any, any>
beforeAll(() => {
toggleWalletDrawer = jest.fn()
useOpenModal = jest.fn()
})
beforeEach(() => {
jest.resetAllMocks()
;(useWeb3React as jest.Mock).mockReturnValue({
account: undefined,
isActive: false,
})
})
it('matches base snapshot', () => {
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
const { asFragment } = render(<SwapBuyFiatButton />)
expect(asFragment()).toMatchSnapshot()
})
it('fiat on ramps available in region, account unconnected', async () => {
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
mockUseOpenModal.mockImplementation(() => useOpenModal)
render(<SwapBuyFiatButton />)
await userEvent.click(screen.getByTestId('buy-fiat-button'))
expect(toggleWalletDrawer).toHaveBeenCalledTimes(1)
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
})
it('fiat on ramps available in region, account connected', async () => {
;(useWeb3React as jest.Mock).mockReturnValue({
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
isActive: true,
})
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
mockUseOpenModal.mockImplementation(() => useOpenModal)
render(<SwapBuyFiatButton />)
expect(screen.getByTestId('buy-fiat-flow-incomplete-indicator')).toBeInTheDocument()
await userEvent.click(screen.getByTestId('buy-fiat-button'))
expect(toggleWalletDrawer).toHaveBeenCalledTimes(0)
expect(useOpenModal).toHaveBeenCalledTimes(1)
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
expect(screen.queryByTestId('buy-fiat-flow-incomplete-indicator')).not.toBeInTheDocument()
})
it('fiat on ramps unavailable in region', async () => {
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
render(<SwapBuyFiatButton />)
await userEvent.click(screen.getByTestId('buy-fiat-button'))
fireEvent.mouseOver(screen.getByTestId('buy-fiat-button'))
expect(await screen.findByTestId('fiat-on-ramp-unavailable-tooltip')).toBeInTheDocument()
expect(await screen.findByText(/Learn more/i)).toHaveAttribute('href', MOONPAY_REGION_AVAILABILITY_ARTICLE)
expect(await screen.findByTestId('buy-fiat-button')).toBeDisabled()
})
})

View 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>
)
}

View File

@@ -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} />

View File

@@ -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>
`;

View File

@@ -1,111 +1,150 @@
// eslint-disable-next-line jest/no-export
export {}
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
import { ConnectionType, getConnections, useGetConnection } from 'connection'
import { renderHook } from 'test-utils/render'
beforeEach(() => {
jest.resetModules()
jest.resetAllMocks()
})
it('Non-injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
const UserAgentMock = jest.requireMock('utils/userAgent')
jest.mock('utils/userAgent', () => ({
isMobile: false,
}))
it('MetaMask Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
describe('connection utility/metadata tests', () => {
const createWalletEnvironment = (ethereum: Window['window']['ethereum'], isMobile = false) => {
UserAgentMock.isMobile = isMobile
global.window.ethereum = ethereum
it('Coinbase Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
const displayed = getConnections().filter((c) => c.shouldDisplay())
const getConnection = renderHook(() => useGetConnection()).result.current
const injected = getConnection(ConnectionType.INJECTED)
const coinbase = getConnection(ConnectionType.COINBASE_WALLET)
const uniswap = getConnection(ConnectionType.UNIWALLET)
const walletconnect = getConnection(ConnectionType.WALLET_CONNECT)
it('Coinbase and MetaMask Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: true }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
return { displayed, injected, coinbase, uniswap, walletconnect }
}
it('Generic Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
it('Non-injected Desktop', async () => {
const { displayed, injected } = createWalletEnvironment(undefined)
it('Generic Injected Mobile Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
})
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeTruthy()
it('MetaMask Mobile Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
})
expect(displayed.length).toEqual(4)
})
it('Coinbase Mobile Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
it('MetaMask-Injected Desktop', async () => {
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true })
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.coinbaseWalletConnection.overrideActivate).toBeUndefined()
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
})
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeFalsy()
it('mWeb Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(false)
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.coinbaseWalletConnection.overrideActivate).toBeDefined()
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(3)
expect(displayed.length).toEqual(4)
})
it('Coinbase-Injected Desktop', async () => {
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true })
expect(displayed.includes(coinbase)).toBe(true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeTruthy()
expect(displayed.length).toEqual(4)
})
it('Coinbase and MetaMask Injected Desktop', async () => {
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true, isMetaMask: true })
expect(displayed.includes(coinbase)).toBe(true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(4)
})
it('Generic Injected Desktop', async () => {
const { displayed, injected } = createWalletEnvironment({ isTrustWallet: true })
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(4)
})
it('Generic Browser Wallet that injects as MetaMask', async () => {
const { displayed, injected } = createWalletEnvironment({ isRabby: true, isMetaMask: true })
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(4)
})
it('Generic Wallet Browser with delayed injection', async () => {
const { injected } = createWalletEnvironment(undefined)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeTruthy()
createWalletEnvironment({ isTrustWallet: true })
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
})
const UNKNOWN_INJECTOR = { isRandomWallet: true } as Window['window']['ethereum']
it('Generic Unknown Injected Wallet Browser', async () => {
const { displayed, injected } = createWalletEnvironment(UNKNOWN_INJECTOR, true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(injected.getIcon?.(/* isDarkMode */ false)).toBe(INJECTED_LIGHT_ICON)
expect(injected.getIcon?.(/* isDarkMode */ true)).toBe(INJECTED_DARK_ICON)
// Ensures we provide multiple connection options if in an unknown injected browser
expect(displayed.length).toEqual(4)
})
it('MetaMask Mobile Browser', async () => {
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true }, true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(1)
})
it('Coinbase Mobile Browser', async () => {
const { displayed, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true }, true)
expect(displayed.includes(coinbase)).toBe(true)
// Expect coinbase option to not override activation in a the cb mobile browser
expect(coinbase.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(1)
})
it('Uninjected mWeb Browser', async () => {
const { displayed, injected, coinbase, walletconnect } = createWalletEnvironment(undefined, true)
expect(displayed.includes(coinbase)).toBe(true)
expect(displayed.includes(walletconnect)).toBe(true)
// Don't show injected connection on plain mWeb browser
expect(displayed.includes(injected)).toBe(false)
// Expect coinbase option to launch coinbase app in a regular mobile browser
expect(coinbase.overrideActivate?.()).toBeTruthy()
expect(displayed.length).toEqual(3)
})
})

View File

@@ -4,22 +4,21 @@ import { GnosisSafe } from '@web3-react/gnosis-safe'
import { MetaMask } from '@web3-react/metamask'
import { Network } from '@web3-react/network'
import { Connector } from '@web3-react/types'
import COINBASE_ICON_URL from 'assets/images/coinbaseWalletIcon.svg'
import GNOSIS_ICON_URL from 'assets/images/gnosis.png'
import METAMASK_ICON_URL from 'assets/images/metamask.svg'
import UNIWALLET_ICON_URL from 'assets/images/uniwallet.svg'
import WALLET_CONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg'
import INJECTED_DARK_ICON_URL from 'assets/svg/browser-wallet-dark.svg'
import INJECTED_LIGHT_ICON_URL from 'assets/svg/browser-wallet-light.svg'
import UNISWAP_LOGO_URL from 'assets/svg/logo.svg'
import COINBASE_ICON from 'assets/images/coinbaseWalletIcon.svg'
import GNOSIS_ICON from 'assets/images/gnosis.png'
import METAMASK_ICON from 'assets/images/metamask.svg'
import UNIWALLET_ICON from 'assets/images/uniwallet.svg'
import WALLET_CONNECT_ICON from 'assets/images/walletConnectIcon.svg'
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
import UNISWAP_LOGO from 'assets/svg/logo.svg'
import { SupportedChainId } from 'constants/chains'
import { useCallback } from 'react'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks'
import { RPC_PROVIDERS } from '../constants/providers'
import { isCoinbaseWallet, isInjected, isMetaMaskWallet } from './utils'
import { getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
import { UniwalletConnect, WalletConnectPopup } from './WalletConnect'
export enum ConnectionType {
@@ -32,13 +31,13 @@ export enum ConnectionType {
}
export interface Connection {
name: string
getName(): string
connector: Connector
hooks: Web3ReactHooks
type: ConnectionType
icon?: string
shouldDisplay?: boolean
overrideActivate?: () => void
getIcon?(isDarkMode: boolean): string
shouldDisplay(): boolean
overrideActivate?: () => boolean
isNew?: boolean
}
@@ -50,73 +49,73 @@ const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
)
export const networkConnection: Connection = {
name: 'Network',
getName: () => 'Network',
connector: web3Network,
hooks: web3NetworkHooks,
type: ConnectionType.NETWORK,
shouldDisplay: false,
shouldDisplay: () => false,
}
const isCoinbaseWalletBrowser = isMobile && isCoinbaseWallet
const isMetaMaskBrowser = isMobile && isMetaMaskWallet
const getIsInjectedMobileBrowser = isCoinbaseWalletBrowser || isMetaMaskBrowser
const getIsCoinbaseWalletBrowser = () => isMobile && getIsCoinbaseWallet()
const getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet()
const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser()
const getShouldAdvertiseMetaMask = !isMetaMaskWallet && !isMobile && (!isInjected || isCoinbaseWallet)
const isGenericInjector = isInjected && !isMetaMaskWallet && !isCoinbaseWallet
const getShouldAdvertiseMetaMask = () =>
!getIsMetaMaskWallet() && !isMobile && (!getIsInjected() || getIsCoinbaseWallet())
const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() && !getIsCoinbaseWallet()
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
const baseInjectedConnection: Omit<Connection, 'icon'> = {
name: isGenericInjector ? 'Browser Wallet' : 'MetaMask',
const injectedConnection: Connection = {
// TODO(WEB-3131) re-add "Install MetaMask" string when no injector is present
getName: () => (getIsGenericInjector() ? 'Browser Wallet' : 'MetaMask'),
connector: web3Injected,
hooks: web3InjectedHooks,
type: ConnectionType.INJECTED,
shouldDisplay: isMetaMaskWallet || getShouldAdvertiseMetaMask || isGenericInjector,
getIcon: (isDarkMode: boolean) =>
getIsGenericInjector() ? (isDarkMode ? INJECTED_DARK_ICON : INJECTED_LIGHT_ICON) : METAMASK_ICON,
shouldDisplay: () => getIsMetaMaskWallet() || getShouldAdvertiseMetaMask() || getIsGenericInjector(),
// If on non-injected, non-mobile browser, prompt user to install Metamask
overrideActivate: getShouldAdvertiseMetaMask ? () => window.open('https://metamask.io/', 'inst_metamask') : undefined,
overrideActivate: () => {
if (getShouldAdvertiseMetaMask()) {
window.open('https://metamask.io/', 'inst_metamask')
return true
}
return false
},
}
export const darkInjectedConnection: Connection = {
...baseInjectedConnection,
icon: isGenericInjector ? INJECTED_DARK_ICON_URL : METAMASK_ICON_URL,
}
export const lightInjectedConnection: Connection = {
...baseInjectedConnection,
icon: isGenericInjector ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL,
}
const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
export const gnosisSafeConnection: Connection = {
name: 'Gnosis Safe',
getName: () => 'Gnosis Safe',
connector: web3GnosisSafe,
hooks: web3GnosisSafeHooks,
type: ConnectionType.GNOSIS_SAFE,
icon: GNOSIS_ICON_URL,
shouldDisplay: false,
getIcon: () => GNOSIS_ICON,
shouldDisplay: () => false,
}
const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnectPopup>(
(actions) => new WalletConnectPopup({ actions, onError })
)
export const walletConnectConnection: Connection = {
name: 'WalletConnect',
getName: () => 'WalletConnect',
connector: web3WalletConnect,
hooks: web3WalletConnectHooks,
type: ConnectionType.WALLET_CONNECT,
icon: WALLET_CONNECT_ICON_URL,
shouldDisplay: !getIsInjectedMobileBrowser,
getIcon: () => WALLET_CONNECT_ICON,
shouldDisplay: () => !getIsInjectedMobileBrowser(),
}
const [web3UniwalletConnect, web3UniwalletConnectHooks] = initializeConnector<UniwalletConnect>(
(actions) => new UniwalletConnect({ actions, onError })
)
export const uniwalletConnectConnection: Connection = {
name: 'Uniswap Wallet',
getName: () => 'Uniswap Wallet',
connector: web3UniwalletConnect,
hooks: web3UniwalletConnectHooks,
type: ConnectionType.UNIWALLET,
icon: UNIWALLET_ICON_URL,
shouldDisplay: Boolean(!getIsInjectedMobileBrowser && !isNonIOSPhone),
getIcon: () => UNIWALLET_ICON,
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
isNew: true,
}
@@ -127,31 +126,35 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba
options: {
url: RPC_URLS[SupportedChainId.MAINNET][0],
appName: 'Uniswap',
appLogoUrl: UNISWAP_LOGO_URL,
appLogoUrl: UNISWAP_LOGO,
reloadOnDisconnect: false,
},
onError,
})
)
export const coinbaseWalletConnection: Connection = {
name: 'Coinbase Wallet',
const coinbaseWalletConnection: Connection = {
getName: () => 'Coinbase Wallet',
connector: web3CoinbaseWallet,
hooks: web3CoinbaseWalletHooks,
type: ConnectionType.COINBASE_WALLET,
icon: COINBASE_ICON_URL,
shouldDisplay: Boolean((isMobile && !getIsInjectedMobileBrowser) || !isMobile || isCoinbaseWalletBrowser),
getIcon: () => COINBASE_ICON,
shouldDisplay: () =>
Boolean((isMobile && !getIsInjectedMobileBrowser()) || !isMobile || getIsCoinbaseWalletBrowser()),
// If on a mobile browser that isn't the coinbase wallet browser, deeplink to the coinbase wallet app
overrideActivate:
isMobile && !getIsInjectedMobileBrowser
? () => window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
: undefined,
overrideActivate: () => {
if (isMobile && !getIsInjectedMobileBrowser()) {
window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
return true
}
return false
},
}
export function getConnections(isDarkMode: boolean) {
export function getConnections() {
return [
uniwalletConnectConnection,
isDarkMode ? darkInjectedConnection : lightInjectedConnection,
injectedConnection,
walletConnectConnection,
coinbaseWalletConnection,
gnosisSafeConnection,
@@ -159,38 +162,29 @@ export function getConnections(isDarkMode: boolean) {
]
}
export function useConnections() {
const isDarkMode = useIsDarkMode()
return getConnections(isDarkMode)
}
export function useGetConnection() {
const isDarkMode = useIsDarkMode()
return useCallback(
(c: Connector | ConnectionType) => {
if (c instanceof Connector) {
const connection = getConnections(isDarkMode).find((connection) => connection.connector === c)
if (!connection) {
throw Error('unsupported connector')
}
return connection
} else {
switch (c) {
case ConnectionType.INJECTED:
return isDarkMode ? darkInjectedConnection : lightInjectedConnection
case ConnectionType.COINBASE_WALLET:
return coinbaseWalletConnection
case ConnectionType.WALLET_CONNECT:
return walletConnectConnection
case ConnectionType.UNIWALLET:
return uniwalletConnectConnection
case ConnectionType.NETWORK:
return networkConnection
case ConnectionType.GNOSIS_SAFE:
return gnosisSafeConnection
}
return useCallback((c: Connector | ConnectionType) => {
if (c instanceof Connector) {
const connection = getConnections().find((connection) => connection.connector === c)
if (!connection) {
throw Error('unsupported connector')
}
},
[isDarkMode]
)
return connection
} else {
switch (c) {
case ConnectionType.INJECTED:
return injectedConnection
case ConnectionType.COINBASE_WALLET:
return coinbaseWalletConnection
case ConnectionType.WALLET_CONNECT:
return walletConnectConnection
case ConnectionType.UNIWALLET:
return uniwalletConnectConnection
case ConnectionType.NETWORK:
return networkConnection
case ConnectionType.GNOSIS_SAFE:
return gnosisSafeConnection
}
}
}, [])
}

View File

@@ -1,15 +1,14 @@
export const isInjected = Boolean(window.ethereum)
export const getIsInjected = () => Boolean(window.ethereum)
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
// This variable should be true only when using the MetaMask extension
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet'
const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet']
export const isMetaMaskWallet = Boolean(
window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag])
)
type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet' | 'isLedgerConnect'
const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet', 'isLedgerConnect']
export const getIsMetaMaskWallet = () =>
Boolean(window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag]))
export const isCoinbaseWallet = Boolean(window.ethereum?.isCoinbaseWallet)
export const getIsCoinbaseWallet = () => Boolean(window.ethereum?.isCoinbaseWallet)
// https://eips.ethereum.org/EIPS/eip-1193#provider-errors
export enum ErrorCode {

View File

@@ -8,7 +8,7 @@ export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30
export const L2_DEADLINE_FROM_NOW = 60 * 5
// transaction popup dismisal amounts
export const DEFAULT_TXN_DISMISS_MS = 25000
export const DEFAULT_TXN_DISMISS_MS = 10000
export const L2_TXN_DISMISS_MS = 5000
export const BIG_INT_ZERO = JSBI.BigInt(0)

View File

@@ -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 } = {}

View File

@@ -5,11 +5,11 @@ export enum FeatureFlag {
traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2',
payWithAnyToken = 'payWithAnyToken',
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
swapWidget = 'swap_widget_replacement_enabled',
statsigDummy = 'web_dummy_gate_amplitude_id',
nftGraphql = 'nft_graphql_migration',
taxService = 'tax_service_banner',
mgtm = 'web_mobile_go_to_market_enabled',
walletMicrosite = 'walletMicrosite',
miniPortfolio = 'miniPortfolio',
detailsV2 = 'details_v2',
}

Some files were not shown because too many files have changed in this diff Show More