ci(release): publish latest release

parent 30ff2ecc
* @uniswap/web-admins
We are back with another (small) round of updates. Check out what is new below: IPFS hash of the deployment:
- CIDv0: `QmX4W6sgCHYCcR4SxeJVbZnj5N1GXR2ERPJomoBjVSbZVZ`
- CIDv1: `bafybeiebsyucxoiaieyyktifwl7glbepaklj4tncplc3rvssq55rpl553a`
Token Details Page Improvements — We took a pass at simplifying the core flows of token detail pages. We clarified the language around contract addresses and made it simpler than ever to copy them to your clipboard. In addition, we added a quick and easy route to our ‘receive’ flow from any given token details page. Share (and send) tokens easier than ever with our app! 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://bafybeiebsyucxoiaieyyktifwl7glbepaklj4tncplc3rvssq55rpl553a.ipfs.dweb.link/
- https://bafybeiebsyucxoiaieyyktifwl7glbepaklj4tncplc3rvssq55rpl553a.ipfs.cf-ipfs.com/
- [ipfs://QmX4W6sgCHYCcR4SxeJVbZnj5N1GXR2ERPJomoBjVSbZVZ/](ipfs://QmX4W6sgCHYCcR4SxeJVbZnj5N1GXR2ERPJomoBjVSbZVZ/)
## 5.13.0 (2024-02-27)
### Features
* **web:** [info] Add Bottom Bar to PDP mWeb (#6444) edd221d
* **web:** [info] add tvl and volume % change to PDP query (#6453) 3054897
* **web:** [info] add warning label to swap modal on PDP page (#6398) f7bacf9
* **web:** [info] candlestick data integration and fallback refactor (#6510) 1136e54
* **web:** [info] candlestick tooltip (#6355) fbb184f
* **web:** [info] Connect PDP data to our BE (#6357) 45a32d6
* **web:** [info] Connect PDP Volume Chart to real data (#6455) 3c7fa86
* **web:** [info] Integrate BE Pool TX queries (#6424) d2df578
* **web:** [info] Migrate Explore and TDP TX tables to our GQL BE (#6502) b674b85
* **web:** [info] pdp loading states (#6504) 57c0279
* **web:** [info] Sortable Token Explore Table Headers (#6435) f0e95d0
* **web:** [info] TDP chart loading states (#6450) d45a2e7
* **web:** [info] use custom PDP seo page title (#6422) f2e6552
* **web:** [info] use dropdown bottom sheet in smaller screens (#6452) 0177aaa
* **web:** [info] Use real price data for PDP Price Chart (#6457) 9493135
* **web:** [info] Various mWeb polish (#6549) 97e313e
* **web:** [uni-tags] adding username to send (#6358) 507bffc
* **web:** [uni-tags] improve Web3Status and account drawer header (#6454) 0c57325
* **web:** [uni-tags] update banner copy and responsive styles (#6401) b8445de
* **web:** add Explore and TDP chart staleness checks (#6458) 71cac56
* **web:** add gas estimate to limits dialog (#6233) 6689ae4
* **web:** add more analytics for limits (#6475) 14de73d
* **web:** Add more limits analytics events (#6496) 1bc5245
* **web:** Add off chain order type to trade events and swap tab click event (#6417) 3ad9ce0
* **web:** add price change speedbump to limit form (#6447) 1f855b5
* **web:** change input currency from LimitPriceInputPanel (#6250) 8313cb9
* **web:** fix limit status updating (#6363) 727de8b
* **web:** handle remote open limits (#6407) 519dbb9
* **web:** invert limit price preset button adjustments (#6518) c93f355
* **web:** mv limits cta to activity tab (#6532) e34eb1c
* **web:** pending states for cancelling limits (#6360) 1a3f5b9
* **web:** redesign limits menu rows (#6575) 11ac692
* **web:** Set default quote currency in limits price display based on stablecoin (#6456) 4b5a48a
* **web:** sync url to swap tab (#6432) ffb2d8c
* **web:** transaction details for insufficient liquidity (#6531) 8d85c98
* **web:** Update progress indicator step titles for limit orders (#6470) 090248e
* **web:** update TOS last updated to 2/16/2024 (#6550) 533b087
* **web:** Use GQL Token Data for Pool Token Logos (#6505) 00e6504
### Bug Fixes
* **web:** [info] add tooltip for turnover on token table (#6507) 50ea4f5
* **web:** [info] block explorer icon not showing dark mode TDP (#6557) f0ca33d
* **web:** [info] explore table mobile hover (#6512) 7abb6f7
* **web:** [info] fix extralong numbers on explore chart section (#6524) db5bbdc
* **web:** [info] fix translations for chart type selector (#6533) ee54af9
* **web:** [info] flickering volume bars (#6581) 60582ac
* **web:** [info] hide chart y-axis on smaller screen (#6506) c64a8f4
* **web:** [info] knock out a bunch of high-pri polish tasks (#6305) e5d0797
* **web:** [info] make return to top clickable again (#6559) 6d65932
* **web:** [info] MOOREE polish (#6486) 0a5b22f
* **web:** [info] more polish (#6434) 41ca7ea
* **web:** [info] pdp matching tdp (#6501) fb8ec9d
* **web:** [info] Prevent infinite loading for fully loaded tables (#6394) 4233a2f
* **web:** [info] pull decimal data for gql tokens (#6558) 3d9a336
* **web:** [info] Show explorer icon in PDP Links in darkmode (#6584) 8bd04e5
* **web:** [info] Show PDP balances on mWeb (#6585) aee363f
* **web:** [info] Show TDP Bottom Bar at exactly 1024px (#6576) d04dd12
* **web:** [limits] empty transaction details with ETH (#6519) 8c1532d
* **web:** [limits] fix transaction details text to match design (#6534) dc4dbd3
* **web:** [limits] making market button the same as percentage (#6516) 6f417c9
* **web:** [limits] update order confirmation flow (#6490) 0966070
* **web:** [uni-tags] inline banner height, icon size, pfp (#6538) 67b10ab
* **web:** align swap box and action buttons (#6497) 7c846bc
* **web:** chain switching search params (#6491) 6c88199
* **web:** chain switching search params (#6491) (#6514) f10d64e
* **web:** Change auction period secs to 0 (#6589) eab18eb
* **web:** clear limits form when submitting a limit (#6515) 9bab70b
* **web:** color code buy/sell on TDP tx table (#6498) eaafcb6
* **web:** convert backend timestamp from sec to ms for X orders (#6540) 4161221
* **web:** de-crowd the swap header bar (#6546) b0fd755
* **web:** disable test and update gql schema (#6517) e3aa751
* **web:** disable text select on the $ in the send form (#6433) 2a9ba50
* **web:** division by zero error in limits menu (#6561) 3190502
* **web:** do not truncate token symbols (#6495) 3f3b28b
* **web:** enable limits for all tokens (#6485) ee6fd22
* **web:** fix cancelation confirmation modal for limit orders (#6583) 8565065
* **web:** fix limit tab breaking after switching to l2s (#6554) 15f2d32
* **web:** ignore touchmove on charts (#6500) 51c7d32
* **web:** invert displayed custom price adjustment to match presets (#6560) a9fbe5c
* **web:** layout bug in token selector (#6484) a4e2de1
* **web:** limit form styling nits (#6253) ca34460
* **web:** limiting max decimals in input (#6446) df01d65
* **web:** limits menu styling nits (#6254) a83faa5
* **web:** lint error in useCurrentPriceAdjustment (#6492) cd1e011
* **web:** long pool name overlap (#6503) 78d4d8c
* **web:** minor design nits for limits (#6522) 450366a
* **web:** Misc. design fixes to Token Detail Page (#6565) 7146990
* **web:** open limits menu button bug (#6389) 7cdcc98
* **web:** revert - clear limits form when submitting a limit (#6515) (#6528) 52ef6fd
* **web:** setState error from LimitForm (#6249) df2aca3
* **web:** stacked tvl rendering overlap (#6573) f090a13
* **web:** styling fixes for limits flow (#6525) 72ed86b
* **web:** swap header<>url navigation bug (#6513) ad8d8d9
* **web:** swap z-indexing (#6499) 1573567
* **web:** truncate currency amount to decimals when parsing (#6586) 2a891d9
* **web:** Use local activities if GraphQL assetActivities query returns nothing (#6494) 137b253
### Code Refactoring
* **web:** [info] tdp provider pattern (#6385) 5c52db7
* **web:** [info] use tdp context in subcomponents (#6400) 99bbedc
Other notable changes:
- Gas estimation and approval bug fix
- Updated Twitter icons to ‘X’
- Unsupported language bug fix
- Android bug fixes
mobile/1.21.1 web/5.13.0
\ No newline at end of file \ No newline at end of file
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
# #
.DS_Store .DS_Store
.tamagui
# Xcode # Xcode
# #
build/ build/
......
...@@ -125,17 +125,17 @@ android { ...@@ -125,17 +125,17 @@ android {
dev { dev {
isDefault(true) isDefault(true)
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionName "1.21.1" versionName "1.22"
dimension "variant" dimension "variant"
} }
beta { beta {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionName "1.21.1" versionName "1.22"
dimension "variant" dimension "variant"
} }
prod { prod {
dimension "variant" dimension "variant"
versionName "1.21.1" versionName "1.22"
} }
} }
...@@ -199,6 +199,10 @@ dependencies { ...@@ -199,6 +199,10 @@ dependencies {
implementation 'com.google.android.play:integrity:1.2.0' implementation 'com.google.android.play:integrity:1.2.0'
// Firebase App Check: Import the BoM for the Firebase platform
implementation(platform("com.google.firebase:firebase-bom:32.7.2"))
implementation("com.google.firebase:firebase-appcheck-playintegrity")
// Guava // Guava
implementation "com.google.guava:guava:24.1-jre" implementation "com.google.guava:guava:24.1-jre"
// Guava fix // Guava fix
......
...@@ -5,6 +5,16 @@ const inProduction = NODE_ENV === 'production' ...@@ -5,6 +5,16 @@ const inProduction = NODE_ENV === 'production'
module.exports = function (api) { module.exports = function (api) {
api.cache.using(() => process.env.NODE_ENV) api.cache.using(() => process.env.NODE_ENV)
var plugins = [ var plugins = [
// disable for now as its causing ci to hang
// process.env.NODE_ENV === 'test'
// ? null
// : [
// '@tamagui/babel-plugin',
// {
// components: ['ui'],
// config: '../../packages/ui/src/tamagui.config.ts',
// },
// ],
[ [
'module:react-native-dotenv', 'module:react-native-dotenv',
{ {
...@@ -30,7 +40,7 @@ module.exports = function (api) { ...@@ -30,7 +40,7 @@ module.exports = function (api) {
'@babel/plugin-proposal-numeric-separator', '@babel/plugin-proposal-numeric-separator',
// automatically require React when using JSX // automatically require React when using JSX
'react-require', 'react-require',
] ].filter(Boolean)
if (inProduction) { if (inProduction) {
// Remove all console statements in production // Remove all console statements in production
...@@ -38,6 +48,10 @@ module.exports = function (api) { ...@@ -38,6 +48,10 @@ module.exports = function (api) {
} }
return { return {
ignore: [
// speeds up compile
'**/@tamagui/**/dist/**',
],
presets: ['module:metro-react-native-babel-preset'], presets: ['module:metro-react-native-babel-preset'],
plugins, plugins,
} }
......
...@@ -644,10 +644,6 @@ PODS: ...@@ -644,10 +644,6 @@ 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):
...@@ -1339,8 +1335,6 @@ PODS: ...@@ -1339,8 +1335,6 @@ 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)
...@@ -1354,7 +1348,6 @@ DEPENDENCIES: ...@@ -1354,7 +1348,6 @@ 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`)
...@@ -1482,8 +1475,6 @@ SPEC REPOS: ...@@ -1482,8 +1475,6 @@ SPEC REPOS:
- SDWebImageWebPCoder - SDWebImageWebPCoder
- Sentry - Sentry
- SentryPrivate - SentryPrivate
- SPAlert
- SPIndicator
- UIImageColors - UIImageColors
- ZXingObjC - ZXingObjC
...@@ -1492,8 +1483,6 @@ EXTERNAL SOURCES: ...@@ -1492,8 +1483,6 @@ 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:
...@@ -1677,7 +1666,6 @@ SPEC CHECKSUMS: ...@@ -1677,7 +1666,6 @@ SPEC CHECKSUMS:
Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b
boost: 0a937fbcfdd646fca221c4f1d9750d7ccfdfc2dc boost: 0a937fbcfdd646fca221c4f1d9750d7ccfdfc2dc
BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33 BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33
Burnt: 708556f6283e1b81767e6642e088819d85d1ea08
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135 EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135
EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903 EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903
...@@ -1790,8 +1778,6 @@ SPEC CHECKSUMS: ...@@ -1790,8 +1778,6 @@ 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
......
...@@ -2450,7 +2450,7 @@ ...@@ -2450,7 +2450,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2496,7 +2496,7 @@ ...@@ -2496,7 +2496,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
...@@ -2542,7 +2542,7 @@ ...@@ -2542,7 +2542,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
...@@ -2588,7 +2588,7 @@ ...@@ -2588,7 +2588,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
...@@ -2630,7 +2630,7 @@ ...@@ -2630,7 +2630,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2673,7 +2673,7 @@ ...@@ -2673,7 +2673,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
...@@ -2716,7 +2716,7 @@ ...@@ -2716,7 +2716,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
...@@ -2759,7 +2759,7 @@ ...@@ -2759,7 +2759,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
...@@ -2795,7 +2795,7 @@ ...@@ -2795,7 +2795,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -2833,7 +2833,7 @@ ...@@ -2833,7 +2833,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3003,7 +3003,7 @@ ...@@ -3003,7 +3003,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -3047,7 +3047,7 @@ ...@@ -3047,7 +3047,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
...@@ -3143,7 +3143,7 @@ ...@@ -3143,7 +3143,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3214,7 +3214,7 @@ ...@@ -3214,7 +3214,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
...@@ -3310,7 +3310,7 @@ ...@@ -3310,7 +3310,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3381,7 +3381,7 @@ ...@@ -3381,7 +3381,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21.1; MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
<string>applinks:app.uniswap.org</string> <string>applinks:app.uniswap.org</string>
<string>applinks:app.corn-staging.com</string> <string>applinks:app.corn-staging.com</string>
</array> </array>
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
<key>com.apple.developer.icloud-container-identifiers</key> <key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>iCloud.Uniswap</string> <string>iCloud.Uniswap</string>
......
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.31.0", "@uniswap/analytics-events": "2.31.0",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "4.0.7", "@uniswap/sdk-core": "4.1.2",
"@uniswap/v3-sdk": "3.10.2", "@uniswap/v3-sdk": "3.10.2",
"@walletconnect/core": "2.10.1", "@walletconnect/core": "2.10.1",
"@walletconnect/react-native-compat": "2.10.1", "@walletconnect/react-native-compat": "2.10.1",
...@@ -156,6 +156,7 @@ ...@@ -156,6 +156,7 @@
"@babel/runtime": "7.18.9", "@babel/runtime": "7.18.9",
"@faker-js/faker": "7.6.0", "@faker-js/faker": "7.6.0",
"@storybook/react": "7.0.2", "@storybook/react": "7.0.2",
"@tamagui/babel-plugin": "1.89.26",
"@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/react-native": "0.71.3", "@types/react-native": "0.71.3",
......
...@@ -47,7 +47,7 @@ import { useAsyncData } from 'utilities/src/react/hooks' ...@@ -47,7 +47,7 @@ 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'
import { uniswapUrls } from 'wallet/src/constants/urls' import { uniswapUrls } from 'wallet/src/constants/urls'
import { initFirebaseAppCheck } from 'wallet/src/features/appCheck/utils' import { initFirebaseAppCheck } from 'wallet/src/features/appCheck'
import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks'
import { EXPERIMENT_NAMES, FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { EXPERIMENT_NAMES, FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors'
...@@ -125,6 +125,8 @@ function App(): JSX.Element | null { ...@@ -125,6 +125,8 @@ function App(): JSX.Element | null {
tier: getStatsigEnvironmentTier(), tier: getStatsigEnvironmentTier(),
}, },
api: uniswapUrls.statsigProxyUrl, api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: true,
disableErrorLogging: true,
}, },
sdkKey: DUMMY_STATSIG_SDK_KEY, sdkKey: DUMMY_STATSIG_SDK_KEY,
user: deviceId ? { userID: deviceId } : {}, user: deviceId ? { userID: deviceId } : {},
......
import React, { ErrorInfo, PropsWithChildren } from 'react' import React, { ErrorInfo, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Image, StyleSheet } from 'react-native'
import RNRestart from 'react-native-restart' import RNRestart from 'react-native-restart'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { Button, Flex, Text } from 'ui/src' import { Button, Flex, Text } from 'ui/src'
import DeadLuni from 'ui/src/assets/graphics/dead-luni.svg' import { DEAD_LUNI } from 'ui/src/assets'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { useAccounts } from 'wallet/src/features/wallet/hooks' import { useAccounts } from 'wallet/src/features/wallet/hooks'
import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice'
...@@ -61,9 +62,15 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element { ...@@ -61,9 +62,15 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
} }
return ( return (
<Flex centered fill gap="$spacing16" px="$spacing16" py="$spacing48"> <Flex
centered
fill
backgroundColor="$surface1"
gap="$spacing16"
px="$spacing16"
py="$spacing48">
<Flex centered grow gap="$spacing36"> <Flex centered grow gap="$spacing36">
<DeadLuni /> <Image source={DEAD_LUNI} style={styles.errorImage} />
<Flex centered gap="$spacing8"> <Flex centered gap="$spacing8">
<Text variant="subheading1">{t('Uh oh!')}</Text> <Text variant="subheading1">{t('Uh oh!')}</Text>
<Text variant="body2">{t('Something crashed.')}</Text> <Text variant="body2">{t('Something crashed.')}</Text>
...@@ -81,3 +88,11 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element { ...@@ -81,3 +88,11 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
</Flex> </Flex>
) )
} }
const styles = StyleSheet.create({
errorImage: {
height: 150,
resizeMode: 'contain',
width: 150,
},
})
import { PropsWithChildren, useCallback } from 'react' import { PropsWithChildren, useCallback } from 'react'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { useAppStackNavigation } from 'src/app/navigation/types' import { useAppStackNavigation } from 'src/app/navigation/types'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { closeModal, openModal } from 'src/features/modals/modalSlice' import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
...@@ -8,11 +9,13 @@ import { ...@@ -8,11 +9,13 @@ import {
NavigateToSwapFlowArgs, NavigateToSwapFlowArgs,
WalletNavigationProvider, WalletNavigationProvider,
} from 'wallet/src/contexts/WalletNavigationContext' } from 'wallet/src/contexts/WalletNavigationContext'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element { export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element {
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity) const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity)
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet()
const navigateToSwapFlow = useNavigateToSwapFlow() const navigateToSwapFlow = useNavigateToSwapFlow()
const navigateToTokenDetails = useNavigateToTokenDetails() const navigateToTokenDetails = useNavigateToTokenDetails()
...@@ -20,6 +23,7 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren): ...@@ -20,6 +23,7 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren):
<WalletNavigationProvider <WalletNavigationProvider
navigateToAccountActivityList={navigateToAccountActivityList} navigateToAccountActivityList={navigateToAccountActivityList}
navigateToAccountTokenList={navigateToAccountTokenList} navigateToAccountTokenList={navigateToAccountTokenList}
navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet}
navigateToSwapFlow={navigateToSwapFlow} navigateToSwapFlow={navigateToSwapFlow}
navigateToTokenDetails={navigateToTokenDetails}> navigateToTokenDetails={navigateToTokenDetails}>
{children} {children}
...@@ -59,3 +63,25 @@ function useNavigateToTokenDetails(): (currencyId: string) => void { ...@@ -59,3 +63,25 @@ function useNavigateToTokenDetails(): (currencyId: string) => void {
[navigation] [navigation]
) )
} }
function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void {
const dispatch = useAppDispatch()
const { data } = useFiatOnRampIpAddressQuery()
const fiatOnRampEligible = Boolean(data?.isBuyAllowed)
return useCallback((): void => {
dispatch(closeModal({ name: ModalName.Send }))
if (fiatOnRampEligible) {
dispatch(openModal({ name: ModalName.FiatOnRamp }))
} else {
dispatch(
openModal({
name: ModalName.WalletConnectScan,
initialState: ScannerModalState.WalletQr,
})
)
}
}, [dispatch, fiatOnRampEligible])
}
...@@ -2,17 +2,18 @@ import React from 'react' ...@@ -2,17 +2,18 @@ import React from 'react'
import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal' import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal'
import { ExperimentsModal } from 'src/app/modals/ExperimentsModal' import { ExperimentsModal } from 'src/app/modals/ExperimentsModal'
import { ExploreModal } from 'src/app/modals/ExploreModal' import { ExploreModal } from 'src/app/modals/ExploreModal'
import { FiatOnRampAggregatorModal } from 'src/app/modals/FiatOnRampModalAggregator'
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 { ViewOnlyExplainerModal } from 'src/app/modals/ViewOnlyExplainerModal' import { ViewOnlyExplainerModal } from 'src/app/modals/ViewOnlyExplainerModal'
import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal' import { LazyModalRenderer } from 'src/app/modals/utils'
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'
import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal'
import { WalletConnectModals } from 'src/components/WalletConnect/WalletConnectModals' import { WalletConnectModals } from 'src/components/WalletConnect/WalletConnectModals'
import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal'
import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal'
import { LockScreenModal } from 'src/features/authentication/LockScreenModal' import { LockScreenModal } from 'src/features/authentication/LockScreenModal'
import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal'
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal' import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal'
import { ScantasticModal } from 'src/features/scantastic/ScantasticModal' import { ScantasticModal } from 'src/features/scantastic/ScantasticModal'
import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal' import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal'
...@@ -23,6 +24,10 @@ import { ModalName } from 'wallet/src/telemetry/constants' ...@@ -23,6 +24,10 @@ import { ModalName } from 'wallet/src/telemetry/constants'
export function AppModals(): JSX.Element { export function AppModals(): JSX.Element {
return ( return (
<> <>
<LazyModalRenderer name={ModalName.ExchangeTransferModal}>
<ExchangeTransferModal />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.Experiments}> <LazyModalRenderer name={ModalName.Experiments}>
<ExperimentsModal /> <ExperimentsModal />
</LazyModalRenderer> </LazyModalRenderer>
......
...@@ -269,37 +269,26 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -269,37 +269,26 @@ exports[`AccountSwitcher renders correctly 1`] = `
</View> </View>
</View> </View>
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={16} hitSlop={[Function]}
onClick={[Function]} minPressDuration={0}
onResponderGrant={[Function]} onBlur={[Function]}
onResponderMove={[Function]} onFocus={[Function]}
onResponderRelease={[Function]} onMouseEnter={[Function]}
onResponderTerminate={[Function]} onMouseLeave={[Function]}
onResponderTerminationRequest={[Function]} onPress={[Function]}
onStartShouldSetResponder={[Function]} onPressIn={[Function]}
onPressOut={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
testID="copy" testID="copy"
...@@ -518,45 +507,27 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -518,45 +507,27 @@ exports[`AccountSwitcher renders correctly 1`] = `
ExpoLinearGradient ExpoLinearGradient
</View> </View>
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={ hitSlop={[Function]}
{ minPressDuration={0}
"bottom": 5, onBlur={[Function]}
"left": 5, onFocus={[Function]}
"right": 5, onMouseEnter={[Function]}
"top": 5, onMouseLeave={[Function]}
} onPress={[Function]}
} onPressIn={[Function]}
onClick={[Function]} onPressOut={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
{ {
"flexDirection": "column",
"marginTop": 16, "marginTop": 16,
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
......
...@@ -2,7 +2,7 @@ import { ImpactFeedbackStyle } from 'expo-haptics' ...@@ -2,7 +2,7 @@ import { ImpactFeedbackStyle } from 'expo-haptics'
import { memo, useMemo } 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 } from 'react-native-wagmi-charts'
import PriceExplorerAnimatedNumber from 'src/components/PriceExplorer/PriceExplorerAnimatedNumber' import PriceExplorerAnimatedNumber from 'src/components/PriceExplorer/PriceExplorerAnimatedNumber'
import { PriceExplorerError } from 'src/components/PriceExplorer/PriceExplorerError' import { PriceExplorerError } from 'src/components/PriceExplorer/PriceExplorerError'
import { DatetimeText, RelativeChangeText } from 'src/components/PriceExplorer/Text' import { DatetimeText, RelativeChangeText } from 'src/components/PriceExplorer/Text'
...@@ -109,9 +109,7 @@ export const PriceExplorer = memo(function PriceExplorer({ ...@@ -109,9 +109,7 @@ export const PriceExplorer = memo(function PriceExplorer({
let content: JSX.Element | null let content: JSX.Element | null
if (forcePlaceholder) { if (forcePlaceholder) {
content = ( content = <PriceExplorerPlaceholder />
<PriceExplorerPlaceholder loading={forcePlaceholder} numberOfDigits={numberOfDigits} />
)
} else if (convertedPriceHistory?.length) { } else if (convertedPriceHistory?.length) {
content = ( content = (
// TODO(MOB-2308): add better loading state // TODO(MOB-2308): add better loading state
...@@ -119,78 +117,57 @@ export const PriceExplorer = memo(function PriceExplorer({ ...@@ -119,78 +117,57 @@ export const PriceExplorer = memo(function PriceExplorer({
<PriceExplorerChart <PriceExplorerChart
additionalPadding={additionalPadding} additionalPadding={additionalPadding}
lastPricePoint={lastPricePoint} lastPricePoint={lastPricePoint}
loading={loading}
numberOfDigits={numberOfDigits}
priceHistory={convertedPriceHistory}
shouldShowAnimatedDot={shouldShowAnimatedDot} shouldShowAnimatedDot={shouldShowAnimatedDot}
spot={convertedSpot}
tokenColor={tokenColor} tokenColor={tokenColor}
/> />
</Flex> </Flex>
) )
} else { } else {
content = <PriceExplorerPlaceholder loading={loading} numberOfDigits={numberOfDigits} /> content = <PriceExplorerPlaceholder />
} }
return ( return (
<Flex overflow="hidden"> <LineChartProvider
data={convertedPriceHistory ?? []}
onCurrentIndexChange={invokeImpact[ImpactFeedbackStyle.Light]}>
<Flex gap="$spacing8" overflow="hidden">
<PriceTextSection
loading={loading}
numberOfDigits={numberOfDigits}
relativeChange={convertedSpot?.relativeChange}
spotPrice={convertedSpot?.value.value}
/>
{content} {content}
<TimeRangeGroup setDuration={setDuration} /> <TimeRangeGroup setDuration={setDuration} />
</Flex> </Flex>
</LineChartProvider>
) )
}) })
function PriceExplorerPlaceholder({ function PriceExplorerPlaceholder(): JSX.Element {
loading,
numberOfDigits,
}: {
loading: boolean
numberOfDigits: PriceNumberOfDigits
}): JSX.Element {
return ( return (
<Flex gap="$spacing8">
<PriceTextSection loading={loading} numberOfDigits={numberOfDigits} />
<Flex my="$spacing24"> <Flex my="$spacing24">
<Loader.Graph /> <Loader.Graph />
</Flex> </Flex>
</Flex>
) )
} }
function PriceExplorerChart({ function PriceExplorerChart({
priceHistory,
spot,
loading,
tokenColor, tokenColor,
additionalPadding, additionalPadding,
shouldShowAnimatedDot, shouldShowAnimatedDot,
lastPricePoint, lastPricePoint,
numberOfDigits,
}: { }: {
priceHistory: TLineChartDataProp
spot?: TokenSpotData
loading: boolean
tokenColor?: string tokenColor?: string
additionalPadding: number additionalPadding: number
shouldShowAnimatedDot: boolean shouldShowAnimatedDot: boolean
lastPricePoint: number lastPricePoint: number
numberOfDigits: PriceNumberOfDigits
}): JSX.Element { }): JSX.Element {
const { chartHeight, chartWidth } = useChartDimensions() const { chartHeight, chartWidth } = useChartDimensions()
const isRTL = I18nManager.isRTL const isRTL = I18nManager.isRTL
return ( return (
<LineChartProvider // TODO(MOB-2166): remove forced LTR direction + scaleX horizontal flip technique once react-native-wagmi-charts fixes this: https://github.com/coinjar/react-native-wagmi-charts/issues/136
data={priceHistory}
onCurrentIndexChange={invokeImpact[ImpactFeedbackStyle.Light]}>
<Flex gap="$spacing8">
<PriceTextSection
loading={loading}
numberOfDigits={numberOfDigits}
relativeChange={spot?.relativeChange}
spotPrice={spot?.value?.value}
/>
{/* TODO(MOB-2166): remove forced LTR direction + scaleX horizontal flip technique once react-native-wagmi-charts fixes this: https://github.com/coinjar/react-native-wagmi-charts/issues/136 */}
<Flex direction="ltr" my="$spacing24" style={{ transform: [{ scaleX: isRTL ? -1 : 1 }] }}> <Flex direction="ltr" my="$spacing24" style={{ transform: [{ scaleX: isRTL ? -1 : 1 }] }}>
<LineChart height={chartHeight} width={chartWidth - additionalPadding} yGutter={20}> <LineChart height={chartHeight} width={chartWidth - additionalPadding} yGutter={20}>
<LineChart.Path color={tokenColor} pathProps={{ isTransitionEnabled: false }}> <LineChart.Path color={tokenColor} pathProps={{ isTransitionEnabled: false }}>
...@@ -221,7 +198,5 @@ function PriceExplorerChart({ ...@@ -221,7 +198,5 @@ function PriceExplorerChart({
/> />
</LineChart> </LineChart>
</Flex> </Flex>
</Flex>
</LineChartProvider>
) )
} }
...@@ -264,10 +264,10 @@ const Numbers = ({ ...@@ -264,10 +264,10 @@ const Numbers = ({
numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1, numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1,
(index) => ( (index) => (
<Animated.View <Animated.View
key={`$number_${index - (commaIndex - decimalPlace.value)}`} key={`$number_${index - commaIndex}`}
style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}> style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}>
<RollNumber <RollNumber
key={`$number_${index - (commaIndex - decimalPlace.value)}`} key={`$number_${index - commaIndex}`}
chars={chars} chars={chars}
commaIndex={commaIndex} commaIndex={commaIndex}
currency={currency} currency={currency}
......
import { maxBy } from 'lodash' import { maxBy } from 'lodash'
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react' import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react'
import { SharedValue } from 'react-native-reanimated' import { SharedValue } from 'react-native-reanimated'
import { TLineChartData } from 'react-native-wagmi-charts' import { TLineChartData } from 'react-native-wagmi-charts'
import { PollingInterval } from 'wallet/src/constants/misc' import { PollingInterval } from 'wallet/src/constants/misc'
...@@ -42,6 +42,11 @@ export function useTokenPriceHistory( ...@@ -42,6 +42,11 @@ export function useTokenPriceHistory(
error: boolean error: boolean
numberOfDigits: PriceNumberOfDigits numberOfDigits: PriceNumberOfDigits
} { } {
const lastPrice = useRef<undefined | number>(undefined)
const lastNumberOfDigits = useRef({
left: 0,
right: 0,
})
const [duration, setDuration] = useState(initialDuration) const [duration, setDuration] = useState(initialDuration)
const { convertFiatAmount } = useLocalizationContext() const { convertFiatAmount } = useLocalizationContext()
...@@ -64,14 +69,15 @@ export function useTokenPriceHistory( ...@@ -64,14 +69,15 @@ export function useTokenPriceHistory(
const offChainData = priceData?.tokenProjects?.[0]?.markets?.[0] const offChainData = priceData?.tokenProjects?.[0]?.markets?.[0]
const onChainData = priceData?.tokenProjects?.[0]?.tokens?.[0]?.market const onChainData = priceData?.tokenProjects?.[0]?.tokens?.[0]?.market
const price = offChainData?.price?.value ?? onChainData?.price?.value const price = offChainData?.price?.value ?? onChainData?.price?.value ?? lastPrice.current
lastPrice.current = price
const priceHistory = offChainData?.priceHistory ?? onChainData?.priceHistory const priceHistory = offChainData?.priceHistory ?? onChainData?.priceHistory
const pricePercentChange24h = const pricePercentChange24h =
offChainData?.pricePercentChange24h?.value ?? onChainData?.pricePercentChange24h?.value ?? 0 offChainData?.pricePercentChange24h?.value ?? onChainData?.pricePercentChange24h?.value ?? 0
const spot = useMemo( const spot = useMemo(
() => () =>
price price !== undefined
? { ? {
value: { value: price }, value: { value: price },
relativeChange: { value: pricePercentChange24h }, relativeChange: { value: pricePercentChange24h },
...@@ -93,16 +99,15 @@ export function useTokenPriceHistory( ...@@ -93,16 +99,15 @@ export function useTokenPriceHistory(
const convertedMaxValue = convertFiatAmount(max?.value).amount const convertedMaxValue = convertFiatAmount(max?.value).amount
if (max) { if (max) {
return { const newNumberOfDigits = {
left: String(convertedMaxValue).split('.')[0]?.length || 10, left: String(convertedMaxValue).split('.')[0]?.length || 10,
right: Number(String(convertedMaxValue.toFixed(10)).split('.')[0]) > 0 ? 2 : 10, right: Number(String(convertedMaxValue.toFixed(10)).split('.')[0]) > 0 ? 2 : 10,
} }
lastNumberOfDigits.current = newNumberOfDigits
return newNumberOfDigits
} }
return { return lastNumberOfDigits.current
left: 0,
right: 0,
}
}, [convertFiatAmount, priceHistory]) }, [convertFiatAmount, priceHistory])
const retry = useCallback(async () => { const retry = useCallback(async () => {
......
import { impactAsync, ImpactFeedbackStyle, selectionAsync } from 'expo-haptics' import { impactAsync, ImpactFeedbackStyle, selectionAsync } from 'expo-haptics'
import React, { useCallback } from 'react' import React, { useCallback, useEffect } from 'react'
import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { setUserProperty } from 'src/features/telemetry'
import { UserPropertyName } from 'src/features/telemetry/constants'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
import { isDevBuild } from 'src/utils/version' import { isDevBuild } from 'src/utils/version'
import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { Flex, Icons, Text, TouchableArea } from 'ui/src'
...@@ -29,6 +31,20 @@ export function AccountHeader(): JSX.Element { ...@@ -29,6 +31,20 @@ export function AccountHeader(): JSX.Element {
const { avatar } = useAvatar(activeAddress) const { avatar } = useAvatar(activeAddress)
const displayName = useDisplayName(activeAddress) const displayName = useDisplayName(activeAddress)
// Log ENS and Unitag ownership for user usage stats
useEffect(() => {
switch (displayName?.type) {
case DisplayNameType.ENS:
setUserProperty(UserPropertyName.HasLoadedENS, true)
return
case DisplayNameType.Unitag:
setUserProperty(UserPropertyName.HasLoadedUnitag, true)
return
default:
return
}
}, [displayName?.type])
const onPressAccountHeader = useCallback(() => { const onPressAccountHeader = useCallback(() => {
dispatch(openModal({ name: ModalName.AccountSwitcher })) dispatch(openModal({ name: ModalName.AccountSwitcher }))
}, [dispatch]) }, [dispatch])
......
...@@ -33,39 +33,28 @@ exports[`AccountHeader renders without error 1`] = ` ...@@ -33,39 +33,28 @@ exports[`AccountHeader renders without error 1`] = `
} }
> >
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={20} hitSlop={[Function]}
onClick={[Function]} minPressDuration={0}
onResponderGrant={[Function]} onBlur={[Function]}
onResponderMove={[Function]} onFocus={[Function]}
onResponderRelease={[Function]} onLongPress={[Function]}
onResponderTerminate={[Function]} onMouseEnter={[Function]}
onResponderTerminationRequest={[Function]} onMouseLeave={[Function]}
onStartShouldSetResponder={[Function]} onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={ style={
{ {
"alignItems": "center", "alignItems": "center",
"flexDirection": "row", "flexDirection": "row",
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
testID="manage" testID="manage"
...@@ -334,37 +323,26 @@ exports[`AccountHeader renders without error 1`] = ` ...@@ -334,37 +323,26 @@ exports[`AccountHeader renders without error 1`] = `
</View> </View>
</View> </View>
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={20} hitSlop={[Function]}
onClick={[Function]} minPressDuration={0}
onResponderGrant={[Function]} onBlur={[Function]}
onResponderMove={[Function]} onFocus={[Function]}
onResponderRelease={[Function]} onMouseEnter={[Function]}
onResponderTerminate={[Function]} onMouseLeave={[Function]}
onResponderTerminationRequest={[Function]} onPress={[Function]}
onStartShouldSetResponder={[Function]} onPressIn={[Function]}
onPressOut={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
...@@ -440,38 +418,27 @@ exports[`AccountHeader renders without error 1`] = ` ...@@ -440,38 +418,27 @@ exports[`AccountHeader renders without error 1`] = `
} }
> >
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={20} hitSlop={[Function]}
onClick={[Function]} minPressDuration={0}
onResponderGrant={[Function]} onBlur={[Function]}
onResponderMove={[Function]} onFocus={[Function]}
onResponderRelease={[Function]} onMouseEnter={[Function]}
onResponderTerminate={[Function]} onMouseLeave={[Function]}
onResponderTerminationRequest={[Function]} onPress={[Function]}
onStartShouldSetResponder={[Function]} onPressIn={[Function]}
onPressOut={[Function]}
style={ style={
{ {
"flexDirection": "column",
"flexShrink": 1, "flexShrink": 1,
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
...@@ -565,38 +532,27 @@ exports[`AccountHeader renders without error 1`] = ` ...@@ -565,38 +532,27 @@ exports[`AccountHeader renders without error 1`] = `
</Text> </Text>
</View> </View>
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={20} hitSlop={[Function]}
onClick={[Function]} minPressDuration={0}
onResponderGrant={[Function]} onBlur={[Function]}
onResponderMove={[Function]} onFocus={[Function]}
onResponderRelease={[Function]} onMouseEnter={[Function]}
onResponderTerminate={[Function]} onMouseLeave={[Function]}
onResponderTerminationRequest={[Function]} onPress={[Function]}
onStartShouldSetResponder={[Function]} onPressIn={[Function]}
onPressOut={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"paddingLeft": 8, "paddingLeft": 8,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
......
...@@ -108,48 +108,31 @@ exports[`AccountList renders without error 1`] = ` ...@@ -108,48 +108,31 @@ exports[`AccountList renders without error 1`] = `
onPress={[Function]} onPress={[Function]}
> >
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={ hitSlop={[Function]}
{ minPressDuration={0}
"bottom": 5, onBlur={[Function]}
"left": 5, onFocus={[Function]}
"right": 5, onLongPress={[Function]}
"top": 5, onMouseEnter={[Function]}
} onMouseLeave={[Function]}
} onPress={[Function]}
onClick={[Function]} onPressIn={[Function]}
onResponderGrant={[Function]} onPressOut={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"paddingBottom": 12, "paddingBottom": 12,
"paddingLeft": 24, "paddingLeft": 24,
"paddingRight": 24, "paddingRight": 24,
"paddingTop": 8, "paddingTop": 8,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
......
...@@ -42,7 +42,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element { ...@@ -42,7 +42,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element {
return ( return (
<Button icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}> <Button icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}>
{isCopied ? t`Copied!` : t`Copy`} {isCopied ? t`Copied` : t`Copy`}
</Button> </Button>
) )
} }
...@@ -264,7 +264,7 @@ function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null { ...@@ -264,7 +264,7 @@ function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null {
px="$spacing12" px="$spacing12"
zIndex={1}> zIndex={1}>
{hasFavoritedTokens && <FavoriteTokensGrid {...props} />} {hasFavoritedTokens && <FavoriteTokensGrid {...props} />}
{hasFavoritedWallets && <FavoriteWalletsGrid showLoading={props.showLoading} />} {hasFavoritedWallets && <FavoriteWalletsGrid {...props} />}
</Flex> </Flex>
) )
} }
......
...@@ -2,18 +2,11 @@ import { ImpactFeedbackStyle } from 'expo-haptics' ...@@ -2,18 +2,11 @@ import { ImpactFeedbackStyle } from 'expo-haptics'
import React, { memo, useCallback } from 'react' import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native' import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { import { FadeIn, SharedValue } from 'react-native-reanimated'
FadeIn,
SharedValue,
interpolate,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton' import RemoveButton from 'src/components/explore/RemoveButton'
import { useExploreTokenContextMenu } from 'src/components/explore/hooks' import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { Loader } from 'src/components/loading' import { Loader } from 'src/components/loading'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks' import { usePollOnFocusOnly } from 'src/utils/hooks'
...@@ -55,8 +48,6 @@ function FavoriteTokenCard({ ...@@ -55,8 +48,6 @@ function FavoriteTokenCard({
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const tokenDetailsNavigation = useTokenDetailsNavigation() const tokenDetailsNavigation = useTokenDetailsNavigation()
const { convertFiatAmountFormatted } = useLocalizationContext() const { convertFiatAmountFormatted } = useLocalizationContext()
const dragAnimationProgress = useSharedValue(0)
const wasTouched = useSharedValue(false)
const { data, networkStatus, startPolling, stopPolling } = useFavoriteTokenCardQuery({ const { data, networkStatus, startPolling, stopPolling } = useFavoriteTokenCardQuery({
variables: currencyIdToContractInput(currencyId), variables: currencyIdToContractInput(currencyId),
...@@ -103,45 +94,14 @@ function FavoriteTokenCard({ ...@@ -103,45 +94,14 @@ function FavoriteTokenCard({
tokenDetailsNavigation.navigate(currencyId) tokenDetailsNavigation.navigate(currencyId)
} }
useAnimatedReaction( const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress)
() => dragActivationProgress.value,
(activationProgress, prev) => {
const prevActivationProgress = prev ?? 0
// If the activation progress is increasing (the user is touching one of the cards)
if (activationProgress > prevActivationProgress) {
if (isTouched.value) {
// If the current card is the one being touched, reset the animation progress
wasTouched.value = true
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
wasTouched.value = false
dragAnimationProgress.value = activationProgress
}
}
// If the activation progress is decreasing (the user is no longer touching one of the cards)
else {
if (isTouched.value || wasTouched.value) {
// If the current card is the one that was being touched, reset the animation progress
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
dragAnimationProgress.value = activationProgress
}
}
}
)
const animatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(dragAnimationProgress.value, [0, 1], [1, 0.5]),
}))
if (isNonPollingRequestInFlight(networkStatus)) { if (isNonPollingRequestInFlight(networkStatus)) {
return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} /> return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
} }
return ( return (
<AnimatedFlex style={animatedStyle}> <AnimatedFlex style={animatedDragStyle}>
<ContextMenu <ContextMenu
actions={menuActions} actions={menuActions}
disabled={isEditing} disabled={isEditing}
......
import { ImpactFeedbackStyle } from 'expo-haptics' import { ImpactFeedbackStyle } from 'expo-haptics'
import { default as React, useCallback, useMemo } from 'react' import { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ViewProps } from 'react-native' import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { SharedValue } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks'
import { useAnimatedCardDragStyle } from 'src/components/explore/hooks'
import RemoveButton from 'src/components/explore/RemoveButton' import RemoveButton from 'src/components/explore/RemoveButton'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, TouchableArea } from 'ui/src' import { AnimatedFlex, Flex, TouchableArea } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme' import { borderRadii, iconSizes } from 'ui/src/theme'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
...@@ -19,12 +21,16 @@ import { DisplayNameType } from 'wallet/src/features/wallet/types' ...@@ -19,12 +21,16 @@ import { DisplayNameType } from 'wallet/src/features/wallet/types'
type FavoriteWalletCardProps = { type FavoriteWalletCardProps = {
address: Address address: Address
isEditing?: boolean isEditing?: boolean
isTouched: SharedValue<boolean>
dragActivationProgress: SharedValue<number>
setIsEditing: (update: boolean) => void setIsEditing: (update: boolean) => void
} & ViewProps } & ViewProps
export default function FavoriteWalletCard({ function FavoriteWalletCard({
address, address,
isEditing, isEditing,
isTouched,
dragActivationProgress,
setIsEditing, setIsEditing,
...rest ...rest
}: FavoriteWalletCardProps): JSX.Element { }: FavoriteWalletCardProps): JSX.Element {
...@@ -51,7 +57,10 @@ export default function FavoriteWalletCard({ ...@@ -51,7 +57,10 @@ export default function FavoriteWalletCard({
] ]
}, [t]) }, [t])
const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress)
return ( return (
<AnimatedFlex style={animatedDragStyle}>
<ContextMenu <ContextMenu
actions={menuActions} actions={menuActions}
disabled={isEditing} disabled={isEditing}
...@@ -70,7 +79,10 @@ export default function FavoriteWalletCard({ ...@@ -70,7 +79,10 @@ export default function FavoriteWalletCard({
{...rest}> {...rest}>
<TouchableArea <TouchableArea
hapticFeedback hapticFeedback
activeOpacity={isEditing ? 1 : undefined}
backgroundColor="$surface2"
borderRadius="$rounded16" borderRadius="$rounded16"
disabled={isEditing}
hapticStyle={ImpactFeedbackStyle.Light} hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4" m="$spacing4"
onLongPress={disableOnPress} onLongPress={disableOnPress}
...@@ -97,5 +109,8 @@ export default function FavoriteWalletCard({ ...@@ -97,5 +109,8 @@ export default function FavoriteWalletCard({
</BaseCard.Shadow> </BaseCard.Shadow>
</TouchableArea> </TouchableArea>
</ContextMenu> </ContextMenu>
</AnimatedFlex>
) )
} }
export default memo(FavoriteWalletCard)
import { default as React, useEffect, useMemo, useState } from 'react' import { default as React, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FadeIn } from 'react-native-reanimated' import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { useAppSelector } from 'src/app/hooks' import { useAppSelector } from 'src/app/hooks'
import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow'
import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard' import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard'
import { Loader } from 'src/components/loading' import { Loader } from 'src/components/loading'
import {
AutoScrollProps,
SortableGrid,
SortableGridChangeEvent,
SortableGridRenderItem,
} from 'src/components/sortableGrid'
import { AnimatedFlex, Flex } from 'ui/src' import { AnimatedFlex, Flex } from 'ui/src'
import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors'
import { setFavoriteWallets } from 'wallet/src/features/favorites/slice'
import { useAppDispatch } from 'wallet/src/state'
const NUM_COLUMNS = 2 const NUM_COLUMNS = 2
const ITEM_FLEX = { flex: 1 / NUM_COLUMNS } const ITEM_FLEX = { flex: 1 / NUM_COLUMNS }
const HALF_WIDTH = { width: '50%' }
type FavoriteWalletsGridProps = AutoScrollProps & {
showLoading: boolean
}
/** Renders the favorite wallets section on the Explore tab */ /** Renders the favorite wallets section on the Explore tab */
export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }): JSX.Element { export function FavoriteWalletsGrid({
showLoading,
...rest
}: FavoriteWalletsGridProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch()
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const isTokenDragged = useSharedValue(false)
const watchedWalletsSet = useAppSelector(selectWatchedAddressSet) const watchedWalletsSet = useAppSelector(selectWatchedAddressSet)
const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet]) const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet])
...@@ -27,8 +43,33 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }): ...@@ -27,8 +43,33 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }):
} }
}, [watchedWalletsSet.size]) }, [watchedWalletsSet.size])
const handleOrderChange = useCallback(
({ data }: SortableGridChangeEvent<string>) => {
dispatch(setFavoriteWallets({ addresses: data }))
},
[dispatch]
)
const renderItem = useCallback<SortableGridRenderItem<string>>(
({ item: address, isTouched, dragActivationProgress }): JSX.Element => (
<FavoriteWalletCard
key={address}
address={address}
dragActivationProgress={dragActivationProgress}
isEditing={isEditing}
isTouched={isTouched}
setIsEditing={setIsEditing}
/>
),
[isEditing]
)
const animatedStyle = useAnimatedStyle(() => ({
zIndex: isTokenDragged.value ? 1 : 0,
}))
return ( return (
<AnimatedFlex entering={FadeIn}> <AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow <FavoriteHeaderRow
editingTitle={t('Edit favorite wallets')} editingTitle={t('Edit favorite wallets')}
isEditing={isEditing} isEditing={isEditing}
...@@ -38,17 +79,21 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }): ...@@ -38,17 +79,21 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }):
{showLoading ? ( {showLoading ? (
<FavoriteWalletsGridLoader /> <FavoriteWalletsGridLoader />
) : ( ) : (
<Flex row flexWrap="wrap"> <SortableGrid
{watchedWalletsList.map((address) => ( {...rest}
<FavoriteWalletCard activeItemOpacity={1}
key={address} data={watchedWalletsList}
address={address} editable={isEditing}
isEditing={isEditing} numColumns={NUM_COLUMNS}
setIsEditing={setIsEditing} renderItem={renderItem}
style={HALF_WIDTH} onChange={handleOrderChange}
onDragEnd={(): void => {
isTokenDragged.value = false
}}
onDragStart={(): void => {
isTokenDragged.value = true
}}
/> />
))}
</Flex>
)} )}
</AnimatedFlex> </AnimatedFlex>
) )
......
import { SharedEventName } from '@uniswap/analytics-events' import { SharedEventName } from '@uniswap/analytics-events'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { NativeSyntheticEvent, Share } from 'react-native' import { NativeSyntheticEvent, Share, ViewStyle } from 'react-native'
import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view'
import {
AnimateStyle,
SharedValue,
interpolate,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks' import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
...@@ -148,3 +156,44 @@ export function useExploreTokenContextMenu({ ...@@ -148,3 +156,44 @@ export function useExploreTokenContextMenu({
return { menuActions, onContextMenuPress } return { menuActions, onContextMenuPress }
} }
export function useAnimatedCardDragStyle(
isTouched: SharedValue<boolean>,
dragActivationProgress: SharedValue<number>
): AnimateStyle<ViewStyle> {
const wasTouched = useSharedValue(false)
const dragAnimationProgress = useSharedValue(0)
useAnimatedReaction(
() => dragActivationProgress.value,
(activationProgress, prev) => {
const prevActivationProgress = prev ?? 0
// If the activation progress is increasing (the user is touching one of the cards)
if (activationProgress > prevActivationProgress) {
if (isTouched.value) {
// If the current card is the one being touched, reset the animation progress
wasTouched.value = true
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
wasTouched.value = false
dragAnimationProgress.value = activationProgress
}
}
// If the activation progress is decreasing (the user is no longer touching one of the cards)
else {
if (isTouched.value || wasTouched.value) {
// If the current card is the one that was being touched, reset the animation progress
dragAnimationProgress.value = 0
} else {
// Otherwise, animate the card
dragAnimationProgress.value = activationProgress
}
}
}
)
return useAnimatedStyle(() => ({
opacity: interpolate(dragAnimationProgress.value, [0, 1], [1, 0.5]),
}))
}
...@@ -468,44 +468,27 @@ exports[`SearchPopularTokens renders without error 2`] = ` ...@@ -468,44 +468,27 @@ exports[`SearchPopularTokens renders without error 2`] = `
onPress={[Function]} onPress={[Function]}
> >
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={ hitSlop={[Function]}
{ minPressDuration={0}
"bottom": 5, onBlur={[Function]}
"left": 5, onFocus={[Function]}
"right": 5, onLongPress={[Function]}
"top": 5, onMouseEnter={[Function]}
} onMouseLeave={[Function]}
} onPress={[Function]}
onClick={[Function]} onPressIn={[Function]}
onResponderGrant={[Function]} onPressOut={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
testID="search-token-item" testID="search-token-item"
...@@ -667,44 +650,27 @@ exports[`SearchPopularTokens renders without error 2`] = ` ...@@ -667,44 +650,27 @@ exports[`SearchPopularTokens renders without error 2`] = `
onPress={[Function]} onPress={[Function]}
> >
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={ hitSlop={[Function]}
{ minPressDuration={0}
"bottom": 5, onBlur={[Function]}
"left": 5, onFocus={[Function]}
"right": 5, onLongPress={[Function]}
"top": 5, onMouseEnter={[Function]}
} onMouseLeave={[Function]}
} onPress={[Function]}
onClick={[Function]} onPressIn={[Function]}
onResponderGrant={[Function]} onPressOut={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
testID="search-token-item" testID="search-token-item"
......
...@@ -2,17 +2,23 @@ import { Currency } from '@uniswap/sdk-core' ...@@ -2,17 +2,23 @@ import { Currency } from '@uniswap/sdk-core'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { useFiatOnRampLogoUrl } from 'src/components/fiatOnRamp/hooks'
import { Loader } from 'src/components/loading' import { Loader } from 'src/components/loading'
import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks' import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks'
import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { Flex, Icons, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { fonts, iconSizes } from 'ui/src/theme' import { fonts, iconSizes } from 'ui/src/theme'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types'
import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
import { ImageUri } from 'wallet/src/features/images/ImageUri' import { ImageUri } from 'wallet/src/features/images/ImageUri'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { getSymbolDisplayText } from 'wallet/src/utils/currency' import { getSymbolDisplayText } from 'wallet/src/utils/currency'
function LogoLoader(): JSX.Element {
return (
<Loader.Box borderRadius="$roundedFull" height={iconSizes.icon40} width={iconSizes.icon40} />
)
}
export function FORQuoteItem({ export function FORQuoteItem({
quote, quote,
serviceProvider, serviceProvider,
...@@ -46,7 +52,8 @@ export function FORQuoteItem({ ...@@ -46,7 +52,8 @@ export function FORQuoteItem({
currencySymbol: baseCurrency.symbol, currencySymbol: baseCurrency.symbol,
}) })
const logoUrl = useFiatOnRampLogoUrl(serviceProvider?.logos) const isDarkMode = useIsDarkMode()
const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode)
return ( return (
<TouchableArea onPress={onPress}> <TouchableArea onPress={onPress}>
...@@ -62,11 +69,17 @@ export function FORQuoteItem({ ...@@ -62,11 +69,17 @@ export function FORQuoteItem({
<QuoteLoader showCarret={showCarret} /> <QuoteLoader showCarret={showCarret} />
) : ( ) : (
<Flex row alignItems="center" gap="$spacing12"> <Flex row alignItems="center" gap="$spacing12">
<Loader.Box <Flex>
borderRadius="$roundedFull" {logoUrl ? (
height={iconSizes.icon40} <ImageUri
width={iconSizes.icon40} fallback={<LogoLoader />}
imageStyle={ServiceProviderLogoStyles.icon}
uri={logoUrl}
/> />
) : (
<LogoLoader />
)}
</Flex>
<Flex shrink gap="$spacing4"> <Flex shrink gap="$spacing4">
<Text color="$neutral1" variant="subheading2"> <Text color="$neutral1" variant="subheading2">
{serviceProvider?.name} {serviceProvider?.name}
...@@ -96,23 +109,6 @@ export function FORQuoteItem({ ...@@ -96,23 +109,6 @@ export function FORQuoteItem({
<Flex /> <Flex />
)} )}
</Flex> </Flex>
{
// TODO: Enable once https://linear.app/uniswap/issue/MOB-2565/implement-service-providers-logo-once-meld-has-added-them-on-their is unblocked
false && logoUrl && (
<ImageUri
fallback={
<Loader.Box
borderRadius="$roundedFull"
height={iconSizes.icon40}
width={iconSizes.icon40}
/>
}
imageStyle={ServiceProviderLogoStyles.icon}
resizeMode="contain"
uri={logoUrl}
/>
)
}
</Flex> </Flex>
)} )}
</Flex> </Flex>
......
import { useIsDarkMode } from 'ui/src'
import { FORLogo } from 'wallet/src/features/fiatOnRamp/types'
export function useFiatOnRampLogoUrl(logos: FORLogo | undefined): string | undefined {
const isDarkMode = useIsDarkMode()
if (!logos) {
return
}
return isDarkMode ? logos.darkLogo : logos.lightLogo
}
import React from 'react'
import { WalletLoader } from 'src/components/loading/WalletLoader'
import { render } from 'src/test/test-utils'
it('renders wallet loader', () => {
const tree = render(<WalletLoader opacity={1} />)
expect(tree).toMatchSnapshot()
})
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders wallet loader 1`] = `
<View
sentry-label="WalletLoader"
style={
{
"alignItems": "center",
"borderBottomColor": "#CECECE",
"borderBottomLeftRadius": 20,
"borderBottomRightRadius": 20,
"borderBottomWidth": 1,
"borderLeftColor": "#CECECE",
"borderLeftWidth": 1,
"borderRightColor": "#CECECE",
"borderRightWidth": 1,
"borderStyle": "solid",
"borderTopColor": "#CECECE",
"borderTopLeftRadius": 20,
"borderTopRightRadius": 20,
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "flex-start",
"opacity": 1,
"overflow": "hidden",
"paddingBottom": 16,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 16,
}
}
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"gap": 12,
"height": 36,
}
}
>
<View
style={
{
"backgroundColor": "#CECECE",
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
"borderTopRightRadius": 999999,
"flexDirection": "column",
"height": 32,
"width": 32,
}
}
/>
<View
style={
{
"alignItems": "flex-start",
"flexDirection": "column",
"width": "100%",
}
}
>
<View
onLayout={[Function]}
style={
{
"flexDirection": "column",
"opacity": 0,
}
}
testID="shimmer-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
testID="text-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"position": "relative",
}
}
>
<View
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "transparent",
"fontFamily": "Basel-Book",
"fontSize": 19,
"lineHeight": 24,
"opacity": 0,
}
}
suppressHighlighting={true}
>
Wallet Nickname
</Text>
</View>
<View
style={
{
"backgroundColor": "#F9F9F9",
"borderBottomLeftRadius": 4,
"borderBottomRightRadius": 4,
"borderTopLeftRadius": 4,
"borderTopRightRadius": 4,
"bottom": "5%",
"flexDirection": "column",
"left": 0,
"position": "absolute",
"right": 0,
"top": "5%",
}
}
/>
</View>
</View>
</View>
<View
onLayout={[Function]}
style={
{
"flexDirection": "column",
"opacity": 0,
}
}
testID="shimmer-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
testID="text-placeholder"
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"position": "relative",
}
}
>
<View
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "transparent",
"fontFamily": "Basel-Book",
"fontSize": 17,
"lineHeight": 24,
"opacity": 0,
}
}
suppressHighlighting={true}
>
0xaaaa...aaaa
</Text>
</View>
<View
style={
{
"backgroundColor": "#F9F9F9",
"borderBottomLeftRadius": 4,
"borderBottomRightRadius": 4,
"borderTopLeftRadius": 4,
"borderTopRightRadius": 4,
"bottom": "5%",
"flexDirection": "column",
"left": 0,
"position": "absolute",
"right": 0,
"top": "5%",
}
}
/>
</View>
</View>
</View>
</View>
</View>
</View>
`;
import React, { memo } from 'react' import React, { memo } from 'react'
import { TransactionLoader } from 'src/components/loading/TransactionLoader' import { TransactionLoader } from 'src/components/loading/TransactionLoader'
import { WalletLoader } from 'src/components/loading/WalletLoader'
import { WaveLoader } from 'src/components/loading/WaveLoader' import { WaveLoader } from 'src/components/loading/WaveLoader'
import { Flex, FlexLoader, FlexLoaderProps, getToken, Skeleton } from 'ui/src' import { Flex, FlexLoader, FlexLoaderProps, getToken, Skeleton } from 'ui/src'
...@@ -12,20 +11,6 @@ function Graph(): JSX.Element { ...@@ -12,20 +11,6 @@ function Graph(): JSX.Element {
) )
} }
function Wallets({ repeat = 1 }: { repeat?: number }): JSX.Element {
return (
<Skeleton>
<Flex gap="$spacing12">
{new Array(repeat).fill(null).map((_, i, { length }) => (
<React.Fragment key={i}>
<WalletLoader opacity={(length - i) / length} />
</React.Fragment>
))}
</Flex>
</Skeleton>
)
}
export const Transaction = memo(function _Transaction({ export const Transaction = memo(function _Transaction({
repeat = 1, repeat = 1,
}: { }: {
...@@ -72,7 +57,6 @@ function Favorite({ height, contrast }: { height?: number; contrast?: boolean }) ...@@ -72,7 +57,6 @@ function Favorite({ height, contrast }: { height?: number; contrast?: boolean })
export const Loader = { export const Loader = {
Box, Box,
Transaction, Transaction,
Wallets,
Graph, Graph,
Image, Image,
Favorite, Favorite,
......
...@@ -30,6 +30,9 @@ export function useAnimatedZIndex(renderIndex: number): SharedValue<number> { ...@@ -30,6 +30,9 @@ export function useAnimatedZIndex(renderIndex: number): SharedValue<number> {
previousActiveIndex: previousActiveIndexValue.value, previousActiveIndex: previousActiveIndexValue.value,
}), }),
({ touchedIndex, previousActiveIndex }) => { ({ touchedIndex, previousActiveIndex }) => {
if (touchedIndex === null) {
return null
}
if (renderIndex === touchedIndex) { if (renderIndex === touchedIndex) {
// Display the currently touched item on top of all other items // Display the currently touched item on top of all other items
zIndexValue.value = 10000 zIndexValue.value = 10000
......
...@@ -49,15 +49,15 @@ export function UnitagsIntroModal(): JSX.Element { ...@@ -49,15 +49,15 @@ export function UnitagsIntroModal(): JSX.Element {
<Flex gap="$spacing24" px="$spacing24" py="$spacing16"> <Flex gap="$spacing24" px="$spacing24" py="$spacing16">
<Flex alignItems="center" gap="$spacing12"> <Flex alignItems="center" gap="$spacing12">
<Text variant="subheading1">{t('Introducing usernames')}</Text> <Text variant="subheading1">{t('Introducing usernames')}</Text>
<Text color="$neutral2" textAlign="center" variant="body3"> <Text color="$neutral2" textAlign="center" variant="body2">
{t( {t(
'Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.' 'Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.'
)} )}
</Text> </Text>
</Flex> </Flex>
<Flex alignItems="center" maxHeight={100}> <Flex alignItems="center" maxHeight={105}>
<Image <Image
maxHeight={100} maxHeight={105}
resizeMode="contain" resizeMode="contain"
source={isDarkMode ? UNITAGS_INTRO_BANNER_DARK : UNITAGS_INTRO_BANNER_LIGHT} source={isDarkMode ? UNITAGS_INTRO_BANNER_DARK : UNITAGS_INTRO_BANNER_LIGHT}
/> />
......
import React, { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Keyboard, TextInput } from 'react-native' import { Keyboard, TextInput } from 'react-native'
import { PasswordInput } from 'src/components/input/PasswordInput' import { PasswordInput } from 'src/components/input/PasswordInput'
import { PasswordError } from 'src/features/onboarding/PasswordError' import { PasswordError } from 'src/features/onboarding/PasswordError'
import { Button, Flex, Icons, Text } from 'ui/src' import { Button, Flex, Icons, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { useDebounce } from 'utilities/src/time/timing'
import { ElementName } from 'wallet/src/telemetry/constants' import { ElementName } from 'wallet/src/telemetry/constants'
import { validatePassword } from 'wallet/src/utils/password' import {
PASSWORD_VALIDATION_DEBOUNCE_MS,
PasswordStrength,
getPasswordStrength,
getPasswordStrengthTextAndColor,
isPasswordStrongEnough,
} from 'wallet/src/utils/password'
export enum PasswordErrors { export enum PasswordErrors {
WeakPassword = 'WeakPassword', WeakPassword = 'WeakPassword',
...@@ -29,9 +36,17 @@ export function CloudBackupPasswordForm({ ...@@ -29,9 +36,17 @@ export function CloudBackupPasswordForm({
const passwordInputRef = useRef<TextInput>(null) const passwordInputRef = useRef<TextInput>(null)
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [error, setError] = useState<PasswordErrors | string | undefined>(undefined) const [error, setError] = useState<PasswordErrors | undefined>(undefined)
const isButtonDisabled = !!error || password.length === 0 const [passwordStrength, setPasswordStrength] = useState(PasswordStrength.NONE)
const debouncedPasswordStrength = useDebounce(passwordStrength, PASSWORD_VALIDATION_DEBOUNCE_MS)
const isStrongPassword = isPasswordStrongEnough({
minStrength: PasswordStrength.MEDIUM,
currentStrength: passwordStrength,
})
const isButtonDisabled =
!!error || password.length === 0 || (!isConfirmation && !isStrongPassword)
const onPasswordChangeText = (newPassword: string): void => { const onPasswordChangeText = (newPassword: string): void => {
if (isConfirmation && newPassword === password) { if (isConfirmation && newPassword === password) {
...@@ -39,15 +54,15 @@ export function CloudBackupPasswordForm({ ...@@ -39,15 +54,15 @@ export function CloudBackupPasswordForm({
} }
// always reset error if not confirmation // always reset error if not confirmation
if (!isConfirmation) { if (!isConfirmation) {
setPasswordStrength(getPasswordStrength(newPassword))
setError(undefined) setError(undefined)
} }
setPassword(newPassword) setPassword(newPassword)
} }
const onPasswordSubmitEditing = (): void => { const onPasswordSubmitEditing = (): void => {
const { valid, validationErrorString } = validatePassword(password) if (!isConfirmation && !isStrongPassword) {
if (!isConfirmation && !valid) { setError(PasswordErrors.WeakPassword)
setError(validationErrorString || PasswordErrors.WeakPassword)
return return
} }
if (isConfirmation && passwordToConfirm !== password) { if (isConfirmation && passwordToConfirm !== password) {
...@@ -59,9 +74,8 @@ export function CloudBackupPasswordForm({ ...@@ -59,9 +74,8 @@ export function CloudBackupPasswordForm({
} }
const onPressNext = (): void => { const onPressNext = (): void => {
const { valid, validationErrorString } = validatePassword(password) if (!isConfirmation && !isStrongPassword) {
if (!isConfirmation && !valid) { setError(PasswordErrors.WeakPassword)
setError(validationErrorString || PasswordErrors.WeakPassword)
return return
} }
if (isConfirmation && passwordToConfirm !== password) { if (isConfirmation && passwordToConfirm !== password) {
...@@ -99,6 +113,7 @@ export function CloudBackupPasswordForm({ ...@@ -99,6 +113,7 @@ export function CloudBackupPasswordForm({
}} }}
onSubmitEditing={onPasswordSubmitEditing} onSubmitEditing={onPasswordSubmitEditing}
/> />
{!isConfirmation && <PasswordStrengthText strength={debouncedPasswordStrength} />}
{error ? <PasswordError errorText={errorText} /> : null} {error ? <PasswordError errorText={errorText} /> : null}
</Flex> </Flex>
{!isConfirmation && ( {!isConfirmation && (
...@@ -118,3 +133,17 @@ export function CloudBackupPasswordForm({ ...@@ -118,3 +133,17 @@ export function CloudBackupPasswordForm({
</> </>
) )
} }
function PasswordStrengthText({ strength }: { strength: PasswordStrength }): JSX.Element {
const { text, color } = getPasswordStrengthTextAndColor(strength)
const hasPassword = strength !== PasswordStrength.NONE
return (
<Flex centered row opacity={hasPassword ? 1 : 0} pt="$spacing12" px="$spacing8">
<Text color={color} variant="body3">
<Trans>This is a {text.toLowerCase()} password</Trans>
</Text>
</Flex>
)
}
...@@ -45,6 +45,13 @@ interface ProfileHeaderProps { ...@@ -45,6 +45,13 @@ interface ProfileHeaderProps {
address: Address address: Address
} }
const HEADER_SOLID_COLOR_OPACITY = 0.1
export const solidHeaderProps = {
minOpacity: HEADER_SOLID_COLOR_OPACITY,
maxOpacity: HEADER_SOLID_COLOR_OPACITY,
}
export const ProfileHeader = memo(function ProfileHeader({ export const ProfileHeader = memo(function ProfileHeader({
address, address,
}: ProfileHeaderProps): JSX.Element { }: ProfileHeaderProps): JSX.Element {
...@@ -167,9 +174,12 @@ export const ProfileHeader = memo(function ProfileHeader({ ...@@ -167,9 +174,12 @@ export const ProfileHeader = memo(function ProfileHeader({
/> />
</Flex> </Flex>
{hasAvatar && avatarColors?.primary ? ( {hasAvatar && avatarColors?.primary ? (
<HeaderRadial color={avatarColors.primary} /> <HeaderRadial color={avatarColors.primary} {...solidHeaderProps} />
) : ( ) : (
<HeaderRadial color={isUniconsV2Enabled ? color : uniconGradientStart} /> <HeaderRadial
color={isUniconsV2Enabled ? color : uniconGradientStart}
{...solidHeaderProps}
/>
)} )}
</AnimatedFlex> </AnimatedFlex>
......
import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { ExchangeTransferConnecting } from 'src/screens/ExchangeTransferConnecting'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ModalName } from 'wallet/src/telemetry/constants'
export function ExchangeTransferModal(): JSX.Element | null {
const dispatch = useAppDispatch()
const onClose = (): void => {
dispatch(closeModal({ name: ModalName.ExchangeTransferModal }))
}
const { initialState } = useAppSelector(selectModalState(ModalName.ExchangeTransferModal))
const serviceProvider = initialState?.serviceProvider
return serviceProvider ? (
<BottomSheetModal
fullScreen
hideHandlebar
hideKeyboardOnDismiss
renderBehindTopInset
name={ModalName.ExchangeTransferModal}
onClose={onClose}>
<ExchangeTransferConnecting serviceProvider={serviceProvider} onClose={onClose} />
</BottomSheetModal>
) : null
}
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
export interface ExchangeTransferModalState {
serviceProvider: FORTransferInstitution
}
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
export const SERVICE_PROVIDER_ICON_SIZE = 90 export const SERVICE_PROVIDER_ICON_SIZE = 90
export const SERVICE_PROVIDER_ICON_BORDER_RADIUS = 20
export function FiatOnRampConnectingView({ export function FiatOnRampConnectingView({
amount, amount,
...@@ -18,7 +19,7 @@ export function FiatOnRampConnectingView({ ...@@ -18,7 +19,7 @@ export function FiatOnRampConnectingView({
serviceProviderName, serviceProviderName,
serviceProviderLogo, serviceProviderLogo,
}: { }: {
amount: string amount?: string
quoteCurrencyCode?: string quoteCurrencyCode?: string
serviceProviderName: string serviceProviderName: string
serviceProviderLogo?: JSX.Element serviceProviderLogo?: JSX.Element
...@@ -48,7 +49,7 @@ export function FiatOnRampConnectingView({ ...@@ -48,7 +49,7 @@ export function FiatOnRampConnectingView({
<Text variant="subheading1"> <Text variant="subheading1">
{t('Connecting you to {{serviceProvider}}', { serviceProvider: serviceProviderName })} {t('Connecting you to {{serviceProvider}}', { serviceProvider: serviceProviderName })}
</Text> </Text>
{quoteCurrencyCode && ( {quoteCurrencyCode && amount && (
<Text color="$neutral2" variant="body2"> <Text color="$neutral2" variant="body2">
{t('Buying {{amount}} worth of {{quoteCurrencyCode}}', { {t('Buying {{amount}} worth of {{quoteCurrencyCode}}', {
amount, amount,
...@@ -73,7 +74,7 @@ const styles = StyleSheet.create({ ...@@ -73,7 +74,7 @@ const styles = StyleSheet.create({
}, },
uniswapLogoWrapper: { uniswapLogoWrapper: {
backgroundColor: '#FFEFF8', // #FFD8EF with 40% opacity on a white background backgroundColor: '#FFEFF8', // #FFD8EF with 40% opacity on a white background
borderRadius: 20, borderRadius: SERVICE_PROVIDER_ICON_BORDER_RADIUS,
height: SERVICE_PROVIDER_ICON_SIZE, height: SERVICE_PROVIDER_ICON_SIZE,
width: SERVICE_PROVIDER_ICON_SIZE, width: SERVICE_PROVIDER_ICON_SIZE,
}, },
......
import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import { BottomSheetFlatList } from '@gorhom/bottom-sheet'
import { ImpactFeedbackStyle } from 'expo-haptics' import { ImpactFeedbackStyle } from 'expo-haptics'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { ListRenderItemInfo } from 'react-native' import { ListRenderItemInfo } from 'react-native'
import { getCountry } from 'react-native-localize' import { getCountry } from 'react-native-localize'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks'
import { openModal } from 'src/features/modals/modalSlice'
import { AnimatedFlex, Flex, Loader, Text, TouchableArea } from 'ui/src' import { AnimatedFlex, Flex, Loader, Text, TouchableArea } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { useFiatOnRampAggregatorTransferInstitutionsQuery } from 'wallet/src/features/fiatOnRamp/api' import { useFiatOnRampAggregatorTransferInstitutionsQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
import { RemoteImage } from 'wallet/src/features/images/RemoteImage' import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
import { ModalName } from 'wallet/src/telemetry/constants'
function key(item: FORTransferInstitution): string { function key(item: FORTransferInstitution): string {
return item.id as string return item.id as string
...@@ -25,7 +27,6 @@ function CEXItemWrapper({ ...@@ -25,7 +27,6 @@ function CEXItemWrapper({
institution: FORTransferInstitution institution: FORTransferInstitution
onSelectTransferInstitution: (transferInstitution: FORTransferInstitution) => void onSelectTransferInstitution: (transferInstitution: FORTransferInstitution) => void
}): JSX.Element | null { }): JSX.Element | null {
const { t } = useTranslation()
const onPress = (): void => onSelectTransferInstitution(institution) const onPress = (): void => onSelectTransferInstitution(institution)
return ( return (
...@@ -53,23 +54,29 @@ function CEXItemWrapper({ ...@@ -53,23 +54,29 @@ function CEXItemWrapper({
{institution.name} {institution.name}
</Text> </Text>
</Flex> </Flex>
<Text color="$neutral3" variant="body3">
{t('Not linked')}
</Text>
</Flex> </Flex>
</TouchableArea> </TouchableArea>
) )
} }
export function TransferInstitutionSelector(): JSX.Element { export function TransferInstitutionSelector({ onClose }: { onClose: () => void }): JSX.Element {
const dispatch = useAppDispatch()
const { data, isLoading } = useFiatOnRampAggregatorTransferInstitutionsQuery({ const { data, isLoading } = useFiatOnRampAggregatorTransferInstitutionsQuery({
countryCode: getCountry(), countryCode: getCountry(),
}) })
// eslint-disable-next-line @typescript-eslint/no-unused-vars const onSelectTransferInstitution = useCallback(
const onSelectTransferInstitution = useCallback((transferInstitution: FORTransferInstitution) => { (transferInstitution: FORTransferInstitution) => {
//TODO(MOB-2603): fetch widget and launch transfer flow dispatch(
}, []) openModal({
name: ModalName.ExchangeTransferModal,
initialState: { serviceProvider: transferInstitution },
})
)
onClose()
},
[dispatch, onClose]
)
const renderItem = useCallback( const renderItem = useCallback(
({ item: institution }: ListRenderItemInfo<FORTransferInstitution>) => ( ({ item: institution }: ListRenderItemInfo<FORTransferInstitution>) => (
......
...@@ -19,6 +19,7 @@ import { ...@@ -19,6 +19,7 @@ import {
isInvalidRequestAmountTooLow, isInvalidRequestAmountTooLow,
} from 'wallet/src/features/fiatOnRamp/utils' } from 'wallet/src/features/fiatOnRamp/utils'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks'
// TODO: https://linear.app/uniswap/issue/MOB-2532/implement-fetching-of-available-fiat-currencies-from-meld // TODO: https://linear.app/uniswap/issue/MOB-2532/implement-fetching-of-available-fiat-currencies-from-meld
const MELD_FIAT_CURRENCY_CODES = ['usd', 'eur'] const MELD_FIAT_CURRENCY_CODES = ['usd', 'eur']
...@@ -60,6 +61,7 @@ export function useFiatOnRampQuotes({ ...@@ -60,6 +61,7 @@ export function useFiatOnRampQuotes({
quotes: FORQuote[] | undefined quotes: FORQuote[] | undefined
} { } {
const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short) const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short)
const walletAddress = useActiveAccountAddress()
const { const {
currentData: quotesResponse, currentData: quotesResponse,
...@@ -72,6 +74,7 @@ export function useFiatOnRampQuotes({ ...@@ -72,6 +74,7 @@ export function useFiatOnRampQuotes({
sourceCurrencyCode: baseCurrencyCode, sourceCurrencyCode: baseCurrencyCode,
destinationCurrencyCode: quoteCurrencyCode, destinationCurrencyCode: quoteCurrencyCode,
countryCode, countryCode,
walletAddress: walletAddress ?? '',
} }
: skipToken, : skipToken,
{ {
......
...@@ -24,6 +24,7 @@ import { FORSupportedToken, MoonpayCurrency } from 'wallet/src/features/fiatOnRa ...@@ -24,6 +24,7 @@ import { FORSupportedToken, MoonpayCurrency } from 'wallet/src/features/fiatOnRa
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { addTransaction } from 'wallet/src/features/transactions/slice' import { addTransaction } from 'wallet/src/features/transactions/slice'
import { import {
FiatPurchaseTransactionInfo,
TransactionDetails, TransactionDetails,
TransactionStatus, TransactionStatus,
TransactionType, TransactionType,
...@@ -58,7 +59,10 @@ export function useFormatExactCurrencyAmount( ...@@ -58,7 +59,10 @@ export function useFormatExactCurrencyAmount(
} }
/** Returns a new externalTransactionId and a callback to store the transaction. */ /** Returns a new externalTransactionId and a callback to store the transaction. */
export function useFiatOnRampTransactionCreator(ownerAddress: string): { export function useFiatOnRampTransactionCreator(
ownerAddress: string,
initialTypeInfo?: Partial<FiatPurchaseTransactionInfo>
): {
externalTransactionId: string externalTransactionId: string
dispatchAddTransaction: () => void dispatchAddTransaction: () => void
} { } {
...@@ -73,7 +77,11 @@ export function useFiatOnRampTransactionCreator(ownerAddress: string): { ...@@ -73,7 +77,11 @@ export function useFiatOnRampTransactionCreator(ownerAddress: string): {
chainId: ChainId.Mainnet, chainId: ChainId.Mainnet,
id: externalTransactionId.current, id: externalTransactionId.current,
from: ownerAddress, from: ownerAddress,
typeInfo: { type: TransactionType.FiatPurchase, syncedWithBackend: false }, typeInfo: {
...initialTypeInfo,
type: TransactionType.FiatPurchase,
syncedWithBackend: false,
},
status: TransactionStatus.Pending, status: TransactionStatus.Pending,
addedTime: Date.now(), addedTime: Date.now(),
hash: '', hash: '',
...@@ -81,7 +89,7 @@ export function useFiatOnRampTransactionCreator(ownerAddress: string): { ...@@ -81,7 +89,7 @@ export function useFiatOnRampTransactionCreator(ownerAddress: string): {
} }
// use addTransaction action so transactionWatcher picks it up // use addTransaction action so transactionWatcher picks it up
dispatch(addTransaction(transactionDetail)) dispatch(addTransaction(transactionDetail))
}, [dispatch, externalTransactionId, ownerAddress]) }, [dispatch, ownerAddress, initialTypeInfo])
return { externalTransactionId: externalTransactionId.current, dispatchAddTransaction } return { externalTransactionId: externalTransactionId.current, dispatchAddTransaction }
} }
......
...@@ -120,7 +120,7 @@ function Inputs({ ...@@ -120,7 +120,7 @@ function Inputs({
py="$none" py="$none"
returnKeyType="done" returnKeyType="done"
scrollEnabled={false} scrollEnabled={false}
selectionColor={colors.neutral1.get()} selectionColor={colors.neutral1.val}
spellCheck={false} spellCheck={false}
testID="import_account_form/input" testID="import_account_form/input"
textAlign={isInputEmpty ? 'left' : backgroundTextAlignment} textAlign={isInputEmpty ? 'left' : backgroundTextAlignment}
......
...@@ -158,41 +158,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = ` ...@@ -158,41 +158,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
} }
> >
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={ hitSlop={[Function]}
{ minPressDuration={0}
"bottom": 5, onBlur={[Function]}
"left": 5, onFocus={[Function]}
"right": 5, onMouseEnter={[Function]}
"top": 5, onMouseLeave={[Function]}
} onPress={[Function]}
} onPressIn={[Function]}
onClick={[Function]} onPressOut={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
{ {
"backgroundColor": "#FFEFFF", "backgroundColor": "#FFEFFF",
...@@ -200,12 +176,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = ` ...@@ -200,12 +176,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
"borderBottomRightRadius": 12, "borderBottomRightRadius": 12,
"borderTopLeftRadius": 12, "borderTopLeftRadius": 12,
"borderTopRightRadius": 12, "borderTopRightRadius": 12,
"onPressIn": undefined, "flexDirection": "column",
"opacity": 1, "opacity": 1,
"paddingBottom": 8, "paddingBottom": 8,
"paddingLeft": 8, "paddingLeft": 8,
"paddingRight": 8, "paddingRight": 8,
"paddingTop": 8, "paddingTop": 8,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
......
...@@ -3,6 +3,7 @@ import { ScannerModalState } from 'src/components/QRCodeScanner/constants' ...@@ -3,6 +3,7 @@ import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' import { TransactionState } from 'wallet/src/features/transactions/transactionState/types'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
...@@ -13,6 +14,9 @@ export interface AppModalState<T> { ...@@ -13,6 +14,9 @@ export interface AppModalState<T> {
export interface ModalsState { export interface ModalsState {
[ModalName.AccountSwitcher]: AppModalState<undefined> [ModalName.AccountSwitcher]: AppModalState<undefined>
[ModalName.ExchangeTransferModal]: AppModalState<{
serviceProvider: FORTransferInstitution
}>
[ModalName.Experiments]: AppModalState<undefined> [ModalName.Experiments]: AppModalState<undefined>
[ModalName.Explore]: AppModalState<ExploreModalState> [ModalName.Explore]: AppModalState<ExploreModalState>
[ModalName.FiatCurrencySelector]: AppModalState<undefined> [ModalName.FiatCurrencySelector]: AppModalState<undefined>
......
...@@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' ...@@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ExploreModalState } from 'src/app/modals/ExploreModalState' import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
import { getKeys } from 'utilities/src/primitives/objects' import { getKeys } from 'utilities/src/primitives/objects'
...@@ -14,6 +15,11 @@ type AccountSwitcherModalParams = { ...@@ -14,6 +15,11 @@ type AccountSwitcherModalParams = {
initialState?: undefined initialState?: undefined
} }
type ExchangeTransferModalParams = {
name: typeof ModalName.ExchangeTransferModal
initialState?: ExchangeTransferModalState
}
type ExperimentsModalParams = { name: typeof ModalName.Experiments; initialState?: undefined } type ExperimentsModalParams = { name: typeof ModalName.Experiments; initialState?: undefined }
type ExploreModalParams = { type ExploreModalParams = {
...@@ -76,6 +82,7 @@ type ViewOnlyExplainerParams = { ...@@ -76,6 +82,7 @@ type ViewOnlyExplainerParams = {
export type OpenModalParams = export type OpenModalParams =
| AccountSwitcherModalParams | AccountSwitcherModalParams
| ExchangeTransferModalParams
| ExperimentsModalParams | ExperimentsModalParams
| ExploreModalParams | ExploreModalParams
| FiatCurrencySelectorParams | FiatCurrencySelectorParams
...@@ -95,6 +102,10 @@ export type OpenModalParams = ...@@ -95,6 +102,10 @@ export type OpenModalParams =
export type CloseModalParams = { name: keyof ModalsState } export type CloseModalParams = { name: keyof ModalsState }
export const initialModalState: ModalsState = { export const initialModalState: ModalsState = {
[ModalName.ExchangeTransferModal]: {
isOpen: false,
initialState: undefined,
},
[ModalName.FiatOnRamp]: { [ModalName.FiatOnRamp]: {
isOpen: false, isOpen: false,
initialState: undefined, initialState: undefined,
......
...@@ -2,44 +2,27 @@ ...@@ -2,44 +2,27 @@
exports[`renders collection preview card 1`] = ` exports[`renders collection preview card 1`] = `
<View <View
accessibilityState={ cancelable={true}
{ disabled={false}
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={ hitSlop={[Function]}
{ minPressDuration={0}
"bottom": 5, onBlur={[Function]}
"left": 5, onFocus={[Function]}
"right": 5, onMouseEnter={[Function]}
"top": 5, onMouseLeave={[Function]}
} onPress={[Function]}
} onPressIn={[Function]}
onClick={[Function]} onPressOut={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
......
...@@ -54,6 +54,7 @@ export enum MobileEventName { ...@@ -54,6 +54,7 @@ export enum MobileEventName {
FiatOnRampQuickActionButtonPressed = 'Fiat OnRamp QuickAction Button Pressed', FiatOnRampQuickActionButtonPressed = 'Fiat OnRamp QuickAction Button Pressed',
FiatOnRampAmountEntered = 'Fiat OnRamp Amount Entered', FiatOnRampAmountEntered = 'Fiat OnRamp Amount Entered',
FiatOnRampWidgetOpened = 'Fiat OnRamp Widget Opened', FiatOnRampWidgetOpened = 'Fiat OnRamp Widget Opened',
NotificationsToggled = 'Notifications Toggled',
OnboardingCompleted = 'Onboarding Completed', OnboardingCompleted = 'Onboarding Completed',
PerformanceReport = 'Performance Report', PerformanceReport = 'Performance Report',
PerformanceGraphql = 'Performance GraphQL', PerformanceGraphql = 'Performance GraphQL',
...@@ -86,6 +87,8 @@ export enum UserPropertyName { ...@@ -86,6 +87,8 @@ export enum UserPropertyName {
AppOpenAuthMethod = 'app_open_auth_method', AppOpenAuthMethod = 'app_open_auth_method',
AppVersion = 'app_version', AppVersion = 'app_version',
DarkMode = 'is_dark_mode', DarkMode = 'is_dark_mode',
HasLoadedENS = 'has_loaded_ens',
HasLoadedUnitag = 'has_loaded_unitag',
IsCloudBackedUp = 'is_cloud_backed_up', IsCloudBackedUp = 'is_cloud_backed_up',
IsHideSmallBalancesEnabled = 'is_hide_small_balances_enabled', IsHideSmallBalancesEnabled = 'is_hide_small_balances_enabled',
IsHideSpamTokensEnabled = 'is_hide_spam_tokens_enabled', IsHideSpamTokensEnabled = 'is_hide_spam_tokens_enabled',
......
...@@ -45,10 +45,27 @@ export const slice = createSlice({ ...@@ -45,10 +45,27 @@ export const slice = createSlice({
state.walletIsFunded = true state.walletIsFunded = true
}, },
setAllowAnalytics: (state, { payload: { enabled } }: PayloadAction<{ enabled: boolean }>) => { setAllowAnalytics: (state, { payload: { enabled } }: PayloadAction<{ enabled: boolean }>) => {
const logToggleEvent = (): void => {
sendWalletAnalyticsEvent(SharedEventName.ANALYTICS_SWITCH_TOGGLED, { enabled }) sendWalletAnalyticsEvent(SharedEventName.ANALYTICS_SWITCH_TOGGLED, { enabled })
analytics.flushEvents() analytics.flushEvents()
// eslint-disable-next-line no-void }
void analytics.setAllowAnalytics(enabled).finally(() => undefined)
// If turning off, log toggle event before turning off analytics
if (!enabled) {
logToggleEvent()
}
analytics
.setAllowAnalytics(enabled)
.then(() => {
// If turned on, log toggle event after turning on analytics
if (enabled) {
logToggleEvent()
}
})
.catch(() => undefined)
// Set enabled in user state
state.allowAnalytics = enabled state.allowAnalytics = enabled
}, },
}, },
......
...@@ -68,6 +68,9 @@ export type MobileEventProperties = { ...@@ -68,6 +68,9 @@ export type MobileEventProperties = {
[MobileEventName.FiatOnRampBannerPressed]: TraceProps [MobileEventName.FiatOnRampBannerPressed]: TraceProps
[MobileEventName.FiatOnRampAmountEntered]: TraceProps & { source: 'chip' | 'textInput' } [MobileEventName.FiatOnRampAmountEntered]: TraceProps & { source: 'chip' | 'textInput' }
[MobileEventName.FiatOnRampWidgetOpened]: TraceProps & { externalTransactionId: string } [MobileEventName.FiatOnRampWidgetOpened]: TraceProps & { externalTransactionId: string }
[MobileEventName.NotificationsToggled]: TraceProps & {
enabled: boolean
}
[MobileEventName.OnboardingCompleted]: OnboardingCompletedProps & TraceProps [MobileEventName.OnboardingCompleted]: OnboardingCompletedProps & TraceProps
[MobileEventName.PerformanceReport]: RenderPassReport [MobileEventName.PerformanceReport]: RenderPassReport
[MobileEventName.PerformanceGraphql]: { [MobileEventName.PerformanceGraphql]: {
......
import { useCallback } from 'react'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import { useAppDispatch } from 'wallet/src/state'
import { ModalName } from 'wallet/src/telemetry/constants'
export function useOnSendEmptyActionPress(): () => void {
const { data } = useFiatOnRampIpAddressQuery()
const dispatch = useAppDispatch()
const fiatOnRampEligible = Boolean(data?.isBuyAllowed)
return useCallback((): void => {
dispatch(closeModal({ name: ModalName.Send }))
if (fiatOnRampEligible) {
dispatch(openModal({ name: ModalName.FiatOnRamp }))
} else {
dispatch(
openModal({
name: ModalName.WalletConnectScan,
initialState: ScannerModalState.WalletQr,
})
)
}
}, [dispatch, fiatOnRampEligible])
}
...@@ -8,7 +8,6 @@ import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect' ...@@ -8,7 +8,6 @@ import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect'
import Trace from 'src/components/Trace/Trace' import Trace from 'src/components/Trace/Trace'
import { Screen } from 'src/components/layout/Screen' import { Screen } from 'src/components/layout/Screen'
import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks'
import { useOnSendEmptyActionPress } from 'src/features/transactions/hooks/useOnSendEmptyActionPress'
import { TransferHeader } from 'src/features/transactions/transfer/TransferHeader' import { TransferHeader } from 'src/features/transactions/transfer/TransferHeader'
import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus' import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus'
import { useWalletRestore } from 'src/features/wallet/hooks' import { useWalletRestore } from 'src/features/wallet/hooks'
...@@ -107,7 +106,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS ...@@ -107,7 +106,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
dispatch, dispatch,
TokenSelectorFlow.Transfer TokenSelectorFlow.Transfer
) )
const onSendEmptyActionPress = useOnSendEmptyActionPress()
// optimization for not rendering InnerContent initially, // optimization for not rendering InnerContent initially,
// when modal is opened with recipient or token selector presented // when modal is opened with recipient or token selector presented
...@@ -211,7 +209,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS ...@@ -211,7 +209,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
variation={TokenSelectorVariation.BalancesOnly} variation={TokenSelectorVariation.BalancesOnly}
onClose={onHideTokenSelector} onClose={onHideTokenSelector}
onSelectCurrency={onSelectCurrency} onSelectCurrency={onSelectCurrency}
onSendEmptyActionPress={onSendEmptyActionPress}
/> />
)} )}
</> </>
......
...@@ -51,7 +51,6 @@ const FIXED_INFO_PILL_WIDTH = 128 ...@@ -51,7 +51,6 @@ const FIXED_INFO_PILL_WIDTH = 128
// Used in dynamic font size width calculation to ignore `.` characters // Used in dynamic font size width calculation to ignore `.` characters
const UNITAG_SUFFIX_CHARS_ONLY = UNITAG_SUFFIX.replaceAll('.', '') const UNITAG_SUFFIX_CHARS_ONLY = UNITAG_SUFFIX.replaceAll('.', '')
const TEXT_INPUT_PLACEHOLDER = 'yourname'
// Accounts for height of image, gap between image and name, and spacing from top of titles // Accounts for height of image, gap between image and name, and spacing from top of titles
const UNITAG_NAME_ANIMATE_DISTANCE_Y = imageSizes.image100 + spacing.spacing48 + spacing.spacing24 const UNITAG_NAME_ANIMATE_DISTANCE_Y = imageSizes.image100 + spacing.spacing48 + spacing.spacing24
...@@ -65,6 +64,8 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { ...@@ -65,6 +64,8 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
const inputPlaceholder = t('yourname')
// In onboarding flow, delete pending accounts and create account actions happen right before navigation // In onboarding flow, delete pending accounts and create account actions happen right before navigation
// So pendingAccountAddress must be fetched in this component and can't be passed in params // So pendingAccountAddress must be fetched in this component and can't be passed in params
const pendingAccountAddress = Object.values(usePendingAccounts())?.[0]?.address const pendingAccountAddress = Object.values(usePendingAccounts())?.[0]?.address
...@@ -139,14 +140,14 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { ...@@ -139,14 +140,14 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
} }
if (text.length === 0) { if (text.length === 0) {
onSetFontSize(TEXT_INPUT_PLACEHOLDER + UNITAG_SUFFIX_CHARS_ONLY) onSetFontSize(inputPlaceholder + UNITAG_SUFFIX_CHARS_ONLY)
} else { } else {
onSetFontSize(text + UNITAG_SUFFIX_CHARS_ONLY) onSetFontSize(text + UNITAG_SUFFIX_CHARS_ONLY)
} }
setUnitagInputValue(text?.trim().toLowerCase()) setUnitagInputValue(text?.trim().toLowerCase())
}, },
[onSetFontSize, setUnitagInputValue] [inputPlaceholder, onSetFontSize]
) )
const onPressAddressTooltip = (): void => { const onPressAddressTooltip = (): void => {
...@@ -256,7 +257,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { ...@@ -256,7 +257,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
gap="$spacing16" gap="$spacing16"
onLayout={(event): void => { onLayout={(event): void => {
onLayout(event) onLayout(event)
onSetFontSize(TEXT_INPUT_PLACEHOLDER + UNITAG_SUFFIX_CHARS_ONLY) onSetFontSize(inputPlaceholder + UNITAG_SUFFIX_CHARS_ONLY)
}}> }}>
{/* Fixed text that animates in when TextInput is animated out */} {/* Fixed text that animates in when TextInput is animated out */}
<AnimatedFlex <AnimatedFlex
...@@ -293,7 +294,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { ...@@ -293,7 +294,7 @@ export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element {
fontWeight="$large" fontWeight="$large"
numberOfLines={1} numberOfLines={1}
p="$none" p="$none"
placeholder={TEXT_INPUT_PLACEHOLDER} placeholder={inputPlaceholder}
placeholderTextColor="$neutral3" placeholderTextColor="$neutral3"
returnKeyType="done" returnKeyType="done"
textAlign="left" textAlign="left"
...@@ -396,6 +397,7 @@ const InfoModal = ({ ...@@ -396,6 +397,7 @@ const InfoModal = ({
}): JSX.Element => { }): JSX.Element => {
const colors = useSporeColors() const colors = useSporeColors()
const { t } = useTranslation() const { t } = useTranslation()
const usernamePlaceholder = t('yourname')
return ( return (
<WarningModal <WarningModal
...@@ -429,7 +431,7 @@ const InfoModal = ({ ...@@ -429,7 +431,7 @@ const InfoModal = ({
shadowOpacity={0.4} shadowOpacity={0.4}
shadowRadius="$spacing4"> shadowRadius="$spacing4">
<Text color="$accent1" variant="buttonLabel4"> <Text color="$accent1" variant="buttonLabel4">
{TEXT_INPUT_PLACEHOLDER} {usernamePlaceholder}
<Text color="$neutral2" variant="buttonLabel4"> <Text color="$neutral2" variant="buttonLabel4">
{UNITAG_SUFFIX} {UNITAG_SUFFIX}
</Text> </Text>
......
...@@ -12,7 +12,7 @@ import { ChangeUnitagModal } from 'src/components/unitags/ChangeUnitagModal' ...@@ -12,7 +12,7 @@ import { ChangeUnitagModal } from 'src/components/unitags/ChangeUnitagModal'
import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal' import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal'
import { DeleteUnitagModal } from 'src/components/unitags/DeleteUnitagModal' import { DeleteUnitagModal } from 'src/components/unitags/DeleteUnitagModal'
import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture'
import { HeaderRadial } from 'src/features/externalProfile/ProfileHeader' import { HeaderRadial, solidHeaderProps } from 'src/features/externalProfile/ProfileHeader'
import { Screens, UnitagScreens } from 'src/screens/Screens' import { Screens, UnitagScreens } from 'src/screens/Screens'
import { import {
Button, Button,
...@@ -341,8 +341,7 @@ export function EditUnitagProfileScreen({ ...@@ -341,8 +341,7 @@ export function EditUnitagProfileScreen({
<HeaderRadial <HeaderRadial
borderRadius={spacing.spacing20} borderRadius={spacing.spacing20}
color={avatarColors.primary} color={avatarColors.primary}
maxOpacity={0.1} {...solidHeaderProps}
minOpacity={0.1}
/> />
) : null} ) : null}
</Flex> </Flex>
......
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getCountry } from 'react-native-localize'
import { useAppDispatch } from 'src/app/hooks'
import {
FiatOnRampConnectingView,
SERVICE_PROVIDER_ICON_BORDER_RADIUS,
SERVICE_PROVIDER_ICON_SIZE,
} from 'src/features/fiatOnRamp/FiatOnRampConnecting'
import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { useFiatOnRampAggregatorTransferWidgetQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
import { openUri } from 'wallet/src/utils/linking'
import { isAndroid } from 'wallet/src/utils/platform'
// Design decision
const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS
const DEFAULT_TRANSFER_AMOUNT = 1
const DEFAULT_TRANSFER_CURRENCY = 'ETH'
export function ExchangeTransferConnecting({
serviceProvider,
onClose,
}: {
serviceProvider: FORTransferInstitution
onClose: () => void
}): JSX.Element {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const activeAccountAddress = useActiveAccountAddressWithThrow()
const [timeoutElapsed, setTimeoutElapsed] = useState(false)
const initialTypeInfo = useMemo(
() => ({ institutionLogoUrl: serviceProvider.icon }),
[serviceProvider.icon]
)
const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator(
activeAccountAddress,
initialTypeInfo
)
const onError = useCallback((): void => {
dispatch(
pushNotification({
type: AppNotificationType.Error,
errorMessage: t('Something went wrong.'),
})
)
onClose()
}, [dispatch, onClose, t])
useTimeout(() => {
setTimeoutElapsed(true)
}, CONNECTING_TIMEOUT)
const {
data: widgetData,
isLoading: widgetLoading,
error: widgetError,
} = useFiatOnRampAggregatorTransferWidgetQuery({
sourceAmount: DEFAULT_TRANSFER_AMOUNT,
sourceCurrencyCode: DEFAULT_TRANSFER_CURRENCY,
countryCode: getCountry(),
institutionId: serviceProvider.id,
walletAddress: activeAccountAddress,
externalSessionId: externalTransactionId,
redirectURL: `${
isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl
}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`,
})
useEffect(() => {
if (widgetError) {
onError()
return
}
async function navigateToWidget(widgetUrl: string): Promise<void> {
onClose()
await openUri(widgetUrl).catch(onError)
dispatchAddTransaction()
}
if (timeoutElapsed && !widgetLoading && widgetData) {
navigateToWidget(widgetData.widgetUrl).catch(() => undefined)
}
}, [
dispatchAddTransaction,
onClose,
onError,
timeoutElapsed,
widgetData,
widgetLoading,
widgetError,
])
return (
<FiatOnRampConnectingView
serviceProviderLogo={
<RemoteImage
borderRadius={SERVICE_PROVIDER_ICON_BORDER_RADIUS}
height={SERVICE_PROVIDER_ICON_SIZE}
uri={serviceProvider.icon}
width={SERVICE_PROVIDER_ICON_SIZE}
/>
}
serviceProviderName={serviceProvider.name}
/>
)
}
...@@ -50,7 +50,7 @@ export function ExploreScreen(): JSX.Element { ...@@ -50,7 +50,7 @@ export function ExploreScreen(): JSX.Element {
const [isSearchMode, setIsSearchMode] = useState<boolean>(false) const [isSearchMode, setIsSearchMode] = useState<boolean>(false)
const textInputRef = useRef<TextInput>(null) const textInputRef = useRef<TextInput>(null)
const onChangeSearchFilter = (newSearchFilter: string): void => { const onSearchChangeText = (newSearchFilter: string): void => {
setSearchQuery(newSearchFilter) setSearchQuery(newSearchFilter)
} }
...@@ -83,9 +83,8 @@ export function ExploreScreen(): JSX.Element { ...@@ -83,9 +83,8 @@ export function ExploreScreen(): JSX.Element {
backgroundColor={isSearchMode ? contrastBackgroundColor : searchBarBackgroundColor} backgroundColor={isSearchMode ? contrastBackgroundColor : searchBarBackgroundColor}
placeholder={t('Search tokens and wallets')} placeholder={t('Search tokens and wallets')}
showShadow={!isSearchMode} showShadow={!isSearchMode}
value={searchQuery}
onCancel={onSearchCancel} onCancel={onSearchCancel}
onChangeText={onChangeSearchFilter} onChangeText={onSearchChangeText}
onFocus={onSearchFocus} onFocus={onSearchFocus}
/> />
</Flex> </Flex>
......
import { NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { skipToken } from '@reduxjs/toolkit/query/react' import { skipToken } from '@reduxjs/toolkit/query/react'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { FiatOnRampStackParamList } from 'src/app/navigation/types' import { FiatOnRampStackParamList } from 'src/app/navigation/types'
import { Loader } from 'src/components/loading'
import { import {
FiatOnRampConnectingView, FiatOnRampConnectingView,
SERVICE_PROVIDER_ICON_SIZE, SERVICE_PROVIDER_ICON_SIZE,
...@@ -12,15 +12,25 @@ import { ...@@ -12,15 +12,25 @@ import {
import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext'
import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks' import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks'
import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils' import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils'
import { closeModal } from 'src/features/modals/modalSlice'
import { FiatOnRampScreens } from 'src/screens/Screens' import { FiatOnRampScreens } from 'src/screens/Screens'
import { Flex, useIsDarkMode } from 'ui/src'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing' import { useTimeout } from 'utilities/src/time/timing'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { useFiatOnRampAggregatorWidgetQuery } from 'wallet/src/features/fiatOnRamp/api' import { useFiatOnRampAggregatorWidgetQuery } from 'wallet/src/features/fiatOnRamp/api'
import {
MELD_ICON_SIZE_MULTIPLIER,
getServiceProviderLogo,
} from 'wallet/src/features/fiatOnRamp/utils'
import { ImageUri } from 'wallet/src/features/images/ImageUri'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { pushNotification } from 'wallet/src/features/notifications/slice' import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types' import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
import { ModalName } from 'wallet/src/telemetry/constants'
import { openUri } from 'wallet/src/utils/linking' import { openUri } from 'wallet/src/utils/linking'
import { isAndroid } from 'wallet/src/utils/platform'
// Design decision // Design decision
const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS
...@@ -34,12 +44,20 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ...@@ -34,12 +44,20 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
const [timeoutElapsed, setTimeoutElapsed] = useState(false) const [timeoutElapsed, setTimeoutElapsed] = useState(false)
const activeAccountAddress = useActiveAccountAddressWithThrow() const activeAccountAddress = useActiveAccountAddressWithThrow()
const { externalTransactionId, dispatchAddTransaction } =
useFiatOnRampTransactionCreator(activeAccountAddress)
const { selectedQuote, serviceProviders, countryCode, baseCurrencyInfo, quoteCurrency, amount } = const { selectedQuote, serviceProviders, countryCode, baseCurrencyInfo, quoteCurrency, amount } =
useFiatOnRampContext() useFiatOnRampContext()
const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders) const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders)
const initialTypeInfo = useMemo(
() => ({ serviceProviderLogo: serviceProvider?.logos }),
[serviceProvider?.logos]
)
const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator(
activeAccountAddress,
initialTypeInfo
)
const onError = useCallback((): void => { const onError = useCallback((): void => {
dispatch( dispatch(
pushNotification({ pushNotification({
...@@ -64,6 +82,9 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ...@@ -64,6 +82,9 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
sourceCurrencyCode: baseCurrencyInfo.code, sourceCurrencyCode: baseCurrencyInfo.code,
walletAddress: activeAccountAddress, walletAddress: activeAccountAddress,
externalSessionId: externalTransactionId, externalSessionId: externalTransactionId,
redirectUrl: `${
isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl
}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`,
} }
: skipToken : skipToken
) )
...@@ -77,11 +98,14 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ...@@ -77,11 +98,14 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
onError() onError()
return return
} }
async function navigateToWidget(widgetUrl: string): Promise<void> {
dispatch(closeModal({ name: ModalName.FiatOnRampAggregator }))
await openUri(widgetUrl).catch(onError)
dispatchAddTransaction()
}
if (timeoutElapsed && !widgetLoading && widgetData) { if (timeoutElapsed && !widgetLoading && widgetData) {
navigation.goBack() navigateToWidget(widgetData.widgetUrl).catch(() => undefined)
openUri(widgetData.widgetUrl).catch(onError)
// TODO: Uncomment this when https://linear.app/uniswap/issue/MOB-2585/implement-polling-of-transaction-once-user-has-checked-out is implmented
// dispatchAddTransaction()
} }
}, [ }, [
navigation, navigation,
...@@ -93,8 +117,12 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ...@@ -93,8 +117,12 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
dispatchAddTransaction, dispatchAddTransaction,
baseCurrencyInfo, baseCurrencyInfo,
serviceProvider, serviceProvider,
dispatch,
]) ])
const isDarkMode = useIsDarkMode()
const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode)
return baseCurrencyInfo && serviceProvider ? ( return baseCurrencyInfo && serviceProvider ? (
<FiatOnRampConnectingView <FiatOnRampConnectingView
amount={addFiatSymbolToNumber({ amount={addFiatSymbolToNumber({
...@@ -104,13 +132,22 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | ...@@ -104,13 +132,22 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element |
})} })}
quoteCurrencyCode={quoteCurrency.currencyInfo?.currency.symbol} quoteCurrencyCode={quoteCurrency.currencyInfo?.currency.symbol}
serviceProviderLogo={ serviceProviderLogo={
<Loader.Box <Flex
borderRadius="$rounded20" alignItems="center"
height={SERVICE_PROVIDER_ICON_SIZE} height={SERVICE_PROVIDER_ICON_SIZE}
width={SERVICE_PROVIDER_ICON_SIZE} justifyContent="center"
/> width={SERVICE_PROVIDER_ICON_SIZE}>
<ImageUri imageStyle={ServiceProviderLogoStyles.icon} uri={logoUrl} />
</Flex>
} }
serviceProviderName={serviceProvider.name} serviceProviderName={serviceProvider.name}
/> />
) : null ) : null
} }
const ServiceProviderLogoStyles = StyleSheet.create({
icon: {
height: SERVICE_PROVIDER_ICON_SIZE * MELD_ICON_SIZE_MULTIPLIER,
width: SERVICE_PROVIDER_ICON_SIZE * MELD_ICON_SIZE_MULTIPLIER,
},
})
...@@ -2,6 +2,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' ...@@ -2,6 +2,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { ComponentProps, useEffect, useRef, useState } from 'react' import React, { ComponentProps, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TextInput, TextInputProps } from 'react-native' import { TextInput, TextInputProps } from 'react-native'
import FastImage from 'react-native-fast-image'
import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated' import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated'
import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks' import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks'
import { FiatOnRampStackParamList } from 'src/app/navigation/types' import { FiatOnRampStackParamList } from 'src/app/navigation/types'
...@@ -23,13 +24,17 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry' ...@@ -23,13 +24,17 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants' import { MobileEventName } from 'src/features/telemetry/constants'
import { MobileEventProperties } from 'src/features/telemetry/types' import { MobileEventProperties } from 'src/features/telemetry/types'
import { FiatOnRampScreens } from 'src/screens/Screens' import { FiatOnRampScreens } from 'src/screens/Screens'
import { AnimatedFlex, Flex, Text } from 'ui/src' import { AnimatedFlex, Flex, Text, useIsDarkMode } from 'ui/src'
import { usePrevious } from 'utilities/src/react/hooks' import { usePrevious } from 'utilities/src/react/hooks'
import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy' import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy'
import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext'
import { HandleBar } from 'wallet/src/components/modals/HandleBar' import { HandleBar } from 'wallet/src/components/modals/HandleBar'
import { useFiatOnRampAggregatorServiceProvidersQuery } from 'wallet/src/features/fiatOnRamp/api' import {
import { FORQuote } from 'wallet/src/features/fiatOnRamp/types' useFiatOnRampAggregatorServiceProvidersQuery,
useFiatOnRampAggregatorTransactionsQuery,
} from 'wallet/src/features/fiatOnRamp/api'
import { FORQuote, FORServiceProvider, FORTransaction } from 'wallet/src/features/fiatOnRamp/types'
import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils'
import { pushNotification } from 'wallet/src/features/notifications/slice' import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types' import { AppNotificationType } from 'wallet/src/features/notifications/types'
...@@ -37,29 +42,45 @@ type Props = NativeStackScreenProps<FiatOnRampStackParamList, FiatOnRampScreens. ...@@ -37,29 +42,45 @@ type Props = NativeStackScreenProps<FiatOnRampStackParamList, FiatOnRampScreens.
function selectInitialQuote( function selectInitialQuote(
quotes: FORQuote[] | undefined, quotes: FORQuote[] | undefined,
lastTransaction: undefined lastTransaction: FORTransaction | undefined
): { quote: FORQuote | undefined; type: InitialQuoteSelection | undefined } { ): { quote: FORQuote | undefined; type: InitialQuoteSelection | undefined } {
if (lastTransaction) { const lastUsedServiceProvider = lastTransaction?.serviceProvider
// setting "Recently used" if (lastUsedServiceProvider) {
// TODO:https://linear.app/uniswap/issue/MOB-2533/implement-recently-used-logic const quote = quotes?.filter((q) => q.serviceProvider === lastUsedServiceProvider)[0]
} else { if (quote) {
// setting "Best overall" return {
const initialQuote = quotes && quotes.length && quotes[0] quote,
if (initialQuote) { type: InitialQuoteSelection.MostRecent,
}
}
}
const bestQuote = quotes && quotes.length && quotes[0]
if (bestQuote) {
return { return {
quote: quotes.reduce<FORQuote>((prev, curr) => { quote: quotes.reduce<FORQuote>((prev, curr) => {
return curr.destinationAmount > prev.destinationAmount ? curr : prev return curr.destinationAmount > prev.destinationAmount ? curr : prev
}, initialQuote), }, bestQuote),
type: InitialQuoteSelection.Best, type: InitialQuoteSelection.Best,
} }
} }
}
return { quote: undefined, type: undefined } return { quote: undefined, type: undefined }
} }
function preloadServiceProviderLogos(
serviceProviders: FORServiceProvider[],
isDarkMode: boolean
): void {
FastImage.preload(
serviceProviders
.map((sp) => ({ uri: getServiceProviderLogo(sp.logos, isDarkMode) }))
.filter((sp) => !!sp.uri)
)
}
export function FiatOnRampScreen({ navigation }: Props): JSX.Element { export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isDarkMode = useIsDarkMode()
const [selection, setSelection] = useState<TextInputProps['selection']>() const [selection, setSelection] = useState<TextInputProps['selection']>()
const [value, setValue] = useState('') const [value, setValue] = useState('')
const [showTokenSelector, setShowTokenSelector] = useState(false) const [showTokenSelector, setShowTokenSelector] = useState(false)
...@@ -109,6 +130,21 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -109,6 +130,21 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
error: serviceProvidersError, error: serviceProvidersError,
} = useFiatOnRampAggregatorServiceProvidersQuery() } = useFiatOnRampAggregatorServiceProvidersQuery()
// preload service provider logos for given quotes for the next screen
useEffect(() => {
if (serviceProvidersResponse?.serviceProviders && quotes) {
const quotesServiceProviderNames = quotes.map((q) => q.serviceProvider)
const serviceProviders = serviceProvidersResponse.serviceProviders.filter(
(sp) => quotesServiceProviderNames.indexOf(sp.serviceProvider) !== -1
)
preloadServiceProviderLogos(serviceProviders, isDarkMode)
}
}, [serviceProvidersResponse, quotes, isDarkMode])
const { currentData: transactionsResponse } = useFiatOnRampAggregatorTransactionsQuery({
limit: 1,
})
const { errorText, errorColor } = useParseFiatOnRampError( const { errorText, errorColor } = useParseFiatOnRampError(
quotesError || serviceProvidersError, quotesError || serviceProvidersError,
meldSupportedFiatCurrency.code meldSupportedFiatCurrency.code
...@@ -117,7 +153,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -117,7 +153,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
const prevQuotes = usePrevious(quotes) const prevQuotes = usePrevious(quotes)
useEffect(() => { useEffect(() => {
if (quotes && (!selectedQuote || prevQuotes !== quotes)) { if (quotes && (!selectedQuote || prevQuotes !== quotes)) {
const { quote, type } = selectInitialQuote(quotes, undefined) const { quote, type } = selectInitialQuote(quotes, transactionsResponse?.transactions[0])
if (!quote) { if (!quote) {
return return
} }
...@@ -128,7 +164,15 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -128,7 +164,15 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
]) ])
setSelectedQuote(quote) setSelectedQuote(quote)
} }
}, [prevQuotes, quotes, selectedQuote, setQuotesSections, setSelectedQuote, t]) }, [
prevQuotes,
quotes,
selectedQuote,
setQuotesSections,
setSelectedQuote,
t,
transactionsResponse?.transactions,
])
useEffect(() => { useEffect(() => {
if (!quotes && (quotesError || serviceProvidersError || !amount)) { if (!quotes && (quotesError || serviceProvidersError || !amount)) {
......
...@@ -13,7 +13,7 @@ import { InitialQuoteSelection } from 'src/features/fiatOnRamp/types' ...@@ -13,7 +13,7 @@ import { InitialQuoteSelection } from 'src/features/fiatOnRamp/types'
import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils' import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils'
import { MobileEventName } from 'src/features/telemetry/constants' import { MobileEventName } from 'src/features/telemetry/constants'
import { FiatOnRampScreens } from 'src/screens/Screens' import { FiatOnRampScreens } from 'src/screens/Screens'
import { AnimatedFlex, Button, Flex, Icons, Inset, Separator, Text } from 'ui/src' import { AnimatedFlex, Button, Flex, GeneratedIcon, Icons, Inset, Separator, Text } from 'ui/src'
import { Trace } from 'utilities/src/telemetry/trace/Trace' import { Trace } from 'utilities/src/telemetry/trace/Trace'
import { HandleBar } from 'wallet/src/components/modals/HandleBar' import { HandleBar } from 'wallet/src/components/modals/HandleBar'
import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks'
...@@ -24,6 +24,17 @@ type Props = NativeStackScreenProps<FiatOnRampStackParamList, FiatOnRampScreens. ...@@ -24,6 +24,17 @@ type Props = NativeStackScreenProps<FiatOnRampStackParamList, FiatOnRampScreens.
const key = (item: FORQuote): string => item.serviceProvider const key = (item: FORQuote): string => item.serviceProvider
function SectionHeader({ Icon, title }: { Icon: GeneratedIcon; title: string }): JSX.Element {
return (
<Flex row alignItems="center" pl="$spacing8">
<Icon color="$neutral3" size="$icon.16" />
<Text color="$neutral2" pl="$spacing4" variant="body3">
{title}
</Text>
</Flex>
)
}
export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Element { export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
...@@ -59,16 +70,12 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele ...@@ -59,16 +70,12 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele
section: { type }, section: { type },
}: { }: {
section: SectionListData<FORQuote, { type?: InitialQuoteSelection }> section: SectionListData<FORQuote, { type?: InitialQuoteSelection }>
}): JSX.Element => { }): JSX.Element => (
return (
<Flex px="$spacing12"> <Flex px="$spacing12">
{type ? ( {type === InitialQuoteSelection.Best ? (
<Flex row alignItems="center" pl="$spacing8"> <SectionHeader Icon={Icons.Verified} title={t('Best overall')} />
<Icons.Verified color="$accent1" size="$icon.16" /> ) : type === InitialQuoteSelection.MostRecent ? (
<Text color="$neutral2" pl="$spacing4" variant="body3"> <SectionHeader Icon={Icons.TimePast} title={t('Recently used')} />
{type === InitialQuoteSelection.Best ? t('Best overall') : t('Recently used')}
</Text>
</Flex>
) : ( ) : (
<Flex centered row gap="$spacing12" my="$spacing12"> <Flex centered row gap="$spacing12" my="$spacing12">
<Separator /> <Separator />
...@@ -80,7 +87,6 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele ...@@ -80,7 +87,6 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele
)} )}
</Flex> </Flex>
) )
}
const onContinue = (): void => { const onContinue = (): void => {
const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders) const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders)
......
...@@ -6,14 +6,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' ...@@ -6,14 +6,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Freeze } from 'react-freeze' import { Freeze } from 'react-freeze'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList, StyleProp, View, ViewProps, ViewStyle } from 'react-native' import { FlatList, StyleProp, View, ViewProps, ViewStyle } from 'react-native'
import { TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler'
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut, FadeOut,
cancelAnimation,
interpolateColor, interpolateColor,
runOnJS,
useAnimatedGestureHandler,
useAnimatedRef, useAnimatedRef,
useAnimatedScrollHandler, useAnimatedScrollHandler,
useAnimatedStyle, useAnimatedStyle,
...@@ -29,7 +25,6 @@ import { ScannerModalState } from 'src/components/QRCodeScanner/constants' ...@@ -29,7 +25,6 @@ import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import Trace from 'src/components/Trace/Trace' import Trace from 'src/components/Trace/Trace'
import TraceTabView from 'src/components/Trace/TraceTabView' import TraceTabView from 'src/components/Trace/TraceTabView'
import { AccountHeader } from 'src/components/accounts/AccountHeader' import { AccountHeader } from 'src/components/accounts/AccountHeader'
import { pulseAnimation } from 'src/components/buttons/utils'
import { ACTIVITY_TAB_DATA_DEPENDENCIES, ActivityTab } from 'src/components/home/ActivityTab' import { ACTIVITY_TAB_DATA_DEPENDENCIES, ActivityTab } from 'src/components/home/ActivityTab'
import { FEED_TAB_DATA_DEPENDENCIES, FeedTab } from 'src/components/home/FeedTab' import { FEED_TAB_DATA_DEPENDENCIES, FeedTab } from 'src/components/home/FeedTab'
import { NFTS_TAB_DATA_DEPENDENCIES, NftsTab } from 'src/components/home/NftsTab' import { NFTS_TAB_DATA_DEPENDENCIES, NftsTab } from 'src/components/home/NftsTab'
...@@ -729,36 +724,18 @@ function ActionButton({ ...@@ -729,36 +724,18 @@ function ActionButton({
iconScale?: number iconScale?: number
}): JSX.Element { }): JSX.Element {
const colors = useSporeColors() const colors = useSporeColors()
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(
() => ({
transform: [{ scale: scale.value }],
}),
[scale]
)
const media = useMedia() const media = useMedia()
const iconSize = media.short ? iconSizes.icon24 : iconSizes.icon28 const iconSize = media.short ? iconSizes.icon24 : iconSizes.icon28
const onGestureEvent = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>({
onStart: () => {
cancelAnimation(scale)
scale.value = pulseAnimation(activeScale)
},
onEnd: () => {
runOnJS(onPress)()
},
})
return ( return (
<Trace logPress element={name} pressEvent={eventName}> <Trace logPress element={name} pressEvent={eventName}>
<TouchableArea hapticFeedback flex={flex} onPress={onPress}> <TouchableArea hapticFeedback flex={flex} scaleTo={activeScale} onPress={onPress}>
<TapGestureHandler onGestureEvent={onGestureEvent}>
<AnimatedFlex <AnimatedFlex
centered centered
fill fill
backgroundColor="$DEP_backgroundActionButton" backgroundColor="$DEP_backgroundActionButton"
borderRadius="$rounded20" borderRadius="$rounded20"
p="$spacing16" p="$spacing16">
style={animatedStyle}>
<Icon <Icon
color={colors.accent1.get()} color={colors.accent1.get()}
height={iconSize * iconScale} height={iconSize * iconScale}
...@@ -766,7 +743,6 @@ function ActionButton({ ...@@ -766,7 +743,6 @@ function ActionButton({
width={iconSize * iconScale} width={iconSize * iconScale}
/> />
</AnimatedFlex> </AnimatedFlex>
</TapGestureHandler>
</TouchableArea> </TouchableArea>
</Trace> </Trace>
) )
......
...@@ -4,17 +4,16 @@ import React, { useCallback, useEffect, useState } from 'react' ...@@ -4,17 +4,16 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { OnboardingStackParamList } from 'src/app/navigation/types' import { OnboardingStackParamList } from 'src/app/navigation/types'
import { Loader } from 'src/components/loading'
import { clearCloudBackups } from 'src/features/CloudBackup/cloudBackupSlice'
import { useCloudBackups } from 'src/features/CloudBackup/hooks'
import { import {
startFetchingCloudStorageBackups, startFetchingCloudStorageBackups,
stopFetchingCloudStorageBackups, stopFetchingCloudStorageBackups,
} from 'src/features/CloudBackup/RNCloudStorageBackupsManager' } from 'src/features/CloudBackup/RNCloudStorageBackupsManager'
import { clearCloudBackups } from 'src/features/CloudBackup/cloudBackupSlice'
import { useCloudBackups } from 'src/features/CloudBackup/hooks'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { OnboardingScreens } from 'src/screens/Screens' import { OnboardingScreens } from 'src/screens/Screens'
import { useAddBackButton } from 'src/utils/useAddBackButton' import { useAddBackButton } from 'src/utils/useAddBackButton'
import { Flex, Icons } from 'ui/src' import { Flex, Icons, Loader } from 'ui/src'
import { imageSizes } from 'ui/src/theme' import { imageSizes } from 'ui/src/theme'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
......
...@@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next' ...@@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { OnboardingStackParamList } from 'src/app/navigation/types' import { OnboardingStackParamList } from 'src/app/navigation/types'
import { Loader } from 'src/components/loading'
import WalletPreviewCard from 'src/features/import/WalletPreviewCard'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { OnboardingScreens } from 'src/screens/Screens' import { OnboardingScreens } from 'src/screens/Screens'
import { Button, Flex } from 'ui/src' import { Button, Flex, Loader } from 'ui/src'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing' import { useTimeout } from 'utilities/src/time/timing'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPreviewCard'
import { useSelectWalletScreenQuery } from 'wallet/src/data/__generated__/types-and-hooks' import { useSelectWalletScreenQuery } from 'wallet/src/data/__generated__/types-and-hooks'
import { ImportType } from 'wallet/src/features/onboarding/types' import { ImportType } from 'wallet/src/features/onboarding/types'
import { import {
......
...@@ -227,48 +227,30 @@ exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = ` ...@@ -227,48 +227,30 @@ exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = `
} }
> >
<View <View
accessibilityState={ cancelable={true}
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true} focusable={true}
hitSlop={ hitSlop={[Function]}
{ minPressDuration={0}
"bottom": 5, onBlur={[Function]}
"left": 5, onFocus={[Function]}
"right": 5, onMouseEnter={[Function]}
"top": 5, onMouseLeave={[Function]}
} onPress={[Function]}
} onPressIn={[Function]}
onClick={[Function]} onPressOut={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
{ {
"flexDirection": "column",
"opacity": 1, "opacity": 1,
"paddingBottom": 4, "paddingBottom": 4,
"paddingLeft": 4, "paddingLeft": 4,
"paddingRight": 4, "paddingRight": 4,
"paddingTop": 4, "paddingTop": 4,
"transform": [
{
"scale": 1,
},
],
} }
} }
> >
......
...@@ -19,7 +19,7 @@ const ACCOUNT_IMAGE_SIZE = 52 ...@@ -19,7 +19,7 @@ const ACCOUNT_IMAGE_SIZE = 52
const ICON_SIZE = 32 const ICON_SIZE = 32
const ICON_BORDER_RADIUS = 100 const ICON_BORDER_RADIUS = 100
function AccountCardItem(): JSX.Element { function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const activeAccountAddress = useActiveAccountAddressWithThrow() const activeAccountAddress = useActiveAccountAddressWithThrow()
...@@ -35,13 +35,17 @@ function AccountCardItem(): JSX.Element { ...@@ -35,13 +35,17 @@ function AccountCardItem(): JSX.Element {
} }
const onPressShowWalletQr = (): void => { const onPressShowWalletQr = (): void => {
dispatch(closeModal({ name: ModalName.ReceiveCryptoModal })) onClose()
dispatch( dispatch(
openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })
) )
} }
return ( return (
<TouchableArea
hapticFeedback
hapticStyle={ImpactFeedbackStyle.Light}
onPress={onPressShowWalletQr}>
<Flex row alignItems="flex-start" gap="$spacing12" px="$spacing8"> <Flex row alignItems="flex-start" gap="$spacing12" px="$spacing8">
<Flex <Flex
fill fill
...@@ -74,10 +78,6 @@ function AccountCardItem(): JSX.Element { ...@@ -74,10 +78,6 @@ function AccountCardItem(): JSX.Element {
<Icons.CopySheets color="$neutral2" size={iconSizes.icon16} /> <Icons.CopySheets color="$neutral2" size={iconSizes.icon16} />
</Flex> </Flex>
</TouchableArea> </TouchableArea>
<TouchableArea
hapticFeedback
hapticStyle={ImpactFeedbackStyle.Light}
onPress={onPressShowWalletQr}>
<Flex <Flex
centered centered
row row
...@@ -87,10 +87,10 @@ function AccountCardItem(): JSX.Element { ...@@ -87,10 +87,10 @@ function AccountCardItem(): JSX.Element {
width={ICON_SIZE}> width={ICON_SIZE}>
<Icons.QrCode color="$neutral2" size={iconSizes.icon16} /> <Icons.QrCode color="$neutral2" size={iconSizes.icon16} />
</Flex> </Flex>
</TouchableArea>
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>
</TouchableArea>
) )
} }
...@@ -119,18 +119,18 @@ export function ReceiveCryptoModal(): JSX.Element { ...@@ -119,18 +119,18 @@ export function ReceiveCryptoModal(): JSX.Element {
{t('Receive crypto')} {t('Receive crypto')}
</Text> </Text>
<Text color="$neutral2" mt="$spacing2" textAlign="center" variant="body3"> <Text color="$neutral2" mt="$spacing2" textAlign="center" variant="body3">
{t('Deposit funds from another wallet or an account')} {t('Fund your wallet by transferring crypto from another wallet or account')}
</Text> </Text>
</Flex> </Flex>
<AccountCardItem /> <AccountCardItem onClose={onClose} />
<Flex centered row shrink gap="$spacing12" py="$spacing8"> <Flex centered row shrink gap="$spacing12" py="$spacing8">
<Separator /> <Separator />
<Text color="$neutral2" textAlign="center" variant="body3"> <Text color="$neutral2" textAlign="center" variant="body3">
{t('From an account')} {t('Link an account')}
</Text> </Text>
<Separator /> <Separator />
</Flex> </Flex>
<TransferInstitutionSelector /> <TransferInstitutionSelector onClose={onClose} />
</Flex> </Flex>
</BottomSheetModal> </BottomSheetModal>
) )
......
...@@ -25,6 +25,8 @@ import { ...@@ -25,6 +25,8 @@ import {
useNotificationOSPermissionsEnabled, useNotificationOSPermissionsEnabled,
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled' } from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { promptPushPermission } from 'src/features/notifications/Onesignal' import { promptPushPermission } from 'src/features/notifications/Onesignal'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants'
import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen' import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen'
import { Screens, UnitagScreens } from 'src/screens/Screens' import { Screens, UnitagScreens } from 'src/screens/Screens'
import { Button, Flex, Text, useSporeColors } from 'ui/src' import { Button, Flex, Text, useSporeColors } from 'ui/src'
...@@ -96,6 +98,7 @@ export function SettingsWallet({ ...@@ -96,6 +98,7 @@ export function SettingsWallet({
) )
const onChangeNotificationSettings = (enabled: boolean): void => { const onChangeNotificationSettings = (enabled: boolean): void => {
sendMobileAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled })
if (notificationOSPermission === NotificationPermission.Enabled) { if (notificationOSPermission === NotificationPermission.Enabled) {
dispatch( dispatch(
editAccountActions.trigger({ editAccountActions.trigger({
......
...@@ -18,7 +18,7 @@ import { TextInput } from 'wallet/src/components/input/TextInput' ...@@ -18,7 +18,7 @@ import { TextInput } from 'wallet/src/components/input/TextInput'
import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts' import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts'
import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' import { useFeatureFlag } from 'wallet/src/features/experiments/hooks'
import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' import { useCanAddressClaimUnitag } from 'wallet/src/features/unitags/hooks'
import { import {
EditAccountAction, EditAccountAction,
editAccountActions, editAccountActions,
...@@ -43,7 +43,7 @@ export function SettingsWalletEdit({ ...@@ -43,7 +43,7 @@ export function SettingsWalletEdit({
const [nickname, setNickname] = useState(displayName?.name) const [nickname, setNickname] = useState(displayName?.name)
const [showEditButton, setShowEditButton] = useState(true) const [showEditButton, setShowEditButton] = useState(true)
const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags)
const { canClaimUnitag } = useCanActiveAddressClaimUnitag() const { canClaimUnitag } = useCanAddressClaimUnitag(address)
const showUnitagBanner = const showUnitagBanner =
unitagsFeatureFlagEnabled && unitagsFeatureFlagEnabled &&
activeAccount?.type === AccountType.SignerMnemonic && activeAccount?.type === AccountType.SignerMnemonic &&
......
...@@ -105,11 +105,10 @@ function HeaderTitleElement({ ...@@ -105,11 +105,10 @@ function HeaderTitleElement({
export function TokenDetailsScreen({ export function TokenDetailsScreen({
route, route,
navigation,
}: AppStackScreenProp<Screens.TokenDetails>): JSX.Element { }: AppStackScreenProp<Screens.TokenDetails>): JSX.Element {
const { currencyId: _currencyId } = route.params const { currencyId: _currencyId } = route.params
// Potentially delays loading of perf-heavy content to speed up navigation // Potentially delays loading of perf-heavy content to speed up navigation
const showSkeleton = useSkeletonLoading(navigation) const showSkeleton = useSkeletonLoading()
const language = useCurrentLanguage() const language = useCurrentLanguage()
// Token details screen query // Token details screen query
......
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useExperiment } from 'statsig-react-native'
import { logger } from 'utilities/src/logger/logger'
import { useTimeout } from 'utilities/src/time/timing'
import { EXPERIMENT_NAMES } from 'wallet/src/features/experiments/constants'
const TIMEOUT_MS = 5000 /**
* Utility hook used to delay rendering initially so that the screen render a skeleton of placeholders
// eslint-disable-next-line @typescript-eslint/no-explicit-any * to allow navigation to progress before rendering heavier components that may appear as lag
type NavigationProp = NativeStackNavigationProp<any, any, any | undefined> */
export function useSkeletonLoading(): boolean {
export function useSkeletonLoading(navigation: NavigationProp): boolean { const [enabled, setEnabled] = useState(true)
const experiment = useExperiment(EXPERIMENT_NAMES.SkeletonLoading)
const focusBasedEnabled = !!experiment.config.getValue('enable_focus_based')
const transitionBasedEnabled = !!experiment.config.getValue('enable_transition_based')
const timeoutBasedEnabled = !!experiment.config.getValue('enable_timeout_based')
const [enabled, setEnabled] = useState(
focusBasedEnabled || transitionBasedEnabled || timeoutBasedEnabled
)
useTimeout(() => {
if (enabled) {
setEnabled(false)
logger.warn(
'useSkeletonLoading',
'useSkeletonLoading',
'Timeout reached to disable enabled state'
)
}
}, TIMEOUT_MS)
useEffect(() => { useEffect(() => {
if (focusBasedEnabled) {
return navigation.addListener('focus', () => setEnabled(false))
} else if (transitionBasedEnabled) {
return navigation.addListener('transitionEnd', () => setEnabled(false))
} else if (timeoutBasedEnabled) {
setTimeout(() => setEnabled(false), 0) setTimeout(() => setEnabled(false), 0)
} })
}, [navigation, focusBasedEnabled, transitionBasedEnabled, timeoutBasedEnabled])
return enabled return enabled
} }
...@@ -43,6 +43,7 @@ ignores: [ ...@@ -43,6 +43,7 @@ ignores: [
## Internal packages / workspaces ## Internal packages / workspaces
"wallet", "wallet",
"utilities", "utilities",
"ui",
## Top level local file paths ## Top level local file paths
"abis", "abis",
"analytics", "analytics",
......
...@@ -16,4 +16,5 @@ REACT_APP_TEMP_API_URL="https://temp.gateway.uniswap.org/v1" ...@@ -16,4 +16,5 @@ REACT_APP_TEMP_API_URL="https://temp.gateway.uniswap.org/v1"
REACT_APP_UNISWAP_API_URL="https://interface.gateway.uniswap.org/v2" REACT_APP_UNISWAP_API_URL="https://interface.gateway.uniswap.org/v2"
REACT_APP_UNISWAP_BASE_API_URL="https://interface.gateway.uniswap.org/" REACT_APP_UNISWAP_BASE_API_URL="https://interface.gateway.uniswap.org/"
REACT_APP_UNISWAP_GATEWAY_DNS="https://interface.gateway.uniswap.org/v2" REACT_APP_UNISWAP_GATEWAY_DNS="https://interface.gateway.uniswap.org/v2"
REACT_APP_UNITAGS_API_URL="https://gateway.uniswap.org/v2/unitags"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395" REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
...@@ -60,6 +60,7 @@ module.exports = { ...@@ -60,6 +60,7 @@ module.exports = {
transformIgnorePatterns: ['d3-array'], transformIgnorePatterns: ['d3-array'],
moduleNameMapper: { moduleNameMapper: {
'd3-array': 'd3-array/dist/d3-array.min.js', 'd3-array': 'd3-array/dist/d3-array.min.js',
'^react-native$': 'react-native-web',
}, },
}) })
}, },
......
...@@ -38,14 +38,14 @@ describe('Landing Page', () => { ...@@ -38,14 +38,14 @@ describe('Landing Page', () => {
cy.viewport(2000, 1600) cy.viewport(2000, 1600)
cy.visit('/swap') cy.visit('/swap')
cy.get(getTestSelector('pool-nav-link')).first().click() cy.get(getTestSelector('pool-nav-link')).first().click()
cy.url().should('include', '/pools') cy.url().should('include', '/pool')
}) })
it('allows navigation to pool on mobile', () => { it('allows navigation to pool on mobile', () => {
cy.viewport('iphone-6') cy.viewport('iphone-6')
cy.visit('/swap') cy.visit('/swap')
cy.get(getTestSelector('pool-nav-link')).last().click() cy.get(getTestSelector('pool-nav-link')).last().click()
cy.url().should('include', '/pools') cy.url().should('include', '/pool')
}) })
it('does not render landing page when / path is blocked', () => { it('does not render landing page when / path is blocked', () => {
......
...@@ -3,7 +3,7 @@ describe('Link', () => { ...@@ -3,7 +3,7 @@ describe('Link', () => {
it('should update route', () => { it('should update route', () => {
cy.viewport(2000, 1600) cy.viewport(2000, 1600)
cy.visit('/swap') cy.visit('/swap')
cy.contains('Pools').click() cy.contains('Pool').click()
cy.get('[data-cy="join-pool-button"]').should('exist') cy.get('[data-cy="join-pool-button"]').should('exist')
}) })
}) })
...@@ -3,13 +3,107 @@ import { getTestSelector } from '../../utils' ...@@ -3,13 +3,107 @@ import { getTestSelector } from '../../utils'
describe('Uni tags support', () => { describe('Uni tags support', () => {
beforeEach(() => { beforeEach(() => {
const unitagSpy = cy.spy().as('unitagSpy')
cy.intercept(/gateway.uniswap.org\/v2\/address/, (req) => {
unitagSpy(req)
})
cy.visit('/swap', { cy.visit('/swap', {
featureFlags: [{ name: FeatureFlag.uniTags, value: true }], featureFlags: [{ name: FeatureFlag.uniTags, value: true }],
}) })
}) })
it('displays claim banner in account drawer', () => { it('displays banner in account drawer', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.contains('Introducing uni.eth usernames')
})
it('displays large banner on page', () => {
cy.get(getTestSelector('large-unitag-banner')).should('be.visible')
})
it('does not display banner on landing page', () => {
cy.visit('/?intro=true', {
featureFlags: [{ name: FeatureFlag.uniTags, value: true }],
})
cy.get(getTestSelector('large-unitag-banner')).should('not.be.visible')
})
it('opens modal and hides itself when accept button is clicked', () => {
cy.get(getTestSelector('large-unitag-banner')).within(() => {
cy.get(getTestSelector('unitag-banner-accept-button')).click()
})
cy.contains('Download the Uniswap app').should('exist')
cy.get(getTestSelector('get-the-app-close-button')).click()
cy.get(getTestSelector('large-unitag-banner')).should('not.be.visible')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get('Introducing uni.eth usernames').should('not.exist')
})
it('hides itself when reject button is clicked', () => {
cy.get(getTestSelector('large-unitag-banner')).within(() => {
cy.get(getTestSelector('unitag-banner-reject-button')).click()
})
cy.get(getTestSelector('large-unitag-banner')).should('not.be.visible')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get('Introducing uni.eth usernames').should('not.exist')
})
it('shows address if no Unitag or ENS exists', () => {
cy.hardhat().then(() => {
const unusedAccount = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [unusedAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains('0xF030...69Fb').should('be.visible')
})
})
})
it('shows Unitag, followed by address, if Unitag exists but not ENS', () => {
cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' })
cy.hardhat().then(() => {
const accountWithUnitag = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountWithUnitag]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains('hayden').should('be.visible')
cy.contains('0xF030...69Fb').should('be.visible')
})
})
})
it('shows ENS, followed by address, if ENS exists but not Unitag', () => {
cy.hardhat().then(() => {
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const haydenENS = 'hayden.eth'
cy.get(getTestSelector('web3-status-connected')).click() cy.get(getTestSelector('web3-status-connected')).click()
cy.contains('Claim your Uniswap username') cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains(haydenENS).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
})
})
it('shows Unitag and more option if user has both Unitag and ENS', () => {
cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' })
cy.hardhat().then(() => {
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const haydenUnitag = 'hayden'
const haydenENS = 'hayden.eth'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains(haydenUnitag).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
cy.get(getTestSelector('secondary-identifiers'))
.trigger('mouseover')
.click()
.within(() => {
cy.contains(haydenENS).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
})
}) })
}) })
...@@ -30,11 +30,11 @@ describe('Navigation', () => { ...@@ -30,11 +30,11 @@ describe('Navigation', () => {
cy.url().should('include', '/nfts') cy.url().should('include', '/nfts')
}) })
it('displays Pools tab', () => { it('displays Pool tab', () => {
cy.get('nav').within(() => { cy.get('nav').within(() => {
cy.contains('Pools').should('be.visible').click() cy.contains('Pool').should('be.visible').click()
}) })
cy.url().should('include', '/pools') cy.url().should('include', '/pool')
}) })
describe('More Menu', () => { describe('More Menu', () => {
...@@ -50,11 +50,11 @@ describe('Navigation', () => { ...@@ -50,11 +50,11 @@ describe('Navigation', () => {
featureFlags: [{ name: FeatureFlag.landingPageV2, value: true }], featureFlags: [{ name: FeatureFlag.landingPageV2, value: true }],
}) })
cy.get('nav').within(() => { cy.get('nav').within(() => {
cy.contains('Pools').should('not.be.visible') cy.contains('Pool').should('not.be.visible')
cy.get(getTestSelector('nav-more-button')).should('be.visible').click() cy.get(getTestSelector('nav-more-button')).should('be.visible').click()
cy.get(getTestSelector('nav-more-menu')).within(() => { cy.get(getTestSelector('nav-more-menu')).within(() => {
cy.contains('Pools').should('be.visible').click() cy.contains('Pool').should('be.visible').click()
cy.url().should('include', '/pools') cy.url().should('include', '/pool')
}) })
}) })
}) })
......
...@@ -199,6 +199,7 @@ describe('Permit2', () => { ...@@ -199,6 +199,7 @@ describe('Permit2', () => {
cy.contains('Approve and swap').click() cy.contains('Approve and swap').click()
// Verify token approval // Verify token approval
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine()) cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI) expectTokenAllowanceForPermit2ToBeMax(DAI)
...@@ -231,7 +232,6 @@ describe('Permit2', () => { ...@@ -231,7 +232,6 @@ describe('Permit2', () => {
}) })
it('prompts signature when existing permit approval is expired', () => { it('prompts signature when existing permit approval is expired', () => {
setupInputs(DAI, USDC_MAINNET)
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) } const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
cy.hardhat() cy.hardhat()
.then(({ approval, wallet }) => .then(({ approval, wallet }) =>
...@@ -241,7 +241,8 @@ describe('Permit2', () => { ...@@ -241,7 +241,8 @@ describe('Permit2', () => {
]) ])
) )
.then(() => { .then(() => {
initiateSwap('Approve and swap') setupInputs(DAI, USDC_MAINNET)
initiateSwap('Sign and swap')
}) })
// Verify permit2 approval // Verify permit2 approval
...@@ -251,7 +252,6 @@ describe('Permit2', () => { ...@@ -251,7 +252,6 @@ describe('Permit2', () => {
}) })
it('prompts signature when existing permit approval amount is too low', () => { it('prompts signature when existing permit approval amount is too low', () => {
setupInputs(DAI, USDC_MAINNET)
const smallAllowance = { amount: 1 } const smallAllowance = { amount: 1 }
cy.hardhat() cy.hardhat()
.then(({ approval, wallet }) => .then(({ approval, wallet }) =>
...@@ -261,7 +261,8 @@ describe('Permit2', () => { ...@@ -261,7 +261,8 @@ describe('Permit2', () => {
]) ])
) )
.then(() => { .then(() => {
initiateSwap('Approve and swap') setupInputs(DAI, USDC_MAINNET)
initiateSwap('Sign and swap')
}) })
// Verify permit2 approval // Verify permit2 approval
......
describe('Pool', () => { describe('Pool', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/pools').then(() => { cy.visit('/pool').then(() => {
cy.wait('@eth_blockNumber') cy.wait('@eth_blockNumber')
}) })
}) })
......
...@@ -53,6 +53,7 @@ describe('Swap', () => { ...@@ -53,6 +53,7 @@ describe('Swap', () => {
}) })
it('swaps ETH for USDC', () => { it('swaps ETH for USDC', () => {
cy.interceptGraphqlOperation('Activity', 'mini-portfolio/empty_activity.json')
cy.visit('/swap') cy.visit('/swap')
cy.hardhat({ automine: false }) cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => { getBalance(USDC_MAINNET).then((initialBalance) => {
......
...@@ -163,7 +163,7 @@ describe('Wallet Dropdown', () => { ...@@ -163,7 +163,7 @@ describe('Wallet Dropdown', () => {
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => { it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
cy.get(getTestSelector('web3-status-connected')).click() cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-buy-crypto')).click() cy.get(getTestSelector('wallet-buy-crypto')).click()
cy.contains('Buy crypto').should('not.be.visible') cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
}) })
it('should use a bottom sheet and dismiss when on a mobile screen size', () => { it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
......
{
"username": "hayden"
}
\ No newline at end of file
...@@ -21,6 +21,12 @@ declare global { ...@@ -21,6 +21,12 @@ declare global {
* @returns {Chainable<Subject>} * @returns {Chainable<Subject>}
*/ */
waitForAmplitudeEvent(eventName: string, requiredProperties?: string[]): Chainable<Subject> waitForAmplitudeEvent(eventName: string, requiredProperties?: string[]): Chainable<Subject>
/**
* Intercepts a specific graphql operation and responds with the given fixture.
* @param {string} operationName - The name of the graphql operation to intercept.
* @param {string} fixturePath - The path to the fixture to respond with.
*/
interceptGraphqlOperation(operationName: string, fixturePath: string): Chainable<Subject>
} }
interface VisitOptions { interface VisitOptions {
serviceWorker?: true serviceWorker?: true
...@@ -96,3 +102,13 @@ Cypress.Commands.add('waitForAmplitudeEvent', (eventName, requiredProperties) => ...@@ -96,3 +102,13 @@ Cypress.Commands.add('waitForAmplitudeEvent', (eventName, requiredProperties) =>
} }
return findAndDiscardEventsUpToTarget() return findAndDiscardEventsUpToTarget()
}) })
Cypress.Commands.add('interceptGraphqlOperation', (operationName, fixturePath) => {
return cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => {
if (req.body.operationName === operationName) {
req.reply({ fixture: fixturePath })
} else {
req.continue()
}
})
})
const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pools'] const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pool']
test.each(defaultUrls)('should inject metadata for valid collections', async (defaultUrl) => { test.each(defaultUrls)('should inject metadata for valid collections', async (defaultUrl) => {
const body = await fetch(new Request(defaultUrl)).then((res) => res.text()) const body = await fetch(new Request(defaultUrl)).then((res) => res.text())
......
...@@ -14,7 +14,7 @@ const forks = { ...@@ -14,7 +14,7 @@ const forks = {
[ChainId.MAINNET]: { [ChainId.MAINNET]: {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
// Temporarily hardcoding this to fix e2e tests as we investigate source of swap tests failing on older blocknumbers // Temporarily hardcoding this to fix e2e tests as we investigate source of swap tests failing on older blocknumbers
blockNumber: 19164140, blockNumber: 19270708,
...forkingConfig, ...forkingConfig,
}, },
[ChainId.POLYGON]: { [ChainId.POLYGON]: {
......
...@@ -198,12 +198,12 @@ ...@@ -198,12 +198,12 @@
"@uniswap/merkle-distributor": "1.0.1", "@uniswap/merkle-distributor": "1.0.1",
"@uniswap/permit2-sdk": "1.2.0", "@uniswap/permit2-sdk": "1.2.0",
"@uniswap/redux-multicall": "1.1.8", "@uniswap/redux-multicall": "1.1.8",
"@uniswap/router-sdk": "1.7.1", "@uniswap/router-sdk": "1.8.0",
"@uniswap/sdk-core": "4.0.7", "@uniswap/sdk-core": "4.1.2",
"@uniswap/smart-order-router": "3.17.3", "@uniswap/smart-order-router": "3.17.3",
"@uniswap/token-lists": "1.0.0-beta.33", "@uniswap/token-lists": "1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "1.4.1", "@uniswap/uniswapx-sdk": "1.4.1",
"@uniswap/universal-router-sdk": "1.5.8", "@uniswap/universal-router-sdk": "1.7.1",
"@uniswap/v2-core": "1.0.1", "@uniswap/v2-core": "1.0.1",
"@uniswap/v2-periphery": "1.1.0-beta.0", "@uniswap/v2-periphery": "1.1.0-beta.0",
"@uniswap/v2-sdk": "4.1.0", "@uniswap/v2-sdk": "4.1.0",
......
...@@ -18,6 +18,18 @@ ...@@ -18,6 +18,18 @@
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>
<url>
<loc>https://app.uniswap.org/limit</loc>
<lastmod>2024-02-21T20:00:00.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://app.uniswap.org/limits</loc>
<lastmod>2024-02-21T20:00:00.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url> <url>
<loc>https://app.uniswap.org/swap</loc> <loc>https://app.uniswap.org/swap</loc>
<lastmod>2023-10-11T19:57:27.976Z</lastmod> <lastmod>2023-10-11T19:57:27.976Z</lastmod>
......
...@@ -10,7 +10,7 @@ const thegraphConfig = require('../graphql.thegraph.config') ...@@ -10,7 +10,7 @@ const thegraphConfig = require('../graphql.thegraph.config')
const exec = promisify(child_process.exec) const exec = promisify(child_process.exec)
function fetchSchema(url, outputFile) { function fetchSchema(url, outputFile) {
exec(`npx --silent get-graphql-schema --h Origin=https://app.uniswap.org ${url}`) exec(`npx --silent get-graphql-schema -h Origin=https://app.uniswap.org ${url}`)
.then(({ stderr, stdout }) => { .then(({ stderr, stdout }) => {
if (stderr) { if (stderr) {
throw new Error(stderr) throw new Error(stderr)
......
...@@ -152,7 +152,7 @@ export const AboutFooter = () => { ...@@ -152,7 +152,7 @@ export const AboutFooter = () => {
<TextLink to="/swap">Swap</TextLink> <TextLink to="/swap">Swap</TextLink>
<TextLink to="/tokens">Tokens</TextLink> <TextLink to="/tokens">Tokens</TextLink>
{!shouldDisableNFTRoutes && <TextLink to="/nfts">NFTs</TextLink>} {!shouldDisableNFTRoutes && <TextLink to="/nfts">NFTs</TextLink>}
<TextLink to="/pools">Pools</TextLink> <TextLink to="/pool">Pool</TextLink>
</LinkGroup> </LinkGroup>
<LinkGroup> <LinkGroup>
<LinkGroupTitle>Protocol</LinkGroupTitle> <LinkGroupTitle>Protocol</LinkGroupTitle>
......
...@@ -50,7 +50,7 @@ export const MORE_CARDS = [ ...@@ -50,7 +50,7 @@ export const MORE_CARDS = [
elementName: InterfaceElementName.ABOUT_PAGE_BUY_CRYPTO_CARD, elementName: InterfaceElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
}, },
{ {
to: '/pools', to: '/pool',
title: 'Earn', title: 'Earn',
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.', description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />, lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,
......
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Column from 'components/Column'
import Row from 'components/Row'
import Tooltip from 'components/Tooltip'
import { SupportArticleURL } from 'constants/supportArticles'
import { ReactNode, useReducer } from 'react'
import { Info } from 'react-feather'
import { Text } from 'rebass'
import styled from 'styled-components'
import { ExternalLink } from 'theme/components'
import { ThemedText } from 'theme/components/text'
const Container = styled(Column)`
position: relative;
height: 100%;
width: 100%;
`
const Tile = styled(ThemeButton)`
height: 100%;
width: 100%;
display: flex;
justify-content: flex-start;
padding: 12px;
border-color: transparent;
border-radius: 16px;
border-style: solid;
border-width: 1px;
`
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
height: 28px;
width: 28px;
fill: ${({ theme }) => theme.accent1};
`
const ActionName = styled(Text)`
font-size: 16px;
font-style: normal;
font-weight: 535;
line-height: 24px;
`
const ErrorContainer = styled(Row)`
width: 100%;
position: absolute;
bottom: -24px;
display: flex;
justify-content: center;
align-items: center;
`
const ErrorText = styled(ThemedText.LabelMicro)`
color: ${({ theme }) => theme.neutral2};
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const ErrorLink = styled(ExternalLink)`
align-items: center;
display: flex;
height: 14px;
justify-content: center;
margin-left: 6px;
width: 14px;
`
const StyledInfoIcon = styled(Info)`
height: 12px;
width: 12px;
flex: 1 1 auto;
stroke: ${({ theme }) => theme.neutral2};
`
export function ActionTile({
dataTestId,
Icon,
name,
onClick,
loading,
disabled,
error,
errorMessage,
errorTooltip,
}: {
dataTestId: string
Icon: ReactNode
name: string
onClick: () => void
loading?: boolean
disabled?: boolean
error?: boolean
errorMessage?: string
errorTooltip?: string
}) {
const [showTooltip, toggleTooltip] = useReducer((isOpen) => !isOpen, false)
return (
<Container>
<Tile
data-testid={dataTestId}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.highSoft}
onClick={onClick}
disabled={disabled}
>
<Column gap="12px">
{loading ? <StyledLoadingButtonSpinner /> : Icon}
<ActionName>{name}</ActionName>
</Column>
</Tile>
{error && (
<ErrorContainer>
<ErrorText>{errorMessage}</ErrorText>
<Tooltip show={showTooltip} text={errorTooltip}>
<ErrorLink
onMouseEnter={toggleTooltip}
onMouseLeave={toggleTooltip}
style={{ color: 'inherit' }}
href={SupportArticleURL.MOONPAY_REGIONAL_AVAILABILITY}
>
<StyledInfoIcon />
</ErrorLink>
</Tooltip>
</ErrorContainer>
)}
</Container>
)
}
...@@ -5,6 +5,7 @@ import WalletModal from 'components/WalletModal' ...@@ -5,6 +5,7 @@ import WalletModal from 'components/WalletModal'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { sendAnalyticsEvent } from 'analytics'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
import AuthenticatedHeader from './AuthenticatedHeader' import AuthenticatedHeader from './AuthenticatedHeader'
import LanguageMenu from './LanguageMenu' import LanguageMenu from './LanguageMenu'
...@@ -17,11 +18,11 @@ const DefaultMenuWrap = styled(Column)` ...@@ -17,11 +18,11 @@ const DefaultMenuWrap = styled(Column)`
` `
export enum MenuState { export enum MenuState {
DEFAULT, DEFAULT = 'default',
SETTINGS, SETTINGS = 'settings',
LANGUAGE_SETTINGS, LANGUAGE_SETTINGS = 'language_settings',
LOCAL_CURRENCY_SETTINGS, LOCAL_CURRENCY_SETTINGS = 'local_currency_settings',
LIMITS, LIMITS = 'limits',
} }
export const miniPortfolioMenuStateAtom = atom(MenuState.DEFAULT) export const miniPortfolioMenuStateAtom = atom(MenuState.DEFAULT)
...@@ -35,7 +36,6 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { ...@@ -35,7 +36,6 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu]) const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu])
const openLanguageSettings = useCallback(() => setMenu(MenuState.LANGUAGE_SETTINGS), [setMenu]) const openLanguageSettings = useCallback(() => setMenu(MenuState.LANGUAGE_SETTINGS), [setMenu])
const openLocalCurrencySettings = useCallback(() => setMenu(MenuState.LOCAL_CURRENCY_SETTINGS), [setMenu]) const openLocalCurrencySettings = useCallback(() => setMenu(MenuState.LOCAL_CURRENCY_SETTINGS), [setMenu])
const openLimitsMenu = useCallback(() => setMenu(MenuState.LIMITS), [setMenu])
const closeLimitsMenu = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu]) const closeLimitsMenu = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu])
useEffect(() => { useEffect(() => {
...@@ -49,11 +49,17 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { ...@@ -49,11 +49,17 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
return return
}, [drawerOpen, menu, closeSettings]) }, [drawerOpen, menu, closeSettings])
useEffect(() => {
if (menu === MenuState.DEFAULT) return // menu is closed, don't log
sendAnalyticsEvent('Portfolio Menu Opened', { name: menu })
}, [menu])
const SubMenu = useMemo(() => { const SubMenu = useMemo(() => {
switch (menu) { switch (menu) {
case MenuState.DEFAULT: case MenuState.DEFAULT:
return isAuthenticated ? ( return isAuthenticated ? (
<AuthenticatedHeader account={account} openSettings={openSettings} openLimitsMenu={openLimitsMenu} /> <AuthenticatedHeader account={account} openSettings={openSettings} />
) : ( ) : (
<WalletModal openSettings={openSettings} /> <WalletModal openSettings={openSettings} />
) )
...@@ -79,7 +85,6 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { ...@@ -79,7 +85,6 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
isAuthenticated, isAuthenticated,
menu, menu,
openLanguageSettings, openLanguageSettings,
openLimitsMenu,
openLocalCurrencySettings, openLocalCurrencySettings,
openSettings, openSettings,
]) ])
......
...@@ -10,7 +10,7 @@ import { useCallback } from 'react' ...@@ -10,7 +10,7 @@ import { useCallback } from 'react'
import { SignatureType } from 'state/signatures/types' import { SignatureType } from 'state/signatures/types'
import styled from 'styled-components' import styled from 'styled-components'
import { EllipsisStyle, ThemedText } from 'theme/components' import { EllipsisStyle, ThemedText } from 'theme/components'
import { shortenAddress } from 'utils' import { shortenAddress } from 'utilities/src/addresses'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo' import { PortfolioLogo } from '../PortfolioLogo'
......
import { ChainId, WETH9 } from '@uniswap/sdk-core' import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { CancelLimitsDialog } from 'components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog' import {
CancelLimitsDialog,
CancellationState,
} from 'components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog'
import { DAI } from 'constants/tokens' import { DAI } from 'constants/tokens'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types' import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types'
...@@ -29,8 +32,19 @@ const mockOrderDetails: UniswapXOrderDetails = { ...@@ -29,8 +32,19 @@ const mockOrderDetails: UniswapXOrderDetails = {
offerer: '0x1234', offerer: '0x1234',
} }
describe('CancelLimitsDialog', () => { jest.mock('hooks/useTransactionGasFee', () => ({
it('should render correctly', () => { ...jest.requireActual('hooks/useTransactionGasFee'),
useTransactionGasFee: jest.fn(),
}))
jest.mock('components/AccountDrawer/MiniPortfolio/Activity/utils', () => ({
useCreateCancelTransactionRequest: jest.fn(),
}))
// TODO(WEB-3741): figure out why this test is failing locally, but not on CI
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('CancelLimitsDialog', () => {
it('should render correctly', async () => {
const mockOnCancel = jest.fn() const mockOnCancel = jest.fn()
const mockOnConfirm = jest.fn() const mockOnConfirm = jest.fn()
render( render(
...@@ -39,13 +53,15 @@ describe('CancelLimitsDialog', () => { ...@@ -39,13 +53,15 @@ describe('CancelLimitsDialog', () => {
onConfirm={mockOnConfirm} onConfirm={mockOnConfirm}
isVisible={true} isVisible={true}
orders={[mockOrderDetails]} orders={[mockOrderDetails]}
cancelling={false} cancelState={CancellationState.REVIEWING_CANCELLATION}
/> />
) )
expect(document.body).toMatchSnapshot() expect(document.body).toMatchSnapshot()
expect( expect(
screen.getByText('Are you sure you want to cancel your limit before it executes or expires?') screen.getByText(
'Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?'
)
).toBeInTheDocument() ).toBeInTheDocument()
}) })
}) })
import { Plural, Trans } from '@lingui/macro' import { Plural, Trans } from '@lingui/macro'
import { Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog' import { useCreateCancelTransactionRequest } from 'components/AccountDrawer/MiniPortfolio/Activity/utils'
import GetHelp from 'components/Button/GetHelp'
import Column from 'components/Column'
import { Container, Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog'
import { LoaderV3 } from 'components/Icons/LoadingSpinner'
import Modal from 'components/Modal'
import Row from 'components/Row'
import { DetailLineItem } from 'components/swap/DetailLineItem'
import { ConfirmedIcon, LogoContainer, SubmittedIcon } from 'components/swap/PendingModalContent/Logos'
import { formatEther } from 'ethers/lib/utils'
import { GasSpeed, useTransactionGasFee } from 'hooks/useTransactionGasFee'
import { useMemo } from 'react'
import { Slash } from 'react-feather' import { Slash } from 'react-feather'
import { UniswapXOrderDetails } from 'state/signatures/types' import { UniswapXOrderDetails } from 'state/signatures/types'
import styled, { useTheme } from 'styled-components'
import { CloseIcon, ExternalLink, ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
const GasEstimateContainer = styled(Row)`
border-top: 1px solid ${({ theme }) => theme.surface3};
margin-top: 16px;
padding-top: 16px;
`
export enum CancellationState { export enum CancellationState {
NOT_STARTED = 'not_started', NOT_STARTED = 'not_started',
REVIEWING_CANCELLATION = 'reviewing_cancellation', REVIEWING_CANCELLATION = 'reviewing_cancellation',
CANCELLING = 'cancelling', PENDING_SIGNATURE = 'pending_cancellation_signature',
PENDING_CONFIRMATION = 'pending_cancellation_confirmation',
CANCELLED = 'cancelled',
} }
type CancelLimitsDialogProps = Partial<Omit<DialogProps, 'isVisible' | 'onCancel'>> & type CancelLimitsDialogProps = Partial<Omit<DialogProps, 'isVisible' | 'onCancel'>> &
Pick<DialogProps, 'isVisible' | 'onCancel'> Pick<DialogProps, 'isVisible' | 'onCancel'>
function useCancelLimitsDialogContent(
state: CancellationState,
orders: UniswapXOrderDetails[]
): { title?: JSX.Element; icon: JSX.Element } {
const theme = useTheme()
switch (state) {
case CancellationState.REVIEWING_CANCELLATION:
return {
title: (
<Plural id="cancelling" value={orders.length} one="Cancel limit" other={`Cancel ${orders.length} limits`} />
),
icon: <Slash />,
}
case CancellationState.PENDING_SIGNATURE:
return {
title: <Trans>Confirm cancellation</Trans>,
icon: <LoaderV3 size="64px" color={theme.accent1} />,
}
case CancellationState.PENDING_CONFIRMATION:
return {
title: <Trans>Cancellation submitted</Trans>,
icon: <SubmittedIcon />,
}
case CancellationState.CANCELLED:
return {
title: <Trans>Cancellation Successful</Trans>,
icon: <ConfirmedIcon />,
}
default:
return {
title: undefined,
icon: <Slash />,
}
}
}
export function CancelLimitsDialog( export function CancelLimitsDialog(
props: CancelLimitsDialogProps & { props: CancelLimitsDialogProps & {
orders: UniswapXOrderDetails[] orders: UniswapXOrderDetails[]
cancelling: boolean cancelState: CancellationState
cancelTxHash?: string
onConfirm: () => void onConfirm: () => void
} }
) { ) {
const { orders, cancelling, onConfirm, onCancel } = props const { orders, cancelState, cancelTxHash, onConfirm, onCancel } = props
const { formatNumber } = useFormatter()
const cancelTransactionParams = useMemo(
() => ({
encodedOrders: orders.map((order) => order.encodedOrder as string),
chainId: orders[0]?.chainId,
}),
[orders]
)
const cancelTransaction = useCreateCancelTransactionRequest(cancelTransactionParams)
const gasEstimate = useTransactionGasFee(cancelTransaction, GasSpeed.Fast)
const { title, icon } = useCancelLimitsDialogContent(cancelState, orders)
if (
[CancellationState.PENDING_SIGNATURE, CancellationState.PENDING_CONFIRMATION, CancellationState.CANCELLED].includes(
cancelState
)
) {
const cancelSubmitted =
(cancelState === CancellationState.CANCELLED || cancelState === CancellationState.PENDING_CONFIRMATION) &&
cancelTxHash
return (
<Modal isOpen $scrollOverlay onDismiss={onCancel} maxHeight={90}>
<Container gap="lg">
<Row gap="10px" width="100%" padding="4px 0px" justify="end" align="center">
<GetHelp />
<CloseIcon onClick={onCancel} />
</Row>
<LogoContainer>{icon}</LogoContainer>
<ThemedText.SubHeaderLarge width="100%" textAlign="center">
{title}
</ThemedText.SubHeaderLarge>
<Row justify="center" marginTop="32px" minHeight="24px">
{cancelSubmitted ? (
<ExternalLink
href={getExplorerLink(orders[0].chainId, cancelTxHash, ExplorerDataType.TRANSACTION)}
color="neutral2"
>
<Trans>View on Explorer</Trans>
</ExternalLink>
) : (
<ThemedText.BodySmall color="neutral2">
<Trans>Proceed in your wallet</Trans>
</ThemedText.BodySmall>
)}
</Row>
</Container>
</Modal>
)
} else if (cancelState === CancellationState.REVIEWING_CANCELLATION) {
return ( return (
<Dialog <Dialog
{...props} {...props}
icon={<Slash size={28} />} icon={icon}
title={ title={title}
<Plural id="cancelling" value={orders.length} one="Cancel limit" other={`Cancel ${orders.length} limits`} />
}
description={ description={
<Column>
<Plural <Plural
id="cancelling-confirmation" id="cancelling-confirmation"
value={orders.length} value={orders.length}
one="Are you sure you want to cancel your limit before it executes or expires?" one="Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?"
other="Are you sure you want to cancel your limits before they execute or expire?" other="Your swaps could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?"
/> />
{gasEstimate?.value && (
<GasEstimateContainer>
<DetailLineItem
LineItem={{
Label: () => <Trans>Network cost</Trans>,
Value: () => (
<span>
{formatNumber({
input: Number(formatEther(gasEstimate.value as string)),
type: NumberType.FiatGasPrice,
})}
</span>
),
}}
/>
</GasEstimateContainer>
)}
</Column>
} }
buttonsConfig={{ buttonsConfig={{
left: { left: {
title: <Trans>Nevermind</Trans>, title: <Trans>Nevermind</Trans>,
onClick: onCancel, onClick: onCancel,
textColor: 'neutral1',
}, },
right: { right: {
title: <Trans>Proceed</Trans>, title: <Trans>Proceed</Trans>,
onClick: onConfirm, onClick: onConfirm,
type: DialogButtonType.Error, type: DialogButtonType.Error,
disabled: cancelling, disabled: cancelState !== CancellationState.REVIEWING_CANCELLATION,
textColor: 'white',
}, },
}} }}
/> />
) )
} else {
// CancellationState.NOT_STARTED
return null
}
} }
...@@ -4,7 +4,7 @@ import { DetailLineItem, LineItemData } from 'components/swap/DetailLineItem' ...@@ -4,7 +4,7 @@ import { DetailLineItem, LineItemData } from 'components/swap/DetailLineItem'
import TradePrice from 'components/swap/TradePrice' import TradePrice from 'components/swap/TradePrice'
import { UniswapXOrderDetails } from 'state/signatures/types' import { UniswapXOrderDetails } from 'state/signatures/types'
import { ExternalLink } from 'theme/components' import { ExternalLink } from 'theme/components'
import { ellipseMiddle } from 'utils/addresses' import { ellipseMiddle } from 'utilities/src/addresses'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
import { formatTimestamp } from '../formatTimestamp' import { formatTimestamp } from '../formatTimestamp'
......
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