ci(release): publish latest release

parent dbce40b7
ignores: [
# Dependencies that depcheck thinks are unused but are actually used
"@graphql-codegen/*",
"@commitlint/*",
"i18next",
# Dependencies that depcheck thinks are missing but are actually present or never used
"@yarnpkg/core",
"@yarnpkg/cli",
"clipanion",
"@yarnpkg/fslib",
"bufferutil",
"utf-8-validate",
"@yarnpkg/parsers",
"@yarnpkg/plugin-git",
"semver",
"typanion",
]
...@@ -46,6 +46,8 @@ packages/wallet/src/i18n/locales/*_old.json ...@@ -46,6 +46,8 @@ packages/wallet/src/i18n/locales/*_old.json
# ci # ci
.ci-cache/ .ci-cache/
# JetBrains # JetBrains
.idea/ .idea/
# Vercel
.vercel
{
"**/*+(.ts?|.tsx)": ["turbo staged:lint:fix staged:format --filter=// --"]
}
diff --git a/src/components/bottomSheet/BottomSheet.tsx b/src/components/bottomSheet/BottomSheet.tsx
index 3b1264fb69d8a32e06cba4e2a1d592cee921b2aa..29e4e6464dc38567a1c97ddffb1e1ae016afbc3f 100644
--- a/src/components/bottomSheet/BottomSheet.tsx
+++ b/src/components/bottomSheet/BottomSheet.tsx
@@ -636,6 +636,7 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
const animateToPositionCompleted = useWorkletCallback(
function animateToPositionCompleted(isFinished?: boolean) {
isForcedClosing.value = false;
+ animatedCurrentIndex.value = animatedNextPositionIndex.value;
if (!isFinished) {
return;
* @uniswap/web-admins
Here again with a new update to our app! Check out what is new below IPFS hash of the deployment:
- CIDv0: `QmRUdEZRqLCxxz4VQnPTTVrZVGDXBDX3kog6khivksne6u`
- CIDv1: `bafybeibotyfb47xlr4se7nnegt7oeufmue7ztobxvsjprwib2uga2tq3ki`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeibotyfb47xlr4se7nnegt7oeufmue7ztobxvsjprwib2uga2tq3ki.ipfs.dweb.link/
- https://bafybeibotyfb47xlr4se7nnegt7oeufmue7ztobxvsjprwib2uga2tq3ki.ipfs.cf-ipfs.com/
- [ipfs://QmRUdEZRqLCxxz4VQnPTTVrZVGDXBDX3kog6khivksne6u/](ipfs://QmRUdEZRqLCxxz4VQnPTTVrZVGDXBDX3kog6khivksne6u/)
## 5.4.0 (2024-01-17)
### Features
* **web:** [info] add PDP volume chart + use new init pattern for TDP volume chart (#5449) 2235363
* **web:** [info] Add TDP Pools Table (#5500) fdaa398
* **web:** [info] PDP My Positions Table (#5521) 76cd654
* **web:** [info] PDP Transactions Table (#5357) 5763046
* **web:** [info] Pools Explore Table (#5309) 7cc1767
* **web:** [info] use BreadcrumbNav in PDP + separate TDP breadcrumb (#5439) 4a482c8
* **web:** [info] use sigFigs to determine exact in vs out for PDP Tx Table (#5580) 6b52b6a
* **web:** [info/explore] add stacked volume charts (#5322) 8863cb1
* **web:** [info/pdp] PDP stats pool balances polish (#5644) f0c7161
* **web:** [landing-page] add modal to download the mobile app (#5612) 245e3e4
* **web:** [landing-page] add navbar button to get the app (#5611) 47167c2
* **web:** A check if we are using iframes (#5654) 2e3c0a2
* **web:** add detail rows to X detail sheet (#5366) a0aa241
* **web:** add feature flag to toggle new DNS gateway for quickroutes (#5569) 35a406a
* **web:** add initial landing page (#5514) e9c5321
* **web:** Add quote intent to requests (#5465) 12c8a99
* **web:** add title to TDP loading state for web crawlers (#5594) 03a032f
* **web:** Basic form for limits (#5457) e4d241a
* **web:** Limits on mainnet only (#5429) 1437fae
* **web:** more dynamic page titles for SEO (#5572) 94e8bdc
* **web:** move time selector to in-chart container (#5433) 11d9978
* **web:** SEO h1 tags for titles (#5379) b13b883
* **web:** token selector groupings and refactor (#5403) 065fb49
* **web:** tvl charts (#5599) db447ab
* **web:** update volume bar style (#5616) fdc75ec
* **web:** use new DNS gateway for other API endpoints (#5570) 0e3dbb5
* **web:** X order modal UI (#5354) 803de62
### Bug Fixes
* **web:** [info] fix PDP 404 flickering + TDP pools table bugs (#5650) 3fb17b7
* **web:** allow vercel.live domain for CSP scripts to allow Vercel Preview Comments (#5697) 9b16897
* **web:** bug with URL prefilled tokens (#5705) 57dfaa7
* **web:** center confirmation modal icons (#5491) 4761373
* **web:** remove framer motion in a few places to improve performance of new landing page (#5641) 14f427c
* **web:** set recent-connection metadata on user state initialization (#5698) c1b7197
* **web:** show default list tokens when searched (#5494) 7feb22f
* **web:** specify WC metadata (#5634) fec88e8
* **web:** update snapshots for 2024 (#5567) 112673a
### Code Refactoring
* **web:** explore and volume chart structure (#5596) 8f22ebf
- Editing Favorite Tokens — We added the ability to drag, drop, and rearrange your favorited tokens. Keep your most important watched tokens close!
- Hidden Token Balances — We updated our wallet to not include the value of hidden tokens in the total wallet balance. Toggle specific tokens to be hidden or shown, and your overall wallet balance will reflect the changes immediately. Out of sight, out of mind.
mobile/1.18 web/5.4.0
\ No newline at end of file \ No newline at end of file
ignores: [
# Dependencies that depcheck thinks are unused but are actually used
"@uniswap/ethers-rs-mobile",
"babel-loader",
"babel-jest",
"babel-plugin-react-native-web",
"babel-plugin-transform-remove-console",
"cross-fetch",
"expo-localization",
"expo-linking",
"madge",
"postinstall-postinstall",
## React Native Usage
"@amplitude/analytics-react-native",
"@react-native-masked-view/masked-view",
"react-native-image-colors",
# Dependencies that depcheck thinks are missing but are actually present or never used
## Internal packages / workspaces
"src",
"ui",
"tsconfig",
"eslint-config-custom",
## Subpackages of installed packages
"@redux-saga/core",
"@ethersproject/constants",
"@react-navigation/elements",
"metro-config",
]
...@@ -10,19 +10,4 @@ module.exports = { ...@@ -10,19 +10,4 @@ module.exports = {
ecmaVersion: 2018, ecmaVersion: 2018,
sourceType: 'module', sourceType: 'module',
}, },
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
],
rules: {},
} }
...@@ -94,9 +94,8 @@ storybook-static/* ...@@ -94,9 +94,8 @@ storybook-static/*
# Private keys # Private keys
.env.local .env.local
# Snyk # Built Items
.dccache ios/assets/
./lib/ ./lib/
# Jest # Jest
......
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
source "https://rubygems.org" source "https://rubygems.org"
gem 'fastlane' gem 'fastlane', '2.214.0'
gem 'cocoapods', '1.14.3'
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path) eval_gemfile(plugins_path) if File.exist?(plugins_path)
...@@ -3,40 +3,98 @@ GEM ...@@ -3,40 +3,98 @@ GEM
specs: specs:
CFPropertyList (3.0.6) CFPropertyList (3.0.6)
rexml rexml
addressable (2.8.5) activesupport (7.1.2)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.3.0)
aws-partitions (1.805.0) aws-partitions (1.877.0)
aws-sdk-core (3.180.3) aws-sdk-core (3.190.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.71.0) aws-sdk-kms (1.75.0)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.188.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.132.1) aws-sdk-s3 (1.142.0)
aws-sdk-core (~> 3, >= 3.179.0) aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6) aws-sigv4 (~> 1.8)
aws-sigv4 (1.6.0) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
base64 (0.2.0)
bigdecimal (3.1.5)
claide (1.1.0) claide (1.1.0)
cocoapods (1.14.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.14.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.14.3)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701) domain_name (0.6.20231109)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1) dotenv (2.8.1)
drb (2.2.0)
ruby2_keywords
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.100.0) escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
excon (0.109.0)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
...@@ -65,7 +123,7 @@ GEM ...@@ -65,7 +123,7 @@ GEM
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.7) fastimage (2.3.0)
fastlane (2.214.0) fastlane (2.214.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
...@@ -107,10 +165,13 @@ GEM ...@@ -107,10 +165,13 @@ GEM
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-get_version_name (0.2.2) fastlane-plugin-get_version_name (0.2.2)
fastlane-plugin-versioning_android (0.1.1) fastlane-plugin-versioning_android (0.1.1)
ffi (1.16.3)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.48.0) google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.1) google-apis-core (0.11.2)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
...@@ -123,26 +184,26 @@ GEM ...@@ -123,26 +184,26 @@ GEM
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0) google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0) google-apis-storage_v1 (0.29.0)
google-apis-core (>= 0.9.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.1)
google-cloud-env (~> 1.0) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (2.1.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.3.1) google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0) google-cloud-storage (1.45.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0) google-apis-storage_v1 (~> 0.29.0)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.7.0) googleauth (1.9.1)
faraday (>= 0.17.3, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
...@@ -150,21 +211,27 @@ GEM ...@@ -150,21 +211,27 @@ GEM
http-cookie (1.0.5) http-cookie (1.0.5)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.3) json (2.7.1)
jwt (2.7.1) jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.20.0)
molinillo (0.8.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
mutex_m (0.2.0)
nanaimo (0.3.0) nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1) naturally (2.2.1)
netrc (0.11.0)
optparse (0.1.1) optparse (0.1.1)
os (1.1.4) os (1.1.4)
plist (3.7.0) plist (3.7.1)
public_suffix (5.0.3) public_suffix (4.0.7)
rake (13.0.6) rake (13.1.0)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
...@@ -172,10 +239,11 @@ GEM ...@@ -172,10 +239,11 @@ GEM
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.6) rexml (3.2.6)
rouge (2.0.7) rouge (2.0.7)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
security (0.1.3) security (0.1.3)
signet (0.17.0) signet (0.18.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
...@@ -188,17 +256,18 @@ GEM ...@@ -188,17 +256,18 @@ GEM
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.1) tty-screen (0.8.2)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0) uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0) unicode-display_width (1.8.0)
webrick (1.8.1) webrick (1.8.1)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.22.0) xcodeproj (1.23.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
...@@ -214,9 +283,10 @@ PLATFORMS ...@@ -214,9 +283,10 @@ PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
fastlane cocoapods (= 1.14.3)
fastlane (= 2.214.0)
fastlane-plugin-get_version_name fastlane-plugin-get_version_name
fastlane-plugin-versioning_android fastlane-plugin-versioning_android
BUNDLED WITH BUNDLED WITH
1.17.2 2.4.10
...@@ -80,15 +80,10 @@ Install a version of `ruby`: ...@@ -80,15 +80,10 @@ Install a version of `ruby`:
Set this as your default version: Set this as your default version:
`rbenv global 3.2.2` `rbenv global 3.2.2`
#### CocoaPods Install cocoapods and fastlane using bundler (make sure to run in `mobile`)
`bundle install`
Install cocoapods: #### Add Xcode Command Line Tools
`gem install cocoapods -v 1.13.0`
If you hit ruby errors around `ActiveSupport.deprecator`, downgrade your `activesupport` package by running:
`gem uninstall activesupport && gem install activesupport -v 7.0.8`
### Add Xcode Command Line Tools
Open Xcode and go to: Open Xcode and go to:
...@@ -96,6 +91,36 @@ Open Xcode and go to: ...@@ -96,6 +91,36 @@ Open Xcode and go to:
And select the version that pops up. And select the version that pops up.
#### JDK (Android)
Taken from [RN instructions](https://reactnative.dev/docs/environment-setup?guide=native&platform=android)
```
brew tap homebrew/cask-versions
brew install --cask zulu17
# Get path to where cask was installed to double-click installer
brew info --cask zulu17
```
Add the following to your .rc file
`export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home`
#### Android Studio
Install [Android Studio](https://developer.android.com/studio)
Add the following to your .rc file
```
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
```
Android Studio should have an emulator already, but if not:
Open the project at `universe/apps/mobile/android`
Tools -> Device Manager to create a new emulator
## Development ## Development
Once all the setup steps above are completed, you're ready to try running the app locally! Once all the setup steps above are completed, you're ready to try running the app locally!
......
...@@ -125,17 +125,17 @@ android { ...@@ -125,17 +125,17 @@ android {
dev { dev {
isDefault(true) isDefault(true)
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionName "1.18" versionName "1.19"
dimension "variant" dimension "variant"
} }
beta { beta {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionName "1.18" versionName "1.19"
dimension "variant" dimension "variant"
} }
prod { prod {
dimension "variant" dimension "variant"
versionName "1.18" versionName "1.19"
} }
} }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.uniswap"> package="com.uniswap">
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
......
...@@ -44,6 +44,7 @@ import androidx.compose.ui.focus.FocusRequester ...@@ -44,6 +44,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
...@@ -153,7 +154,6 @@ private fun SeedPhrasePasteButton( ...@@ -153,7 +154,6 @@ private fun SeedPhrasePasteButton(
) { ) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val isDarkTheme = isSystemInDarkTheme()
Button( Button(
onClick = { onClick = {
...@@ -166,7 +166,7 @@ private fun SeedPhrasePasteButton( ...@@ -166,7 +166,7 @@ private fun SeedPhrasePasteButton(
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
contentColor = UniswapTheme.colors.neutral2, contentColor = UniswapTheme.colors.neutral2,
backgroundColor = if (isDarkTheme) UniswapTheme.colors.surface3 else UniswapTheme.colors.surface2 backgroundColor = UniswapTheme.colors.surface3.compositeOver(UniswapTheme.colors.surface2),
), ),
shape = UniswapTheme.shapes.buttonMedium, shape = UniswapTheme.shapes.buttonMedium,
contentPadding = PaddingValues( contentPadding = PaddingValues(
......
...@@ -5,9 +5,9 @@ import './wdyr' ...@@ -5,9 +5,9 @@ import './wdyr'
import { AppRegistry } from 'react-native' import { AppRegistry } from 'react-native'
import 'react-native-gesture-handler' import 'react-native-gesture-handler'
import 'react-native-reanimated' import 'react-native-reanimated'
import App from 'src/app/App'
import 'src/logbox' import 'src/logbox'
import 'src/polyfills' import 'src/polyfills'
import App from 'src/app/App'
import { name as appName } from './app.json' import { name as appName } from './app.json'
AppRegistry.registerComponent(appName, () => App) AppRegistry.registerComponent(appName, () => App)
...@@ -644,6 +644,10 @@ PODS: ...@@ -644,6 +644,10 @@ PODS:
- BoringSSL-GRPC/Implementation (0.0.24): - BoringSSL-GRPC/Implementation (0.0.24):
- BoringSSL-GRPC/Interface (= 0.0.24) - BoringSSL-GRPC/Interface (= 0.0.24)
- BoringSSL-GRPC/Interface (0.0.24) - BoringSSL-GRPC/Interface (0.0.24)
- Burnt (0.11.4):
- ExpoModulesCore
- SPAlert
- SPIndicator
- DoubleConversion (1.1.6) - DoubleConversion (1.1.6)
- EthersRS (0.0.5) - EthersRS (0.0.5)
- EXApplication (5.1.1): - EXApplication (5.1.1):
...@@ -706,11 +710,6 @@ PODS: ...@@ -706,11 +710,6 @@ PODS:
- Firebase/Firestore (10.15.0): - Firebase/Firestore (10.15.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseFirestore (~> 10.15.0) - FirebaseFirestore (~> 10.15.0)
- Firebase/RemoteConfig (10.15.0):
- Firebase/CoreOnly
- FirebaseRemoteConfig (~> 10.15.0)
- FirebaseABTesting (10.15.0):
- FirebaseCore (~> 10.0)
- FirebaseAppCheckInterop (10.15.0) - FirebaseAppCheckInterop (10.15.0)
- FirebaseAuth (10.15.0): - FirebaseAuth (10.15.0):
- FirebaseAppCheckInterop (~> 10.0) - FirebaseAppCheckInterop (~> 10.0)
...@@ -738,17 +737,6 @@ PODS: ...@@ -738,17 +737,6 @@ PODS:
- "gRPC-C++ (~> 1.50.1)" - "gRPC-C++ (~> 1.50.1)"
- leveldb-library (~> 1.22) - leveldb-library (~> 1.22)
- nanopb (< 2.30910.0, >= 2.30908.0) - nanopb (< 2.30910.0, >= 2.30908.0)
- FirebaseInstallations (10.15.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseRemoteConfig (10.15.0):
- FirebaseABTesting (~> 10.0)
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- fmt (6.2.1) - fmt (6.2.1)
- glog (0.3.5) - glog (0.3.5)
- GoogleUtilities (7.11.5): - GoogleUtilities (7.11.5):
...@@ -1139,11 +1127,6 @@ PODS: ...@@ -1139,11 +1127,6 @@ PODS:
- React - React
- react-native-context-menu-view (1.6.0): - react-native-context-menu-view (1.6.0):
- React - React
- react-native-flipper-performance-plugin (0.3.1):
- React-Core
- react-native-flipper-performance-plugin/FBDefines (= 0.3.1)
- react-native-flipper-performance-plugin/FBDefines (0.3.1):
- React-Core
- react-native-get-random-values (1.8.0): - react-native-get-random-values (1.8.0):
- React-Core - React-Core
- react-native-image-picker (7.0.1): - react-native-image-picker (7.0.1):
...@@ -1289,10 +1272,6 @@ PODS: ...@@ -1289,10 +1272,6 @@ PODS:
- nanopb (< 2.30910.0, >= 2.30908.0) - nanopb (< 2.30910.0, >= 2.30908.0)
- React-Core - React-Core
- RNFBApp - RNFBApp
- RNFBRemoteConfig (18.4.0):
- Firebase/RemoteConfig (= 10.15.0)
- React-Core
- RNFBApp
- RNFlashList (1.4.3): - RNFlashList (1.4.3):
- React-Core - React-Core
- RNGestureHandler (2.9.0): - RNGestureHandler (2.9.0):
...@@ -1349,6 +1328,8 @@ PODS: ...@@ -1349,6 +1328,8 @@ PODS:
- Sentry/HybridSDK (8.7.1): - Sentry/HybridSDK (8.7.1):
- SentryPrivate (= 8.7.1) - SentryPrivate (= 8.7.1)
- SentryPrivate (8.7.1) - SentryPrivate (8.7.1)
- SPAlert (4.2.0)
- SPIndicator (1.6.4)
- UIImageColors (2.1.0) - UIImageColors (2.1.0)
- Yoga (1.14.0) - Yoga (1.14.0)
- ZXingObjC/Core (3.6.5) - ZXingObjC/Core (3.6.5)
...@@ -1362,6 +1343,7 @@ DEPENDENCIES: ...@@ -1362,6 +1343,7 @@ DEPENDENCIES:
- Apollo (= 1.2.1) - Apollo (= 1.2.1)
- Argon2Swift (= 1.0.3) - Argon2Swift (= 1.0.3)
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
- Burnt (from `../../../node_modules/burnt/ios`)
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- "EthersRS (from `../../../node_modules/@uniswap/ethers-rs-mobile`)" - "EthersRS (from `../../../node_modules/@uniswap/ethers-rs-mobile`)"
- EXApplication (from `../../../node_modules/expo-application/ios`) - EXApplication (from `../../../node_modules/expo-application/ios`)
...@@ -1409,7 +1391,6 @@ DEPENDENCIES: ...@@ -1409,7 +1391,6 @@ DEPENDENCIES:
- React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
- react-native-appsflyer (from `../../../node_modules/react-native-appsflyer`) - react-native-appsflyer (from `../../../node_modules/react-native-appsflyer`)
- react-native-context-menu-view (from `../../../node_modules/react-native-context-menu-view`) - react-native-context-menu-view (from `../../../node_modules/react-native-context-menu-view`)
- react-native-flipper-performance-plugin (from `../../../node_modules/react-native-flipper-performance-plugin`)
- react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`) - react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`)
- react-native-image-picker (from `../../../node_modules/react-native-image-picker`) - react-native-image-picker (from `../../../node_modules/react-native-image-picker`)
- react-native-mmkv (from `../../../node_modules/react-native-mmkv`) - react-native-mmkv (from `../../../node_modules/react-native-mmkv`)
...@@ -1444,7 +1425,6 @@ DEPENDENCIES: ...@@ -1444,7 +1425,6 @@ DEPENDENCIES:
- "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)"
- "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)" - "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)"
- "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)" - "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)"
- "RNFBRemoteConfig (from `../../../node_modules/@react-native-firebase/remote-config`)"
- "RNFlashList (from `../../../node_modules/@shopify/flash-list`)" - "RNFlashList (from `../../../node_modules/@shopify/flash-list`)"
- RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`)
- RNImageColors (from `../../../node_modules/react-native-image-colors`) - RNImageColors (from `../../../node_modules/react-native-image-colors`)
...@@ -1465,14 +1445,11 @@ SPEC REPOS: ...@@ -1465,14 +1445,11 @@ SPEC REPOS:
- Argon2Swift - Argon2Swift
- BoringSSL-GRPC - BoringSSL-GRPC
- Firebase - Firebase
- FirebaseABTesting
- FirebaseAppCheckInterop - FirebaseAppCheckInterop
- FirebaseAuth - FirebaseAuth
- FirebaseCore - FirebaseCore
- FirebaseCoreInternal - FirebaseCoreInternal
- FirebaseFirestore - FirebaseFirestore
- FirebaseInstallations
- FirebaseRemoteConfig
- fmt - fmt
- GoogleUtilities - GoogleUtilities
- "gRPC-C++" - "gRPC-C++"
...@@ -1492,6 +1469,8 @@ SPEC REPOS: ...@@ -1492,6 +1469,8 @@ SPEC REPOS:
- SDWebImageWebPCoder - SDWebImageWebPCoder
- Sentry - Sentry
- SentryPrivate - SentryPrivate
- SPAlert
- SPIndicator
- UIImageColors - UIImageColors
- ZXingObjC - ZXingObjC
...@@ -1500,6 +1479,8 @@ EXTERNAL SOURCES: ...@@ -1500,6 +1479,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/@amplitude/analytics-react-native" :path: "../../../node_modules/@amplitude/analytics-react-native"
boost: boost:
:podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec"
Burnt:
:path: "../../../node_modules/burnt/ios"
DoubleConversion: DoubleConversion:
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EthersRS: EthersRS:
...@@ -1584,8 +1565,6 @@ EXTERNAL SOURCES: ...@@ -1584,8 +1565,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-appsflyer" :path: "../../../node_modules/react-native-appsflyer"
react-native-context-menu-view: react-native-context-menu-view:
:path: "../../../node_modules/react-native-context-menu-view" :path: "../../../node_modules/react-native-context-menu-view"
react-native-flipper-performance-plugin:
:path: "../../../node_modules/react-native-flipper-performance-plugin"
react-native-get-random-values: react-native-get-random-values:
:path: "../../../node_modules/react-native-get-random-values" :path: "../../../node_modules/react-native-get-random-values"
react-native-image-picker: react-native-image-picker:
...@@ -1654,8 +1633,6 @@ EXTERNAL SOURCES: ...@@ -1654,8 +1633,6 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/@react-native-firebase/auth" :path: "../../../node_modules/@react-native-firebase/auth"
RNFBFirestore: RNFBFirestore:
:path: "../../../node_modules/@react-native-firebase/firestore" :path: "../../../node_modules/@react-native-firebase/firestore"
RNFBRemoteConfig:
:path: "../../../node_modules/@react-native-firebase/remote-config"
RNFlashList: RNFlashList:
:path: "../../../node_modules/@shopify/flash-list" :path: "../../../node_modules/@shopify/flash-list"
RNGestureHandler: RNGestureHandler:
...@@ -1685,6 +1662,7 @@ SPEC CHECKSUMS: ...@@ -1685,6 +1662,7 @@ SPEC CHECKSUMS:
Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b
boost: 57d2868c099736d80fcd648bf211b4431e51a558 boost: 57d2868c099736d80fcd648bf211b4431e51a558
BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33 BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33
Burnt: 708556f6283e1b81767e6642e088819d85d1ea08
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135 EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135
EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903 EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903
...@@ -1708,14 +1686,11 @@ SPEC CHECKSUMS: ...@@ -1708,14 +1686,11 @@ SPEC CHECKSUMS:
FBLazyVector: 24e08bf294faea0abc0278abb2fcad7f3e446f6f FBLazyVector: 24e08bf294faea0abc0278abb2fcad7f3e446f6f
FBReactNativeSpec: cc06081bbc8420e1c0580008ff6d7af324f32f31 FBReactNativeSpec: cc06081bbc8420e1c0580008ff6d7af324f32f31
Firebase: 66043bd4579e5b73811f96829c694c7af8d67435 Firebase: 66043bd4579e5b73811f96829c694c7af8d67435
FirebaseABTesting: 7fa3bca17f79ac433301d20d5cd33401f7738dca
FirebaseAppCheckInterop: a8c555b1c2db1d9445e6c3a08a848167ddb7eb51 FirebaseAppCheckInterop: a8c555b1c2db1d9445e6c3a08a848167ddb7eb51
FirebaseAuth: a55ec5f7f8a5b1c2dd750235c1bb419bfb642445 FirebaseAuth: a55ec5f7f8a5b1c2dd750235c1bb419bfb642445
FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e
FirebaseCoreInternal: 2f4bee5ed00301b5e56da0849268797a2dd31fb4 FirebaseCoreInternal: 2f4bee5ed00301b5e56da0849268797a2dd31fb4
FirebaseFirestore: b4c0eaaf24efda5732ec21d9e6c788d083118ca6 FirebaseFirestore: b4c0eaaf24efda5732ec21d9e6c788d083118ca6
FirebaseInstallations: cae95cab0f965ce05b805189de1d4c70b11c76fb
FirebaseRemoteConfig: 64b6ada098c649304114a817effd7e5f87229b11
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084
...@@ -1749,7 +1724,6 @@ SPEC CHECKSUMS: ...@@ -1749,7 +1724,6 @@ SPEC CHECKSUMS:
React-logger: a3f6ca0d018749852a2a6f07c154bfc6fcd4195a React-logger: a3f6ca0d018749852a2a6f07c154bfc6fcd4195a
react-native-appsflyer: 153e96b97ecc0c26b14fc79911675e51cfd35b36 react-native-appsflyer: 153e96b97ecc0c26b14fc79911675e51cfd35b36
react-native-context-menu-view: d3b3e77985d5b05674a70f8e7eafe404dfa5bbcc react-native-context-menu-view: d3b3e77985d5b05674a70f8e7eafe404dfa5bbcc
react-native-flipper-performance-plugin: 2b873b68da3e368afeaf29c9c7a8c2b0ff908c4f
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
react-native-image-picker: 1569cfade34b3a011191ce262423e6ce2f8db5a1 react-native-image-picker: 1569cfade34b3a011191ce262423e6ce2f8db5a1
react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f
...@@ -1786,7 +1760,6 @@ SPEC CHECKSUMS: ...@@ -1786,7 +1760,6 @@ SPEC CHECKSUMS:
RNFBApp: a3026bdd951dd7a3a88e8e6518b53ddd2f8b3809 RNFBApp: a3026bdd951dd7a3a88e8e6518b53ddd2f8b3809
RNFBAuth: 553c6e66d3c70e086799104dad4a554c2663c337 RNFBAuth: 553c6e66d3c70e086799104dad4a554c2663c337
RNFBFirestore: a60e6005e071b31360a5bf651eb403b36c7db7de RNFBFirestore: a60e6005e071b31360a5bf651eb403b36c7db7de
RNFBRemoteConfig: 01f960b676e0afbe38d7f9035cfc265a6a3f71b2
RNFlashList: ade81b4e928ebd585dd492014d40fb8d0e848aab RNFlashList: ade81b4e928ebd585dd492014d40fb8d0e848aab
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1 RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1
...@@ -1800,10 +1773,12 @@ SPEC CHECKSUMS: ...@@ -1800,10 +1773,12 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
Sentry: 11776f6a25a128808d793d0d41bb7ad873b5ae4f Sentry: 11776f6a25a128808d793d0d41bb7ad873b5ae4f
SentryPrivate: b3c448eacdabe9eab7679a2e0af609c608f91572 SentryPrivate: b3c448eacdabe9eab7679a2e0af609c608f91572
SPAlert: 735da1f16a887e294719217572ce1f936d8c8782
SPIndicator: 93e0a4fb23de51294ac48e874c0f081a5e293e4f
UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe
Yoga: 135109c9b8c5d1a8af3a58d21cd4c7aa7f3bf555 Yoga: 135109c9b8c5d1a8af3a58d21cd4c7aa7f3bf555
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 632909767e5e0022317f148f858c5b6f587e5900 PODFILE CHECKSUM: 632909767e5e0022317f148f858c5b6f587e5900
COCOAPODS: 1.13.0 COCOAPODS: 1.14.3
...@@ -44,7 +44,7 @@ class SeedPhraseInputViewModel: ObservableObject { ...@@ -44,7 +44,7 @@ class SeedPhraseInputViewModel: ObservableObject {
inputPlaceholder: rawRNStrings["inputPlaceholder"] ?? "", inputPlaceholder: rawRNStrings["inputPlaceholder"] ?? "",
pasteButton: rawRNStrings["pasteButton"] ?? "", pasteButton: rawRNStrings["pasteButton"] ?? "",
errorInvalidWord: rawRNStrings["errorInvalidWord"] ?? "", errorInvalidWord: rawRNStrings["errorInvalidWord"] ?? "",
errorPhraseLength: rawRNStrings["errorPhaseLength"] ?? "", errorPhraseLength: rawRNStrings["errorPhraseLength"] ?? "",
errorWrongPhrase: rawRNStrings["errorWrongPhrase"] ?? "", errorWrongPhrase: rawRNStrings["errorWrongPhrase"] ?? "",
errorInvalidPhrase: rawRNStrings["errorInvalidPhrase"] ?? "" errorInvalidPhrase: rawRNStrings["errorInvalidPhrase"] ?? ""
) )
......
...@@ -25,8 +25,6 @@ jest.mock('@sentry/react-native', () => ({ ...@@ -25,8 +25,6 @@ jest.mock('@sentry/react-native', () => ({
ReactNativeTracing: jest.fn(), ReactNativeTracing: jest.fn(),
})) }))
require('react-native-reanimated/src/reanimated2/jestUtils').setUpTests()
// Disables animated driver warning // Disables animated driver warning
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper') jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
...@@ -40,7 +38,7 @@ jest.mock('react-native-onesignal', () => { ...@@ -40,7 +38,7 @@ jest.mock('react-native-onesignal', () => {
promptForPushNotificationsWithUserResponse: jest.fn(), promptForPushNotificationsWithUserResponse: jest.fn(),
setNotificationWillShowInForegroundHandler: jest.fn(), setNotificationWillShowInForegroundHandler: jest.fn(),
setNotificationOpenedHandler: jest.fn(), setNotificationOpenedHandler: jest.fn(),
getDeviceState: () => ({ userId: 'dummyUserId' }), getDeviceState: () => ({ userId: 'dummyUserId', pushToken: 'dummyPushToken' }),
} }
}) })
...@@ -75,21 +73,47 @@ jest.mock('react-native', () => { ...@@ -75,21 +73,47 @@ jest.mock('react-native', () => {
jest.mock('react-native-safe-area-context', () => ({ jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn().mockImplementation(() => ({})), useSafeAreaInsets: jest.fn().mockImplementation(() => ({})),
SafeAreaProvider: jest.fn(({ children }) => children),
})) }))
jest.mock('@react-navigation/elements', () => ({ jest.mock('@react-navigation/elements', () => ({
useHeaderHeight: jest.fn().mockImplementation(() => 200), useHeaderHeight: jest.fn().mockImplementation(() => 200),
})) }))
global.__reanimatedWorkletInit = () => ({}) require('react-native-reanimated').setUpTests()
jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'))
jest.mock('wallet/src/features/language/LocalizationContext', () => MockLocalizationContext) jest.mock('wallet/src/features/language/LocalizationContext', () => MockLocalizationContext)
jest.mock('react-native/Libraries/Share/Share', () => { jest.mock('react-native/Libraries/Share/Share', () => ({
return { share: jest.fn(),
share: jest.fn(), }))
}
})
jest.mock('react-native-localize', () => mockRNLocalize) jest.mock('react-native-localize', () => mockRNLocalize)
jest.mock('@react-native-firebase/auth', () => () => ({
signInAnonymously: jest.fn(),
}))
jest.mock('react-native/Libraries/Linking/Linking', () => ({
openURL: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
canOpenURL: jest.fn(),
getInitialURL: jest.fn(),
}))
jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate hook can use it without a warning being shown
useTranslation: () => {
return {
t: (str) => str,
i18n: {
changeLanguage: () => new Promise(jest.fn()),
},
}
},
initReactI18next: {
type: '3rdParty',
init: jest.fn(),
},
}))
const presets = require('../../config/jest-presets/jest/jest-preset') const preset = require('../../config/jest-presets/jest/jest-preset')
module.exports = { module.exports = {
...presets, ...preset,
preset: 'jest-expo', preset: 'jest-expo',
displayName: 'Mobile Wallet', displayName: 'Mobile Wallet',
collectCoverageFrom: [ collectCoverageFrom: [
...@@ -22,7 +22,7 @@ module.exports = { ...@@ -22,7 +22,7 @@ module.exports = {
], ],
// we map core/web to tamagui's test bundle, this just makes setup simpler for jest // we map core/web to tamagui's test bundle, this just makes setup simpler for jest
moduleNameMapper: { moduleNameMapper: {
...presets.moduleNameMapper, ...preset.moduleNameMapper,
'@tamagui/core': '@tamagui/core/native-test', '@tamagui/core': '@tamagui/core/native-test',
'@tamagui/web': '@tamagui/core/native-test', '@tamagui/web': '@tamagui/core/native-test',
}, },
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools') const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools')
const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix() const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix()
const path = require('path') const path = require('path')
const { getDefaultConfig } = require('metro-config') const { getDefaultConfig } = require('metro-config')
......
...@@ -10,12 +10,11 @@ ...@@ -10,12 +10,11 @@
"android:beta:release": "react-native run-android --variant=betaRelease --appIdSuffix=beta", "android:beta:release": "react-native run-android --variant=betaRelease --appIdSuffix=beta",
"android:prod": "react-native run-android --variant=prodDebug", "android:prod": "react-native run-android --variant=prodDebug",
"android:prod:release": "react-native run-android --variant=prodRelease", "android:prod:release": "react-native run-android --variant=prodRelease",
"check:deps:usage": "./scripts/checkDepsUsage.sh",
"clean": "react-native-clean-project", "clean": "react-native-clean-project",
"debug": "react-devtools", "debug": "react-devtools",
"deduplicate": "yarn-deduplicate --strategy=fewer", "deduplicate": "yarn-deduplicate --strategy=fewer",
"deploy:ios:local:dev": "yarn prepare-for-pr && fastlane ios buildAndShip --env dev", "depcheck": "depcheck",
"deploy:ios:local:beta": "yarn prepare-for-pr && fastlane ios buildAndShip --env beta",
"deploy:ios:local:prod": "yarn prepare-for-pr && fastlane ios buildAndShip --env prod",
"env:android:keystore:download": "bash ./scripts/downloadAndroidKeystore.sh", "env:android:keystore:download": "bash ./scripts/downloadAndroidKeystore.sh",
"env:fastlane:download": "bash ./scripts/downloadFastlaneEnv.sh", "env:fastlane:download": "bash ./scripts/downloadFastlaneEnv.sh",
"env:fastlane:upload": "bash ./scripts/uploadFastlaneEnv.sh", "env:fastlane:upload": "bash ./scripts/uploadFastlaneEnv.sh",
...@@ -26,7 +25,7 @@ ...@@ -26,7 +25,7 @@
"link:assets": "react-native-asset", "link:assets": "react-native-asset",
"graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate",
"hardhat": "hardhat node", "hardhat": "hardhat node",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 5", "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 6",
"ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios", "ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios",
"ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift", "ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift",
"ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"", "ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"",
...@@ -34,16 +33,11 @@ ...@@ -34,16 +33,11 @@
"ios:beta": "react-native run-ios --configuration Beta", "ios:beta": "react-native run-ios --configuration Beta",
"ios:bundle": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios' --assets-dest='./ios'", "ios:bundle": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios' --assets-dest='./ios'",
"ios:release": "react-native run-ios --configuration Release", "ios:release": "react-native run-ios --configuration Release",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lmu": "yarn prepare-for-pr",
"prepare-for-pr": "yarn && yarn pod && yarn-deduplicate && yarn lint --fix && yarn typecheck && yarn test -u && yarn apollo:generate && yarn i18n:extract",
"start": "NODE_ENV=development react-native start", "start": "NODE_ENV=development react-native start",
"start:production": "NODE_ENV=production react-native start --reset-cache", "start:production": "NODE_ENV=production react-native start --reset-cache",
"storybook": "storybook dev -p 6006", "test": "jest",
"storybook:build": "storybook build",
"storybook:chromatic": "chromatic --build-script-name=storybook:build",
"test": "jest --forceExit",
"snapshots": "jest -u", "snapshots": "jest -u",
"test:e2e:build": "RN_SRC_EXT=e2e.js detox build -c ios.sim.debug", "test:e2e:build": "RN_SRC_EXT=e2e.js detox build -c ios.sim.debug",
"test:e2e:packager": "RN_SRC_EXT=e2e.js,e2e.ts yarn start:production --no-interactive", "test:e2e:packager": "RN_SRC_EXT=e2e.js,e2e.ts yarn start:production --no-interactive",
...@@ -53,12 +47,8 @@ ...@@ -53,12 +47,8 @@
"pod": "./scripts/podinstall.sh" "pod": "./scripts/podinstall.sh"
}, },
"dependencies": { "dependencies": {
"@amplitude/analytics-node": "1.0.0",
"@amplitude/analytics-react-native": "1.4.0", "@amplitude/analytics-react-native": "1.4.0",
"@amplitude/analytics-types": "0.13.0",
"@analytics/core": "0.11.1",
"@apollo/client": "3.7.11", "@apollo/client": "3.7.11",
"@ethersproject/hash": "5.6.1",
"@ethersproject/shims": "5.6.0", "@ethersproject/shims": "5.6.0",
"@formatjs/intl-datetimeformat": "4.5.1", "@formatjs/intl-datetimeformat": "4.5.1",
"@formatjs/intl-getcanonicallocales": "1.9.0", "@formatjs/intl-getcanonicallocales": "1.9.0",
...@@ -72,39 +62,34 @@ ...@@ -72,39 +62,34 @@
"@react-native-firebase/app": "18.4.0", "@react-native-firebase/app": "18.4.0",
"@react-native-firebase/auth": "18.4.0", "@react-native-firebase/auth": "18.4.0",
"@react-native-firebase/firestore": "18.4.0", "@react-native-firebase/firestore": "18.4.0",
"@react-native-firebase/remote-config": "18.4.0",
"@react-native-masked-view/masked-view": "0.2.9", "@react-native-masked-view/masked-view": "0.2.9",
"@react-navigation/bottom-tabs": "6.3.2",
"@react-navigation/core": "6.2.2", "@react-navigation/core": "6.2.2",
"@react-navigation/drawer": "6.3.1",
"@react-navigation/native": "6.0.11", "@react-navigation/native": "6.0.11",
"@react-navigation/native-stack": "6.7.0", "@react-navigation/native-stack": "6.7.0",
"@react-navigation/stack": "6.2.2", "@react-navigation/stack": "6.2.2",
"@reduxjs/toolkit": "1.9.3", "@reduxjs/toolkit": "1.9.3",
"@sentry/react": "7.80.0",
"@sentry/react-native": "5.5.0", "@sentry/react-native": "5.5.0",
"@shopify/flash-list": "1.4.3", "@shopify/flash-list": "1.4.3",
"@shopify/react-native-performance": "4.1.2", "@shopify/react-native-performance": "4.1.2",
"@shopify/react-native-performance-navigation": "3.0.0", "@shopify/react-native-performance-navigation": "3.0.0",
"@shopify/react-native-skia": "0.1.187", "@shopify/react-native-skia": "0.1.187",
"@uniswap/analytics": "1.5.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.28.0", "@uniswap/analytics-events": "2.29.0",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/permit2-sdk": "1.2.0", "@uniswap/permit2-sdk": "1.2.0",
"@uniswap/router-sdk": "1.7.1", "@uniswap/router-sdk": "1.7.1",
"@uniswap/sdk-core": "4.0.7", "@uniswap/sdk-core": "4.0.7",
"@uniswap/universal-router-sdk": "1.5.8", "@uniswap/universal-router-sdk": "1.5.8",
"@uniswap/v2-sdk": "3.2.3",
"@uniswap/v3-sdk": "3.10.0", "@uniswap/v3-sdk": "3.10.0",
"@walletconnect/core": "2.10.1",
"@walletconnect/react-native-compat": "2.10.1", "@walletconnect/react-native-compat": "2.10.1",
"@walletconnect/utils": "2.10.1", "@walletconnect/utils": "2.10.1",
"@walletconnect/web3wallet": "1.9.1", "@walletconnect/web3wallet": "1.9.1",
"apollo3-cache-persist": "0.14.1", "apollo3-cache-persist": "0.14.1",
"async-mutex": "0.3.2",
"babel-plugin-transform-inline-environment-variables": "0.4.4", "babel-plugin-transform-inline-environment-variables": "0.4.4",
"babel-plugin-transform-remove-console": "6.9.4", "babel-plugin-transform-remove-console": "6.9.4",
"cross-fetch": "3.1.5", "cross-fetch": "3.1.5",
"d3-scale": "4.0.2",
"d3-shape": "3.0.1",
"dayjs": "1.11.7", "dayjs": "1.11.7",
"ethers": "5.7.2", "ethers": "5.7.2",
"expo": "48.0.19", "expo": "48.0.19",
...@@ -121,8 +106,9 @@ ...@@ -121,8 +106,9 @@
"expo-screen-capture": "4.2.0", "expo-screen-capture": "4.2.0",
"expo-store-review": "~6.2.1", "expo-store-review": "~6.2.1",
"expo-web-browser": "12.0.0", "expo-web-browser": "12.0.0",
"formik": "2.2.9",
"fuse.js": "6.5.3", "fuse.js": "6.5.3",
"jsbi": "3.2.5",
"lodash": "4.17.21",
"no-yolo-signatures": "0.0.2", "no-yolo-signatures": "0.0.2",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"react": "18.2.0", "react": "18.2.0",
...@@ -155,68 +141,47 @@ ...@@ -155,68 +141,47 @@
"react-native-webview": "11.23.1", "react-native-webview": "11.23.1",
"react-native-widgetkit": "1.0.9", "react-native-widgetkit": "1.0.9",
"react-redux": "8.0.5", "react-redux": "8.0.5",
"redux": "4.2.1",
"redux-mock-store": "1.5.4",
"redux-persist": "6.0.0", "redux-persist": "6.0.0",
"redux-saga": "1.2.2", "redux-saga": "1.2.2",
"rive-react-native": "6.1.1", "rive-react-native": "6.1.1",
"statsig-react-native": "4.11.0", "statsig-react-native": "4.11.0",
"typed-redux-saga": "1.5.0", "typed-redux-saga": "1.5.0",
"utilities": "workspace:^", "utilities": "workspace:^",
"uuid": "9.0.0", "wallet": "workspace:^"
"wallet": "workspace:^",
"wcag-contrast": "3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.12.9", "@babel/core": "7.12.9",
"@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-logical-assignment-operators": "7.16.7", "@babel/plugin-proposal-logical-assignment-operators": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-numeric-separator": "7.16.7", "@babel/plugin-proposal-numeric-separator": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/runtime": "7.18.9", "@babel/runtime": "7.18.9",
"@faker-js/faker": "7.6.0", "@faker-js/faker": "7.6.0",
"@firebase/app-types": "0.8.1",
"@firebase/functions-types": "0.5.1",
"@react-native-community/eslint-config": "3.0.3",
"@rtk-query/codegen-openapi": "1.0.0-alpha.1",
"@storybook/addon-actions": "7.0.2",
"@storybook/addon-essentials": "7.0.2",
"@storybook/addon-links": "7.0.2",
"@storybook/addon-mdx-gfm": "7.0.2",
"@storybook/addon-react-native-web": "0.0.20",
"@storybook/react": "7.0.2", "@storybook/react": "7.0.2",
"@storybook/react-webpack5": "7.0.2",
"@testing-library/react-hooks": "7.0.2", "@testing-library/react-hooks": "7.0.2",
"@testing-library/react-native": "11.5.0", "@testing-library/react-native": "11.5.0",
"@types/cors": "2.8.13",
"@types/d3-scale": "4.0.1",
"@types/d3-shape": "3.0.2",
"@types/jest": "29.5.0",
"@types/react-native": "0.71.3", "@types/react-native": "0.71.3",
"@types/uuid": "9.0.1", "@types/redux-mock-store": "1.0.6",
"@types/wcag-contrast": "3.0.0", "@walletconnect/types": "2.8.6",
"@welldone-software/why-did-you-render": "7.0.1", "@welldone-software/why-did-you-render": "7.0.1",
"babel-jest": "29.6.1", "babel-jest": "29.6.1",
"babel-loader": "8.2.3", "babel-loader": "8.2.3",
"babel-plugin-react-native-web": "0.17.5", "babel-plugin-react-native-web": "0.17.5",
"babel-plugin-react-require": "4.0.0", "babel-plugin-react-require": "4.0.0",
"chromatic": "6.3.4", "core-js": "2.6.12",
"cross-env": "7.0.3",
"detox": "19.7.0", "detox": "19.7.0",
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-plugin-detox": "1.0.0",
"eslint-plugin-no-unsanitized": "4.0.1",
"eslint-plugin-security": "1.5.0",
"eslint-plugin-spellcheck": "0.0.20",
"firebase-functions": "4.1.0",
"hardhat": "2.14.0", "hardhat": "2.14.0",
"jest": "29.6.4", "jest": "29.6.4",
"jest-expo": "49.0.0", "jest-expo": "49.0.0",
"jest-transform-stub": "2.0.0", "jest-extended": "4.0.1",
"jest-transformer-svg": "2.0.0", "jest-transformer-svg": "2.0.0",
"madge": "6.1.0", "madge": "6.1.0",
"metro-react-native-babel-preset": "0.72.4",
"mockdate": "3.0.5", "mockdate": "3.0.5",
"postinstall-postinstall": "2.1.0", "postinstall-postinstall": "2.1.0",
"prettier-plugin-organize-imports": "3.1.1",
"react-apollo": "3.1.5",
"react-devtools": "4.28.0", "react-devtools": "4.28.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-native-apollo-devtools-client": "1.0.4", "react-native-apollo-devtools-client": "1.0.4",
...@@ -224,18 +189,12 @@ ...@@ -224,18 +189,12 @@
"react-native-clean-project": "4.0.1", "react-native-clean-project": "4.0.1",
"react-native-dotenv": "3.2.0", "react-native-dotenv": "3.2.0",
"react-native-flipper": "0.187.1", "react-native-flipper": "0.187.1",
"react-native-flipper-performance-plugin": "0.3.1",
"react-native-mmkv-flipper-plugin": "1.0.0", "react-native-mmkv-flipper-plugin": "1.0.0",
"react-native-monorepo-tools": "1.2.1", "react-native-monorepo-tools": "1.2.1",
"react-native-svg-transformer": "1.0.0", "react-native-svg-transformer": "1.0.0",
"react-native-web": "0.18.12",
"react-svg-loader": "3.0.3",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"redux-flipper": "2.0.2", "redux-flipper": "2.0.2",
"redux-saga-test-plan": "4.0.4", "redux-saga-test-plan": "4.0.4",
"storybook": "7.0.2",
"storybook-addon-apollo-client": "4.1.4",
"storybook-dark-mode": "3.0.0",
"typescript": "4.9.4", "typescript": "4.9.4",
"yarn-deduplicate": "6.0.0" "yarn-deduplicate": "6.0.0"
}, },
......
#!/bin/bash
mv src/package.json src/ignore.json
yarn run depcheck
result_status=$?
mv src/ignore.json src/package.json
exit $result_status
import os
ENV_DEFAULTS_FILE = '../../.env.defaults'
ENV_DEFAULTS_LOCAL_FILE = '../../.env.defaults.local'
SWIFT_FILE_PATH = 'ios/WidgetsCore/Env.swift'
SWIFT_ENV_VARIABLES = ['UNISWAP_API_BASE_URL','UNISWAP_API_KEY']
def to_swift_constant_line(key, value):
return f' static let {key.upper()} = "{value}"'
def process_lines(lines, search_vars):
env_var_declarations = []
for line in lines:
line = line.strip()
if line and not line.startswith('#'):
# Split variable name and value
key, value = line.split('=', 1)
if key in search_vars:
env_var_declarations.append(to_swift_constant_line(key.upper(), value))
search_vars.remove(key)
return env_var_declarations
# convert env variables to swift constants and writes to a swift file.
def copy_env_vars_to_swift(env_defaults_file, env_defaults_local_file, swift_file, env_variables):
envs_left_to_find = env_variables.copy()
env_var_declarations = []
# Search for env vars in the system first
for key in env_variables:
if key in os.environ:
env_var_declarations.append(to_swift_constant_line(key.upper(), os.environ[key]))
envs_left_to_find.remove(key)
# read from local env file if it exists
if os.path.isfile(env_defaults_local_file):
with open(env_defaults_local_file, 'r') as f:
env_lines = f.readlines()
env_var_declarations.extend(process_lines(env_lines, envs_left_to_find))
# read from checked in env file for non-secret variables
with open(env_defaults_file, 'r') as f:
default_env_lines = f.readlines()
env_var_declarations.extend(process_lines(default_env_lines, envs_left_to_find))
# write to swift file
with open(swift_file, 'w') as f:
f.write('struct Env {\n')
f.write('\n'.join(env_var_declarations))
f.write('\n}')
# If not all env variables are set
if len(env_variables) != len(env_var_declarations):
print('WARNING: Not all environment variables were converted to Swift.')
exit(1)
copy_env_vars_to_swift(ENV_DEFAULTS_FILE, ENV_DEFAULTS_LOCAL_FILE, SWIFT_FILE_PATH, SWIFT_ENV_VARIABLES)
#!/bin/bash
cd ios/ && bundle install && bundle exec pod install && cd ..
import os
from os.path import isfile, join
def find_closing_quote_index(s, starting_index=0):
for i in range(starting_index, len(s)):
if s[i] == '"':
return i
return None
def grab_svg_attribute_prop(prop_name, line):
# if prop_name = fill-rule, then this function
# will return the string between the quotes:
# <path fill-rule="<whatever>" --> returns <whatever>
i = line.index(f"{prop_name}=")
idx_after_quote = i + len(prop_name) + 2
j = find_closing_quote_index(line, idx_after_quote)
if not j:
return None
return line[idx_after_quote:j]
folder_location = "../src/assets/unicons/"
folders = ["Container", "Emblem"]
unicons_location = '../src/components/unicons'
def generate_arrays_from_svgs():
print("Generating ShapeSvg Arrays")
for folder in folders:
folder_path = join(folder_location, folder)
print("Looking in", folder_path)
print("Found", len(os.listdir(folder_path)), "files")
result = "import { PathProps } from 'src/components/unicons/types'\nexport const svgPaths: PathProps[][] = ["
count = 0
for filename in os.listdir(folder_path):
if not isfile(join(folder_path, filename)) or filename[-4:] != ".svg":
continue
f = open(join(folder_path, filename), "r")
cur_svgs = []
for line in f:
if "svg" in line:
continue
if "<path" in line:
cur_svg = "\n\t\t{"
# add line to result but make it into jsx
# adding the path attribute to pathProps
path = grab_svg_attribute_prop("d", line)
if path:
cur_svg += f"\n\t\t\tpath: '{path}',"
# add fillType attribute if it exists
if "fill-rule" in line:
# hard coding this for now, non hardcoded version below
cur_svg += f"\n\t\t\tfillType: 'evenOdd'"
cur_svg += "\n\t\t},"
cur_svgs.append(cur_svg)
result += "\n\t["
for svg in cur_svgs:
result += svg
result += "\n\t],"
count += 1
result += "\n]"
f_ts = open(join(unicons_location, folder+".ts"), "w")
f_ts.write(result)
f_ts.close()
def delete_individual_ts_files():
answer = input(
f"Are you sure you want to delete all .ts and .tsx files from the following folders: {folders}? (Yes / No)\n")
if answer != "Yes":
print("Aborted")
return
print("Deleting individual .ts and .tsx files")
for folder in folders:
folder_path = join(folder_location, folder)
count = 0
print("Looking in", folder_path)
print("Found", len(os.listdir(folder_path)), "files")
for filename in os.listdir(folder_path):
if not isfile(join(folder_path, filename)) or (filename[-3:] != ".ts" and filename[-4:] != ".tsx"):
continue
os.remove(join(folder_path, filename))
count += 1
print(f"Deleted {count} files from {folder_path}")
def generate_individual_ts_files():
print("Generating individual .ts files")
for folder in folders:
folder_path = join(folder_location, folder)
print("Looking in", folder_path)
print("Found", len(os.listdir(folder_path)), "files")
for filename in os.listdir(folder_path):
if not isfile(join(folder_path, filename)) or filename[-4:] != ".svg":
continue
f = open(join(folder_path, filename), "r")
result = "export const pathProps = {"
for line in f:
if "svg" in line:
continue
if "<path" in line:
# add line to result but make it into jsx
# adding the path attribute to pathProps
path = grab_svg_attribute_prop("d", line)
if path:
result += f"\n\tpath: '{path}',"
# add fillType attribute if it exists
if "fill-rule" in line:
# hard coding this for now, non hardcoded version below
result += f"\n\tfillType: 'evenOdd'"
# i = line.index("fill-rule") + 10
# if j:
# j = find_closing_quote_index(line, i+1)
# result += f"\n\tfillType: '{line[i+1:j]}'"
# write the result to new file
result += "\n}\n"
f_tsx = open(join(folder_path, filename[:-4]+".ts"), "w")
f_tsx.write(result)
f_tsx.close()
print("Done generating individual ts files")
if __name__ == "__main__":
generate_arrays_from_svgs()
import React from 'react' import React from 'react'
import 'react-native' import 'react-native'
import mockRNLocalize from 'react-native-localize/mock' import mockRNLocalize from 'react-native-localize/mock'
import { act } from 'react-test-renderer'
import App from 'src/app/App' import App from 'src/app/App'
import { render } from 'src/test/test-utils' import { render } from 'src/test/test-utils'
jest.mock('src/data/usePersistedApolloClient', () => {
return {
usePersistedApolloClient: (): undefined => undefined,
}
})
jest.mock('react-native-localize', () => mockRNLocalize) jest.mock('react-native-localize', () => mockRNLocalize)
it('renders correctly', () => { it('renders correctly', async () => {
render(<App />) render(<App />)
await act(async () => {
// Wait for component cleanup
})
}) })
...@@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react-native' ...@@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react-native'
import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance' import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance'
import { default as React, PropsWithChildren, StrictMode, useCallback, useEffect } from 'react' import { default as React, PropsWithChildren, StrictMode, useCallback, useEffect } from 'react'
import { NativeModules, StatusBar } from 'react-native' import { NativeModules, StatusBar } from 'react-native'
import appsFlyer from 'react-native-appsflyer'
import { getUniqueId } from 'react-native-device-info' import { getUniqueId } from 'react-native-device-info'
import { GestureHandlerRootView } from 'react-native-gesture-handler' import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { SafeAreaProvider } from 'react-native-safe-area-context' import { SafeAreaProvider } from 'react-native-safe-area-context'
...@@ -28,6 +29,7 @@ import { initOneSignal } from 'src/features/notifications/Onesignal' ...@@ -28,6 +29,7 @@ import { initOneSignal } from 'src/features/notifications/Onesignal'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants' import { MobileEventName } from 'src/features/telemetry/constants'
import { shouldLogScreen } from 'src/features/telemetry/directLogScreens' import { shouldLogScreen } from 'src/features/telemetry/directLogScreens'
import { selectAllowAnalytics } from 'src/features/telemetry/selectors'
import { TransactionHistoryUpdater } from 'src/features/transactions/TransactionHistoryUpdater' import { TransactionHistoryUpdater } from 'src/features/transactions/TransactionHistoryUpdater'
import { import {
processWidgetEvents, processWidgetEvents,
...@@ -40,6 +42,7 @@ import { getSentryEnvironment, getStatsigEnvironmentTier } from 'src/utils/versi ...@@ -40,6 +42,7 @@ import { getSentryEnvironment, getStatsigEnvironmentTier } from 'src/utils/versi
import { Statsig, StatsigProvider } from 'statsig-react-native' import { Statsig, StatsigProvider } from 'statsig-react-native'
import { flexStyles } from 'ui/src' import { flexStyles } from 'ui/src'
import { registerConsoleOverrides } from 'utilities/src/logger/console' import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks' import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext' import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
import { config } from 'wallet/src/config' import { config } from 'wallet/src/config'
...@@ -211,9 +214,29 @@ function AppOuter(): JSX.Element | null { ...@@ -211,9 +214,29 @@ function AppOuter(): JSX.Element | null {
} }
function AppInner(): JSX.Element { function AppInner(): JSX.Element {
const dispatch = useAppDispatch()
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const themeSetting = useCurrentAppearanceSetting() const themeSetting = useCurrentAppearanceSetting()
const dispatch = useAppDispatch() const allowAnalytics = useAppSelector(selectAllowAnalytics)
useEffect(() => {
if (allowAnalytics) {
appsFlyer.startSdk()
logger.info('AppsFlyer', 'status', 'started')
} else {
appsFlyer.stop(!allowAnalytics, (res: unknown) => {
if (typeof res === 'string' && res === 'Success') {
logger.info('AppsFlyer', 'status', 'stopped')
} else {
logger.warn(
'AppsFlyer',
'stop',
`Got an error when trying to stop the AppsFlyer SDK: ${res}`
)
}
})
}
}, [allowAnalytics])
useEffect(() => { useEffect(() => {
dispatch(updateLanguage(null)) dispatch(updateLanguage(null))
......
...@@ -18,7 +18,7 @@ export function* appSelect<T>(fn: (state: MobileState) => T): SagaGenerator<T> { ...@@ -18,7 +18,7 @@ export function* appSelect<T>(fn: (state: MobileState) => T): SagaGenerator<T> {
return state return state
} }
const MIN_INPUT_DECIMAL_PAD_GAP = spacing.spacing12 const MIN_INPUT_DECIMAL_PAD_GAP = spacing.spacing8
export function useShouldShowNativeKeyboard(): { export function useShouldShowNativeKeyboard(): {
onInputPanelLayout: (event: LayoutChangeEvent) => void onInputPanelLayout: (event: LayoutChangeEvent) => void
...@@ -73,7 +73,9 @@ export function useDynamicFontSizing( ...@@ -73,7 +73,9 @@ export function useDynamicFontSizing(
const textInputElementWidthRef = useRef(0) const textInputElementWidthRef = useRef(0)
const onLayout = useCallback((event: LayoutChangeEvent) => { const onLayout = useCallback((event: LayoutChangeEvent) => {
if (textInputElementWidthRef.current) return if (textInputElementWidthRef.current) {
return
}
const width = event.nativeEvent.layout.width const width = event.nativeEvent.layout.width
textInputElementWidthRef.current = width textInputElementWidthRef.current = width
......
...@@ -54,6 +54,8 @@ import { ...@@ -54,6 +54,8 @@ import {
v52Schema, v52Schema,
v53Schema, v53Schema,
v54Schema, v54Schema,
v55Schema,
v56Schema,
v5Schema, v5Schema,
v6Schema, v6Schema,
v7Schema, v7Schema,
...@@ -99,9 +101,13 @@ import { account, fiatOnRampTxDetailsFailed, txDetailsConfirmed } from 'wallet/s ...@@ -99,9 +101,13 @@ import { account, fiatOnRampTxDetailsFailed, txDetailsConfirmed } from 'wallet/s
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const getAllKeysOfNestedObject = (obj: any, prefix = ''): string[] => { const getAllKeysOfNestedObject = (obj: any, prefix = ''): string[] => {
const keys = Object.keys(obj) const keys = Object.keys(obj)
if (!keys.length && prefix !== '') return [prefix.slice(0, -1)] if (!keys.length && prefix !== '') {
return [prefix.slice(0, -1)]
}
return keys.reduce<string[]>((res, el) => { return keys.reduce<string[]>((res, el) => {
if (Array.isArray(obj[el])) return [...res] if (Array.isArray(obj[el])) {
return [...res]
}
if (typeof obj[el] === 'object' && obj[el] !== null) { if (typeof obj[el] === 'object' && obj[el] !== null) {
return [...res, ...getAllKeysOfNestedObject(obj[el], prefix + el + '.')] return [...res, ...getAllKeysOfNestedObject(obj[el], prefix + el + '.')]
...@@ -176,8 +182,12 @@ describe('Redux state migrations', () => { ...@@ -176,8 +182,12 @@ describe('Redux state migrations', () => {
const initialStateKeys = new Set(getAllKeysOfNestedObject(initialState)) const initialStateKeys = new Set(getAllKeysOfNestedObject(initialState))
for (const key of initialStateKeys) { for (const key of initialStateKeys) {
if (latestSchemaKeys.has(key)) latestSchemaKeys.delete(key) if (latestSchemaKeys.has(key)) {
if (migratedSchemaKeys.has(key)) migratedSchemaKeys.delete(key) latestSchemaKeys.delete(key)
}
if (migratedSchemaKeys.has(key)) {
migratedSchemaKeys.delete(key)
}
initialStateKeys.delete(key) initialStateKeys.delete(key)
} }
...@@ -1250,4 +1260,35 @@ describe('Redux state migrations', () => { ...@@ -1250,4 +1260,35 @@ describe('Redux state migrations', () => {
expect(v55.behaviorHistory.hasViewedReviewScreen).toBe(false) expect(v55.behaviorHistory.hasViewedReviewScreen).toBe(false)
}) })
it('migrates from v55 to 56', () => {
const v55Stub = { ...v55Schema }
const v56 = migrations[56](v55Stub)
expect(v56.telemetry.allowAnalytics).toBe(true)
expect(v56.telemetry.lastHeartbeat).toBe(0)
})
it('migrates from v56 to 57', () => {
const v56Stub = {
...v56Schema,
wallet: {
...v56Schema.wallet,
accounts: [
{
type: AccountType.Readonly,
address: '0x',
name: 'Test Account 1',
pending: false,
hideSpamTokens: true,
},
],
},
}
const v57 = migrations[57](v56Stub)
expect(v57.wallet.settings.hideSmallBalances).toBe(true)
expect(v57.wallet.settings.hideSpamTokens).toBe(true)
expect(v57.wallet.accounts[0].showSpamTokens).toBeUndefined()
expect(v57.wallet.accounts[0].showSmallBalances).toBeUndefined()
})
}) })
...@@ -61,7 +61,9 @@ export const migrations = { ...@@ -61,7 +61,9 @@ export const migrations = {
2: (state: any) => { 2: (state: any) => {
const newState = { ...state } const newState = { ...state }
const oldFollowingAddresses = state?.favorites?.followedAddresses const oldFollowingAddresses = state?.favorites?.followedAddresses
if (oldFollowingAddresses) newState.favorites.watchedAddresses = oldFollowingAddresses if (oldFollowingAddresses) {
newState.favorites.watchedAddresses = oldFollowingAddresses
}
delete newState?.favorites?.followedAddresses delete newState?.favorites?.followedAddresses
return newState return newState
}, },
...@@ -221,7 +223,9 @@ export const migrations = { ...@@ -221,7 +223,9 @@ export const migrations = {
17: (state: any) => { 17: (state: any) => {
const accounts: Record<Address, Account> | undefined = state?.wallet?.accounts const accounts: Record<Address, Account> | undefined = state?.wallet?.accounts
if (!accounts) return if (!accounts) {
return
}
for (const account of Object.values(accounts)) { for (const account of Object.values(accounts)) {
account.pushNotificationsEnabled = false account.pushNotificationsEnabled = false
...@@ -251,10 +255,14 @@ export const migrations = { ...@@ -251,10 +255,14 @@ export const migrations = {
}>( }>(
(tempState, chainIdString) => { (tempState, chainIdString) => {
const chainId = toSupportedChainId(chainIdString) const chainId = toSupportedChainId(chainIdString)
if (!chainId) return tempState if (!chainId) {
return tempState
}
const chainInfo = chainState?.byChainId[chainId] const chainInfo = chainState?.byChainId[chainId]
if (!chainInfo) return tempState if (!chainInfo) {
return tempState
}
tempState.byChainId[chainId] = chainInfo tempState.byChainId[chainId] = chainInfo
return tempState return tempState
...@@ -266,10 +274,14 @@ export const migrations = { ...@@ -266,10 +274,14 @@ export const migrations = {
const newBlockState = Object.keys(blockState?.byChainId ?? {}).reduce<any>( const newBlockState = Object.keys(blockState?.byChainId ?? {}).reduce<any>(
(tempState, chainIdString) => { (tempState, chainIdString) => {
const chainId = toSupportedChainId(chainIdString) const chainId = toSupportedChainId(chainIdString)
if (!chainId) return tempState if (!chainId) {
return tempState
}
const blockInfo = blockState?.byChainId[chainId] const blockInfo = blockState?.byChainId[chainId]
if (!blockInfo) return tempState if (!blockInfo) {
return tempState
}
tempState.byChainId[chainId] = blockInfo tempState.byChainId[chainId] = blockInfo
return tempState return tempState
...@@ -281,15 +293,21 @@ export const migrations = { ...@@ -281,15 +293,21 @@ export const migrations = {
const newTransactionState = Object.keys(transactionState ?? {}).reduce<TransactionStateMap>( const newTransactionState = Object.keys(transactionState ?? {}).reduce<TransactionStateMap>(
(tempState, address) => { (tempState, address) => {
const txs = transactionState?.[address] const txs = transactionState?.[address]
if (!txs) return tempState if (!txs) {
return tempState
}
const newAddressTxState = Object.keys(txs).reduce<ChainIdToTxIdToDetails>( const newAddressTxState = Object.keys(txs).reduce<ChainIdToTxIdToDetails>(
(tempAddressState, chainIdString) => { (tempAddressState, chainIdString) => {
const chainId = toSupportedChainId(chainIdString) const chainId = toSupportedChainId(chainIdString)
if (!chainId) return tempAddressState if (!chainId) {
return tempAddressState
}
const txInfo = txs[chainId] const txInfo = txs[chainId]
if (!txInfo) return tempAddressState if (!txInfo) {
return tempAddressState
}
tempAddressState[chainId] = txInfo tempAddressState[chainId] = txInfo
return tempAddressState return tempAddressState
...@@ -728,4 +746,40 @@ export const migrations = { ...@@ -728,4 +746,40 @@ export const migrations = {
return newState return newState
}, },
56: function addAllowAnalyticsSwitch(state: any) {
const newState = { ...state }
newState.telemetry = {
...state.telemetry,
allowAnalytics: true,
lastHeartbeat: 0,
}
return newState
},
57: function moveSettingStateToGlobal(state: any) {
const newState = { ...state }
// get old accounts
const accounts = newState?.wallet?.accounts ?? {}
const firstAccountKey = Object.keys(accounts)[0]
// Read setting from the first wallet, or assign default value
const hideSmallBalances = firstAccountKey ? !accounts[firstAccountKey].showSmallBalances : true // default to true
const hideSpamTokens = firstAccountKey ? !accounts[firstAccountKey].showSpamTokens : true // default to true
newState.wallet.settings.hideSmallBalances = hideSmallBalances
newState.wallet.settings.hideSpamTokens = hideSpamTokens
// delete old account specific state
const accountKeys = Object.keys(accounts ?? {})
for (const accountKey of accountKeys) {
delete accounts[accountKey].showSmallBalances
delete accounts[accountKey].showSpamTokens
}
return newState
},
} }
import { MockedResponse } from '@apollo/client/testing'
import React from 'react' import React from 'react'
import { act } from 'react-test-renderer'
import { PreloadedState } from 'redux' import { PreloadedState } from 'redux'
import { AccountSwitcher } from 'src/app/modals/AccountSwitcherModal' import { AccountSwitcher } from 'src/app/modals/AccountSwitcherModal'
import { MobileState } from 'src/app/reducer' import { MobileState } from 'src/app/reducer'
import { initialModalState } from 'src/features/modals/modalSlice' import { initialModalState } from 'src/features/modals/modalSlice'
import { ModalName } from 'src/features/telemetry/constants' import { ModalName } from 'src/features/telemetry/constants'
import { Portfolios } from 'src/test/gqlFixtures'
import { render } from 'src/test/test-utils' import { render } from 'src/test/test-utils'
import { import { mockWalletPreloadedState } from 'wallet/src/test/fixtures'
AccountListDocument,
AccountListQuery,
} from 'wallet/src/data/__generated__/types-and-hooks'
import { mockWalletPreloadedState, SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures'
import { noOpFunction } from 'wallet/src/test/utils' import { noOpFunction } from 'wallet/src/test/utils'
const preloadedState = { const preloadedState = {
...@@ -22,28 +17,15 @@ const preloadedState = { ...@@ -22,28 +17,15 @@ const preloadedState = {
}, },
} as unknown as PreloadedState<MobileState> } as unknown as PreloadedState<MobileState>
const AccountListMock: MockedResponse<AccountListQuery> = {
request: {
query: AccountListDocument,
variables: {
addresses: [SAMPLE_SEED_ADDRESS_1],
},
},
result: {
data: {
portfolios: Portfolios,
},
},
}
// TODO [MOB-259]: Figure out how to do snapshot tests when there is a BottomSheetModal // TODO [MOB-259]: Figure out how to do snapshot tests when there is a BottomSheetModal
describe(AccountSwitcher, () => { describe(AccountSwitcher, () => {
it('renders correctly', () => { it('renders correctly', async () => {
const tree = render(<AccountSwitcher onClose={noOpFunction} />, { const tree = render(<AccountSwitcher onClose={noOpFunction} />, { preloadedState })
preloadedState,
mocks: [AccountListMock], await act(async () => {
}).toJSON() // Wait until the component is rendered
})
expect(tree).toMatchSnapshot() expect(tree.toJSON()).toMatchSnapshot()
}) })
}) })
...@@ -94,7 +94,9 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme ...@@ -94,7 +94,9 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme
} }
const onManageWallet = (): void => { const onManageWallet = (): void => {
if (!activeAccountAddress) return if (!activeAccountAddress) {
return
}
dispatch(closeModal({ name: ModalName.AccountSwitcher })) dispatch(closeModal({ name: ModalName.AccountSwitcher }))
navigate(Screens.SettingsStack, { navigate(Screens.SettingsStack, {
......
...@@ -5,6 +5,7 @@ import { ExploreModal } from 'src/app/modals/ExploreModal' ...@@ -5,6 +5,7 @@ import { ExploreModal } from 'src/app/modals/ExploreModal'
import { SwapModal } from 'src/app/modals/SwapModal' import { SwapModal } from 'src/app/modals/SwapModal'
import { TransferTokenModal } from 'src/app/modals/TransferTokenModal' import { TransferTokenModal } from 'src/app/modals/TransferTokenModal'
import { LazyModalRenderer } from 'src/app/modals/utils' import { LazyModalRenderer } from 'src/app/modals/utils'
import { ViewOnlyExplainerModal } from 'src/app/modals/ViewOnlyExplainerModal'
import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal' import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal'
import { RemoveWalletModal } from 'src/components/RemoveWallet/RemoveWalletModal' import { RemoveWalletModal } from 'src/components/RemoveWallet/RemoveWalletModal'
import { RestoreWalletModal } from 'src/components/RestoreWalletModal/RestoreWalletModal' import { RestoreWalletModal } from 'src/components/RestoreWalletModal/RestoreWalletModal'
...@@ -73,6 +74,10 @@ export function AppModals(): JSX.Element { ...@@ -73,6 +74,10 @@ export function AppModals(): JSX.Element {
<LazyModalRenderer name={ModalName.UnitagsIntro}> <LazyModalRenderer name={ModalName.UnitagsIntro}>
<UnitagsIntroModal /> <UnitagsIntroModal />
</LazyModalRenderer> </LazyModalRenderer>
<LazyModalRenderer name={ModalName.ViewOnlyExplainer}>
<ViewOnlyExplainerModal />
</LazyModalRenderer>
</> </>
) )
} }
...@@ -10,12 +10,27 @@ import { closeModal } from 'src/features/modals/modalSlice' ...@@ -10,12 +10,27 @@ import { closeModal } from 'src/features/modals/modalSlice'
import { ModalName } from 'src/features/telemetry/constants' import { ModalName } from 'src/features/telemetry/constants'
import { selectCustomEndpoint } from 'src/features/tweaks/selectors' import { selectCustomEndpoint } from 'src/features/tweaks/selectors'
import { setCustomEndpoint } from 'src/features/tweaks/slice' import { setCustomEndpoint } from 'src/features/tweaks/slice'
import { Statsig } from 'statsig-react' import {
import { useExperimentWithExposureLoggingDisabled } from 'statsig-react-native' ConfigResult,
import { Accordion } from 'tamagui' Statsig,
import { Button, Flex, Icons, Text, useDeviceInsets } from 'ui/src' useExperimentWithExposureLoggingDisabled,
} from 'statsig-react-native'
import {
Accordion,
Button,
Flex,
Icons,
Separator,
Text,
useDeviceInsets,
useSporeColors,
} from 'ui/src'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { EXPERIMENT_NAMES, FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import {
EXPERIMENT_NAMES,
EXPERIMENT_VALUES_BY_EXPERIMENT,
FEATURE_FLAGS,
} from 'wallet/src/features/experiments/constants'
import { useFeatureFlagWithExposureLoggingDisabled } from 'wallet/src/features/experiments/hooks' import { useFeatureFlagWithExposureLoggingDisabled } from 'wallet/src/features/experiments/hooks'
export function ExperimentsModal(): JSX.Element { export function ExperimentsModal(): JSX.Element {
...@@ -127,7 +142,7 @@ export function ExperimentsModal(): JSX.Element { ...@@ -127,7 +142,7 @@ export function ExperimentsModal(): JSX.Element {
Overridden experiments are reset when the app is restarted Overridden experiments are reset when the app is restarted
</Text> </Text>
<Flex gap="$spacing12" mt="$spacing12"> <Flex gap="$spacing24" mt="$spacing12">
{Object.values(EXPERIMENT_NAMES).map((experiment) => { {Object.values(EXPERIMENT_NAMES).map((experiment) => {
return <ExperimentRow key={experiment} name={experiment} /> return <ExperimentRow key={experiment} name={experiment} />
})} })}
...@@ -183,22 +198,64 @@ function ExperimentRow({ name }: { name: string }): JSX.Element { ...@@ -183,22 +198,64 @@ function ExperimentRow({ name }: { name: string }): JSX.Element {
gap="$spacing16" gap="$spacing16"
justifyContent="space-between" justifyContent="space-between"
paddingStart="$spacing16"> paddingStart="$spacing16">
<Text variant="body1">{key}</Text> <Text variant="body2">{key}</Text>
{typeof value === 'boolean' && ( <ExperimentValueSwitch
<Switch configValueContent={value}
value={value} configValueName={key}
onValueChange={(newValue: boolean): void => { experiment={experiment}
Statsig.overrideConfig(name, { ...experiment.config.value, [key]: newValue }) />
}}
/>
)}
</Flex> </Flex>
)) ))
return ( return (
<Flex> <>
<Text variant="body1">{name}</Text> <Separator />
<Flex gap="$spacing4">{params}</Flex> <Flex>
</Flex> <Text variant="body1">{name}</Text>
<Flex gap="$spacing4">{params}</Flex>
</Flex>
</>
) )
} }
function ExperimentValueSwitch({
experiment,
configValueContent,
configValueName,
}: {
experiment: ConfigResult
configValueContent: unknown
configValueName: string
}): JSX.Element {
const colors = useSporeColors()
const experimentName = experiment.config.getName()
const onValueChange = (newValue: boolean | string): void => {
Statsig.overrideConfig(experimentName, {
...experiment.config.value,
[configValueName]: newValue,
})
}
if (typeof configValueContent === 'boolean') {
return <Switch value={configValueContent} onValueChange={onValueChange} />
}
const variants = EXPERIMENT_VALUES_BY_EXPERIMENT[experimentName]?.[configValueName]
if (variants && typeof configValueContent === 'string') {
return (
<Flex gap="$spacing8">
{Object.entries(variants).map(([_, value]) => (
<Flex key={value} gap="$spacing4" onPressOut={(): void => onValueChange(value)}>
<Text color={value === configValueContent ? colors.accent1.val : colors.neutral1.val}>
{value}
</Text>
</Flex>
))}
</Flex>
)
}
return <Text variant="body3">Unknown Variants</Text>
}
...@@ -4,8 +4,11 @@ import { BottomSheetModal } from 'src/components/modals/BottomSheetModal' ...@@ -4,8 +4,11 @@ import { BottomSheetModal } from 'src/components/modals/BottomSheetModal'
import { closeModal } from 'src/features/modals/modalSlice' import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState' import { selectModalState } from 'src/features/modals/selectModalState'
import { ModalName } from 'src/features/telemetry/constants' import { ModalName } from 'src/features/telemetry/constants'
import { TransferFlow as TransferFlowRewrite } from 'src/features/transactions/swapRewrite/transfer/TransferFlow'
import { TransferFlow } from 'src/features/transactions/transfer/TransferFlow' import { TransferFlow } from 'src/features/transactions/transfer/TransferFlow'
import { useSporeColors } from 'ui/src' import { useSporeColors } from 'ui/src'
import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { useFeatureFlag } from 'wallet/src/features/experiments/hooks'
export function TransferTokenModal(): JSX.Element { export function TransferTokenModal(): JSX.Element {
const colors = useSporeColors() const colors = useSporeColors()
...@@ -16,7 +19,11 @@ export function TransferTokenModal(): JSX.Element { ...@@ -16,7 +19,11 @@ export function TransferTokenModal(): JSX.Element {
appDispatch(closeModal({ name: ModalName.Send })) appDispatch(closeModal({ name: ModalName.Send }))
}, [appDispatch]) }, [appDispatch])
return ( const isSendRewriteEnabled = useFeatureFlag(FEATURE_FLAGS.SendRewrite)
return isSendRewriteEnabled ? (
<TransferFlowRewrite />
) : (
<BottomSheetModal <BottomSheetModal
fullScreen fullScreen
hideHandlebar hideHandlebar
......
import { useTranslation } from 'react-i18next'
import { navigate } from 'src/app/navigation/rootNavigation'
import { BottomSheetModal } from 'src/components/modals/BottomSheetModal'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { ModalName } from 'src/features/telemetry/constants'
import { OnboardingScreens, Screens } from 'src/screens/Screens'
import { Button, Flex, Text } from 'ui/src'
import ViewOnlyWalletDark from 'ui/src/assets/graphics/view-only-wallet-dark.svg'
import ViewOnlyWalletLight from 'ui/src/assets/graphics/view-only-wallet-light.svg'
import { useIsDarkMode } from 'wallet/src/features/appearance/hooks'
import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types'
import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
import { useAppDispatch } from 'wallet/src/state'
const WALLET_IMAGE_ASPECT_RATIO = 327 / 215
export function ViewOnlyExplainerModal(): JSX.Element {
const { t } = useTranslation()
const activeAccountAddress = useActiveAccountAddress()
const dispatch = useAppDispatch()
const hasImportedSeedPhrase = useNativeAccountExists()
const isDarkMode = useIsDarkMode()
const onClose = (): void => {
dispatch(closeModal({ name: ModalName.ViewOnlyExplainer }))
}
const onPressImportWallet = (): void => {
if (hasImportedSeedPhrase && activeAccountAddress) {
dispatch(openModal({ name: ModalName.RemoveWallet }))
} else {
navigate(Screens.OnboardingStack, {
screen: OnboardingScreens.SeedPhraseInput,
params: { importType: ImportType.SeedPhrase, entryPoint: OnboardingEntryPoint.Sidebar },
})
}
onClose()
}
const WalletImage = isDarkMode ? ViewOnlyWalletDark : ViewOnlyWalletLight
return (
<BottomSheetModal name={ModalName.ViewOnlyExplainer} onClose={onClose}>
<Flex gap="$spacing12" pb="$spacing24" pt="$spacing12" px="$spacing24">
<Flex gap="$spacing16" pb="$spacing16">
<Flex style={{ aspectRatio: WALLET_IMAGE_ASPECT_RATIO }}>
<WalletImage height="100%" preserveAspectRatio="xMidYMid slice" width="100%" />
</Flex>
<Flex alignItems="center" gap="$spacing4">
<Text variant="subheading1">{t('This wallet is view-only')}</Text>
<Text color="$neutral2" textAlign="center" variant="body2">
{t(
'To swap, buy, send, and receive tokens, you need to import this wallet’s recovery phrase.'
)}
</Text>
</Flex>
</Flex>
<Flex gap="$spacing8">
<Button
alignSelf="center"
borderRadius="$rounded20"
paddingHorizontal={40}
theme="primary"
onPress={onPressImportWallet}>
{t('Import wallet')}
</Button>
<Button
alignSelf="center"
backgroundColor={undefined}
borderRadius="$rounded20"
color="$neutral2"
paddingHorizontal={40}
theme="secondary"
onPress={onClose}>
{t('Maybe later')}
</Button>
</Flex>
</Flex>
</BottomSheetModal>
)
}
...@@ -235,6 +235,7 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -235,6 +235,7 @@ exports[`AccountSwitcher renders correctly 1`] = `
} }
> >
<Text <Text
adjustsFontSizeToFit={true}
allowFontScaling={true} allowFontScaling={true}
maxFontSizeMultiplier={1.4} maxFontSizeMultiplier={1.4}
numberOfLines={1} numberOfLines={1}
...@@ -466,7 +467,13 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -466,7 +467,13 @@ exports[`AccountSwitcher renders correctly 1`] = `
<RCTScrollView <RCTScrollView
CellRendererComponent={[Function]} CellRendererComponent={[Function]}
ListHeaderComponent={<React.Fragment />} ListHeaderComponent={<React.Fragment />}
animatedStyle={
{
"value": {},
}
}
bounces={false} bounces={false}
collapsable={false}
data={[]} data={[]}
getItem={[Function]} getItem={[Function]}
getItemCount={[Function]} getItemCount={[Function]}
......
...@@ -49,6 +49,7 @@ import { SettingsCloudBackupPasswordConfirmScreen } from 'src/screens/SettingsCl ...@@ -49,6 +49,7 @@ import { SettingsCloudBackupPasswordConfirmScreen } from 'src/screens/SettingsCl
import { SettingsCloudBackupPasswordCreateScreen } from 'src/screens/SettingsCloudBackupPasswordCreateScreen' import { SettingsCloudBackupPasswordCreateScreen } from 'src/screens/SettingsCloudBackupPasswordCreateScreen'
import { SettingsCloudBackupProcessingScreen } from 'src/screens/SettingsCloudBackupProcessingScreen' import { SettingsCloudBackupProcessingScreen } from 'src/screens/SettingsCloudBackupProcessingScreen'
import { SettingsCloudBackupStatus } from 'src/screens/SettingsCloudBackupStatus' import { SettingsCloudBackupStatus } from 'src/screens/SettingsCloudBackupStatus'
import { SettingsPrivacyScreen } from 'src/screens/SettingsPrivacyScreen'
import { SettingsScreen } from 'src/screens/SettingsScreen' import { SettingsScreen } from 'src/screens/SettingsScreen'
import { SettingsViewSeedPhraseScreen } from 'src/screens/SettingsViewSeedPhraseScreen' import { SettingsViewSeedPhraseScreen } from 'src/screens/SettingsViewSeedPhraseScreen'
import { SettingsWallet } from 'src/screens/SettingsWallet' import { SettingsWallet } from 'src/screens/SettingsWallet'
...@@ -115,12 +116,12 @@ function SettingsStackGroup(): JSX.Element { ...@@ -115,12 +116,12 @@ function SettingsStackGroup(): JSX.Element {
component={SettingsAppearanceScreen} component={SettingsAppearanceScreen}
name={Screens.SettingsAppearance} name={Screens.SettingsAppearance}
/> />
<SettingsStack.Screen component={SettingsPrivacyScreen} name={Screens.SettingsPrivacy} />
</SettingsStack.Navigator> </SettingsStack.Navigator>
) )
} }
export function WrappedHomeScreen(props: AppStackScreenProp<Screens.Home>): JSX.Element { export function WrappedHomeScreen(props: AppStackScreenProp<Screens.Home>): JSX.Element {
useBiometricCheck()
const activeAccount = useActiveAccountWithThrow() const activeAccount = useActiveAccountWithThrow()
// Adding `key` forces a full re-render and re-mount when switching accounts // Adding `key` forces a full re-render and re-mount when switching accounts
// to avoid issues with wrong cached data being shown in some memoized components that are already mounted. // to avoid issues with wrong cached data being shown in some memoized components that are already mounted.
...@@ -300,6 +301,7 @@ export function UnitagStackNavigator(): JSX.Element { ...@@ -300,6 +301,7 @@ export function UnitagStackNavigator(): JSX.Element {
export function AppStackNavigator(): JSX.Element { export function AppStackNavigator(): JSX.Element {
const finishedOnboarding = useAppSelector(selectFinishedOnboarding) const finishedOnboarding = useAppSelector(selectFinishedOnboarding)
useBiometricCheck()
return ( return (
<AppStack.Navigator <AppStack.Navigator
......
...@@ -10,7 +10,7 @@ export type RootNavigationArgs<RouteName extends keyof RootParamList> = ...@@ -10,7 +10,7 @@ export type RootNavigationArgs<RouteName extends keyof RootParamList> =
function isNavigationRefReady(): boolean { function isNavigationRefReady(): boolean {
if (!navigationRef.isReady()) { if (!navigationRef.isReady()) {
logger.error('Navigator was called before it was initialized', { logger.error(new Error('Navigator was called before it was initialized'), {
tags: { file: 'rootNavigation', function: 'navigate' }, tags: { file: 'rootNavigation', function: 'navigate' },
}) })
return false return false
...@@ -36,7 +36,9 @@ export function goBack(): void { ...@@ -36,7 +36,9 @@ export function goBack(): void {
return return
} }
if (navigationRef.canGoBack()) navigationRef.goBack() if (navigationRef.canGoBack()) {
navigationRef.goBack()
}
} }
export function dispatchNavigationAction( export function dispatchNavigationAction(
......
...@@ -19,7 +19,7 @@ type NFTItemScreenParams = { ...@@ -19,7 +19,7 @@ type NFTItemScreenParams = {
fallbackData?: NFTItem fallbackData?: NFTItem
} }
export type CloudBackupFormParms = { export type CloudBackupFormParams = {
address: Address address: Address
password: string password: string
} }
...@@ -37,21 +37,22 @@ export type ExploreStackParamList = { ...@@ -37,21 +37,22 @@ export type ExploreStackParamList = {
} }
export type SettingsStackParamList = { export type SettingsStackParamList = {
[Screens.Dev]: undefined
[Screens.Settings]: undefined [Screens.Settings]: undefined
[Screens.SettingsWallet]: { address: Address }
[Screens.SettingsWalletEdit]: { address: Address }
[Screens.SettingsWalletManageConnection]: { address: Address }
[Screens.SettingsHelpCenter]: undefined
[Screens.SettingsBiometricAuth]: undefined
[Screens.SettingsAppearance]: undefined [Screens.SettingsAppearance]: undefined
[Screens.SettingsLanguage]: undefined [Screens.SettingsBiometricAuth]: undefined
[Screens.WebView]: { headerTitle: string; uriLink: string } [Screens.SettingsCloudBackupPasswordConfirm]: CloudBackupFormParams
[Screens.Dev]: undefined
[Screens.SettingsCloudBackupPasswordCreate]: { address: Address } [Screens.SettingsCloudBackupPasswordCreate]: { address: Address }
[Screens.SettingsCloudBackupPasswordConfirm]: CloudBackupFormParms [Screens.SettingsCloudBackupProcessing]: CloudBackupFormParams
[Screens.SettingsCloudBackupProcessing]: CloudBackupFormParms
[Screens.SettingsCloudBackupStatus]: { address: Address } [Screens.SettingsCloudBackupStatus]: { address: Address }
[Screens.SettingsHelpCenter]: undefined
[Screens.SettingsLanguage]: undefined
[Screens.SettingsPrivacy]: undefined
[Screens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean } [Screens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean }
[Screens.SettingsWallet]: { address: Address }
[Screens.SettingsWalletEdit]: { address: Address }
[Screens.SettingsWalletManageConnection]: { address: Address }
[Screens.WebView]: { headerTitle: string; uriLink: string }
} }
export type OnboardingStackBaseParams = { export type OnboardingStackBaseParams = {
...@@ -64,8 +65,8 @@ export type OnboardingStackParamList = { ...@@ -64,8 +65,8 @@ export type OnboardingStackParamList = {
[OnboardingScreens.BackupCloudPasswordCreate]: { [OnboardingScreens.BackupCloudPasswordCreate]: {
address: Address address: Address
} & OnboardingStackBaseParams } & OnboardingStackBaseParams
[OnboardingScreens.BackupCloudPasswordConfirm]: CloudBackupFormParms & OnboardingStackBaseParams [OnboardingScreens.BackupCloudPasswordConfirm]: CloudBackupFormParams & OnboardingStackBaseParams
[OnboardingScreens.BackupCloudProcessing]: CloudBackupFormParms & OnboardingStackBaseParams [OnboardingScreens.BackupCloudProcessing]: CloudBackupFormParams & OnboardingStackBaseParams
[OnboardingScreens.Backup]: OnboardingStackBaseParams [OnboardingScreens.Backup]: OnboardingStackBaseParams
[OnboardingScreens.Landing]: OnboardingStackBaseParams [OnboardingScreens.Landing]: OnboardingStackBaseParams
[OnboardingScreens.EditName]: OnboardingStackBaseParams [OnboardingScreens.EditName]: OnboardingStackBaseParams
......
...@@ -406,6 +406,27 @@ export const v55Schema = { ...@@ -406,6 +406,27 @@ export const v55Schema = {
}, },
} }
export const v56Schema = {
...v55Schema,
telemetry: {
...v55Schema.telemetry,
allowAnalytics: true,
lastHeartbeat: 0,
},
}
export const v57Schema = {
...v56Schema,
wallet: {
...v56Schema.wallet,
settings: {
...v56Schema.wallet.settings,
hideSmallBalances: true,
hideSpamTokens: true,
},
},
}
// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer // TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema // export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v54Schema => v54Schema export const getSchema = (): typeof v57Schema => v57Schema
...@@ -75,7 +75,7 @@ export const persistConfig = { ...@@ -75,7 +75,7 @@ export const persistConfig = {
key: 'root', key: 'root',
storage: reduxStorage, storage: reduxStorage,
whitelist, whitelist,
version: 55, version: 57,
migrate: createMigrate(migrations), migrate: createMigrate(migrations),
} }
...@@ -91,10 +91,18 @@ const sentryReduxEnhancer = Sentry.createReduxEnhancer({ ...@@ -91,10 +91,18 @@ const sentryReduxEnhancer = Sentry.createReduxEnhancer({
return action return action
}, },
stateTransformer: (state: MobileState): Maybe<MobileState> => {
// Do not log the state if a user has opted out of analytics.
if (state.telemetry.allowAnalytics) {
return state
} else {
return null
}
},
}) })
const middlewares: Middleware[] = [fiatOnRampApi.middleware, fiatOnRampAggregatorApi.middleware] const middlewares: Middleware[] = [fiatOnRampApi.middleware, fiatOnRampAggregatorApi.middleware]
if (isNonJestDev()) { if (isNonJestDev) {
const createDebugger = require('redux-flipper').default const createDebugger = require('redux-flipper').default
middlewares.push(createDebugger()) middlewares.push(createDebugger())
} }
......
...@@ -6,7 +6,7 @@ import { AccountIcon, AccountIconProps } from 'src/components/AccountIcon' ...@@ -6,7 +6,7 @@ import { AccountIcon, AccountIconProps } from 'src/components/AccountIcon'
import { NotificationBadge } from 'src/components/notifications/Badge' import { NotificationBadge } from 'src/components/notifications/Badge'
import { ElementName } from 'src/features/telemetry/constants' import { ElementName } from 'src/features/telemetry/constants'
import { setClipboard } from 'src/utils/clipboard' import { setClipboard } from 'src/utils/clipboard'
import { ColorTokens, Flex, Icons, SpaceTokens, Text, TouchableArea } from 'ui/src' import { ColorTokens, Flex, Icons, SpaceTokens, Text, TextProps, TouchableArea } from 'ui/src'
import { fonts } from 'ui/src/theme' import { fonts } from 'ui/src/theme'
import { useENSAvatar } from 'wallet/src/features/ens/api' import { useENSAvatar } from 'wallet/src/features/ens/api'
import { pushNotification } from 'wallet/src/features/notifications/slice' import { pushNotification } from 'wallet/src/features/notifications/slice'
...@@ -44,16 +44,26 @@ function CopyButtonWrapper({ ...@@ -44,16 +44,26 @@ function CopyButtonWrapper({
children, children,
onPress, onPress,
}: PropsWithChildren<CopyButtonWrapperProps>): JSX.Element { }: PropsWithChildren<CopyButtonWrapperProps>): JSX.Element {
if (onPress) if (onPress) {
return ( return (
<TouchableArea hapticFeedback hitSlop={16} testID={ElementName.Copy} onPress={onPress}> <TouchableArea hapticFeedback hitSlop={16} testID={ElementName.Copy} onPress={onPress}>
{children} {children}
</TouchableArea> </TouchableArea>
) )
}
return <>{children}</> return <>{children}</>
} }
// This seems to work for most font sizes and screens, but could probably be improved and abstracted
// if we find more uses for it in other areas.
function getLineHeightForAdjustedFontSize(nameLength: number): number {
// as name gets longer, number gets smaller down to 1, past 50 just 1
const lineHeightBase = 50 - Math.min(49, nameLength)
const scale = 1.2
return lineHeightBase * scale
}
/** Helper component to display identicon and formatted address */ /** Helper component to display identicon and formatted address */
export function AddressDisplay({ export function AddressDisplay({
allowFontScaling = true, allowFontScaling = true,
...@@ -83,7 +93,9 @@ export function AddressDisplay({ ...@@ -83,7 +93,9 @@ export function AddressDisplay({
const showAddressAsSubtitle = !hideAddressInSubtitle && displayName?.type !== 'address' const showAddressAsSubtitle = !hideAddressInSubtitle && displayName?.type !== 'address'
const onPressCopyAddress = async (): Promise<void> => { const onPressCopyAddress = async (): Promise<void> => {
if (!address) return if (!address) {
return
}
await impactAsync() await impactAsync()
await setClipboard(address) await setClipboard(address)
dispatch( dispatch(
...@@ -112,6 +124,20 @@ export function AddressDisplay({ ...@@ -112,6 +124,20 @@ export function AddressDisplay({
) )
}, [address, avatar, showIconBackground, showViewOnlyBadge, size]) }, [address, avatar, showIconBackground, showViewOnlyBadge, size])
const name = displayName?.name || ''
// since adjustsFontSizeToFit doesnt really work adjusting line height properly
// manually adjust lineHeight things to keep vertical center
const dynamicSizedTextVerticalStyles: TextProps =
name.length > 20
? {
adjustsFontSizeToFit: true,
lineHeight: getLineHeightForAdjustedFontSize(name.length),
}
: {
lineHeight: fonts[variant].lineHeight,
}
return ( return (
<Flex alignItems={contentAlign} flexDirection={direction} gap={horizontalGap}> <Flex alignItems={contentAlign} flexDirection={direction} gap={horizontalGap}>
{showAccountIcon && {showAccountIcon &&
...@@ -125,13 +151,16 @@ export function AddressDisplay({ ...@@ -125,13 +151,16 @@ export function AddressDisplay({
onPress={showCopy && !showAddressAsSubtitle ? onPressCopyAddress : undefined}> onPress={showCopy && !showAddressAsSubtitle ? onPressCopyAddress : undefined}>
<Flex centered row gap="$spacing12"> <Flex centered row gap="$spacing12">
<Text <Text
adjustsFontSizeToFit
allowFontScaling={allowFontScaling} allowFontScaling={allowFontScaling}
color={textColor} color={textColor}
ellipsizeMode="tail" ellipsizeMode="tail"
fontFamily="$heading"
fontSize={mainSize}
numberOfLines={1} numberOfLines={1}
testID={`address-display/name/${displayName?.name}`} testID={`address-display/name/${displayName?.name}`}
variant={variant}> {...dynamicSizedTextVerticalStyles}>
{displayName?.name} {name}
</Text> </Text>
{showCopy && !showAddressAsSubtitle && ( {showCopy && !showAddressAsSubtitle && (
<Icons.CopySheets color="$neutral1" size={mainSize} /> <Icons.CopySheets color="$neutral1" size={mainSize} />
......
...@@ -86,9 +86,10 @@ const RollNumber = ({ ...@@ -86,9 +86,10 @@ const RollNumber = ({
} }
}) })
const numbers = NUMBER_ARRAY.map((char) => { const numbers = NUMBER_ARRAY.map((char, idx) => {
return ( return (
<Animated.Text <Animated.Text
key={idx}
allowFontScaling={false} allowFontScaling={false}
style={[animatedFontStyle, AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT }]}> style={[animatedFontStyle, AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT }]}>
{char} {char}
...@@ -342,6 +343,7 @@ export const AnimatedFontStyles = StyleSheet.create({ ...@@ -342,6 +343,7 @@ export const AnimatedFontStyles = StyleSheet.create({
fontStyle: { fontStyle: {
fontFamily: fonts.heading2.family, fontFamily: fonts.heading2.family,
fontSize: fonts.heading2.fontSize, fontSize: fonts.heading2.fontSize,
// special case for the home screen balance, instead of using the heading2 font weight
fontWeight: '500', fontWeight: '500',
lineHeight: fonts.heading2.lineHeight, lineHeight: fonts.heading2.lineHeight,
top: 1, top: 1,
......
import React, { PropsWithChildren } from 'react' import React, { PropsWithChildren } from 'react'
export function DevelopmentOnly<T>({ children }: PropsWithChildren<T>): JSX.Element | null { export function DevelopmentOnly<T>({ children }: PropsWithChildren<T>): JSX.Element | null {
if (!__DEV__ || !children) return null if (!__DEV__ || !children) {
return null
}
return <>{children}</> return <>{children}</>
} }
...@@ -3,6 +3,7 @@ import ContextMenu from 'react-native-context-menu-view' ...@@ -3,6 +3,7 @@ import ContextMenu from 'react-native-context-menu-view'
import { useNFTMenu } from 'src/features/nfts/hooks' import { useNFTMenu } from 'src/features/nfts/hooks'
import { Flex, TouchableArea } from 'ui/src' import { Flex, TouchableArea } from 'ui/src'
import { borderRadii } from 'ui/src/theme' import { borderRadii } from 'ui/src/theme'
import noop from 'utilities/src/react/noop'
import { NFTViewer } from 'wallet/src/features/images/NFTViewer' import { NFTViewer } from 'wallet/src/features/images/NFTViewer'
import { import {
ESTIMATED_NFT_LIST_ITEM_SIZE, ESTIMATED_NFT_LIST_ITEM_SIZE,
...@@ -37,6 +38,8 @@ export function NftView({ ...@@ -37,6 +38,8 @@ export function NftView({
hapticFeedback hapticFeedback
activeOpacity={1} activeOpacity={1}
hapticStyle={ImpactFeedbackStyle.Light} hapticStyle={ImpactFeedbackStyle.Light}
// Needed to fix long press issue with context menu on Android
onLongPress={noop}
onPress={onPress}> onPress={onPress}>
<Flex <Flex
alignItems="center" alignItems="center"
......
...@@ -74,7 +74,7 @@ exports[`NetworkFee renders a NetworkFee in a loading state 1`] = ` ...@@ -74,7 +74,7 @@ exports[`NetworkFee renders a NetworkFee in a loading state 1`] = `
"flexShrink": 1, "flexShrink": 1,
"fontFamily": "Basel-Book", "fontFamily": "Basel-Book",
"fontSize": 15, "fontSize": 15,
"lineHeight": 16, "lineHeight": 20,
} }
} }
suppressHighlighting={true} suppressHighlighting={true}
...@@ -198,6 +198,18 @@ exports[`NetworkFee renders a NetworkFee in a loading state 1`] = ` ...@@ -198,6 +198,18 @@ exports[`NetworkFee renders a NetworkFee in a loading state 1`] = `
</Text> </Text>
</View> </View>
<View <View
animatedStyle={
{
"value": {
"transform": [
{
"rotateZ": "0deg",
},
],
},
}
}
collapsable={false}
sentry-label="SpinningLoader" sentry-label="SpinningLoader"
style={ style={
{ {
...@@ -357,7 +369,7 @@ exports[`NetworkFee renders a NetworkFee in an error state 1`] = ` ...@@ -357,7 +369,7 @@ exports[`NetworkFee renders a NetworkFee in an error state 1`] = `
"flexShrink": 1, "flexShrink": 1,
"fontFamily": "Basel-Book", "fontFamily": "Basel-Book",
"fontSize": 15, "fontSize": 15,
"lineHeight": 16, "lineHeight": 20,
} }
} }
suppressHighlighting={true} suppressHighlighting={true}
...@@ -488,7 +500,7 @@ exports[`NetworkFee renders a NetworkFee in an error state 1`] = ` ...@@ -488,7 +500,7 @@ exports[`NetworkFee renders a NetworkFee in an error state 1`] = `
"color": "#7D7D7D", "color": "#7D7D7D",
"fontFamily": "Basel-Book", "fontFamily": "Basel-Book",
"fontSize": 15, "fontSize": 15,
"lineHeight": 16, "lineHeight": 20,
} }
} }
suppressHighlighting={true} suppressHighlighting={true}
...@@ -573,7 +585,7 @@ exports[`NetworkFee renders a NetworkFee normally 1`] = ` ...@@ -573,7 +585,7 @@ exports[`NetworkFee renders a NetworkFee normally 1`] = `
"flexShrink": 1, "flexShrink": 1,
"fontFamily": "Basel-Book", "fontFamily": "Basel-Book",
"fontSize": 15, "fontSize": 15,
"lineHeight": 16, "lineHeight": 20,
} }
} }
suppressHighlighting={true} suppressHighlighting={true}
...@@ -704,7 +716,7 @@ exports[`NetworkFee renders a NetworkFee normally 1`] = ` ...@@ -704,7 +716,7 @@ exports[`NetworkFee renders a NetworkFee normally 1`] = `
"color": "#222222", "color": "#222222",
"fontFamily": "Basel-Book", "fontFamily": "Basel-Book",
"fontSize": 15, "fontSize": 15,
"lineHeight": 16, "lineHeight": 20,
} }
} }
suppressHighlighting={true} suppressHighlighting={true}
......
import { ImpactFeedbackStyle } from 'expo-haptics' import { ImpactFeedbackStyle } from 'expo-haptics'
import { memo, useEffect, useMemo, useState } from 'react' import { memo, useMemo } from 'react'
import { I18nManager } from 'react-native' import { I18nManager } from 'react-native'
import { SharedValue } from 'react-native-reanimated' import { SharedValue } from 'react-native-reanimated'
import { LineChart, LineChartProvider, TLineChartDataProp } from 'react-native-wagmi-charts' import { LineChart, LineChartProvider, TLineChartDataProp } from 'react-native-wagmi-charts'
...@@ -27,19 +27,14 @@ type PriceTextProps = { ...@@ -27,19 +27,14 @@ type PriceTextProps = {
spotPrice?: number spotPrice?: number
} }
function PriceTextSection({ function PriceTextSection({ loading, numberOfDigits, spotPrice }: PriceTextProps): JSX.Element {
loading,
relativeChange,
numberOfDigits,
spotPrice,
}: PriceTextProps): JSX.Element {
const price = useLineChartPrice(spotPrice) const price = useLineChartPrice(spotPrice)
const currency = useAppFiatCurrencyInfo() const currency = useAppFiatCurrencyInfo()
const mx = spacing.spacing12 const mx = spacing.spacing12
return ( return (
<Flex mx={mx}> <Flex mx={mx}>
{/* Specify maxWidth to allow text scalling. onLayout was sometimes called after more {/* Specify maxWidth to allow text scaling. onLayout was sometimes called after more
than 5 seconds which is not acceptable so we have to provide the approximate width than 5 seconds which is not acceptable so we have to provide the approximate width
of the PriceText component explicitly. */} of the PriceText component explicitly. */}
<PriceExplorerAnimatedNumber <PriceExplorerAnimatedNumber
...@@ -48,7 +43,7 @@ function PriceTextSection({ ...@@ -48,7 +43,7 @@ function PriceTextSection({
price={price} price={price}
/> />
<Flex row gap="$spacing4"> <Flex row gap="$spacing4">
<RelativeChangeText loading={loading} spotRelativeChange={relativeChange} /> <RelativeChangeText loading={loading} />
<DatetimeText loading={loading} /> <DatetimeText loading={loading} />
</Flex> </Flex>
</Flex> </Flex>
...@@ -70,19 +65,8 @@ export const PriceExplorer = memo(function PriceExplorer({ ...@@ -70,19 +65,8 @@ export const PriceExplorer = memo(function PriceExplorer({
forcePlaceholder?: boolean forcePlaceholder?: boolean
onRetry: () => void onRetry: () => void
}): JSX.Element { }): JSX.Element {
const [fetchComplete, setFetchComplete] = useState(false)
const onFetchComplete = (): void => {
setFetchComplete(true)
}
const { data, loading, error, refetch, setDuration, selectedDuration, numberOfDigits } = const { data, loading, error, refetch, setDuration, selectedDuration, numberOfDigits } =
useTokenPriceHistory(currencyId, onFetchComplete) useTokenPriceHistory(currencyId)
useEffect(() => {
if (loading && fetchComplete) {
setFetchComplete(false)
}
}, [loading, fetchComplete])
const { convertFiatAmount } = useLocalizationContext() const { convertFiatAmount } = useLocalizationContext()
const conversionRate = convertFiatAmount().amount const conversionRate = convertFiatAmount().amount
...@@ -115,7 +99,9 @@ export const PriceExplorer = memo(function PriceExplorer({ ...@@ -115,7 +99,9 @@ export const PriceExplorer = memo(function PriceExplorer({
) { ) {
// Propagate retry up while refetching, if available // Propagate retry up while refetching, if available
const refetchAndRetry = (): void => { const refetchAndRetry = (): void => {
if (refetch) refetch() if (refetch) {
refetch()
}
onRetry() onRetry()
} }
return <PriceExplorerError showRetry={error !== undefined} onRetry={refetchAndRetry} /> return <PriceExplorerError showRetry={error !== undefined} onRetry={refetchAndRetry} />
...@@ -129,18 +115,18 @@ export const PriceExplorer = memo(function PriceExplorer({ ...@@ -129,18 +115,18 @@ export const PriceExplorer = memo(function PriceExplorer({
} else if (convertedPriceHistory?.length) { } else if (convertedPriceHistory?.length) {
content = ( content = (
// TODO(MOB-2308): add better loading state // TODO(MOB-2308): add better loading state
// <Flex opacity={fetchComplete ? 1 : 0.35}> <Flex opacity={!loading ? 1 : 0.35}>
<PriceExplorerChart <PriceExplorerChart
additionalPadding={additionalPadding} additionalPadding={additionalPadding}
lastPricePoint={lastPricePoint} lastPricePoint={lastPricePoint}
loading={loading} loading={loading}
numberOfDigits={numberOfDigits} numberOfDigits={numberOfDigits}
priceHistory={convertedPriceHistory} priceHistory={convertedPriceHistory}
shouldShowAnimatedDot={shouldShowAnimatedDot} shouldShowAnimatedDot={shouldShowAnimatedDot}
spot={convertedSpot} spot={convertedSpot}
tokenColor={tokenColor} tokenColor={tokenColor}
/> />
// </Flex> </Flex>
) )
} else { } else {
content = <PriceExplorerPlaceholder loading={loading} numberOfDigits={numberOfDigits} /> content = <PriceExplorerPlaceholder loading={loading} numberOfDigits={numberOfDigits} />
......
...@@ -12,28 +12,28 @@ import Animated, { ...@@ -12,28 +12,28 @@ import Animated, {
import { import {
ADDITIONAL_WIDTH_FOR_ANIMATIONS, ADDITIONAL_WIDTH_FOR_ANIMATIONS,
AnimatedCharStyles, AnimatedCharStyles,
AnimatedFontStyles,
DIGIT_HEIGHT, DIGIT_HEIGHT,
NUMBER_ARRAY, NUMBER_ARRAY,
NUMBER_WIDTH_ARRAY, NUMBER_WIDTH_ARRAY,
TopAndBottomGradient, TopAndBottomGradient,
} from 'src/components/AnimatedNumber' } from 'src/components/AnimatedNumber'
import { ValueAndFormatted } from 'src/components/PriceExplorer/usePrice' import { ValueAndFormattedWithAnimation } from 'src/components/PriceExplorer/usePrice'
import { PriceNumberOfDigits } from 'src/components/PriceExplorer/usePriceHistory' import { PriceNumberOfDigits } from 'src/components/PriceExplorer/usePriceHistory'
import { useSporeColors } from 'ui/src' import { useSporeColors } from 'ui/src'
import { TextLoaderWrapper } from 'ui/src/components/text/Text' import { TextLoaderWrapper } from 'ui/src/components/text/Text'
import { fonts } from 'ui/src/theme'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
const NumbersMain = ({ const NumbersMain = ({
color, color,
backgroundColor, backgroundColor,
hidePlacehodler, hidePlaceholder,
}: { }: {
color: string color: string
backgroundColor: string backgroundColor: string
hidePlacehodler(): void hidePlaceholder(): void
}): JSX.Element | null => { }): JSX.Element | null => {
const [showNumers, setShowNumbers] = useState(false) const [showNumbers, setShowNumbers] = useState(false)
const hideNumbers = useSharedValue(true) const hideNumbers = useSharedValue(true)
const animatedTextStyle = useAnimatedStyle(() => { const animatedTextStyle = useAnimatedStyle(() => {
...@@ -49,11 +49,11 @@ const NumbersMain = ({ ...@@ -49,11 +49,11 @@ const NumbersMain = ({
}, []) }, [])
const onLayout = (): void => { const onLayout = (): void => {
hidePlacehodler() hidePlaceholder()
hideNumbers.value = false hideNumbers.value = false
} }
if (showNumers) { if (showNumbers) {
return ( return (
<Animated.Text <Animated.Text
allowFontScaling={false} allowFontScaling={false}
...@@ -82,7 +82,7 @@ const RollNumber = ({ ...@@ -82,7 +82,7 @@ const RollNumber = ({
index, index,
shouldAnimate, shouldAnimate,
decimalPlace, decimalPlace,
hidePlacehodler, hidePlaceholder,
commaIndex, commaIndex,
currency, currency,
}: { }: {
...@@ -90,7 +90,7 @@ const RollNumber = ({ ...@@ -90,7 +90,7 @@ const RollNumber = ({
index: number index: number
shouldAnimate: SharedValue<boolean> shouldAnimate: SharedValue<boolean>
decimalPlace: SharedValue<number> decimalPlace: SharedValue<number>
hidePlacehodler(): void hidePlaceholder(): void
commaIndex: number commaIndex: number
currency: FiatCurrencyInfo currency: FiatCurrencyInfo
}): JSX.Element => { }): JSX.Element => {
...@@ -139,6 +139,34 @@ const RollNumber = ({ ...@@ -139,6 +139,34 @@ const RollNumber = ({
} }
}) })
// need it in case the current value is eg $999.00 but maximum value in chart is more than $1,000.00
// so it can hide the comma to avoid something like $,999.00
const animatedWrapperSeparatorStyle = useAnimatedStyle(() => {
const isSeparator =
(index - commaIndex) % 4 === 0 &&
index - commaIndex < 0 &&
index > commaIndex - decimalPlace.value
if (!isSeparator) {
return {
width: withTiming(0),
}
}
const digitWidth =
chars.value[index - (commaIndex - decimalPlace.value)] === currency.groupingSeparator ? 8 : 0
const rowWidth = Math.max(digitWidth, 0)
return {
transform: [
{
translateY: transformY.value,
},
],
width: shouldAnimate.value ? withTiming(rowWidth) : rowWidth,
}
})
if (index === commaIndex) { if (index === commaIndex) {
return ( return (
<Animated.Text <Animated.Text
...@@ -159,15 +187,17 @@ const RollNumber = ({ ...@@ -159,15 +187,17 @@ const RollNumber = ({
index > commaIndex - decimalPlace.value index > commaIndex - decimalPlace.value
) { ) {
return ( return (
<Animated.Text <Animated.View style={animatedWrapperSeparatorStyle}>
allowFontScaling={false} <Animated.Text
style={[ allowFontScaling={false}
animatedFontStyle, style={[
AnimatedFontStyles.fontStyle, animatedFontStyle,
{ height: DIGIT_HEIGHT, backgroundColor: colors.surface1.val }, AnimatedFontStyles.fontStyle,
]}> { height: DIGIT_HEIGHT, backgroundColor: colors.surface1.val },
{currency.groupingSeparator} ]}>
</Animated.Text> {currency.groupingSeparator}
</Animated.Text>
</Animated.View>
) )
} }
...@@ -182,7 +212,7 @@ const RollNumber = ({ ...@@ -182,7 +212,7 @@ const RollNumber = ({
<MemoizedNumbers <MemoizedNumbers
backgroundColor={colors.surface1.val} backgroundColor={colors.surface1.val}
color={index >= commaIndex ? colors.neutral3.val : colors.neutral1.val} color={index >= commaIndex ? colors.neutral3.val : colors.neutral1.val}
hidePlacehodler={hidePlacehodler} hidePlaceholder={hidePlaceholder}
/> />
</Animated.View> </Animated.View>
) )
...@@ -190,12 +220,12 @@ const RollNumber = ({ ...@@ -190,12 +220,12 @@ const RollNumber = ({
const Numbers = ({ const Numbers = ({
price, price,
hidePlacehodler, hidePlaceholder,
numberOfDigits, numberOfDigits,
currency, currency,
}: { }: {
price: ValueAndFormatted price: ValueAndFormattedWithAnimation
hidePlacehodler(): void hidePlaceholder(): void
numberOfDigits: PriceNumberOfDigits numberOfDigits: PriceNumberOfDigits
currency: FiatCurrencyInfo currency: FiatCurrencyInfo
}): JSX.Element[] => { }): JSX.Element[] => {
...@@ -207,17 +237,21 @@ const Numbers = ({ ...@@ -207,17 +237,21 @@ const Numbers = ({
return price.formatted.value.indexOf(currency.decimalSeparator) return price.formatted.value.indexOf(currency.decimalSeparator)
}, [price]) }, [price])
const commaIndex = numberOfDigits.left + Math.floor((numberOfDigits.left - 1) / 3)
return _.times( return _.times(
numberOfDigits.left + numberOfDigits.right + Math.floor(numberOfDigits.left / 3) + 1, numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1,
(index) => ( (index) => (
<Animated.View style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}> <Animated.View
key={`$number_${index - (commaIndex - decimalPlace.value)}`}
style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}>
<RollNumber <RollNumber
key={index === 0 ? `$sign` : `$_number_${numberOfDigits.left - 1 - index}`} key={`$number_${index - (commaIndex - decimalPlace.value)}`}
chars={chars} chars={chars}
commaIndex={numberOfDigits.left + Math.floor(numberOfDigits.left / 3)} commaIndex={commaIndex}
currency={currency} currency={currency}
decimalPlace={decimalPlace} decimalPlace={decimalPlace}
hidePlacehodler={hidePlacehodler} hidePlaceholder={hidePlaceholder}
index={index} index={index}
shouldAnimate={price.shouldAnimate} shouldAnimate={price.shouldAnimate}
/> />
...@@ -239,7 +273,7 @@ const PriceExplorerAnimatedNumber = ({ ...@@ -239,7 +273,7 @@ const PriceExplorerAnimatedNumber = ({
numberOfDigits, numberOfDigits,
currency, currency,
}: { }: {
price: ValueAndFormatted price: ValueAndFormattedWithAnimation
numberOfDigits: PriceNumberOfDigits numberOfDigits: PriceNumberOfDigits
currency: FiatCurrencyInfo currency: FiatCurrencyInfo
}): JSX.Element => { }): JSX.Element => {
...@@ -254,7 +288,7 @@ const PriceExplorerAnimatedNumber = ({ ...@@ -254,7 +288,7 @@ const PriceExplorerAnimatedNumber = ({
} }
}) })
const hidePlacehodler = (): void => { const hidePlaceholder = (): void => {
hideShimmer.value = true hideShimmer.value = true
} }
...@@ -274,7 +308,7 @@ const PriceExplorerAnimatedNumber = ({ ...@@ -274,7 +308,7 @@ const PriceExplorerAnimatedNumber = ({
<View style={RowWrapper.wrapperStyle}> <View style={RowWrapper.wrapperStyle}>
<TopAndBottomGradient /> <TopAndBottomGradient />
{currency.symbolAtFront && currencySymbol} {currency.symbolAtFront && currencySymbol}
{Numbers({ price, hidePlacehodler, numberOfDigits, currency })} {Numbers({ price, hidePlaceholder, numberOfDigits, currency })}
{!currency.symbolAtFront && currencySymbol} {!currency.symbolAtFront && currencySymbol}
</View> </View>
</> </>
...@@ -295,3 +329,12 @@ export const Shimmer = StyleSheet.create({ ...@@ -295,3 +329,12 @@ export const Shimmer = StyleSheet.create({
width: 200, width: 200,
}, },
}) })
const AnimatedFontStyles = StyleSheet.create({
fontStyle: {
fontFamily: fonts.heading2.family,
fontSize: fonts.heading2.fontSize,
fontWeight: fonts.heading2.fontWeight,
lineHeight: fonts.heading2.lineHeight,
},
})
import React from 'react' import React from 'react'
import * as charts from 'react-native-wagmi-charts' import * as charts from 'react-native-wagmi-charts'
import { DatetimeText, PriceText, RelativeChangeText } from 'src/components/PriceExplorer/Text' import { DatetimeText, PriceText, RelativeChangeText } from 'src/components/PriceExplorer/Text'
import { Amounts } from 'src/test/gqlFixtures'
import { render, within } from 'src/test/test-utils' import { render, within } from 'src/test/test-utils'
import { Amounts } from 'wallet/src/test/gqlFixtures'
jest.mock('react-native-wagmi-charts') jest.mock('react-native-wagmi-charts')
const mockedUseLineChartPrice = charts.useLineChartPrice as jest.Mock const mockedUseLineChartPrice = charts.useLineChartPrice as jest.Mock
...@@ -47,8 +47,9 @@ describe(PriceText, () => { ...@@ -47,8 +47,9 @@ describe(PriceText, () => {
const animatedText = await tree.findByTestId('price-text') const animatedText = await tree.findByTestId('price-text')
const wholePart = await within(animatedText).findByTestId('wholePart') const wholePart = await within(animatedText).findByTestId('wholePart')
const decimalPart = await within(animatedText).findByTestId('decimalPart') const decimalPart = await within(animatedText).findByTestId('decimalPart')
expect(wholePart.props.animatedProps.text).toBe(`$${Amounts.sm.value}`)
expect(decimalPart.props.animatedProps.text).toBe(`.00`) expect(wholePart.props.text).toBe(`$${Amounts.sm.value}`)
expect(decimalPart.props.text).toBe(`.00`)
}) })
}) })
...@@ -56,11 +57,11 @@ describe(RelativeChangeText, () => { ...@@ -56,11 +57,11 @@ describe(RelativeChangeText, () => {
it('renders without error', () => { it('renders without error', () => {
mockedUseLineChart.mockReturnValue({ mockedUseLineChart.mockReturnValue({
isActive: { value: false }, isActive: { value: false },
data: [{ value: 10 }, { value: 9 }],
currentIndex: { value: 1 },
}) })
const tree = render( const tree = render(<RelativeChangeText loading={false} />)
<RelativeChangeText loading={false} spotRelativeChange={{ value: Amounts.md.value }} />
)
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
...@@ -68,11 +69,11 @@ describe(RelativeChangeText, () => { ...@@ -68,11 +69,11 @@ describe(RelativeChangeText, () => {
it('renders loading state', () => { it('renders loading state', () => {
mockedUseLineChart.mockReturnValue({ mockedUseLineChart.mockReturnValue({
isActive: { value: false }, isActive: { value: false },
data: [{ value: 10 }, { value: 9 }],
currentIndex: { value: 1 },
}) })
const tree = render( const tree = render(<RelativeChangeText loading={true} />)
<RelativeChangeText loading={true} spotRelativeChange={{ value: Amounts.md.value }} />
)
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
......
import React from 'react' import React from 'react'
import { SharedValue, useAnimatedStyle } from 'react-native-reanimated' import { useAnimatedStyle } from 'react-native-reanimated'
import { useLineChartDatetime } from 'react-native-wagmi-charts' import { useLineChartDatetime } from 'react-native-wagmi-charts'
import { AnimatedText } from 'src/components/text/AnimatedText' import { AnimatedText } from 'src/components/text/AnimatedText'
import { Flex, Icons, useSporeColors } from 'ui/src' import { Flex, Icons, useSporeColors } from 'ui/src'
...@@ -10,13 +10,7 @@ import { isAndroid } from 'wallet/src/utils/platform' ...@@ -10,13 +10,7 @@ import { isAndroid } from 'wallet/src/utils/platform'
import { AnimatedDecimalNumber } from './AnimatedDecimalNumber' import { AnimatedDecimalNumber } from './AnimatedDecimalNumber'
import { useLineChartPrice, useLineChartRelativeChange } from './usePrice' import { useLineChartPrice, useLineChartRelativeChange } from './usePrice'
export function PriceText({ export function PriceText({ maxWidth }: { loading: boolean; maxWidth?: number }): JSX.Element {
loading,
maxWidth,
}: {
loading: boolean
maxWidth?: number
}): JSX.Element {
const price = useLineChartPrice() const price = useLineChartPrice()
const colors = useSporeColors() const colors = useSporeColors()
const currency = useAppFiatCurrency() const currency = useAppFiatCurrency()
...@@ -44,16 +38,10 @@ export function PriceText({ ...@@ -44,16 +38,10 @@ export function PriceText({
) )
} }
export function RelativeChangeText({ export function RelativeChangeText({ loading }: { loading: boolean }): JSX.Element {
loading,
spotRelativeChange,
}: {
loading: boolean
spotRelativeChange?: SharedValue<number>
}): JSX.Element {
const colors = useSporeColors() const colors = useSporeColors()
const relativeChange = useLineChartRelativeChange({ spotRelativeChange }) const relativeChange = useLineChartRelativeChange()
const styles = useAnimatedStyle(() => ({ const styles = useAnimatedStyle(() => ({
color: relativeChange.value.value > 0 ? colors.statusSuccess.val : colors.statusCritical.val, color: relativeChange.value.value > 0 ? colors.statusSuccess.val : colors.statusCritical.val,
...@@ -102,7 +90,9 @@ export function DatetimeText({ loading }: { loading: boolean }): JSX.Element | n ...@@ -102,7 +90,9 @@ export function DatetimeText({ loading }: { loading: boolean }): JSX.Element | n
// `datetime` when scrubbing the chart // `datetime` when scrubbing the chart
const datetime = useLineChartDatetime({ locale }) const datetime = useLineChartDatetime({ locale })
if (loading) return null if (loading) {
return null
}
return <AnimatedText color="$neutral2" text={datetime.formatted} variant="body1" /> return <AnimatedText color="$neutral2" text={datetime.formatted} variant="body1" />
} }
...@@ -25,7 +25,9 @@ export function TimeRangeLabel({ index, label, selectedIndex, transition }: Prop ...@@ -25,7 +25,9 @@ export function TimeRangeLabel({ index, label, selectedIndex, transition }: Prop
const style = useAnimatedStyle(() => { const style = useAnimatedStyle(() => {
const selected = index === selectedIndex.value const selected = index === selectedIndex.value
if (!selected) return { color: colors.neutral2.val } if (!selected) {
return { color: colors.neutral2.val }
}
const color = interpolateColor( const color = interpolateColor(
transition.value, transition.value,
......
import { renderHook } from '@testing-library/react-native'
import { Dimensions } from 'react-native'
import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions'
import { heightBreakpoints } from 'ui/src/theme'
const sharedDimensions = {
height: 1000,
width: 1000,
scale: 1,
fontScale: 1,
}
describe(useChartDimensions, () => {
it('returns small chart height for small screens', () => {
jest
.spyOn(Dimensions, 'get')
.mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short - 1 })
const { result } = renderHook(() => useChartDimensions())
expect(result.current).toEqual({
chartHeight: 130,
chartWidth: 1000,
buttonWidth: expect.any(Number),
labelWidth: expect.any(Number),
})
})
it('returns large chart height for large screens', () => {
jest
.spyOn(Dimensions, 'get')
.mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short })
const { result } = renderHook(() => useChartDimensions())
expect(result.current).toEqual({
chartHeight: 215,
chartWidth: 1000,
buttonWidth: expect.any(Number),
labelWidth: expect.any(Number),
})
})
})
import { waitFor } from '@testing-library/react-native'
import { makeMutable } from 'react-native-reanimated'
import {
TLineChartData,
useLineChart,
useLineChartPrice as useRNWagmiChartLineChartPrice,
} from 'react-native-wagmi-charts'
import { act } from 'react-test-renderer'
import { renderHookWithProviders } from 'src/test/render'
import { useLineChartPrice, useLineChartRelativeChange } from './usePrice'
jest.mock('react-native-wagmi-charts')
const cursorValue = makeMutable('')
const cursorFormattedValue = makeMutable('-')
const currentIndex = makeMutable(0)
const isActive = makeMutable(false)
const mockData = (
args: { data?: TLineChartData; currentIndex?: number; isActive?: boolean } = {}
): void => {
currentIndex.value = args.currentIndex ?? 0
isActive.value = args.isActive ?? false
// react-native-wagmi-charts is mocked so we can mock the return
// of useLineChart
const mockedFunction = useLineChart as ReturnType<typeof jest.fn>
mockedFunction.mockReturnValue({
data: args.data ?? [],
currentIndex,
isActive,
})
}
const mockCursorPrice = (value?: string): void => {
cursorValue.value = value ?? ''
cursorFormattedValue.value = value ? `$${value}` : '-'
// react-native-wagmi-charts is mocked so we can mock the return
// of useLineChartPrice
const mockedFunction = useRNWagmiChartLineChartPrice as ReturnType<typeof jest.fn>
mockedFunction.mockReturnValue({
value: cursorValue,
formatted: cursorFormattedValue,
})
}
describe(useLineChartPrice, () => {
beforeEach(() => {
const originalModule = jest.requireActual('react-native-wagmi-charts')
;(useLineChart as ReturnType<typeof jest.fn>).mockImplementation(originalModule.useLineChart)
;(useRNWagmiChartLineChartPrice as ReturnType<typeof jest.fn>).mockImplementation(
originalModule.useLineChartPrice
)
})
afterAll(() => {
jest.resetAllMocks()
})
it('returns correct initial values', () => {
const { result } = renderHookWithProviders(useLineChartPrice)
expect(result.current).toEqual({
value: expect.objectContaining({ value: 0 }),
formatted: expect.objectContaining({ value: '-' }),
shouldAnimate: expect.objectContaining({ value: true }),
})
})
describe('when there is no active cursor price', () => {
beforeEach(() => {
// Mock data before all test to show that the currentSpot has higher
// priority than the last value from data
mockData({
data: [
{ value: 1, timestamp: 1 },
{ value: 2, timestamp: 2 },
],
})
})
it('returns last value from data if currentSpot is not provided', async () => {
const { result, rerender } = renderHookWithProviders(useLineChartPrice)
expect(result.current).toEqual({
value: expect.objectContaining({ value: 2 }),
formatted: expect.objectContaining({ value: '$2.00' }),
shouldAnimate: expect.objectContaining({ value: true }),
})
// Update data
mockData({
data: [
{ value: 1, timestamp: 1 },
{ value: 2, timestamp: 2 },
{ value: 3, timestamp: 3 },
],
})
// Re-render to trigger the update (normally the useLineChart hook
// would trigger re-render)
await act(() => rerender())
await waitFor(() => {
expect(result.current).toEqual({
value: expect.objectContaining({ value: 3 }),
formatted: expect.objectContaining({ value: '$3.00' }),
shouldAnimate: expect.objectContaining({ value: true }),
})
})
})
it('returns currentSpot if it is provided', async () => {
const { result, rerender } = renderHookWithProviders(useLineChartPrice, {
initialProps: [1],
})
expect(result.current).toEqual({
value: expect.objectContaining({ value: 1 }),
formatted: expect.objectContaining({ value: '$1.00' }),
shouldAnimate: expect.objectContaining({ value: true }),
})
await act(() => {
rerender([2])
})
await waitFor(() => {
expect(result.current).toEqual({
value: expect.objectContaining({ value: 2 }),
formatted: expect.objectContaining({ value: '$2.00' }),
shouldAnimate: expect.objectContaining({ value: true }),
})
})
})
})
describe('when there is an active cursor price', () => {
beforeEach(() => {
// Mock data before all test to show that the currentSpot has higher
// priority than the last value from data
mockData({
data: [
{ value: 1, timestamp: 1 },
{ value: 2, timestamp: 2 },
],
})
})
it('returns active cursor price even if currentSpot and data are provided', async () => {
mockCursorPrice('3')
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [4],
})
expect(result.current).toEqual({
value: expect.objectContaining({ value: 3 }),
formatted: expect.objectContaining({ value: '$3.00' }),
shouldAnimate: expect.objectContaining({ value: true }),
})
})
it('updates returned active cursor price when it changes', async () => {
mockCursorPrice('1')
const { result } = renderHookWithProviders(useLineChartPrice, {
initialProps: [4],
})
expect(result.current).toEqual(
expect.objectContaining({
value: expect.objectContaining({ value: 1 }),
formatted: expect.objectContaining({ value: '$1.00' }),
})
)
mockCursorPrice('2') // updates shared values
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({
value: expect.objectContaining({ value: 2 }),
formatted: expect.objectContaining({ value: '$2.00' }),
})
)
})
})
it('sets shouldAnimate to false when cursor price changes', async () => {
mockCursorPrice() // uze mocked value and formatted value
const { result } = renderHookWithProviders(useLineChartPrice)
// first update (previous value will be null as it's the first update after initial render)
mockCursorPrice('1')
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({
value: expect.objectContaining({ value: 1 }),
shouldAnimate: expect.objectContaining({ value: true }),
})
)
})
// second update (shouldAnimate should be false when the chart is
// scrubbed and the cursor price changes)
mockCursorPrice('2')
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({
value: expect.objectContaining({ value: 2 }),
shouldAnimate: expect.objectContaining({ value: false }),
})
)
})
})
})
})
describe(useLineChartRelativeChange, () => {
const chartData1 = [
{ timestamp: 1, value: 1 },
{ timestamp: 2, value: 0.1 },
{ timestamp: 3, value: 10 },
{ timestamp: 4, value: 5 },
]
const chartData2 = [
{ timestamp: 1, value: 1 },
{ timestamp: 2, value: 0.1 },
{ timestamp: 3, value: 10 },
{ timestamp: 4, value: 20 },
]
beforeAll(() => {
mockData()
})
it('returns correct initial values', () => {
const { result } = renderHookWithProviders(() => useLineChartRelativeChange())
expect(result.current).toEqual({
value: expect.objectContaining({ value: 0 }),
formatted: expect.objectContaining({ value: '0.00%' }),
})
})
describe('when spotRelativeChange is not provided', () => {
it('calculates relative change based on the open and close price values', () => {
mockData({ data: chartData1 })
const { result } = renderHookWithProviders(() => useLineChartRelativeChange())
// 1 -> 5 (+400%)
expect(result.current).toEqual({
value: expect.objectContaining({ value: 400 }),
formatted: expect.objectContaining({ value: '400.00%' }),
})
})
it('updates the relative change when the currentIndex changes when active', async () => {
mockData({ data: chartData1 })
const { result } = renderHookWithProviders(() => useLineChartRelativeChange())
// 1 -> 5 (+400%)
expect(result.current).toEqual(
expect.objectContaining({
value: expect.objectContaining({ value: 400 }),
formatted: expect.objectContaining({ value: '400.00%' }),
})
)
currentIndex.value = 2
isActive.value = true
// 1 -> 10 (+900%)
await waitFor(() => {
expect(result.current).toEqual(
expect.objectContaining({
value: expect.objectContaining({ value: 900 }),
formatted: expect.objectContaining({ value: '900.00%' }),
})
)
})
})
it('updates the relative change when the data changes', async () => {
mockData({ data: chartData1 })
const { result, rerender } = renderHookWithProviders(() => useLineChartRelativeChange())
// 1 -> 5 (+400%)
expect(result.current).toEqual({
value: expect.objectContaining({ value: 400 }),
formatted: expect.objectContaining({ value: '400.00%' }),
})
await act(() => {
mockData({ data: chartData2 })
// Trigger rerender (it will be normally triggered when the data
// returned from the useLineChart hook changes)
rerender()
})
// 1 -> 20 (+1900%)
await waitFor(() => {
expect(result.current).toEqual({
value: expect.objectContaining({ value: 1900 }),
formatted: expect.objectContaining({ value: '1900.00%' }),
})
})
})
})
})
...@@ -13,17 +13,20 @@ import { numberToLocaleStringWorklet, numberToPercentWorklet } from 'src/utils/r ...@@ -13,17 +13,20 @@ import { numberToLocaleStringWorklet, numberToPercentWorklet } from 'src/utils/r
import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { useCurrentLocale } from 'wallet/src/features/language/hooks' import { useCurrentLocale } from 'wallet/src/features/language/hooks'
export type ValueAndFormatted<U = number, V = string, B = boolean> = { export type ValueAndFormatted = {
value: Readonly<SharedValue<U>> value: Readonly<SharedValue<number>>
formatted: Readonly<SharedValue<V>> formatted: Readonly<SharedValue<string>>
shouldAnimate: Readonly<SharedValue<B>> }
export type ValueAndFormattedWithAnimation = ValueAndFormatted & {
shouldAnimate: Readonly<SharedValue<boolean>>
} }
/** /**
* Wrapper around react-native-wagmi-chart#useLineChartPrice * Wrapper around react-native-wagmi-chart#useLineChartPrice
* @returns latest price when not scrubbing and active price when scrubbing * @returns latest price when not scrubbing and active price when scrubbing
*/ */
export function useLineChartPrice(currentSpot?: number): ValueAndFormatted { export function useLineChartPrice(currentSpot?: number): ValueAndFormattedWithAnimation {
const { value: activeCursorPrice } = useRNWagmiChartLineChartPrice({ const { value: activeCursorPrice } = useRNWagmiChartLineChartPrice({
// do not round // do not round
precision: 18, precision: 18,
...@@ -55,14 +58,15 @@ export function useLineChartPrice(currentSpot?: number): ValueAndFormatted { ...@@ -55,14 +58,15 @@ export function useLineChartPrice(currentSpot?: number): ValueAndFormatted {
return currentSpot ?? data[data.length - 1]?.value ?? 0 return currentSpot ?? data[data.length - 1]?.value ?? 0
}) })
const priceFormatted = useDerivedValue(() => { const priceFormatted = useDerivedValue(() => {
const { symbol, code } = currencyInfo
return numberToLocaleStringWorklet( return numberToLocaleStringWorklet(
price.value, price.value,
locale, locale,
{ {
style: 'currency', style: 'currency',
currency: currencyInfo.code, currency: code,
}, },
currencyInfo.symbol symbol
) )
}) })
...@@ -80,24 +84,10 @@ export function useLineChartPrice(currentSpot?: number): ValueAndFormatted { ...@@ -80,24 +84,10 @@ export function useLineChartPrice(currentSpot?: number): ValueAndFormatted {
* @returns % change for the active history duration when not scrubbing and % * @returns % change for the active history duration when not scrubbing and %
* change between active index and period start when scrubbing * change between active index and period start when scrubbing
*/ */
export function useLineChartRelativeChange({ export function useLineChartRelativeChange(): ValueAndFormatted {
spotRelativeChange,
}: {
spotRelativeChange?: SharedValue<number>
}): ValueAndFormatted {
const { currentIndex, data, isActive } = useLineChart() const { currentIndex, data, isActive } = useLineChart()
const shouldAnimate = useSharedValue(false)
const relativeChange = useDerivedValue(() => { const relativeChange = useDerivedValue(() => {
if (!isActive.value && Boolean(spotRelativeChange)) {
// break early when chart is not active (scrubbing) and spot relative
// change is available
// this should only happen for the daily HistoryDuration where calculating
// relative change from historical data leads to data inconsistencies in
// the ui
return spotRelativeChange?.value ?? 0
}
// when scrubbing, compute relative change from open price // when scrubbing, compute relative change from open price
const openPrice = data[0]?.value const openPrice = data[0]?.value
...@@ -116,9 +106,9 @@ export function useLineChartRelativeChange({ ...@@ -116,9 +106,9 @@ export function useLineChartRelativeChange({
return change return change
}) })
const relativeChangeFormattted = useDerivedValue(() => { const relativeChangeFormatted = useDerivedValue(() => {
return numberToPercentWorklet(relativeChange.value, { precision: 2, absolute: true }) return numberToPercentWorklet(relativeChange.value, { precision: 2, absolute: true })
}) })
return { value: relativeChange, formatted: relativeChangeFormattted, shouldAnimate } return { value: relativeChange, formatted: relativeChangeFormatted }
} }
...@@ -3,6 +3,7 @@ import { PermissionStatus } from 'expo-modules-core' ...@@ -3,6 +3,7 @@ import { PermissionStatus } from 'expo-modules-core'
import React, { memo, useCallback, useMemo, useState } from 'react' import React, { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native' import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native'
import { launchImageLibrary } from 'react-native-image-picker'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg'
import PasteButton from 'src/components/buttons/PasteButton' import PasteButton from 'src/components/buttons/PasteButton'
...@@ -52,15 +53,54 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -52,15 +53,54 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
const [permissionResponse, requestPermissionResponse] = BarCodeScanner.usePermissions() const [permissionResponse, requestPermissionResponse] = BarCodeScanner.usePermissions()
const permissionStatus = permissionResponse?.status const permissionStatus = permissionResponse?.status
const [isReadingImageFile, setIsReadingImageFile] = useState(false)
const [overlayLayout, setOverlayLayout] = useState<LayoutRectangle | null>() const [overlayLayout, setOverlayLayout] = useState<LayoutRectangle | null>()
const [infoLayout, setInfoLayout] = useState<LayoutRectangle | null>() const [infoLayout, setInfoLayout] = useState<LayoutRectangle | null>()
const [connectionLayout, setConnectionLayout] = useState<LayoutRectangle | null>() const [bottomLayout, setBottomLayout] = useState<LayoutRectangle | null>()
const handleBarCodeScanned = (result: BarCodeScannerResult): void => { const handleBarCodeScanned = useCallback(
if (shouldFreezeCamera) return (result: BarCodeScannerResult): void => {
const data = result?.data if (shouldFreezeCamera) {
onScanCode(data) return
} }
const data = result?.data
onScanCode(data)
setIsReadingImageFile(false)
},
[onScanCode, shouldFreezeCamera]
)
const onPickImageFilePress = useCallback(async (): Promise<void> => {
if (isReadingImageFile) {
return
}
setIsReadingImageFile(true)
const response = await launchImageLibrary({
mediaType: 'photo',
selectionLimit: 1,
})
const uri = response.assets?.[0]?.uri
if (!uri) {
setIsReadingImageFile(false)
return
}
const result = (
await BarCodeScanner.scanFromURLAsync(uri, [BarCodeScanner.Constants.BarCodeType.qr])
)[0]
if (!result) {
Alert.alert(t('No QR code found'))
setIsReadingImageFile(false)
return
}
handleBarCodeScanned(result)
}, [handleBarCodeScanned, isReadingImageFile, t])
// Check for camera permissions, handle cases where not granted or undetermined // Check for camera permissions, handle cases where not granted or undetermined
const getPermissionStatuses = useCallback(async (): Promise<void> => { const getPermissionStatuses = useCallback(async (): Promise<void> => {
...@@ -99,7 +139,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -99,7 +139,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
height={Math.max(dimensions.fullHeight, CAMERA_ASPECT_RATIO * dimensions.fullWidth)} height={Math.max(dimensions.fullHeight, CAMERA_ASPECT_RATIO * dimensions.fullWidth)}
overflow="hidden" overflow="hidden"
width={dimensions.fullWidth}> width={dimensions.fullWidth}>
{permissionStatus === PermissionStatus.GRANTED && ( {permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && (
<BarCodeScanner <BarCodeScanner
barCodeTypes={[BarCodeScanner.Constants.BarCodeType.qr]} barCodeTypes={[BarCodeScanner.Constants.BarCodeType.qr]}
style={StyleSheet.absoluteFillObject} style={StyleSheet.absoluteFillObject}
...@@ -193,20 +233,36 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -193,20 +233,36 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
</Flex> </Flex>
) : null} ) : null}
</DevelopmentOnly> </DevelopmentOnly>
{isWalletConnectModal && props.numConnections > 0 && (
<Flex
alignItems="center"
bottom={0}
gap="$spacing24"
position="absolute"
style={{
transform: [
{
translateY: bottomLayout ? bottomLayout.height + spacing.spacing24 : 0,
},
],
}}
onLayout={(event: LayoutChangeEvent): void =>
setBottomLayout(event.nativeEvent.layout)
}>
<Flex <Flex
bottom={0} centered
position="absolute" backgroundColor={colors.surface1.val}
style={{ borderRadius="$roundedFull"
transform: [ p="$spacing12"
{ onPress={onPickImageFilePress}>
translateY: connectionLayout ? connectionLayout.height + spacing.spacing24 : 0, {isReadingImageFile ? (
}, <SpinningLoader size={iconSizes.icon28} />
], ) : (
}} <Icons.Photo color="$neutral1" size={iconSizes.icon28} />
onLayout={(event: LayoutChangeEvent): void => )}
setConnectionLayout(event.nativeEvent.layout) </Flex>
}>
{isWalletConnectModal && props.numConnections > 0 && (
<Button <Button
fontFamily="$body" fontFamily="$body"
icon={<Icons.Global color="$neutral2" />} icon={<Icons.Global color="$neutral2" />}
...@@ -218,8 +274,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E ...@@ -218,8 +274,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
numConnections: props.numConnections, numConnections: props.numConnections,
})} })}
</Button> </Button>
</Flex> )}
)} </Flex>
</Flex> </Flex>
</Flex> </Flex>
</AnimatedFlex> </AnimatedFlex>
...@@ -242,7 +298,9 @@ const GradientOverlay = memo(function GradientOverlay({ ...@@ -242,7 +298,9 @@ const GradientOverlay = memo(function GradientOverlay({
const [size, setSize] = useState<{ width: number; height: number } | null>(null) const [size, setSize] = useState<{ width: number; height: number } | null>(null)
const pathWithHole = useMemo(() => { const pathWithHole = useMemo(() => {
if (!size) return '' if (!size) {
return ''
}
const { width: W, height: H } = size const { width: W, height: H } = size
const iconMaskOffset = SCAN_ICON_MASK_OFFSET_RATIO * scannerSize const iconMaskOffset = SCAN_ICON_MASK_OFFSET_RATIO * scannerSize
const paddingX = Math.max(0, (W - scannerSize) / 2) + iconMaskOffset const paddingX = Math.max(0, (W - scannerSize) / 2) + iconMaskOffset
......
import React from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { AddressDisplay } from 'src/components/AddressDisplay' import { AddressDisplay } from 'src/components/AddressDisplay'
import { GradientBackground } from 'src/components/gradients/GradientBackground' import { GradientBackground } from 'src/components/gradients/GradientBackground'
import { UniconThemedGradient } from 'src/components/gradients/UniconThemedGradient' import { UniconThemedGradient } from 'src/components/gradients/UniconThemedGradient'
import WarningModal from 'src/components/modals/WarningModal/WarningModal'
import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode' import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode'
import { LearnMoreLink } from 'src/components/text/LearnMoreLink' import { LearnMoreLink } from 'src/components/text/LearnMoreLink'
import { useUniconColors } from 'src/components/unicons/utils' import { useUniconColors } from 'src/components/unicons/utils'
import { AnimatedFlex, Text, useMedia, useSporeColors } from 'ui/src' import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos'
import { spacing } from 'ui/src/theme' import { ModalName } from 'src/features/telemetry/constants'
import { AnimatedFlex, Flex, Icons, Text, useMedia, useSporeColors } from 'ui/src'
import { iconSizes, spacing } from 'ui/src/theme'
import { ALL_SUPPORTED_CHAIN_IDS } from 'wallet/src/constants/chains'
import { uniswapUrls } from 'wallet/src/constants/urls' import { uniswapUrls } from 'wallet/src/constants/urls'
import { useIsDarkMode } from 'wallet/src/features/appearance/hooks' import { useIsDarkMode } from 'wallet/src/features/appearance/hooks'
...@@ -21,13 +25,16 @@ export function WalletQRCode({ address }: Props): JSX.Element | null { ...@@ -21,13 +25,16 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const gradientData = useUniconColors(address) const gradientData = useUniconColors(address)
const { t } = useTranslation() const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const media = useMedia() const media = useMedia()
const QR_CODE_SIZE = media.short ? 175 : 220 const QR_CODE_SIZE = media.short ? 175 : 220
const UNICON_SIZE = QR_CODE_SIZE / 2.8 const UNICON_SIZE = QR_CODE_SIZE / 2.8
if (!address) return null if (!address) {
return null
}
return ( return (
<> <>
...@@ -70,12 +77,40 @@ export function WalletQRCode({ address }: Props): JSX.Element | null { ...@@ -70,12 +77,40 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
size={QR_CODE_SIZE} size={QR_CODE_SIZE}
/> />
<Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3"> <Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3">
{t( {t('You can send tokens on all of our supported networks to this address.')}
'Only send tokens on Ethereum, Arbitrum, Optimism, Polygon, Base, BNB networks to this address.'
)}{' '}
</Text> </Text>
<LearnMoreLink url={uniswapUrls.helpArticleUrls.supportedNetworks} /> <Flex row gap="$spacing4">
<NetworkLogos negativeGap chains={ALL_SUPPORTED_CHAIN_IDS} />
<Icons.RotatableChevron
color="$neutral3"
direction="down"
height={iconSizes.icon20}
width={iconSizes.icon20}
onPress={(): void => setShowModal(true)}
/>
</Flex>
</AnimatedFlex> </AnimatedFlex>
{showModal && (
<WarningModal
backgroundIconColor={colors.surface1.val}
caption={t(
'Uniswap Wallet supports tokens on Ethereum, Polygon, Arbitrum, Optimism, Base, and BNB Chain. Right now, we only support NFTs on Ethereum.'
)}
closeText={t('Close')}
icon={
<NetworkLogos
centered
negativeGap
chains={ALL_SUPPORTED_CHAIN_IDS}
size={iconSizes.icon28}
/>
}
modalName={ModalName.QRCodeNetworkInfo}
title={t('Supported Networks')}
onClose={(): void => setShowModal(false)}>
<LearnMoreLink url={uniswapUrls.helpArticleUrls.supportedNetworks} />
</WarningModal>
)}
</> </>
) )
} }
...@@ -33,7 +33,9 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E ...@@ -33,7 +33,9 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
const onScanCode = async (uri: string): Promise<void> => { const onScanCode = async (uri: string): Promise<void> => {
// don't scan any QR codes if camera is frozen // don't scan any QR codes if camera is frozen
if (shouldFreezeCamera) return if (shouldFreezeCamera) {
return
}
await selectionAsync() await selectionAsync()
setShouldFreezeCamera(true) setShouldFreezeCamera(true)
......
import { faker } from '@faker-js/faker'
import {
AutocompleteOption,
filterRecipientByNameAndAddress,
filterRecipientsByAddress,
filterRecipientsByName,
} from 'src/components/RecipientSelect/filter'
import { SearchableRecipient } from 'wallet/src/features/address/types'
import { SearchableRecipients } from 'wallet/src/test/fixtures'
const options: [AutocompleteOption<SearchableRecipient>, AutocompleteOption<SearchableRecipient>] =
[
{
data: SearchableRecipients[0],
key: SearchableRecipients[0].address,
},
{
data: SearchableRecipients[1],
key: SearchableRecipients[1].address,
},
]
describe(filterRecipientsByName, () => {
it('returns empty array if searchPattern is empty', () => {
expect(filterRecipientsByName(null, [])).toEqual([])
expect(filterRecipientsByName('', [])).toEqual([])
})
it('returns all exact matches and similar recipients', () => {
expect(filterRecipientsByName('Recipient ', options)).toEqual(options)
// Even though there is no recipient with name "Recipient 3 name", it should
// still return all recipients with similar names
expect(filterRecipientsByName('Recipient 3', options)).toEqual(options)
expect(filterRecipientsByName('Recipient 3 name', options)).toEqual(options)
})
it('does not return recipients with names that differ too much', () => {
const newAddress = faker.finance.ethereumAddress()
const newOption: AutocompleteOption<SearchableRecipient> = {
data: {
address: newAddress,
name: 'Very different name',
},
key: newAddress,
}
const updatedOptions = [...options, newOption]
// Only the new one is returned
expect(filterRecipientsByName('Very different name', updatedOptions)).toEqual([newOption])
// All similar names are returned (except the new one that is too different)
expect(filterRecipientsByName('Recipient 3', updatedOptions)).toEqual(options)
})
it('returns the same result irrespective of the casing', () => {
expect(filterRecipientsByName('recipient 3', options)).toEqual(options)
expect(filterRecipientsByName('RECIPIENT 3', options)).toEqual(options)
expect(filterRecipientsByName('recipient 3 name', options)).toEqual(options)
expect(filterRecipientsByName('RECIPIENT 3 NAME', options)).toEqual(options)
})
})
describe(filterRecipientsByAddress, () => {
it('returns empty array if searchPattern is empty', () => {
expect(filterRecipientsByAddress(null, [])).toEqual([])
expect(filterRecipientsByAddress('', [])).toEqual([])
})
it('returns all exact matches without similar addresses', () => {
expect(filterRecipientsByAddress('0x', options)).toEqual(options)
// Returns only the first one as it has exactly the same beginning
expect(filterRecipientsByAddress(options[0].data.address.slice(0, 3), options)).toEqual([
options[0],
])
// Returns only the second one as it has exactly the same beginning
expect(filterRecipientsByAddress(options[1].data.address.slice(0, 3), options)).toEqual([
options[1],
])
})
it('returns the same result irrespective of the casing', () => {
expect(filterRecipientsByAddress(options[0].data.address.toLowerCase(), options)).toEqual([
options[0],
])
expect(filterRecipientsByAddress(options[0].data.address.toUpperCase(), options)).toEqual([
options[0],
])
})
})
describe(filterRecipientByNameAndAddress, () => {
const option1: AutocompleteOption<SearchableRecipient> = {
data: {
address: '0x123',
name: 'Recipient123',
},
key: '0x123',
}
const option2: AutocompleteOption<SearchableRecipient> = {
data: {
address: '0x456',
name: 'Recipient2',
},
key: '0x456',
}
const option3: AutocompleteOption<SearchableRecipient> = {
data: {
address: '0x789',
name: 'Recipient0x456123',
},
key: '0x789',
}
const newOptions = [option1, option2, option3]
it('returns empty array if searchPattern is empty', () => {
expect(filterRecipientByNameAndAddress(null, [])).toEqual([])
expect(filterRecipientByNameAndAddress('', [])).toEqual([])
})
it('returns recipients matching by name (exact and similar) or address (exact)', () => {
// All names are matched
expect(filterRecipientByNameAndAddress('Recipient', newOptions)).toEqual(newOptions)
// All options are matched by name (option1 is exact, option2 and option3 are similar)
expect(filterRecipientByNameAndAddress('Recipient123', newOptions)).toEqual(newOptions)
// option1 is matched by name and address, option3 is matched by name
expect(filterRecipientByNameAndAddress('123', newOptions)).toEqual([option1, option3])
// option2 is matched by name and address, option3 is matched by name
expect(filterRecipientByNameAndAddress('0x456', newOptions)).toEqual([option2, option3])
// only option3 is matched by name as address must be exact
expect(filterRecipientByNameAndAddress('456', newOptions)).toEqual([option3])
})
})
...@@ -2,7 +2,7 @@ import Fuse from 'fuse.js' ...@@ -2,7 +2,7 @@ import Fuse from 'fuse.js'
import { unique } from 'utilities/src/primitives/array' import { unique } from 'utilities/src/primitives/array'
import { SearchableRecipient } from 'wallet/src/features/address/types' import { SearchableRecipient } from 'wallet/src/features/address/types'
type AutocompleteOption<T> = { data: T; key: string } export type AutocompleteOption<T> = { data: T; key: string }
const defaultOptions: Fuse.IFuseOptions<AutocompleteOption<SearchableRecipient>> = { const defaultOptions: Fuse.IFuseOptions<AutocompleteOption<SearchableRecipient>> = {
includeMatches: true, includeMatches: true,
...@@ -27,7 +27,9 @@ export function filterRecipientsByName( ...@@ -27,7 +27,9 @@ export function filterRecipientsByName(
searchPattern: string | null, searchPattern: string | null,
list: AutocompleteOption<SearchableRecipient>[] list: AutocompleteOption<SearchableRecipient>[]
): AutocompleteOption<SearchableRecipient>[] { ): AutocompleteOption<SearchableRecipient>[] {
if (!searchPattern) return [] if (!searchPattern) {
return []
}
const fuse = new Fuse(list, searchNameOptions) const fuse = new Fuse(list, searchNameOptions)
...@@ -40,7 +42,9 @@ export function filterRecipientsByAddress( ...@@ -40,7 +42,9 @@ export function filterRecipientsByAddress(
searchPattern: string | null, searchPattern: string | null,
list: AutocompleteOption<SearchableRecipient>[] list: AutocompleteOption<SearchableRecipient>[]
): AutocompleteOption<SearchableRecipient>[] { ): AutocompleteOption<SearchableRecipient>[] {
if (!searchPattern) return [] if (!searchPattern) {
return []
}
const fuse = new Fuse(list, searchAddressOptions) const fuse = new Fuse(list, searchAddressOptions)
...@@ -53,7 +57,9 @@ export function filterRecipientByNameAndAddress( ...@@ -53,7 +57,9 @@ export function filterRecipientByNameAndAddress(
searchPattern: string | null, searchPattern: string | null,
list: AutocompleteOption<SearchableRecipient>[] list: AutocompleteOption<SearchableRecipient>[]
): AutocompleteOption<SearchableRecipient>[] { ): AutocompleteOption<SearchableRecipient>[] {
if (!searchPattern) return [] if (!searchPattern) {
return []
}
// run both fuses and remove dupes // run both fuses and remove dupes
return unique( return unique(
......
This diff is collapsed.
...@@ -53,9 +53,10 @@ export function useRecipients(): { ...@@ -53,9 +53,10 @@ export function useRecipients(): {
const sections = useMemo(() => { const sections = useMemo(() => {
const sectionsArr = [] const sectionsArr = []
if (validatedAddressRecipient.length) { if (validatedAddressRecipient.length) {
sectionsArr.push({ sectionsArr.push({
title: t('Search Results'), title: t('Search results'),
data: validatedAddressRecipient, data: validatedAddressRecipient,
}) })
} }
......
import { filterSections } from 'src/components/RecipientSelect/utils'
import {
RecipientSections,
SAMPLE_SEED_ADDRESS_1,
SAMPLE_SEED_ADDRESS_2,
} from 'wallet/src/test/fixtures'
describe(filterSections, () => {
it('returns empty array if filteredAddresses is empty', () => {
expect(filterSections(RecipientSections, [])).toEqual([])
})
it('filters out empty sections', () => {
// SAMPLE_SEED_ADDRESS_1 and SAMPLE_SEED_ADDRESS_2 are all addresses used in the fixture
expect(
filterSections(RecipientSections, [SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2])
).toEqual([RecipientSections[0], RecipientSections[1], RecipientSections[3]])
})
it('returns sections corresponding to the filtered addresses with matching addresses', () => {
expect(filterSections(RecipientSections, [SAMPLE_SEED_ADDRESS_1])).toEqual([
{
title: RecipientSections[0].title,
data: [RecipientSections[0].data[0]], // only the first item in the first section matches
},
RecipientSections[1],
])
expect(filterSections(RecipientSections, [SAMPLE_SEED_ADDRESS_2])).toEqual([
{
title: RecipientSections[0].title,
data: [RecipientSections[0].data[1]], // only the second item in the first section matches
},
RecipientSections[3],
])
})
})
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { CheckBox } from 'src/components/buttons/CheckBox'
import { SpinningLoader } from 'src/components/loading/SpinningLoader' import { SpinningLoader } from 'src/components/loading/SpinningLoader'
import { ElementName } from 'src/features/telemetry/constants' import { ElementName } from 'src/features/telemetry/constants'
import { Button, Flex, Text } from 'ui/src' import { Button, CheckBox, Flex, Text } from 'ui/src'
export function RemoveLastMnemonicWalletFooter({ export function RemoveLastMnemonicWalletFooter({
onPress, onPress,
...@@ -29,12 +28,9 @@ export function RemoveLastMnemonicWalletFooter({ ...@@ -29,12 +28,9 @@ export function RemoveLastMnemonicWalletFooter({
checked={checkBoxAccepted} checked={checkBoxAccepted}
text={ text={
<Flex> <Flex>
<Text color="$neutral1" variant="subheading2">
{t('I backed up my recovery phrase')}
</Text>
<Text color="$neutral2" variant="body3"> <Text color="$neutral2" variant="body3">
{t( {t(
'I understand that Uniswap Labs can’t help me recover my wallets if I failed to do so' 'I backed up my recovery phrase and understand that Uniswap Labs can’t help me recover my wallets if I failed to do so.'
)} )}
</Text> </Text>
</Flex> </Flex>
...@@ -42,7 +38,7 @@ export function RemoveLastMnemonicWalletFooter({ ...@@ -42,7 +38,7 @@ export function RemoveLastMnemonicWalletFooter({
onCheckPressed={onCheckPressed} onCheckPressed={onCheckPressed}
/> />
</Flex> </Flex>
<Flex centered row> <Flex centered row mt="$spacing8">
<Button <Button
fill fill
disabled={!checkBoxAccepted} disabled={!checkBoxAccepted}
......
...@@ -112,7 +112,9 @@ export function RemoveWalletModal(): JSX.Element | null { ...@@ -112,7 +112,9 @@ export function RemoveWalletModal(): JSX.Element | null {
const onPress = async (): Promise<void> => { const onPress = async (): Promise<void> => {
// we want to call onRemoveWallet only once // we want to call onRemoveWallet only once
if (inProgress) return if (inProgress) {
return
}
if (currentStep === RemoveWalletStep.Warning) { if (currentStep === RemoveWalletStep.Warning) {
setCurrentStep(RemoveWalletStep.Final) setCurrentStep(RemoveWalletStep.Final)
} else if (currentStep === RemoveWalletStep.Final) { } else if (currentStep === RemoveWalletStep.Final) {
...@@ -170,12 +172,12 @@ export function RemoveWalletModal(): JSX.Element | null { ...@@ -170,12 +172,12 @@ export function RemoveWalletModal(): JSX.Element | null {
<Text textAlign="center" variant="body1"> <Text textAlign="center" variant="body1">
{title} {title}
</Text> </Text>
<Text color="$neutral2" textAlign="center" variant="body2"> <Text color="$neutral2" textAlign="center" variant="body3">
{description} {description}
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>
<Flex centered gap="$spacing24"> <Flex centered gap="$spacing16">
{currentStep === RemoveWalletStep.Final && isRemovingRecoveryPhrase ? ( {currentStep === RemoveWalletStep.Final && isRemovingRecoveryPhrase ? (
<> <>
<AssociatedAccountsList accounts={associatedAccounts} /> <AssociatedAccountsList accounts={associatedAccounts} />
......
...@@ -89,7 +89,7 @@ export const useModalContent = ({ ...@@ -89,7 +89,7 @@ export const useModalContent = ({
<Trans t={t}> <Trans t={t}>
<Text color="$neutral1" variant="body1"> <Text color="$neutral1" variant="body1">
You’re removing your{' '} You’re removing your{' '}
<Text color="$statusCritical" variant="body1"> <Text color="$neutral1" variant="body1">
recovery phrase recovery phrase
</Text> </Text>
</Text> </Text>
...@@ -98,14 +98,14 @@ export const useModalContent = ({ ...@@ -98,14 +98,14 @@ export const useModalContent = ({
description: isAndroid ? ( description: isAndroid ? (
<Trans t={t}> <Trans t={t}>
Make sure you’ve written down your recovery phrase or backed it up on Google Drive.{' '} Make sure you’ve written down your recovery phrase or backed it up on Google Drive.{' '}
<Text color="$neutral2" maxFontSizeMultiplier={1.4} variant="buttonLabel3"> <Text color="$statusCritical" maxFontSizeMultiplier={1.4} variant="body3">
You will not be able to access your funds otherwise. You will not be able to access your funds otherwise.
</Text> </Text>
</Trans> </Trans>
) : ( ) : (
<Trans t={t}> <Trans t={t}>
Make sure you’ve written down your recovery phrase or backed it up on iCloud.{' '} Make sure you’ve written down your recovery phrase or backed it up on iCloud.{' '}
<Text color="$neutral2" maxFontSizeMultiplier={1.4} variant="buttonLabel3"> <Text color="$statusCritical" maxFontSizeMultiplier={1.4} variant="body3">
You will not be able to access your funds otherwise. You will not be able to access your funds otherwise.
</Text> </Text>
</Trans> </Trans>
......
...@@ -24,6 +24,7 @@ export function TokenDetailsHeader({ ...@@ -24,6 +24,7 @@ export function TokenDetailsHeader({
<Flex gap="$spacing12" mx="$spacing16"> <Flex gap="$spacing12" mx="$spacing16">
<TokenLogo <TokenLogo
chainId={fromGraphQLChain(token?.chain) ?? undefined} chainId={fromGraphQLChain(token?.chain) ?? undefined}
name={token?.project?.name ?? undefined}
symbol={token?.symbol ?? undefined} symbol={token?.symbol ?? undefined}
url={tokenProject?.logoUrl ?? undefined} url={tokenProject?.logoUrl ?? undefined}
/> />
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment