ci(release): publish latest release

parent 4daac4d2
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `Qmf6XKUvbFPHvKbi1atVQVyMwr4iVxCSpLHffiTio8Hw2h` - CIDv0: `QmeAaU5F67PX1jZemajRJjA6sDvJ1X6ddvgjjqDHfTePx7`
- CIDv1: `bafybeihy7ayma2n3ltw6zn2aewegv4eyvxo4f52n2ixpnxgyx47xidce5i` - CIDv1: `bafybeihlezropz7qe2rutxp4solf46m3hgpl7c53fevxscbltlz2fo6exq`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,54 +10,41 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,54 +10,41 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeihy7ayma2n3ltw6zn2aewegv4eyvxo4f52n2ixpnxgyx47xidce5i.ipfs.dweb.link/ - https://bafybeihlezropz7qe2rutxp4solf46m3hgpl7c53fevxscbltlz2fo6exq.ipfs.dweb.link/
- https://bafybeihy7ayma2n3ltw6zn2aewegv4eyvxo4f52n2ixpnxgyx47xidce5i.ipfs.cf-ipfs.com/ - https://bafybeihlezropz7qe2rutxp4solf46m3hgpl7c53fevxscbltlz2fo6exq.ipfs.cf-ipfs.com/
- [ipfs://Qmf6XKUvbFPHvKbi1atVQVyMwr4iVxCSpLHffiTio8Hw2h/](ipfs://Qmf6XKUvbFPHvKbi1atVQVyMwr4iVxCSpLHffiTio8Hw2h/) - [ipfs://QmeAaU5F67PX1jZemajRJjA6sDvJ1X6ddvgjjqDHfTePx7/](ipfs://QmeAaU5F67PX1jZemajRJjA6sDvJ1X6ddvgjjqDHfTePx7/)
## 5.9.0 (2024-02-12) ## 5.10.0 (2024-02-20)
### Features ### Features
* **web:** [info] fix TDP/PDP header mobile responsiveness (#5636) 64edb1a * **web:** [info] active liquidity chart (#6237) 1133601
* **web:** [info] move chart type selector and time selector to below chart (#5880) 524734a * **web:** [info] add p0 info analytics (#6338) fdaade8
* **web:** add copy tooltip behavior (#5919) aed3633 * **web:** [info] dot grids (#6327) e4e8720
* **web:** add limit price inversion (#6198) de2d048 * **web:** [uni-tags] add accept / reject to banners (#6309) 34a5aa7
* **web:** Allow feature flag overriding through URL parameters (#6182) 01181cf * **web:** [uni-tags] add banner to account drawer (#6209) 995722d
* **web:** change output currency from limit price panel (#6192) 8651e59 * **web:** [uni-tags] add banner to swap page (#6234) b41f304
* **web:** deploy v2 everywhere feature flag (#6161) dfa90e7 * **web:** add tables error states (#6228) 262f8a0
* **web:** do not reset scroll between tabs (#6090) 4d0d682 * **web:** fix PDP in-chart price headers (#6211) a6070ae
* **web:** explore/tokens and /explore should not be interchangeable (#6088) f4ca806 * **web:** implement TDP TVL chart gql queries (#6190) dabe043
* **web:** Make limit price section first and auto-fill with USDC if no output currency selected (#6013) b8ecb58 * **web:** implement TDP volumes chart gql queries (#6188) d044d6b
* **web:** more dns gateway updates (#5964) 5051029 * **web:** pool protocol switcher (#6339) f81cc26
* **web:** outage banner for arbitrum, optimism, polygon (#6218) 30af199 * **web:** replace account drawer header buttons with action tiles (#6240) fba32e9
* **web:** remove increment buttons from Limit Price Input (#6189) b27d874 * **web:** v2 everywhere (#6164) 5c8ec8d
* **web:** Update submission endpoint for limits (#6236) 35c1e29
* **web:** use pill toggle for TDP/PDP chart times (#6002) f3631b4
* **web:** use protocol stats for explore charts (#6030) d42aa99
### Bug Fixes ### Bug Fixes
* **web:** [landing-page] add missing translations, improve layout responsiveness, update brand assets (#6166) 38e9e47 * **web:** [info] fix pool table description ellipsis width (#6148) b83c2a3
* **web:** de-flake swap flow logging test (#6150) 0f7214e * **web:** avoid re-rendering the App constantly (#6296) 45de35c
* **web:** de-flake TDP cypress test (#6147) c999070 * **web:** disable statsig metrics (#6368) f554c79
* **web:** fix and re-enable swap e2e tests (#6143) 6922d5b * **web:** fix e2e tests (#6365) 4343ae9
* **web:** followup fixes for outage banner (#6229) a6db458 * **web:** fix importing some wallet paths causing errors due to react-native-dotenv (#6255) 6b7fc53
* **web:** left aligned input send (#6144) cd926ae * **web:** import v2 pool goes to add v3 pool (#6273) 8f52943
* **web:** make new landing page enabled by default (#6287) ce952ad * **web:** Keep base/quote tokens stable if user has edited the limit price value (#6284) 1dfe744
* **web:** re-enable some cypress tests (#6122) 0aaecb1 * **web:** make new landing page enabled by default (#6285) a09d8fe
* **web:** send currency logos in send review are extending too far on safari (#6137) b6eecf2 * **web:** use chainId instead of chainName for analytics (#6390) 81dc15b
* **web:** send numbers cutoff on safari (#6136) eed1540 * **web:** using correct favicon url (#6298) 46d76a8
* **web:** swap out OP for LDO on homepage (#6206) ea0ea23
* **web:** update gql schema (#6246) 07432fe
* **web:** use sentence casing (#6046) d2958ee
### Tests
* **web:** add e2e test for cancelling X order (#6146) 131aedc
* **web:** update permit2 tests to use new ConfirmSwapModalV2 (#6165) 02c6883
* **web:** use ConfirmSwapModalV2 in swap errors e2e tests (#6167) 25e4bad
web/5.9.0 web/5.10.0
\ No newline at end of file \ No newline at end of file
...@@ -13,6 +13,7 @@ ignores: [ ...@@ -13,6 +13,7 @@ ignores: [
## React Native Usage ## React Native Usage
"@amplitude/analytics-react-native", "@amplitude/analytics-react-native",
"@react-native-masked-view/masked-view", "@react-native-masked-view/masked-view",
"@react-native-firebase/app-check",
"react-native-image-colors", "react-native-image-colors",
# Dependencies that depcheck thinks are missing but are actually present or never used # Dependencies that depcheck thinks are missing but are actually present or never used
## Internal packages / workspaces ## Internal packages / workspaces
......
...@@ -125,17 +125,17 @@ android { ...@@ -125,17 +125,17 @@ android {
dev { dev {
isDefault(true) isDefault(true)
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionName "1.21" versionName "1.22"
dimension "variant" dimension "variant"
} }
beta { beta {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionName "1.21" versionName "1.22"
dimension "variant" dimension "variant"
} }
prod { prod {
dimension "variant" dimension "variant"
versionName "1.21" versionName "1.22"
} }
} }
......
...@@ -702,6 +702,9 @@ PODS: ...@@ -702,6 +702,9 @@ PODS:
- React-Core (= 0.71.13) - React-Core (= 0.71.13)
- React-jsi (= 0.71.13) - React-jsi (= 0.71.13)
- ReactCommon/turbomodule/core (= 0.71.13) - ReactCommon/turbomodule/core (= 0.71.13)
- Firebase/AppCheck (10.15.0):
- Firebase/CoreOnly
- FirebaseAppCheck (~> 10.15.0)
- Firebase/Auth (10.15.0): - Firebase/Auth (10.15.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAuth (~> 10.15.0) - FirebaseAuth (~> 10.15.0)
...@@ -710,6 +713,10 @@ PODS: ...@@ -710,6 +713,10 @@ PODS:
- Firebase/Firestore (10.15.0): - Firebase/Firestore (10.15.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseFirestore (~> 10.15.0) - FirebaseFirestore (~> 10.15.0)
- FirebaseAppCheck (10.15.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseAppCheckInterop (10.15.0) - FirebaseAppCheckInterop (10.15.0)
- FirebaseAuth (10.15.0): - FirebaseAuth (10.15.0):
- FirebaseAppCheckInterop (~> 10.0) - FirebaseAppCheckInterop (~> 10.0)
...@@ -1263,6 +1270,10 @@ PODS: ...@@ -1263,6 +1270,10 @@ PODS:
- RNFBApp (18.4.0): - RNFBApp (18.4.0):
- Firebase/CoreOnly (= 10.15.0) - Firebase/CoreOnly (= 10.15.0)
- React-Core - React-Core
- RNFBAppCheck (18.4.0):
- Firebase/AppCheck (= 10.15.0)
- React-Core
- RNFBApp
- RNFBAuth (18.4.0): - RNFBAuth (18.4.0):
- Firebase/Auth (= 10.15.0) - Firebase/Auth (= 10.15.0)
- React-Core - React-Core
...@@ -1423,6 +1434,7 @@ DEPENDENCIES: ...@@ -1423,6 +1434,7 @@ DEPENDENCIES:
- RNDeviceInfo (from `../../../node_modules/react-native-device-info`) - RNDeviceInfo (from `../../../node_modules/react-native-device-info`)
- RNFastImage (from `../../../node_modules/react-native-fast-image`) - RNFastImage (from `../../../node_modules/react-native-fast-image`)
- "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)"
- "RNFBAppCheck (from `../../../node_modules/@react-native-firebase/app-check`)"
- "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)" - "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)"
- "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)" - "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)"
- "RNFlashList (from `../../../node_modules/@shopify/flash-list`)" - "RNFlashList (from `../../../node_modules/@shopify/flash-list`)"
...@@ -1445,6 +1457,7 @@ SPEC REPOS: ...@@ -1445,6 +1457,7 @@ SPEC REPOS:
- Argon2Swift - Argon2Swift
- BoringSSL-GRPC - BoringSSL-GRPC
- Firebase - Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop - FirebaseAppCheckInterop
- FirebaseAuth - FirebaseAuth
- FirebaseCore - FirebaseCore
...@@ -1629,6 +1642,8 @@ EXTERNAL SOURCES: ...@@ -1629,6 +1642,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-fast-image" :path: "../../../node_modules/react-native-fast-image"
RNFBApp: RNFBApp:
:path: "../../../node_modules/@react-native-firebase/app" :path: "../../../node_modules/@react-native-firebase/app"
RNFBAppCheck:
:path: "../../../node_modules/@react-native-firebase/app-check"
RNFBAuth: RNFBAuth:
:path: "../../../node_modules/@react-native-firebase/auth" :path: "../../../node_modules/@react-native-firebase/auth"
RNFBFirestore: RNFBFirestore:
...@@ -1686,6 +1701,7 @@ SPEC CHECKSUMS: ...@@ -1686,6 +1701,7 @@ SPEC CHECKSUMS:
FBLazyVector: 24e08bf294faea0abc0278abb2fcad7f3e446f6f FBLazyVector: 24e08bf294faea0abc0278abb2fcad7f3e446f6f
FBReactNativeSpec: cc06081bbc8420e1c0580008ff6d7af324f32f31 FBReactNativeSpec: cc06081bbc8420e1c0580008ff6d7af324f32f31
Firebase: 66043bd4579e5b73811f96829c694c7af8d67435 Firebase: 66043bd4579e5b73811f96829c694c7af8d67435
FirebaseAppCheck: 66eea1c882cddd1bce9d92a0a7efd596f7204782
FirebaseAppCheckInterop: a8c555b1c2db1d9445e6c3a08a848167ddb7eb51 FirebaseAppCheckInterop: a8c555b1c2db1d9445e6c3a08a848167ddb7eb51
FirebaseAuth: a55ec5f7f8a5b1c2dd750235c1bb419bfb642445 FirebaseAuth: a55ec5f7f8a5b1c2dd750235c1bb419bfb642445
FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e
...@@ -1758,6 +1774,7 @@ SPEC CHECKSUMS: ...@@ -1758,6 +1774,7 @@ SPEC CHECKSUMS:
RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae
RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660 RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660
RNFBApp: a3026bdd951dd7a3a88e8e6518b53ddd2f8b3809 RNFBApp: a3026bdd951dd7a3a88e8e6518b53ddd2f8b3809
RNFBAppCheck: c5363a0be62f961edfcdf82ed353c69bc37a39f4
RNFBAuth: 553c6e66d3c70e086799104dad4a554c2663c337 RNFBAuth: 553c6e66d3c70e086799104dad4a554c2663c337
RNFBFirestore: a60e6005e071b31360a5bf651eb403b36c7db7de RNFBFirestore: a60e6005e071b31360a5bf651eb403b36c7db7de
RNFlashList: 4b4b6b093afc0df60ae08f9cbf6ccd4c836c667a RNFlashList: 4b4b6b093afc0df60ae08f9cbf6ccd4c836c667a
......
...@@ -2450,7 +2450,7 @@ ...@@ -2450,7 +2450,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.21; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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;
......
#import "AppDelegate.h" #import "AppDelegate.h"
#import "RNFBAppCheckModule.h"
#import <Firebase.h> #import <Firebase.h>
#import "Uniswap-Swift.h" #import "Uniswap-Swift.h"
...@@ -13,9 +14,11 @@ ...@@ -13,9 +14,11 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{ {
// must be first line in startup routine // Must be first line in startup routine
[ReactNativePerformance onAppStarted]; [ReactNativePerformance onAppStarted];
// Must be before [FIRApp configure], initializes RNFBAppCheckModule
[RNFBAppCheckModule sharedInstance];
[FIRApp configure]; [FIRApp configure];
// This is needed so universal links opened from OneSignal notifications navigate to the proper page. // This is needed so universal links opened from OneSignal notifications navigate to the proper page.
......
...@@ -95,6 +95,14 @@ jest.mock('@react-native-firebase/auth', () => () => ({ ...@@ -95,6 +95,14 @@ jest.mock('@react-native-firebase/auth', () => () => ({
signInAnonymously: jest.fn(), signInAnonymously: jest.fn(),
})) }))
jest.mock('@react-native-firebase/app-check', () => () => ({
appCheck: jest.fn(),
newReactNativeFirebaseAppCheckProvider: jest.fn(() => ({
configure: jest.fn(),
})),
initializeAppCheck: jest.fn().mockReturnValue(Promise.resolve()), // Return a resolved Promise
}))
jest.mock('react-native/Libraries/Linking/Linking', () => ({ jest.mock('react-native/Libraries/Linking/Linking', () => ({
openURL: jest.fn(), openURL: jest.fn(),
addEventListener: jest.fn(), addEventListener: jest.fn(),
......
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
"@react-native-async-storage/async-storage": "1.17.10", "@react-native-async-storage/async-storage": "1.17.10",
"@react-native-community/netinfo": "9.3.0", "@react-native-community/netinfo": "9.3.0",
"@react-native-firebase/app": "18.4.0", "@react-native-firebase/app": "18.4.0",
"@react-native-firebase/app-check": "18.4.0",
"@react-native-firebase/auth": "18.4.0", "@react-native-firebase/auth": "18.4.0",
"@react-native-firebase/firestore": "18.4.0", "@react-native-firebase/firestore": "18.4.0",
"@react-native-masked-view/masked-view": "0.2.9", "@react-native-masked-view/masked-view": "0.2.9",
...@@ -75,9 +76,9 @@ ...@@ -75,9 +76,9 @@
"@shopify/react-native-performance-navigation": "3.0.0", "@shopify/react-native-performance-navigation": "3.0.0",
"@shopify/react-native-skia": "0.1.187", "@shopify/react-native-skia": "0.1.187",
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.30.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",
......
...@@ -47,6 +47,7 @@ import { useAsyncData } from 'utilities/src/react/hooks' ...@@ -47,6 +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'
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'
...@@ -103,6 +104,7 @@ if (!__DEV__) { ...@@ -103,6 +104,7 @@ if (!__DEV__) {
initOneSignal() initOneSignal()
initAppsFlyer() initAppsFlyer()
initFirebaseAppCheck()
initializeTranslation() initializeTranslation()
function App(): JSX.Element | null { function App(): JSX.Element | null {
...@@ -123,6 +125,8 @@ function App(): JSX.Element | null { ...@@ -123,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>
......
...@@ -243,7 +243,6 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -243,7 +243,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
{ {
"alignItems": "center", "alignItems": "center",
"flexDirection": "row", "flexDirection": "row",
"gap": 2,
"justifyContent": "center", "justifyContent": "center",
} }
} }
...@@ -270,37 +269,26 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -270,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"
...@@ -519,45 +507,27 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -519,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,
},
],
} }
} }
> >
......
...@@ -336,7 +336,11 @@ export function UnitagStackNavigator(): JSX.Element { ...@@ -336,7 +336,11 @@ export function UnitagStackNavigator(): JSX.Element {
name={UnitagScreens.UnitagConfirmation} name={UnitagScreens.UnitagConfirmation}
options={{ ...navOptions.noHeader, gestureEnabled: false }} options={{ ...navOptions.noHeader, gestureEnabled: false }}
/> />
<UnitagStack.Screen component={EditUnitagProfileScreen} name={UnitagScreens.EditProfile} /> <UnitagStack.Screen
component={EditUnitagProfileScreen}
name={UnitagScreens.EditProfile}
options={{ ...navOptions.noHeader, gestureEnabled: false }}
/>
</UnitagStack.Group> </UnitagStack.Group>
</UnitagStack.Navigator> </UnitagStack.Navigator>
) )
......
...@@ -68,14 +68,17 @@ export type OnboardingStackBaseParams = { ...@@ -68,14 +68,17 @@ export type OnboardingStackBaseParams = {
unitagClaim?: UnitagClaim unitagClaim?: UnitagClaim
} }
export type UnitagEntryPoint = OnboardingScreens.Landing | Screens.Home | Screens.Settings
export type SharedUnitagScreenParams = { export type SharedUnitagScreenParams = {
[UnitagScreens.ClaimUnitag]: { [UnitagScreens.ClaimUnitag]: {
entryPoint: OnboardingScreens.Landing | Screens.Home entryPoint: UnitagEntryPoint
address?: Address address?: Address
} }
[UnitagScreens.ChooseProfilePicture]: { [UnitagScreens.ChooseProfilePicture]: {
entryPoint: OnboardingScreens.Landing | Screens.Home entryPoint: UnitagEntryPoint
unitag: string unitag: string
unitagFontSize: number
address: Address address: Address
} }
} }
......
import React, { memo, useMemo } from 'react' import React, { memo, useMemo } from 'react'
import { ImageSourcePropType, StyleSheet } from 'react-native' import { ImageSourcePropType } from 'react-native'
import QRCode from 'src/components/QRCodeScanner/custom-qr-code-generator' import QRCode from 'src/components/QRCodeScanner/custom-qr-code-generator'
import { ColorTokens, Flex, Unicon, useSporeColors, useUniconColors } from 'ui/src' import {
import { borderRadii, opacify } from 'ui/src/theme' ColorTokens,
Flex,
useIsDarkMode,
useSporeColors,
useUniconColors,
useUniconV2Colors,
} from 'ui/src'
import { borderRadii } from 'ui/src/theme'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { useFeatureFlag } from 'wallet/src/features/experiments/hooks'
import { useAvatar } from 'wallet/src/features/wallet/hooks'
import { passesContrast, useExtractedColors } from 'wallet/src/utils/colors'
import { isAndroid } from 'wallet/src/utils/platform' import { isAndroid } from 'wallet/src/utils/platform'
type AvatarColors = {
primary: string
base: string
detail: string
}
type ColorProps = {
smartColor: string
gradientProps: {
enableLinearGradient?: boolean
linearGradient?: string[]
gradientDirection?: string[]
color?: string
}
}
const useColorProps = (address: Address, color?: string): ColorProps => {
const colors = useSporeColors()
const gradientData = useUniconColors(address)
const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2)
const isDarkMode = useIsDarkMode()
const uniconV2Color = useUniconV2Colors(address, isDarkMode) as { color: string }
const { avatar, loading: avatarLoading } = useAvatar(address)
const { colors: avatarColors } = useExtractedColors(avatar) as { colors: AvatarColors }
const hasAvatar = !!avatar && !avatarLoading
const smartColor: string = useMemo<string>(() => {
const contrastThreshold = 3 // WCAG AA standard for contrast
const backgroundColor = colors.surface2.val // replace with your actual background color
if (hasAvatar && avatarColors && avatarColors.primary) {
if (passesContrast(avatarColors.primary, backgroundColor, contrastThreshold)) {
return avatarColors.primary
}
if (passesContrast(avatarColors.base, backgroundColor, contrastThreshold)) {
return avatarColors.base
}
if (passesContrast(avatarColors.detail, backgroundColor, contrastThreshold)) {
return avatarColors.detail
}
// Modify the color if it doesn't pass the contrast check
// Replace 'modifiedColor' with the actual color you want to use
return colors.neutral1.val as string
}
return isUniconsV2Enabled ? uniconV2Color.color : '$transparent'
}, [
avatarColors,
hasAvatar,
isUniconsV2Enabled,
uniconV2Color.color,
colors.surface2.val,
colors.neutral1.val,
])
const gradientProps = useMemo(() => {
let gradientPropsObject: {
enableLinearGradient?: boolean
linearGradient?: string[]
gradientDirection?: string[]
color?: string
} = {}
gradientPropsObject = {
enableLinearGradient: isUniconsV2Enabled ? false : true,
linearGradient: [gradientData.gradientStart, gradientData.gradientEnd],
color: isUniconsV2Enabled ? color : gradientData.gradientStart,
// TODO(MOB-2822): see if we can remove ternary
gradientDirection: ['0%', '0%', isAndroid ? '150%' : '100%', '0%'],
}
return gradientPropsObject
}, [gradientData.gradientEnd, gradientData.gradientStart, isUniconsV2Enabled, color])
return { smartColor, gradientProps }
}
type AddressQRCodeProps = { type AddressQRCodeProps = {
address: Address address: Address
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H' errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H'
...@@ -24,9 +111,9 @@ export const AddressQRCode = ({ ...@@ -24,9 +111,9 @@ export const AddressQRCode = ({
safeAreaSize, safeAreaSize,
safeAreaColor, safeAreaColor,
}: AddressQRCodeProps): JSX.Element => { }: AddressQRCodeProps): JSX.Element => {
const colors = useSporeColors()
const backgroundColorValue = backgroundColor const backgroundColorValue = backgroundColor
const gradientData = useUniconColors(address) const { gradientProps } = useColorProps(address, color)
const colors = useSporeColors()
const safeAreaProps = useMemo(() => { const safeAreaProps = useMemo(() => {
let safeAreaPropsObject: { let safeAreaPropsObject: {
...@@ -44,36 +131,18 @@ export const AddressQRCode = ({ ...@@ -44,36 +131,18 @@ export const AddressQRCode = ({
// this could eventually be set to an SVG version of the Unicon which would ensure it's perfectly centered, but for now we can just use an empty logo image to create a blank circle in the middle of the QR code // this could eventually be set to an SVG version of the Unicon which would ensure it's perfectly centered, but for now we can just use an empty logo image to create a blank circle in the middle of the QR code
logoBackgroundColor: colors.surface1.val, logoBackgroundColor: colors.surface1.val,
logoBorderRadius: borderRadii.roundedFull, logoBorderRadius: borderRadii.roundedFull,
// note: this QR code library doesn't actually create a "safe" space in the middle, it just adds the logo on top, so that's why ecl is set to H (high error correction level) by default to ensure the QR code is still readable even if the middle of the QR code is partially obscured // note: this QR code library doesn't actually create a 'safe' space in the middle, it just adds the logo on top, so that's why ecl is set to H (high error correction level) by default to ensure the QR code is still readable even if the middle of the QR code is partially obscured
} }
} }
return safeAreaPropsObject return safeAreaPropsObject
}, [safeAreaSize, safeAreaColor, colors]) }, [safeAreaSize, safeAreaColor, colors])
const gradientProps = useMemo(() => {
let gradientPropsObject: {
enableLinearGradient?: boolean
linearGradient?: string[]
gradientDirection?: string[]
color?: string
} = {}
if (!color) {
gradientPropsObject = {
enableLinearGradient: true,
linearGradient: [gradientData.gradientStart, gradientData.gradientEnd],
color: gradientData.gradientStart,
gradientDirection: ['0%', '0%', isAndroid ? '150%' : '100%', '0%'],
}
}
return gradientPropsObject
}, [color, gradientData])
return ( return (
<QRCode <QRCode
backgroundColor={backgroundColorValue} backgroundColor={backgroundColorValue}
color={color} color={color}
ecl={errorCorrectionLevel} ecl={errorCorrectionLevel}
overlayColor={colors.neutral1.val}
{...safeAreaProps} {...safeAreaProps}
{...gradientProps} {...gradientProps}
size={size} size={size}
...@@ -88,26 +157,27 @@ type QRCodeDisplayProps = { ...@@ -88,26 +157,27 @@ type QRCodeDisplayProps = {
size: number size: number
backgroundColor?: ColorTokens backgroundColor?: ColorTokens
containerBackgroundColor?: ColorTokens containerBackgroundColor?: ColorTokens
overlayColor?: ColorTokens
safeAreaColor?: ColorTokens safeAreaColor?: ColorTokens
logoSize?: number logoSize?: number
overlayOpacityPercent?: number
hideOutline?: boolean hideOutline?: boolean
displayShadow?: boolean displayShadow?: boolean
color?: string
} }
const _QRCodeDisplay = ({ const _QRCodeDisplay = ({
address, address,
errorCorrectionLevel = 'Q', errorCorrectionLevel = 'H',
size, size,
backgroundColor = '$surface1',
containerBackgroundColor, containerBackgroundColor,
overlayOpacityPercent, color,
logoSize = 32, logoSize = 32,
safeAreaColor, safeAreaColor,
hideOutline = false, hideOutline = false,
displayShadow = false, displayShadow = false,
}: QRCodeDisplayProps): JSX.Element => { }: QRCodeDisplayProps): JSX.Element => {
const colors = useSporeColors() const { avatar } = useAvatar(address)
const { smartColor } = useColorProps(address, color)
return ( return (
<Flex <Flex
...@@ -115,37 +185,25 @@ const _QRCodeDisplay = ({ ...@@ -115,37 +185,25 @@ const _QRCodeDisplay = ({
backgroundColor={containerBackgroundColor} backgroundColor={containerBackgroundColor}
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded32" borderRadius="$rounded32"
borderWidth={hideOutline ? 0 : 2} borderWidth={hideOutline ? 0 : 1}
justifyContent="center" justifyContent="center"
p="$spacing24" p="$spacing12"
position="relative" position="relative"
shadowColor="$sporeBlack" shadowColor="$sporeBlack"
shadowOffset={{ width: 0, height: 16 }} shadowOffset={{ width: 0, height: 16 }}
shadowOpacity={displayShadow ? 0.1 : 0} shadowOpacity={displayShadow ? 0.1 : 0}
shadowRadius={16}> shadowRadius={16}>
<Flex> <Flex alignItems="center">
<AddressQRCode
address={address}
backgroundColor={backgroundColor}
errorCorrectionLevel={errorCorrectionLevel}
safeAreaColor={safeAreaColor}
safeAreaSize={logoSize / 1.5}
size={size}
/>
{overlayOpacityPercent && (
<Flex style={StyleSheet.absoluteFill}>
<AddressQRCode <AddressQRCode
address={address} address={address}
backgroundColor="$transparent" backgroundColor={containerBackgroundColor}
color={opacify(overlayOpacityPercent, colors.neutral1.val)} color={smartColor}
errorCorrectionLevel={errorCorrectionLevel} errorCorrectionLevel={errorCorrectionLevel}
safeAreaColor={safeAreaColor} safeAreaColor={safeAreaColor}
safeAreaSize={logoSize / 1.5} safeAreaSize={logoSize}
size={size} size={size}
/> />
</Flex> </Flex>
)}
</Flex>
<Flex <Flex
alignItems="center" alignItems="center"
backgroundColor="$transparent" backgroundColor="$transparent"
...@@ -154,10 +212,13 @@ const _QRCodeDisplay = ({ ...@@ -154,10 +212,13 @@ const _QRCodeDisplay = ({
pl="$spacing2" pl="$spacing2"
position="absolute" position="absolute"
pt="$spacing2"> pt="$spacing2">
<Unicon <AccountIcon
showBorder
address={address} address={address}
backgroundColor={colors.surface1.val} avatarUri={avatar}
borderColor="$surface2"
borderWidth={4}
showBackground={true}
showBorder={true}
size={logoSize} size={logoSize}
/> />
</Flex> </Flex>
......
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { GradientBackground } from 'src/components/gradients/GradientBackground'
import { UniconThemedGradient } from 'src/components/gradients/UniconThemedGradient'
import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode' import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode'
import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos'
import { import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src'
AnimatedFlex,
Flex,
Icons,
Text,
TouchableArea,
useIsDarkMode,
useMedia,
useSporeColors,
useUniconColors,
} from 'ui/src'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal'
...@@ -30,15 +18,13 @@ interface Props { ...@@ -30,15 +18,13 @@ interface Props {
export function WalletQRCode({ address }: Props): JSX.Element | null { export function WalletQRCode({ address }: Props): JSX.Element | null {
const colors = useSporeColors() const colors = useSporeColors()
const isDarkMode = useIsDarkMode()
const gradientData = useUniconColors(address)
const { t } = useTranslation() const { t } = useTranslation()
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const media = useMedia() const media = useMedia()
const QR_CODE_SIZE = media.short ? 175 : 220 const QR_CODE_SIZE = media.short ? 220 : 240
const UNICON_SIZE = QR_CODE_SIZE / 2.8 const UNICON_SIZE = QR_CODE_SIZE / 4
if (!address) { if (!address) {
return null return null
...@@ -46,45 +32,34 @@ export function WalletQRCode({ address }: Props): JSX.Element | null { ...@@ -46,45 +32,34 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
return ( return (
<> <>
<GradientBackground>
<UniconThemedGradient
middleOut
borderRadius="$rounded16"
gradientEndColor={colors.surface1.val}
gradientStartColor={gradientData.glow}
opacity={isDarkMode ? 0.24 : 0.2}
/>
</GradientBackground>
<AnimatedFlex <AnimatedFlex
centered centered
grow grow
$short={{ mb: spacing.none, mx: spacing.spacing48 }} $short={{ mb: spacing.none, mx: spacing.spacing48 }}
entering={FadeIn} entering={FadeIn}
exiting={FadeOut} exiting={FadeOut}
gap="$spacing24" gap="$spacing8"
mb="$spacing8" mb="$spacing8"
mx="$spacing60" mx="$spacing60"
py="$spacing24"> py="$spacing24">
<AddressDisplay <AddressDisplay
includeUnitagSuffix includeUnitagSuffix
showCopy showCopy
showCopyWrapperButton
address={address} address={address}
captionVariant="body1" captionVariant="body2"
showAccountIcon={false} showAccountIcon={false}
variant="heading3" variant="heading3"
/> />
<QRCodeDisplay <QRCodeDisplay
hideOutline hideOutline
address={address} address={address}
backgroundColor="$surface1" containerBackgroundColor={colors.surface1.val}
containerBackgroundColor="$surface1" displayShadow={false}
displayShadow={true}
logoSize={UNICON_SIZE} logoSize={UNICON_SIZE}
overlayOpacityPercent={10}
safeAreaColor="$surface1" safeAreaColor="$surface1"
size={QR_CODE_SIZE} size={QR_CODE_SIZE}
/> />
<Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3"> <Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3">
{t('You can send tokens on all of our supported networks to this address.')} {t('You can send tokens on all of our supported networks to this address.')}
</Text> </Text>
......
...@@ -13,6 +13,8 @@ export interface QRCodeProps { ...@@ -13,6 +13,8 @@ export interface QRCodeProps {
color?: string color?: string
/* the color of the background */ /* the color of the background */
backgroundColor?: string backgroundColor?: string
/* the color of the background */
overlayColor?: string
/* an image source object. example {uri: 'base64string'} or {require('pathToImage')} */ /* an image source object. example {uri: 'base64string'} or {require('pathToImage')} */
logo?: ImageSourcePropType logo?: ImageSourcePropType
/* logo size in pixels */ /* logo size in pixels */
......
...@@ -2,74 +2,52 @@ ...@@ -2,74 +2,52 @@
// Custom matric renderer from: https://github.com/awesomejerry/react-native-qrcode-svg/pull/139/files // Custom matric renderer from: https://github.com/awesomejerry/react-native-qrcode-svg/pull/139/files
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import Svg, { ClipPath, Defs, G, Image, LinearGradient, Path, Rect, Stop } from 'react-native-svg' import Svg, { Defs, G, LinearGradient, Path, Rect, Stop } from 'react-native-svg'
import genMatrix from 'src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js' import genMatrix from 'src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js'
import transformMatrixIntoPath from 'src/components/QRCodeScanner/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js' import transformMatrixIntoPath from 'src/components/QRCodeScanner/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js'
import { useMedia } from 'ui/src'
const renderLogo = ({ const QREyes = ({ x = -1, y = -1, fillColor, size }) => (
size, <G transform={`scale(${size / 120})`} x={x} y={y}>
logo, <Path
logoBackgroundColor, clip-rule="evenodd"
logoSize, d="M0 12C0 5.37258 5.37258 0 12 0H28C34.6274 0 40 5.37258 40 12V28C40 34.6274 34.6274 40 28 40H12C5.37258 40 0 34.6274 0 28V12ZM28 6.27451H12C8.8379 6.27451 6.27451 8.8379 6.27451 12V28C6.27451 31.1621 8.8379 33.7255 12 33.7255H28C31.1621 33.7255 33.7255 31.1621 33.7255 28V12C33.7255 8.8379 31.1621 6.27451 28 6.27451Z"
logoMargin, fill={fillColor}
logoBorderRadius, fill-rule="evenodd"
}) => {
const logoPosition = (size - logoSize - logoMargin * 2) / 2
const logoBackgroundSize = logoSize + logoMargin * 2
const logoBackgroundBorderRadius = logoBorderRadius + (logoMargin / logoSize) * logoBorderRadius
return (
<G x={logoPosition} y={logoPosition}>
<Defs>
<ClipPath id="clip-logo-background">
<Rect
height={logoBackgroundSize}
rx={logoBackgroundBorderRadius}
ry={logoBackgroundBorderRadius}
width={logoBackgroundSize}
/>
</ClipPath>
<ClipPath id="clip-logo">
<Rect height={logoSize} rx={logoBorderRadius} ry={logoBorderRadius} width={logoSize} />
</ClipPath>
</Defs>
<G>
<Rect
clipPath="url(#clip-logo-background)"
fill={logoBackgroundColor}
height={logoBackgroundSize}
width={logoBackgroundSize}
/> />
</G> <Path
<G x={logoMargin} y={logoMargin}> d="M11 17C11 13.6863 13.6863 11 17 11H23C26.3137 11 29 13.6863 29 17V23C29 26.3137 26.3137 29 23 29H17C13.6863 29 11 26.3137 11 23V17Z"
<Image fill={fillColor}
clipPath="url(#clip-logo)"
height={logoSize}
href={logo}
preserveAspectRatio="xMidYMid slice"
width={logoSize}
/> />
</G> </G>
)
const QREyeBG = ({ x = -1, y = -1, size, backgroundColor }) => (
<G transform={`scale(${size / 120})`} x={x} y={y}>
<Path d="M0 0H40V40H0V0Z" fill={backgroundColor} />
</G> </G>
) )
}
const QREyeWrapper = ({ x = 0, y = 0, backgroundColor, overlayColor, fillColor, size }) => (
<>
<QREyeBG backgroundColor={backgroundColor} size={size} x={x} y={y} />
<QREyes fillColor={fillColor} size={size} x={x} y={y} />
<QREyes fillColor={overlayColor} size={size} x={x} y={y} />
</>
)
const QRCode = ({ const QRCode = ({
value = 'this is a QR code', value = 'Wallet QR code',
size = 100, size = 190,
color = 'sporeBlack', color,
backgroundColor = 'sporeWhite', backgroundColor,
overlayColor = '#FFFFFF',
borderRadius = 24, borderRadius = 24,
logo, quietZone = 8,
logoSize = size * 0.2,
logoBackgroundColor = 'transparent',
logoMargin = -2,
logoBorderRadius = 0,
quietZone = 4,
enableLinearGradient = false, enableLinearGradient = false,
gradientDirection = ['0%', '0%', '100%', '100%'], gradientDirection = ['0%', '0%', '100%', '100%'],
linearGradient = ['rgb(255,0,0)', 'rgb(0,255,255)'], linearGradient = ['rgb(255,255,255)', 'rgb(0,255,255)'],
ecl = 'M', ecl = 'H',
getRef, getRef,
onError, onError,
}) => { }) => {
...@@ -86,12 +64,15 @@ const QRCode = ({ ...@@ -86,12 +64,15 @@ const QRCode = ({
} }
}, [value, size, ecl, onError]) }, [value, size, ecl, onError])
const media = useMedia()
if (!result) { if (!result) {
return null return null
} }
const { path } = result const { path } = result
const eyeSize = media.short ? 126 : 138
return ( return (
<Svg <Svg
ref={getRef} ref={getRef}
...@@ -104,9 +85,9 @@ const QRCode = ({ ...@@ -104,9 +85,9 @@ const QRCode = ({
id="grad" id="grad"
x1={gradientDirection[0]} x1={gradientDirection[0]}
x2={gradientDirection[2]} x2={gradientDirection[2]}
y1={gradientDirection[1]} y1={gradientDirection[0]}
y2={gradientDirection[3]}> y2={gradientDirection[2]}>
<Stop offset="0" stopColor={linearGradient[0]} stopOpacity="1" /> <Stop offset="0" stopColor={color} stopOpacity="1" />
<Stop offset="1" stopColor={linearGradient[1]} stopOpacity="1" /> <Stop offset="1" stopColor={linearGradient[1]} stopOpacity="1" />
<Stop offset="1" stopColor={linearGradient[2]} stopOpacity="1" /> <Stop offset="1" stopColor={linearGradient[2]} stopOpacity="1" />
</LinearGradient> </LinearGradient>
...@@ -123,16 +104,28 @@ const QRCode = ({ ...@@ -123,16 +104,28 @@ const QRCode = ({
</G> </G>
<G> <G>
<Path d={path} fill={enableLinearGradient ? 'url(#grad)' : color} /> <Path d={path} fill={enableLinearGradient ? 'url(#grad)' : color} />
<Path d={path} fill={enableLinearGradient ? overlayColor + '66' : overlayColor + '2D'} />
<QREyeWrapper
backgroundColor={backgroundColor}
fillColor={color}
overlayColor={overlayColor + '2D'}
size={eyeSize}
/>
<QREyeWrapper
backgroundColor={backgroundColor}
fillColor={color}
overlayColor={overlayColor + '2D'}
size={eyeSize}
y={size - eyeSize / 3}
/>
<QREyeWrapper
backgroundColor={backgroundColor}
fillColor={color}
overlayColor={overlayColor + '2D'}
size={eyeSize}
x={size - eyeSize / 3}
/>
</G> </G>
{logo &&
renderLogo({
size,
logo,
logoSize,
logoBackgroundColor,
logoMargin,
logoBorderRadius,
})}
</Svg> </Svg>
) )
} }
......
...@@ -199,7 +199,7 @@ export const TokenBalanceListInner = forwardRef< ...@@ -199,7 +199,7 @@ export const TokenBalanceListInner = forwardRef<
{!balancesById ? ( {!balancesById ? (
isNonPollingRequestInFlight(networkStatus) ? ( isNonPollingRequestInFlight(networkStatus) ? (
<Flex px="$spacing24" style={containerProps?.loadingContainerStyle}> <Flex px="$spacing24" style={containerProps?.loadingContainerStyle}>
<Loader.Token repeat={6} /> <Loader.Token withPrice repeat={6} />
</Flex> </Flex>
) : ( ) : (
<Flex fill grow justifyContent="center" style={containerProps?.emptyContainerStyle}> <Flex fill grow justifyContent="center" style={containerProps?.emptyContainerStyle}>
......
...@@ -3,16 +3,15 @@ import React, { memo, useCallback, useMemo, useRef } from 'react' ...@@ -3,16 +3,15 @@ import React, { memo, useCallback, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ListRenderItemInfo } from 'react-native' import { ListRenderItemInfo } from 'react-native'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { Flex, Icons, Inset, Loader, Text, TouchableArea } from 'ui/src' import { Flex, Inset, Loader } from 'ui/src'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem' import { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem'
import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { ElementName } from 'wallet/src/telemetry/constants'
import { CurrencyId } from 'wallet/src/utils/currencyId' import { CurrencyId } from 'wallet/src/utils/currencyId'
interface Props { interface Props {
onSelectCurrency: (currency: FiatOnRampCurrency) => void onSelectCurrency: (currency: FiatOnRampCurrency) => void
onBack: () => void
onRetry: () => void onRetry: () => void
error: boolean error: boolean
loading: boolean loading: boolean
...@@ -52,7 +51,6 @@ function TokenOptionItemWrapper({ ...@@ -52,7 +51,6 @@ function TokenOptionItemWrapper({
function _TokenFiatOnRampList({ function _TokenFiatOnRampList({
onSelectCurrency, onSelectCurrency,
onBack,
error, error,
onRetry, onRetry,
list, list,
...@@ -71,8 +69,6 @@ function _TokenFiatOnRampList({ ...@@ -71,8 +69,6 @@ function _TokenFiatOnRampList({
if (error) { if (error) {
return ( return (
<>
<Header onBack={onBack} />
<Flex centered grow> <Flex centered grow>
<BaseCard.ErrorState <BaseCard.ErrorState
retryButtonLabel="Retry" retryButtonLabel="Retry"
...@@ -80,27 +76,20 @@ function _TokenFiatOnRampList({ ...@@ -80,27 +76,20 @@ function _TokenFiatOnRampList({
onRetry={onRetry} onRetry={onRetry}
/> />
</Flex> </Flex>
</>
) )
} }
if (loading) { if (loading) {
return ( return <Loader.Token repeat={5} />
<Flex>
<Header onBack={onBack} />
<Loader.Token repeat={5} />
</Flex>
)
} }
return ( return (
<Flex grow>
<Header onBack={onBack} />
<BottomSheetFlatList <BottomSheetFlatList
ref={flatListRef} ref={flatListRef}
ListEmptyComponent={<Flex />} ListEmptyComponent={<Flex />}
ListFooterComponent={<Inset all="$spacing36" />} ListFooterComponent={<Inset all="$spacing36" />}
data={list} data={list}
focusHook={useBottomSheetFocusHook}
keyExtractor={key} keyExtractor={key}
keyboardDismissMode="on-drag" keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always" keyboardShouldPersistTaps="always"
...@@ -108,20 +97,6 @@ function _TokenFiatOnRampList({ ...@@ -108,20 +97,6 @@ function _TokenFiatOnRampList({
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
windowSize={5} windowSize={5}
/> />
</Flex>
)
}
function Header({ onBack }: { onBack: () => void }): JSX.Element {
const { t } = useTranslation()
return (
<Flex row justifyContent="space-between" my="$spacing16">
<TouchableArea testID={ElementName.Back} onPress={onBack}>
<Icons.RotatableChevron color="$neutral1" />
</TouchableArea>
<Text variant="body1">{t('Select a token to buy')}</Text>
<Flex width={24} />
</Flex>
) )
} }
......
import React from 'react' import React from 'react'
import { Flex, Separator, Text, Unicon, useSporeColors } from 'ui/src' import { Flex, Separator, Text, Unicon, UniconV2, useSporeColors } from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg' import Check from 'ui/src/assets/icons/check.svg'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { useFeatureFlag } from 'wallet/src/features/experiments/hooks'
import { Account } from 'wallet/src/features/wallet/accounts/types' import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { shortenAddress } from 'wallet/src/utils/addresses' import { shortenAddress } from 'wallet/src/utils/addresses'
...@@ -15,13 +17,18 @@ const ICON_SIZE = 24 ...@@ -15,13 +17,18 @@ const ICON_SIZE = 24
export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Element => { export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Element => {
const colors = useSporeColors() const colors = useSporeColors()
const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2)
const displayName = useDisplayName(account.address) const displayName = useDisplayName(account.address)
return ( return (
<> <>
<Separator /> <Separator />
<Flex row alignItems="center" justifyContent="space-between" px="$spacing24" py="$spacing8"> <Flex row alignItems="center" justifyContent="space-between" px="$spacing24" py="$spacing8">
{isUniconsV2Enabled ? (
<UniconV2 address={account.address} size={ICON_SIZE} />
) : (
<Unicon address={account.address} size={ICON_SIZE} /> <Unicon address={account.address} size={ICON_SIZE} />
)}
<Flex shrink alignItems="center" p="$none"> <Flex shrink alignItems="center" p="$none">
<DisplayNameText <DisplayNameText
displayName={displayName} displayName={displayName}
......
...@@ -56,9 +56,9 @@ function PortfolioValue({ ...@@ -56,9 +56,9 @@ function PortfolioValue({
return ( return (
<Text color="$neutral2" loading={isLoading} variant="subheading2"> <Text color="$neutral2" loading={isLoading} variant="subheading2">
{portfolioValue {portfolioValue === undefined
? convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance) ? t('N/A')
: t('N/A')} : convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance)}
</Text> </Text>
) )
} }
......
...@@ -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,
},
],
} }
} }
> >
...@@ -561,42 +528,31 @@ exports[`AccountHeader renders without error 1`] = ` ...@@ -561,42 +528,31 @@ exports[`AccountHeader renders without error 1`] = `
} }
suppressHighlighting={true} suppressHighlighting={true}
> >
.cantswim.eth .uni.eth
</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,
},
],
} }
} }
> >
...@@ -403,7 +386,6 @@ exports[`AccountList renders without error 1`] = ` ...@@ -403,7 +386,6 @@ exports[`AccountList renders without error 1`] = `
{ {
"alignItems": "center", "alignItems": "center",
"flexDirection": "row", "flexDirection": "row",
"gap": 2,
"justifyContent": "center", "justifyContent": "center",
} }
} }
......
...@@ -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]),
}))
}
...@@ -49,7 +49,7 @@ export function SearchEmptySection(): JSX.Element { ...@@ -49,7 +49,7 @@ export function SearchEmptySection(): JSX.Element {
searchHistory.map((historyItem: SearchResult) => { searchHistory.map((historyItem: SearchResult) => {
if (!unitagFeatureFlagEnabled && historyItem.type === SearchResultType.Unitag) { if (!unitagFeatureFlagEnabled && historyItem.type === SearchResultType.Unitag) {
return { return {
type: SearchResultType.ENSAddress, type: SearchResultType.WalletByAddress,
address: historyItem.address, address: historyItem.address,
searchId: historyItem.searchId, searchId: historyItem.searchId,
} }
......
...@@ -4,11 +4,13 @@ import { FlatList, ListRenderItemInfo } from 'react-native' ...@@ -4,11 +4,13 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader' import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { useWalletSearchResults } from 'src/components/explore/search/hooks'
import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem' import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem'
import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem' import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem'
import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem' import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnitagItem' import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnitagItem'
import { SearchWalletByAddressItem } from 'src/components/explore/search/items/SearchWalletByAddressItem'
import { import {
formatNFTCollectionSearchResults, formatNFTCollectionSearchResults,
formatTokenSearchResults, formatTokenSearchResults,
...@@ -19,16 +21,12 @@ import { logger } from 'utilities/src/logger/logger' ...@@ -19,16 +21,12 @@ import { logger } from 'utilities/src/logger/logger'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains'
import { SafetyLevel, useExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks' import { SafetyLevel, useExploreSearchQuery } from 'wallet/src/data/__generated__/types-and-hooks'
import { useENS } from 'wallet/src/features/ens/useENS'
import { SearchContext } from 'wallet/src/features/search/SearchContext' import { SearchContext } from 'wallet/src/features/search/SearchContext'
import { import {
NFTCollectionSearchResult, NFTCollectionSearchResult,
SearchResultType, SearchResultType,
TokenSearchResult, TokenSearchResult,
WalletSearchResult,
} from 'wallet/src/features/search/SearchResult' } from 'wallet/src/features/search/SearchResult'
import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress'
import { useUnitagByAddress, useUnitagByName } from 'wallet/src/features/unitags/hooks'
import i18n from 'wallet/src/i18n/i18n' import i18n from 'wallet/src/i18n/i18n'
import { getValidAddress } from 'wallet/src/utils/addresses' import { getValidAddress } from 'wallet/src/utils/addresses'
import { SEARCH_RESULT_HEADER_KEY } from './constants' import { SEARCH_RESULT_HEADER_KEY } from './constants'
...@@ -66,6 +64,10 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -66,6 +64,10 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
variables: { searchQuery, nftCollectionsFilter: { nameQuery: searchQuery } }, variables: { searchQuery, nftCollectionsFilter: { nameQuery: searchQuery } },
}) })
const onRetry = useCallback(async () => {
await refetch()
}, [refetch])
const tokenResults = useMemo<TokenSearchResult[] | undefined>(() => { const tokenResults = useMemo<TokenSearchResult[] | undefined>(() => {
if (!searchResultsData || !searchResultsData.searchTokens) { if (!searchResultsData || !searchResultsData.searchTokens) {
return return
...@@ -74,6 +76,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -74,6 +76,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return formatTokenSearchResults(searchResultsData.searchTokens, searchQuery) return formatTokenSearchResults(searchResultsData.searchTokens, searchQuery)
}, [searchQuery, searchResultsData]) }, [searchQuery, searchResultsData])
// Search for matching NFT collections
const nftCollectionResults = useMemo<NFTCollectionSearchResult[] | undefined>(() => { const nftCollectionResults = useMemo<NFTCollectionSearchResult[] | undefined>(() => {
if (!searchResultsData || !searchResultsData.nftCollections) { if (!searchResultsData || !searchResultsData.nftCollections) {
return return
...@@ -82,86 +86,25 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -82,86 +86,25 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return formatNFTCollectionSearchResults(searchResultsData.nftCollections) return formatNFTCollectionSearchResults(searchResultsData.nftCollections)
}, [searchResultsData]) }, [searchResultsData])
// Search for matching ENS // Search for matching wallets
const {
address: ensAddress,
name: ensName,
loading: ensLoading,
} = useENS(ChainId.Mainnet, searchQuery, true)
// Search for matching Unitag by name const {
const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(searchQuery) wallets: walletSearchResults,
loading: walletsLoading,
exactENSMatch,
exactUnitagMatch,
} = useWalletSearchResults(searchQuery)
const validAddress: Address | undefined = useMemo( const validAddress: Address | undefined = useMemo(
() => getValidAddress(searchQuery, true, false) ?? undefined, () => getValidAddress(searchQuery, true, false) ?? undefined,
[searchQuery] [searchQuery]
) )
// Search for matching Unitag by address
const { unitag: unitagByAddress, loading: unitagByAddressLoading } =
useUnitagByAddress(validAddress)
// Search for matching EOA wallet address
const { isSmartContractAddress, loading: loadingIsSmartContractAddress } =
useIsSmartContractAddress(validAddress, ChainId.Mainnet)
const walletsLoading =
ensLoading || loadingIsSmartContractAddress || unitagLoading || unitagByAddressLoading
const onRetry = useCallback(async () => {
await refetch()
}, [refetch])
const hasENSResult = ensName && ensAddress
const hasEOAResult = validAddress && !isSmartContractAddress
const walletSearchResults: WalletSearchResult[] = useMemo(() => {
const results: WalletSearchResult[] = []
if (unitagByName?.address?.address && unitagByName?.username) {
results.push({
type: SearchResultType.Unitag,
address: unitagByName.address.address,
unitag: unitagByName.username,
})
}
// Do not show ENS result if it is the same as the Unitag result
if (hasENSResult && ensAddress !== unitagByName?.address?.address) {
results.push({
type: SearchResultType.ENSAddress,
address: ensAddress,
ensName,
})
}
if (unitagByAddress?.username && validAddress) {
results.push({
type: SearchResultType.Unitag,
address: validAddress,
unitag: unitagByAddress.username,
})
}
// Do not show EOA address result if there is a Unitag result by address
if (hasEOAResult && !unitagByAddress) {
results.push({
type: SearchResultType.ENSAddress,
address: validAddress,
})
}
return results as WalletSearchResult[]
}, [ensAddress, ensName, unitagByName, unitagByAddress, hasENSResult, hasEOAResult, validAddress])
const countTokenResults = tokenResults?.length ?? 0 const countTokenResults = tokenResults?.length ?? 0
const countNftCollectionResults = nftCollectionResults?.length ?? 0 const countNftCollectionResults = nftCollectionResults?.length ?? 0
const countWalletResults = walletSearchResults.length const countWalletResults = walletSearchResults.length
const countTotalResults = countTokenResults + countNftCollectionResults + countWalletResults const countTotalResults = countTokenResults + countNftCollectionResults + countWalletResults
// Only consider queries with the .eth suffix as an exact ENS match
const exactENSMatch =
ensName?.toLowerCase() === searchQuery.toLowerCase() && searchQuery.includes('.eth')
const prefixTokenMatch = tokenResults?.find((res: TokenSearchResult) => const prefixTokenMatch = tokenResults?.find((res: TokenSearchResult) =>
isPrefixTokenMatch(res, searchQuery) isPrefixTokenMatch(res, searchQuery)
) )
...@@ -175,8 +118,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -175,8 +118,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified)) const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified))
const showWalletSectionFirst = const showWalletSectionFirst = exactUnitagMatch || (exactENSMatch && !prefixTokenMatch)
unitagByName || unitagByAddress || (exactENSMatch && !prefixTokenMatch)
const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults
const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => { const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => {
...@@ -294,6 +236,8 @@ export const renderSearchItem = ({ ...@@ -294,6 +236,8 @@ export const renderSearchItem = ({
return <SearchENSAddressItem searchContext={searchContext} searchResult={searchResult} /> return <SearchENSAddressItem searchContext={searchContext} searchResult={searchResult} />
case SearchResultType.Unitag: case SearchResultType.Unitag:
return <SearchUnitagItem searchContext={searchContext} searchResult={searchResult} /> return <SearchUnitagItem searchContext={searchContext} searchResult={searchResult} />
case SearchResultType.WalletByAddress:
return <SearchWalletByAddressItem searchContext={searchContext} searchResult={searchResult} />
case SearchResultType.NFTCollection: case SearchResultType.NFTCollection:
return <SearchNFTCollectionItem collection={searchResult} searchContext={searchContext} /> return <SearchNFTCollectionItem collection={searchResult} searchContext={searchContext} />
case SearchResultType.Etherscan: case SearchResultType.Etherscan:
......
...@@ -49,7 +49,7 @@ exports[`SearchPopularTokens renders without error 1`] = ` ...@@ -49,7 +49,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{ {
"alignItems": "center", "alignItems": "center",
"flexDirection": "row", "flexDirection": "row",
"flexShrink": 1, "flexGrow": 1,
"gap": 12, "gap": 12,
"overflow": "hidden", "overflow": "hidden",
} }
...@@ -74,7 +74,7 @@ exports[`SearchPopularTokens renders without error 1`] = ` ...@@ -74,7 +74,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{ {
"alignItems": "flex-start", "alignItems": "flex-start",
"flexDirection": "column", "flexDirection": "column",
"flexShrink": 1, "flexGrow": 1,
} }
} }
> >
...@@ -228,7 +228,7 @@ exports[`SearchPopularTokens renders without error 1`] = ` ...@@ -228,7 +228,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{ {
"alignItems": "center", "alignItems": "center",
"flexDirection": "row", "flexDirection": "row",
"flexShrink": 1, "flexGrow": 1,
"gap": 12, "gap": 12,
"overflow": "hidden", "overflow": "hidden",
} }
...@@ -253,7 +253,7 @@ exports[`SearchPopularTokens renders without error 1`] = ` ...@@ -253,7 +253,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{ {
"alignItems": "flex-start", "alignItems": "flex-start",
"flexDirection": "column", "flexDirection": "column",
"flexShrink": 1, "flexGrow": 1,
} }
} }
> >
...@@ -405,7 +405,7 @@ exports[`SearchPopularTokens renders without error 2`] = ` ...@@ -405,7 +405,7 @@ exports[`SearchPopularTokens renders without error 2`] = `
"name": "Ethereum", "name": "Ethereum",
"safetyLevel": "VERIFIED", "safetyLevel": "VERIFIED",
"symbol": "ETH", "symbol": "ETH",
"type": 2, "type": 1,
}, },
{ {
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
...@@ -414,7 +414,7 @@ exports[`SearchPopularTokens renders without error 2`] = ` ...@@ -414,7 +414,7 @@ exports[`SearchPopularTokens renders without error 2`] = `
"name": "USD Coin", "name": "USD Coin",
"safetyLevel": "VERIFIED", "safetyLevel": "VERIFIED",
"symbol": "USDC", "symbol": "USDC",
"type": 2, "type": 1,
}, },
] ]
} }
...@@ -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"
......
import { useMemo } from 'react'
import { ChainId } from 'wallet/src/constants/chains'
import { useENS } from 'wallet/src/features/ens/useENS'
import { SearchResultType, WalletSearchResult } from 'wallet/src/features/search/SearchResult'
import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress'
import { useUnitagByAddress, useUnitagByName } from 'wallet/src/features/unitags/hooks'
import { getValidAddress } from 'wallet/src/utils/addresses'
// eslint-disable-next-line complexity
export function useWalletSearchResults(query: string): {
wallets: WalletSearchResult[]
loading: boolean
exactENSMatch: boolean
exactUnitagMatch: boolean
} {
const validAddress: Address | undefined = useMemo(
() => getValidAddress(query, true, false) ?? undefined,
[query]
)
const querySkippedIfValidAddress = validAddress ? null : query
// Search for matching .eth if not a valid address
const {
address: dotEthAddress,
name: dotEthName,
loading: dotEthLoading,
} = useENS(ChainId.Mainnet, querySkippedIfValidAddress, true)
// Search for exact match for ENS if not a valid address
const {
address: ensAddress,
name: ensName,
loading: ensLoading,
} = useENS(ChainId.Mainnet, querySkippedIfValidAddress, false)
// Search for matching Unitag by name
const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(query)
// Search for matching Unitag by address
const { unitag: unitagByAddress, loading: unitagByAddressLoading } =
useUnitagByAddress(validAddress)
// Search for matching EOA wallet address
const { isSmartContractAddress, loading: loadingIsSmartContractAddress } =
useIsSmartContractAddress(validAddress, ChainId.Mainnet)
const hasENSResult = dotEthName && dotEthAddress
const hasEOAResult = validAddress && !isSmartContractAddress
// Consider when to show sections
// Only consider queries with the .eth suffix as an exact ENS match
const exactENSMatch = dotEthName?.toLowerCase() === query.toLowerCase() && query.includes('.eth')
const results: WalletSearchResult[] = []
// Prioritize unitags
if (unitagByName?.address?.address && unitagByName?.username) {
results.push({
type: SearchResultType.Unitag,
address: unitagByName.address.address,
unitag: unitagByName.username,
})
}
// Add full address if relevant
if (unitagByAddress?.username && validAddress) {
results.push({
type: SearchResultType.Unitag,
address: validAddress,
unitag: unitagByAddress.username,
})
}
// Add the raw ENS result if available and a unitag by address was not already added
if (!validAddress && ensAddress && ensName && !unitagByAddress?.username) {
results.push({
type: SearchResultType.ENSAddress,
address: ensAddress,
ensName,
isRawName: !ensName.endsWith('.eth'), // Ensure raw name is used for subdomains only
})
}
// Add ENS result if it's different than the unitag result and raw ENS result
if (
!validAddress &&
hasENSResult &&
dotEthAddress !== unitagByName?.address?.address &&
dotEthAddress !== ensAddress
) {
results.push({
type: SearchResultType.ENSAddress,
address: dotEthAddress,
ensName: dotEthName,
})
}
// Do not show EOA address result if there is a Unitag result by address
if (hasEOAResult && !unitagByAddress?.username) {
results.push({
type: SearchResultType.WalletByAddress,
address: validAddress,
})
}
// Ensure loading is returned
const walletsLoading =
dotEthLoading ||
ensLoading ||
loadingIsSmartContractAddress ||
unitagLoading ||
unitagByAddressLoading
return {
loading: walletsLoading,
wallets: results,
exactENSMatch,
exactUnitagMatch: !!(unitagByName || unitagByAddress),
}
}
...@@ -23,23 +23,28 @@ export function SearchENSAddressItem({ ...@@ -23,23 +23,28 @@ export function SearchENSAddressItem({
// Use `savedPrimaryEnsName` for WalletSearchResults that are stored in the search history // Use `savedPrimaryEnsName` for WalletSearchResults that are stored in the search history
// so that we don't have to do an additional ENS fetch when loading search history // so that we don't have to do an additional ENS fetch when loading search history
const { address, ensName, primaryENSName: savedPrimaryENSName } = searchResult const { address, ensName, primaryENSName: savedPrimaryENSName, isRawName } = searchResult
const formattedAddress = sanitizeAddressText(shortenAddress(address)) const formattedAddress = sanitizeAddressText(shortenAddress(address))
// Get the completed name if it's not a raw name
const completedENSName = isRawName ? ensName : getCompletedENSName(ensName ?? null)
/* /*
* Fetch primary ENS associated with `address` since it may resolve to an * Fetch primary ENS associated with `address` since it may resolve to an
* ENS different than the `ensName` searched * ENS different than the `ensName` searched
* ex. if searching `uni.eth` resolves to 0x123, and the primary ENS for 0x123 * ex. if searching `uni.eth` resolves to 0x123, and the primary ENS for 0x123
* is `uniswap.eth`, then we should show "uni.eth | owned by uniswap.eth" * is `uniswap.eth`, then we should show "uni.eth | owned by uniswap.eth"
*/ */
const completedENSName = getCompletedENSName(ensName ?? null)
const { data: fetchedPrimaryENSName, loading: isFetchingPrimaryENSName } = useENSName( const { data: fetchedPrimaryENSName, loading: isFetchingPrimaryENSName } = useENSName(
savedPrimaryENSName ? undefined : address savedPrimaryENSName ? undefined : address
) )
const primaryENSName = savedPrimaryENSName ?? fetchedPrimaryENSName const primaryENSName = savedPrimaryENSName ?? fetchedPrimaryENSName
const isPrimaryENSName = completedENSName === primaryENSName const isPrimaryENSName = completedENSName === primaryENSName
const showOwnedBy = !isFetchingPrimaryENSName && !isPrimaryENSName
const showAddress = searchResult.isRawName
const showOwnedBy = !isFetchingPrimaryENSName && !isPrimaryENSName && !showAddress
const showSecondLine = showAddress || showOwnedBy
const { data: avatar } = useENSAvatar(address) const { data: avatar } = useENSAvatar(address)
...@@ -55,11 +60,13 @@ export function SearchENSAddressItem({ ...@@ -55,11 +60,13 @@ export function SearchENSAddressItem({
variant="body1"> variant="body1">
{completedENSName || formattedAddress} {completedENSName || formattedAddress}
</Text> </Text>
{showOwnedBy ? ( {showSecondLine ? (
<Text color="$neutral2" ellipsizeMode="tail" numberOfLines={1} variant="subheading2"> <Text color="$neutral2" ellipsizeMode="tail" numberOfLines={1} variant="subheading2">
{t('Owned by {{owner}}', { {showOwnedBy &&
t('Owned by {{owner}}', {
owner: primaryENSName || formattedAddress, owner: primaryENSName || formattedAddress,
})} })}
{showAddress && formattedAddress}
</Text> </Text>
) : null} ) : null}
</Flex> </Flex>
......
import React from 'react'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex, Text } from 'ui/src'
import { imageSizes } from 'ui/src/theme'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { useENSAvatar, useENSName } from 'wallet/src/features/ens/api'
import { SearchContext } from 'wallet/src/features/search/SearchContext'
import { WalletByAddressSearchResult } from 'wallet/src/features/search/SearchResult'
import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses'
type SearchWalletByAddressItemProps = {
searchResult: WalletByAddressSearchResult
searchContext?: SearchContext
}
export function SearchWalletByAddressItem({
searchResult,
searchContext,
}: SearchWalletByAddressItemProps): JSX.Element {
const { address } = searchResult
const formattedAddress = sanitizeAddressText(shortenAddress(address))
const { data: ensName } = useENSName(address)
const { data: avatar } = useENSAvatar(address)
return (
<SearchWalletItemBase searchContext={searchContext} searchResult={searchResult}>
<Flex row alignItems="center" gap="$spacing12" px="$spacing8" py="$spacing12">
<AccountIcon address={address} avatarUri={avatar} size={imageSizes.image40} />
<Flex shrink>
<Text
ellipsizeMode="tail"
numberOfLines={1}
testID={`address-display/name/${ensName}`}
variant="body1">
{ensName || formattedAddress}
</Text>
{ensName ? (
<Text color="$neutral2" ellipsizeMode="tail" numberOfLines={1} variant="subheading2">
{formattedAddress}
</Text>
) : null}
</Flex>
</Flex>
</SearchWalletItemBase>
)
}
...@@ -12,7 +12,11 @@ import { TouchableArea } from 'ui/src' ...@@ -12,7 +12,11 @@ import { TouchableArea } from 'ui/src'
import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors'
import { SearchContext } from 'wallet/src/features/search/SearchContext' import { SearchContext } from 'wallet/src/features/search/SearchContext'
import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice'
import { SearchResultType, WalletSearchResult } from 'wallet/src/features/search/SearchResult' import {
extractDomain,
SearchResultType,
WalletSearchResult,
} from 'wallet/src/features/search/SearchResult'
type SearchWalletItemBaseProps = { type SearchWalletItemBaseProps = {
searchResult: WalletSearchResult searchResult: WalletSearchResult
...@@ -33,31 +37,37 @@ export function SearchWalletItemBase({ ...@@ -33,31 +37,37 @@ export function SearchWalletItemBase({
const onPress = (): void => { const onPress = (): void => {
navigate(address) navigate(address)
if (searchContext) { if (searchContext) {
const walletName =
type === SearchResultType.Unitag
? searchResult.unitag
: type === SearchResultType.ENSAddress
? searchResult.ensName
: undefined
sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, { sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, {
query: searchContext.query, query: searchContext.query,
name: name: walletName,
type === SearchResultType.Unitag ? searchResult.unitag : searchResult.ensName ?? address,
address, address,
type: 'address', type: 'address',
domain: walletName ? extractDomain(walletName, type) : undefined,
suggestion_count: searchContext.suggestionCount, suggestion_count: searchContext.suggestionCount,
position: searchContext.position, position: searchContext.position,
isHistory: searchContext.isHistory, isHistory: searchContext.isHistory,
}) })
} }
if (type === SearchResultType.Unitag) { if (type === SearchResultType.ENSAddress) {
dispatch( dispatch(
addToSearchHistory({ addToSearchHistory({
searchResult, searchResult: {
...searchResult,
primaryENSName: searchResult.primaryENSName,
},
}) })
) )
} else { } else {
dispatch( dispatch(
addToSearchHistory({ addToSearchHistory({
searchResult: { searchResult,
...searchResult,
primaryENSName: searchResult.primaryENSName ?? undefined,
},
}) })
) )
} }
......
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
......
...@@ -20,7 +20,8 @@ import { parseUnitagErrorCode } from 'wallet/src/features/unitags/utils' ...@@ -20,7 +20,8 @@ import { parseUnitagErrorCode } from 'wallet/src/features/unitags/utils'
import { useWalletSigners } from 'wallet/src/features/wallet/context' import { useWalletSigners } from 'wallet/src/features/wallet/context'
import { useAccount } from 'wallet/src/features/wallet/hooks' import { useAccount } from 'wallet/src/features/wallet/hooks'
import { useAppDispatch } from 'wallet/src/state' import { useAppDispatch } from 'wallet/src/state'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ElementName, ModalName, UnitagEventName } from 'wallet/src/telemetry/constants'
import { isIOS } from 'wallet/src/utils/platform' import { isIOS } from 'wallet/src/utils/platform'
export function ChangeUnitagModal({ export function ChangeUnitagModal({
...@@ -121,6 +122,7 @@ export function ChangeUnitagModal({ ...@@ -121,6 +122,7 @@ export function ChangeUnitagModal({
// If change succeeded, exit the modal and display a success message // If change succeeded, exit the modal and display a success message
if (changeResponse.success) { if (changeResponse.success) {
sendWalletAnalyticsEvent(UnitagEventName.UnitagChanged)
triggerRefetchUnitags() triggerRefetchUnitags()
dispatch( dispatch(
pushNotification({ pushNotification({
...@@ -230,7 +232,7 @@ export function ChangeUnitagModal({ ...@@ -230,7 +232,7 @@ export function ChangeUnitagModal({
returnKeyType="done" returnKeyType="done"
value={newUnitag} value={newUnitag}
width="100%" width="100%"
onChangeText={setNewUnitag} onChangeText={(text: string) => setNewUnitag(text.trim().toLowerCase())}
onSubmitEditing={onFinishEditing} onSubmitEditing={onFinishEditing}
/> />
<Flex position="absolute" right="$spacing20" top="$spacing20"> <Flex position="absolute" right="$spacing20" top="$spacing20">
...@@ -259,7 +261,7 @@ export function ChangeUnitagModal({ ...@@ -259,7 +261,7 @@ export function ChangeUnitagModal({
width="100%"> width="100%">
<Text color="$neutral2" variant="body3"> <Text color="$neutral2" variant="body3">
{t( {t(
'Once you change your username, you never claim it again. You can only change it 2 times.' 'Once you change your username, you can never claim it again. You can only change it 2 times.'
)} )}
</Text> </Text>
</Flex> </Flex>
......
...@@ -2,7 +2,7 @@ import React, { useState } from 'react' ...@@ -2,7 +2,7 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { selectPhotoFromLibrary } from 'src/components/unitags/AvatarSelection' import { selectPhotoFromLibrary } from 'src/components/unitags/AvatarSelection'
import { ChooseNftModal } from 'src/components/unitags/ChooseNftModal' import { ChooseNftModal } from 'src/components/unitags/ChooseNftModal'
import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src' import { Flex, Icons, Text, useSporeColors } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { ElementName, ModalName } from 'wallet/src/telemetry/constants'
...@@ -23,7 +23,6 @@ export const ChoosePhotoOptionsModal = ({ ...@@ -23,7 +23,6 @@ export const ChoosePhotoOptionsModal = ({
showRemoveOption, showRemoveOption,
}: ChoosePhotoOptionsProps): JSX.Element => { }: ChoosePhotoOptionsProps): JSX.Element => {
const colors = useSporeColors() const colors = useSporeColors()
const { t } = useTranslation()
const [showNftsList, setShowNftsList] = useState(false) const [showNftsList, setShowNftsList] = useState(false)
const onPressNftsList = async (): Promise<void> => { const onPressNftsList = async (): Promise<void> => {
...@@ -42,10 +41,11 @@ export const ChoosePhotoOptionsModal = ({ ...@@ -42,10 +41,11 @@ export const ChoosePhotoOptionsModal = ({
const onPressCameraRoll = async (): Promise<void> => { const onPressCameraRoll = async (): Promise<void> => {
const selectedPhoto = await selectPhotoFromLibrary() const selectedPhoto = await selectPhotoFromLibrary()
// Close needs to happen before setting the photo, otherwise the handler can get cut short
onClose()
if (selectedPhoto) { if (selectedPhoto) {
setPhotoUri(selectedPhoto) setPhotoUri(selectedPhoto)
} }
onClose()
} }
const options = [ const options = [
...@@ -88,16 +88,6 @@ export const ChoosePhotoOptionsModal = ({ ...@@ -88,16 +88,6 @@ export const ChoosePhotoOptionsModal = ({
</Flex> </Flex>
))} ))}
</Flex> </Flex>
<Flex centered row>
<Button
fill
backgroundColor="$surface1"
color="$accent1"
theme="secondary"
onPress={onClose}>
{t('Close')}
</Button>
</Flex>
</Flex> </Flex>
</BottomSheetModal> </BottomSheetModal>
{showNftsList && address && ( {showNftsList && address && (
......
...@@ -10,7 +10,8 @@ import { useUnitagUpdater } from 'wallet/src/features/unitags/context' ...@@ -10,7 +10,8 @@ import { useUnitagUpdater } from 'wallet/src/features/unitags/context'
import { useWalletSigners } from 'wallet/src/features/wallet/context' import { useWalletSigners } from 'wallet/src/features/wallet/context'
import { useAccount } from 'wallet/src/features/wallet/hooks' import { useAccount } from 'wallet/src/features/wallet/hooks'
import { useAppDispatch } from 'wallet/src/state' import { useAppDispatch } from 'wallet/src/state'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ElementName, ModalName, UnitagEventName } from 'wallet/src/telemetry/constants'
export function DeleteUnitagModal({ export function DeleteUnitagModal({
unitag, unitag,
...@@ -51,6 +52,7 @@ export function DeleteUnitagModal({ ...@@ -51,6 +52,7 @@ export function DeleteUnitagModal({
} }
if (deleteResponse?.success) { if (deleteResponse?.success) {
sendWalletAnalyticsEvent(UnitagEventName.UnitagRemoved)
triggerRefetchUnitags() triggerRefetchUnitags()
dispatch( dispatch(
pushNotification({ pushNotification({
......
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Keyboard, StyleProp, ViewStyle } from 'react-native'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { Button, Flex, Image, Text, useDeviceDimensions, useIsDarkMode } from 'ui/src' import { Screens } from 'src/screens/Screens'
import {
Flex,
Image,
Text,
TouchableArea,
useDeviceDimensions,
useIsDarkMode,
useSporeColors,
} from 'ui/src'
import { UNITAGS_BANNER_VERTICAL_DARK, UNITAGS_BANNER_VERTICAL_LIGHT } from 'ui/src/assets' import { UNITAGS_BANNER_VERTICAL_DARK, UNITAGS_BANNER_VERTICAL_LIGHT } from 'ui/src/assets'
import { borderRadii, iconSizes, spacing } from 'ui/src/theme'
import { setHasSkippedUnitagPrompt } from 'wallet/src/features/behaviorHistory/slice' import { setHasSkippedUnitagPrompt } from 'wallet/src/features/behaviorHistory/slice'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'wallet/src/features/unitags/constants'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ElementName, ModalName, UnitagEventName } from 'wallet/src/telemetry/constants'
const IMAGE_ASPECT_RATIO = 0.4 const IMAGE_ASPECT_RATIO = 0.42
const IMAGE_SCREEN_WIDTH_PROPORTION = 0.2 const IMAGE_SCREEN_WIDTH_PROPORTION = 0.18
const COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION = 0.16 const COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION = 0.15
export function UnitagBanner({ export function UnitagBanner({
address, address,
compact, compact,
entryPoint,
}: { }: {
address: Address address: Address
compact?: boolean compact?: boolean
entryPoint: Screens.Home | Screens.Settings
}): JSX.Element { }): JSX.Element {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { t } = useTranslation() const { t } = useTranslation()
const { fullWidth } = useDeviceDimensions() const { fullWidth } = useDeviceDimensions()
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const colors = useSporeColors()
const imageWidth = compact const imageWidth = compact
? COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth ? COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth
: IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth : IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth
const imageHeight = imageWidth / IMAGE_ASPECT_RATIO const imageHeight = imageWidth / IMAGE_ASPECT_RATIO
const analyticsEntryPoint = entryPoint === Screens.Home ? 'home' : 'settings'
const onPressClaimNow = (): void => { const onPressClaimNow = (): void => {
dispatch(openModal({ name: ModalName.UnitagsIntro, initialState: { address } })) Keyboard.dismiss()
sendWalletAnalyticsEvent(UnitagEventName.UnitagBannerActionTaken, {
action: 'claim',
entryPoint: analyticsEntryPoint,
})
dispatch(openModal({ name: ModalName.UnitagsIntro, initialState: { address, entryPoint } }))
} }
const onPressMaybeLater = (): void => { const onPressMaybeLater = (): void => {
sendWalletAnalyticsEvent(UnitagEventName.UnitagBannerActionTaken, {
action: 'dismiss',
entryPoint: analyticsEntryPoint,
})
dispatch(setHasSkippedUnitagPrompt(true)) dispatch(setHasSkippedUnitagPrompt(true))
} }
const baseButtonStyle: StyleProp<ViewStyle> = {
backgroundColor: colors.accent1.get(),
borderRadius: borderRadii.rounded20,
justifyContent: 'center',
height: iconSizes.icon36,
paddingVertical: spacing.spacing8,
paddingHorizontal: spacing.spacing12,
}
return ( return (
<Flex <Flex
grow grow
row row
alignContent="space-between"
backgroundColor={compact ? '$surface2' : '$background'} backgroundColor={compact ? '$surface2' : '$background'}
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={compact ? undefined : '$spacing1'} borderWidth={compact ? undefined : '$spacing1'}
mt="$spacing12" mt="$spacing12"
overflow="hidden" overflow="hidden"
pl="$spacing16"
py="$spacing16"
shadowColor="$neutral3" shadowColor="$neutral3"
shadowOpacity={0.4} shadowOpacity={0.4}
shadowRadius="$spacing4"> shadowRadius="$spacing4">
{compact ? ( {compact ? (
<Flex <Flex
fill fill
row
$short={{ mr: '$spacing32' }} $short={{ mr: '$spacing32' }}
gap="$spacing16"
justifyContent="space-between" justifyContent="space-between"
p="$spacing16"
onPress={onPressClaimNow}> onPress={onPressClaimNow}>
<Flex row gap="$none">
<Text color="$neutral2" variant="subheading2"> <Text color="$neutral2" variant="subheading2">
<Text color="$accent1" variant="buttonLabel3"> <Text color="$accent1" variant="buttonLabel3">
{t('Claim a username ')} {t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
</Text> </Text>
{t('to create a public username and customizable profile.')} {t(' and build out your customizable profile.')}
</Text> </Text>
</Flex> </Flex>
</Flex>
) : ( ) : (
<Flex fill gap="$spacing16" justifyContent="space-between" p="$spacing16"> <Flex fill gap="$spacing16" justifyContent="space-between">
<Flex gap="$spacing8"> <Flex gap="$spacing4">
<Text variant="subheading2">{t('Claim your Uniswap username')}</Text> <Text variant="subheading2">
{t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
</Text>
<Text color="$neutral2" variant="body3"> <Text color="$neutral2" variant="body3">
{t('Sharing your address and connecting with friends has never been easier.')} {t('Build a personalized web3 profile and easily share your address with friends.')}
</Text> </Text>
</Flex> </Flex>
<Flex row gap="$spacing8"> <Flex row gap="$spacing2">
<Button {/* TODO: replace with Button when it's extensible enough to accommodate designs */}
borderRadius="$rounded24" <TouchableArea
fontSize="$small" style={{
...baseButtonStyle,
backgroundColor: colors.accent1.get(),
}}
testID={ElementName.Confirm} testID={ElementName.Confirm}
theme="primary"
onPress={onPressClaimNow}> onPress={onPressClaimNow}>
<Text color="white" variant="buttonLabel4">
{t('Claim now')} {t('Claim now')}
</Button> </Text>
<Button </TouchableArea>
backgroundless <TouchableArea
borderRadius="$rounded24" style={{
color="$neutral3" ...baseButtonStyle,
fontSize="$small" backgroundColor: colors.transparent.get(),
}}
testID={ElementName.Cancel} testID={ElementName.Cancel}
theme="secondary"
onPress={onPressMaybeLater}> onPress={onPressMaybeLater}>
<Text color="$neutral3" variant="buttonLabel4">
{t('Maybe later')} {t('Maybe later')}
</Button> </Text>
</TouchableArea>
</Flex> </Flex>
</Flex> </Flex>
)} )}
<Flex justifyContent={compact ? 'flex-start' : 'center'} width={imageWidth}> <Flex mr={compact ? -(imageWidth / 6) : -(imageWidth / 12)} width={imageWidth}>
<Image <Image
alignSelf="center" alignSelf="center"
position="absolute" position="absolute"
...@@ -105,6 +151,7 @@ export function UnitagBanner({ ...@@ -105,6 +151,7 @@ export function UnitagBanner({
height: imageHeight, height: imageHeight,
uri: isDarkMode ? UNITAGS_BANNER_VERTICAL_DARK : UNITAGS_BANNER_VERTICAL_LIGHT, uri: isDarkMode ? UNITAGS_BANNER_VERTICAL_DARK : UNITAGS_BANNER_VERTICAL_LIGHT,
}} }}
top={compact ? -(imageHeight * 0.19) : -(imageHeight * 0.22)}
/> />
</Flex> </Flex>
</Flex> </Flex>
......
...@@ -2,21 +2,23 @@ import React from 'react' ...@@ -2,21 +2,23 @@ import React from 'react'
import { Flex, useSporeColors } from 'ui/src' import { Flex, useSporeColors } from 'ui/src'
import { isSVGUri } from 'utilities/src/format/urls' import { isSVGUri } from 'utilities/src/format/urls'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { useENSAvatar } from 'wallet/src/features/ens/api'
import { ImageUri } from 'wallet/src/features/images/ImageUri' import { ImageUri } from 'wallet/src/features/images/ImageUri'
import { RemoteSvg } from 'wallet/src/features/images/RemoteSvg' import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
export function UnitagProfilePicture({ export function UnitagProfilePicture({
address, address,
profilePictureUri, unitagAvatarUri,
size, size,
}: { }: {
address: Address address: Address
size: number size: number
profilePictureUri?: string unitagAvatarUri?: string
}): JSX.Element { }): JSX.Element {
const colors = useSporeColors() const colors = useSporeColors()
const { data: ensAvatar } = useENSAvatar(address)
return profilePictureUri ? ( return unitagAvatarUri ? (
<Flex <Flex
shrink shrink
backgroundColor="$surface1" backgroundColor="$surface1"
...@@ -27,22 +29,23 @@ export function UnitagProfilePicture({ ...@@ -27,22 +29,23 @@ export function UnitagProfilePicture({
shadowOpacity={0.4} shadowOpacity={0.4}
shadowRadius="$spacing4" shadowRadius="$spacing4"
width={size}> width={size}>
{isSVGUri(profilePictureUri) ? ( {isSVGUri(unitagAvatarUri) ? (
<RemoteSvg <RemoteImage
backgroundColor={colors.surface1.val} backgroundColor={colors.surface1.val}
height={size} height={size}
imageHttpUrl={profilePictureUri} uri={unitagAvatarUri}
width={size} width={size}
/> />
) : ( ) : (
<ImageUri resizeMode="cover" uri={profilePictureUri} /> <ImageUri resizeMode="cover" uri={unitagAvatarUri} />
)} )}
</Flex> </Flex>
) : ( ) : (
<AccountIcon <AccountIcon
address={address} address={address}
avatarUri={profilePictureUri} avatarUri={ensAvatar}
showBackground={true} showBackground={true}
showBorder={true}
size={size} size={size}
/> />
) )
......
...@@ -16,15 +16,15 @@ export const UnitagWithProfilePicture = ({ ...@@ -16,15 +16,15 @@ export const UnitagWithProfilePicture = ({
<Flex centered gap={-spacing.spacing24}> <Flex centered gap={-spacing.spacing24}>
<UnitagProfilePicture <UnitagProfilePicture
address={address} address={address}
profilePictureUri={profilePictureUri}
size={imageSizes.image100} size={imageSizes.image100}
unitagAvatarUri={profilePictureUri}
/> />
<Flex <Flex
row row
backgroundColor="$surface1" backgroundColor="$surface1"
borderRadius="$rounded32" borderRadius="$rounded32"
px="$spacing12" px="$spacing12"
py="$spacing8" py="$spacing16"
shadowColor="$neutral3" shadowColor="$neutral3"
shadowOpacity={0.4} shadowOpacity={0.4}
shadowRadius="$spacing4" shadowRadius="$spacing4"
......
import { SharedEventName } from '@uniswap/analytics-events'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import 'react-native-reanimated' import 'react-native-reanimated'
...@@ -11,26 +12,35 @@ import { Button, Flex, GeneratedIcon, Icons, Image, Text, useIsDarkMode } from ' ...@@ -11,26 +12,35 @@ import { Button, Flex, GeneratedIcon, Icons, Image, Text, useIsDarkMode } from '
import { UNITAGS_INTRO_BANNER_DARK, UNITAGS_INTRO_BANNER_LIGHT } from 'ui/src/assets' import { UNITAGS_INTRO_BANNER_DARK, UNITAGS_INTRO_BANNER_LIGHT } from 'ui/src/assets'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
export function UnitagsIntroModal(): JSX.Element { export function UnitagsIntroModal(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const appDispatch = useAppDispatch() const appDispatch = useAppDispatch()
const address = useAppSelector(selectModalState(ModalName.UnitagsIntro)).initialState?.address const modalState = useAppSelector(selectModalState(ModalName.UnitagsIntro)).initialState
const address = modalState?.address
const entryPoint = modalState?.entryPoint
const onClose = (): void => { const onClose = (): void => {
appDispatch(closeModal({ name: ModalName.UnitagsIntro })) appDispatch(closeModal({ name: ModalName.UnitagsIntro }))
} }
const onPressClaimOneNow = (): void => { const onPressClaimOneNow = (): void => {
if (!entryPoint) {
throw new Error('Missing entry point in UnitagsIntroModal')
}
navigate(Screens.UnitagStack, { navigate(Screens.UnitagStack, {
screen: UnitagScreens.ClaimUnitag, screen: UnitagScreens.ClaimUnitag,
params: { params: {
entryPoint: Screens.Home, entryPoint,
address, address,
}, },
}) })
if (address) {
sendWalletAnalyticsEvent(SharedEventName.TERMS_OF_SERVICE_ACCEPTED, { address })
}
onClose() onClose()
} }
...@@ -38,10 +48,10 @@ export function UnitagsIntroModal(): JSX.Element { ...@@ -38,10 +48,10 @@ export function UnitagsIntroModal(): JSX.Element {
<BottomSheetModal name={ModalName.UnitagsIntro} onClose={onClose}> <BottomSheetModal name={ModalName.UnitagsIntro} onClose={onClose}>
<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="body3">
{t( {t(
'Say goodbye to 0x addresses. Usernames are readable addresses that make it easier to receive crypto and connect with friends.' 'Say goodbye to 0x addresses. Usernames are readable names that make it easier to send and receive crypto.'
)} )}
</Text> </Text>
</Flex> </Flex>
...@@ -57,7 +67,7 @@ export function UnitagsIntroModal(): JSX.Element { ...@@ -57,7 +67,7 @@ export function UnitagsIntroModal(): JSX.Element {
<BodyItem Icon={Icons.Ticket} title={t('Free to claim')} /> <BodyItem Icon={Icons.Ticket} title={t('Free to claim')} />
<BodyItem Icon={Icons.Lightning} title={t('Powered by ENS subdomains')} /> <BodyItem Icon={Icons.Lightning} title={t('Powered by ENS subdomains')} />
</Flex> </Flex>
<Flex gap="$spacing8" mt="$spacing16"> <Flex gap="$spacing8">
<Button size="medium" theme="primary" onPress={onPressClaimOneNow}> <Button size="medium" theme="primary" onPress={onPressClaimOneNow}>
{t('Continue')} {t('Continue')}
</Button> </Button>
......
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>
)
}
...@@ -17,7 +17,7 @@ import { ...@@ -17,7 +17,7 @@ import {
EditAccountAction, EditAccountAction,
editAccountActions, editAccountActions,
} from 'wallet/src/features/wallet/accounts/editAccountSaga' } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types'
import { useAccount } from 'wallet/src/features/wallet/hooks' import { useAccount } from 'wallet/src/features/wallet/hooks'
import { isAndroid } from 'wallet/src/utils/platform' import { isAndroid } from 'wallet/src/utils/platform'
...@@ -44,6 +44,11 @@ export function CloudBackupProcessingAnimation({ ...@@ -44,6 +44,11 @@ export function CloudBackupProcessingAnimation({
const colors = useSporeColors() const colors = useSporeColors()
const account = useAccount(accountAddress) const account = useAccount(accountAddress)
if (account.type !== AccountType.SignerMnemonic) {
throw new Error('Account is not mnemonic account')
}
const mnemonicId = account?.mnemonicId
const [processing, doneProcessing] = useReducer(() => false, true) const [processing, doneProcessing] = useReducer(() => false, true)
// Handle finished backing up to Cloud // Handle finished backing up to Cloud
...@@ -60,7 +65,7 @@ export function CloudBackupProcessingAnimation({ ...@@ -60,7 +65,7 @@ export function CloudBackupProcessingAnimation({
const backup = useCallback(async () => { const backup = useCallback(async () => {
try { try {
// Ensure processing state is shown for at least 1s // Ensure processing state is shown for at least 1s
await promiseMinDelay(backupMnemonicToCloudStorage(accountAddress, password), ONE_SECOND_MS) await promiseMinDelay(backupMnemonicToCloudStorage(mnemonicId, password), ONE_SECOND_MS)
dispatch( dispatch(
editAccountActions.trigger({ editAccountActions.trigger({
...@@ -92,7 +97,7 @@ export function CloudBackupProcessingAnimation({ ...@@ -92,7 +97,7 @@ export function CloudBackupProcessingAnimation({
] ]
) )
} }
}, [accountAddress, dispatch, onErrorPress, password, t]) }, [accountAddress, dispatch, mnemonicId, onErrorPress, password, t])
/** /**
* Delays cloud backup to avoid android oauth consent screen blocking navigation transition * Delays cloud backup to avoid android oauth consent screen blocking navigation transition
......
...@@ -40,10 +40,14 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme ...@@ -40,10 +40,14 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme
}, [address]) }, [address])
const onReportProfile = useCallback(async () => { const onReportProfile = useCallback(async () => {
openUri(uniswapUrls.reportUnitagUrl).catch((e) => const params = new URLSearchParams()
params.append('tf_11041337007757', address) // Wallet Address
params.append('tf_7005922218125', 'report_unitag') // Report Type Dropdown
const prefilledRequestUrl = uniswapUrls.helpRequestUrl + '?' + params.toString()
openUri(prefilledRequestUrl).catch((e) =>
logger.error(e, { tags: { file: 'ProfileContextMenu', function: 'reportProfileLink' } }) logger.error(e, { tags: { file: 'ProfileContextMenu', function: 'reportProfileLink' } })
) )
}, []) }, [address])
const onPressShare = useCallback(async () => { const onPressShare = useCallback(async () => {
if (!address) { if (!address) {
......
...@@ -22,11 +22,14 @@ import { ...@@ -22,11 +22,14 @@ import {
useIsDarkMode, useIsDarkMode,
useSporeColors, useSporeColors,
useUniconColors, useUniconColors,
useUniconV2Colors,
} from 'ui/src' } from 'ui/src'
import { ENS_LOGO } from 'ui/src/assets' import { ENS_LOGO } from 'ui/src/assets'
import { iconSizes, imageSizes, spacing } from 'ui/src/theme' import { iconSizes, imageSizes, spacing } from 'ui/src/theme'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useENSDescription, useENSName, useENSTwitterUsername } from 'wallet/src/features/ens/api' import { useENSDescription, useENSName, useENSTwitterUsername } from 'wallet/src/features/ens/api'
import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { useFeatureFlag } from 'wallet/src/features/experiments/hooks'
import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors'
import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types'
import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks'
...@@ -67,21 +70,29 @@ export const ProfileHeader = memo(function ProfileHeader({ ...@@ -67,21 +70,29 @@ export const ProfileHeader = memo(function ProfileHeader({
const showENSName = primaryENSName && primaryENSName !== displayName?.name const showENSName = primaryENSName && primaryENSName !== displayName?.name
const { colors: avatarColors } = useExtractedColors(avatar) const { colors: avatarColors } = useExtractedColors(avatar)
const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2)
const hasAvatar = !!avatar && !avatarLoading const hasAvatar = !!avatar && !avatarLoading
// Unicon colors // Unicon colors
const { gradientStart: uniconGradientStart, gradientEnd: uniconGradientEnd } = const { gradientStart: uniconGradientStart, gradientEnd: uniconGradientEnd } =
useUniconColors(address) useUniconColors(address)
// UniconV2 colors
const { color } = useUniconV2Colors(address)
// Wait for avatar, then render avatar extracted colors or unicon colors if no avatar // Wait for avatar, then render avatar extracted colors or unicon colors if no avatar
const fixedGradientColors = useMemo(() => { const fixedGradientColors: [string, string] = useMemo(() => {
if (avatarLoading || (hasAvatar && !avatarColors)) { if (avatarLoading || (hasAvatar && !avatarColors)) {
return [colors.surface1.val, colors.surface1.val] return [colors.surface1.val, colors.surface1.val]
} }
if (hasAvatar && avatarColors && avatarColors.base) { if (hasAvatar && avatarColors && avatarColors.base) {
return [avatarColors.base, avatarColors.base] return [avatarColors.base, avatarColors.base]
} }
return [uniconGradientStart, uniconGradientEnd] return [
isUniconsV2Enabled ? color : uniconGradientStart,
isUniconsV2Enabled ? color : uniconGradientEnd,
]
}, [ }, [
avatarColors, avatarColors,
hasAvatar, hasAvatar,
...@@ -89,6 +100,8 @@ export const ProfileHeader = memo(function ProfileHeader({ ...@@ -89,6 +100,8 @@ export const ProfileHeader = memo(function ProfileHeader({
colors.surface1, colors.surface1,
uniconGradientEnd, uniconGradientEnd,
uniconGradientStart, uniconGradientStart,
color,
isUniconsV2Enabled,
]) ])
const onPressFavorite = useToggleWatchedWalletCallback(address) const onPressFavorite = useToggleWatchedWalletCallback(address)
...@@ -153,7 +166,11 @@ export const ProfileHeader = memo(function ProfileHeader({ ...@@ -153,7 +166,11 @@ export const ProfileHeader = memo(function ProfileHeader({
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
/> />
</Flex> </Flex>
{hasAvatar && avatarColors?.primary ? <HeaderRadial color={avatarColors.primary} /> : null} {hasAvatar && avatarColors?.primary ? (
<HeaderRadial color={avatarColors.primary} />
) : (
<HeaderRadial color={isUniconsV2Enabled ? color : uniconGradientStart} />
)}
</AnimatedFlex> </AnimatedFlex>
{/* header row */} {/* header row */}
...@@ -270,9 +287,13 @@ export const ProfileHeader = memo(function ProfileHeader({ ...@@ -270,9 +287,13 @@ export const ProfileHeader = memo(function ProfileHeader({
export const HeaderRadial = memo(function HeaderRadial({ export const HeaderRadial = memo(function HeaderRadial({
color, color,
borderRadius, borderRadius,
minOpacity,
maxOpacity,
}: { }: {
color: string color: string
borderRadius?: number borderRadius?: number
minOpacity?: number
maxOpacity?: number
}): JSX.Element { }): JSX.Element {
return ( return (
<Svg height="100%" width="100%"> <Svg height="100%" width="100%">
...@@ -281,8 +302,8 @@ export const HeaderRadial = memo(function HeaderRadial({ ...@@ -281,8 +302,8 @@ export const HeaderRadial = memo(function HeaderRadial({
<Rect height="100%" rx={borderRadius} width="100%" /> <Rect height="100%" rx={borderRadius} width="100%" />
</ClipPath> </ClipPath>
<RadialGradient cy="-0.1" id="background" rx="0.8" ry="1.1"> <RadialGradient cy="-0.1" id="background" rx="0.8" ry="1.1">
<Stop offset="0" stopColor={color} stopOpacity="0.6" /> <Stop offset="0" stopColor={color} stopOpacity={maxOpacity ?? '0.6'} />
<Stop offset="1" stopColor={color} stopOpacity="0" /> <Stop offset="1" stopColor={color} stopOpacity={minOpacity ?? '0'} />
</RadialGradient> </RadialGradient>
</Defs> </Defs>
<Rect <Rect
......
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
}
import React, { memo, useMemo } from 'react'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { TokenFiatOnRampList } from 'src/components/TokenSelector/TokenFiatOnRampList'
import Trace from 'src/components/Trace/Trace'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { AnimatedFlex } from 'ui/src'
import { SectionName } from 'wallet/src/telemetry/constants'
import { useAllCommonBaseCurrencies } from 'wallet/src/components/TokenSelector/hooks'
import { CurrencyInfo, GqlResult } from 'wallet/src/features/dataApi/types'
import { useFiatOnRampAggregatorSupportedTokensQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORSupportedToken } from 'wallet/src/features/fiatOnRamp/types'
import { ElementName } from 'wallet/src/telemetry/constants'
interface Props {
onBack: () => void
onSelectCurrency: (currency: FiatOnRampCurrency) => void
sourceCurrencyCode: string
countryCode: string
}
const findTokenOptionForFiatOnRampToken = (
commonBaseCurrencies: CurrencyInfo[] | undefined,
fiatOnRampToken: FORSupportedToken
): Maybe<CurrencyInfo> => {
return (commonBaseCurrencies || []).find(
(item) =>
item &&
fiatOnRampToken.cryptoCurrencyCode.toLowerCase() === item.currency.symbol?.toLowerCase() &&
fiatOnRampToken.chainId === item.currency.chainId.toString()
)
}
function useFiatOnRampTokenList(
supportedTokens: FORSupportedToken[] | undefined
): GqlResult<FiatOnRampCurrency[]> {
const {
data: commonBaseCurrencies,
error: commonBaseCurrenciesError,
loading: commonBaseCurrenciesLoading,
refetch: refetchCommonBaseCurrencies,
} = useAllCommonBaseCurrencies()
const data = useMemo(
() =>
(supportedTokens || [])
.map((fiatOnRampToken) => ({
currencyInfo: findTokenOptionForFiatOnRampToken(commonBaseCurrencies, fiatOnRampToken),
}))
.filter((item) => !!item.currencyInfo),
[commonBaseCurrencies, supportedTokens]
)
return {
data,
loading: commonBaseCurrenciesLoading,
error: commonBaseCurrenciesError,
refetch: refetchCommonBaseCurrencies,
}
}
function _FiatOnRampAggregatorTokenSelector({
onSelectCurrency,
onBack,
sourceCurrencyCode,
countryCode,
}: Props): JSX.Element {
const {
data: supportedTokensResponse,
isLoading: supportedTokensLoading,
error: supportedTokensQueryError,
refetch: supportedTokensQueryRefetch,
} = useFiatOnRampAggregatorSupportedTokensQuery({ fiatCurrency: sourceCurrencyCode, countryCode })
const {
data: tokenList,
loading: tokenListLoading,
error: tokenListError,
refetch: tokenListRefetch,
} = useFiatOnRampTokenList(supportedTokensResponse?.supportedTokens)
const loading = supportedTokensLoading || tokenListLoading
const error = Boolean(supportedTokensQueryError || tokenListError)
const onRetry = async (): Promise<void> => {
if (supportedTokensQueryError) {
await supportedTokensQueryRefetch?.()
}
if (tokenListError) {
tokenListRefetch?.()
}
}
return (
<Trace
logImpression
element={ElementName.FiatOnRampAggregatorTokenSelector}
section={SectionName.TokenSelector}>
<AnimatedFlex
entering={FadeIn}
exiting={FadeOut}
gap="$spacing12"
overflow="hidden"
px="$spacing16"
width="100%">
<TokenFiatOnRampList
error={error}
list={tokenList}
loading={loading}
onBack={onBack}
onRetry={onRetry}
onSelectCurrency={onSelectCurrency}
/>
</AnimatedFlex>
</Trace>
)
}
export const FiatOnRampAggregatorTokenSelector = memo(_FiatOnRampAggregatorTokenSelector)
...@@ -127,7 +127,7 @@ export function FiatOnRampAmountSection({ ...@@ -127,7 +127,7 @@ export function FiatOnRampAmountSection({
const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2) const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2)
return ( return (
<Flex gap="$spacing16" onLayout={onInputPanelLayout}> <Flex onLayout={onInputPanelLayout}>
<Flex <Flex
grow grow
alignItems="center" alignItems="center"
...@@ -137,7 +137,7 @@ export function FiatOnRampAmountSection({ ...@@ -137,7 +137,7 @@ export function FiatOnRampAmountSection({
<AnimatedFlex <AnimatedFlex
height={spacing.spacing24} height={spacing.spacing24}
/* We want to reserve the space here, so when error occurs - layout does not jump */ /* We want to reserve the space here, so when error occurs - layout does not jump */
mt="$spacing48"> mt={appFiatCurrencySupported ? '$spacing48' : '$spacing24'}>
{debouncedErrorText && errorColor && ( {debouncedErrorText && errorColor && (
<Text color={errorColor} textAlign="center" variant="buttonLabel4"> <Text color={errorColor} textAlign="center" variant="buttonLabel4">
{debouncedErrorText} {debouncedErrorText}
......
...@@ -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,
}, },
......
...@@ -20,12 +20,12 @@ import { fonts, iconSizes, spacing } from 'ui/src/theme' ...@@ -20,12 +20,12 @@ import { fonts, iconSizes, spacing } from 'ui/src/theme'
import { bubbleToTop } from 'utilities/src/primitives/array' import { bubbleToTop } from 'utilities/src/primitives/array'
import { useDebounce } from 'utilities/src/time/timing' import { useDebounce } from 'utilities/src/time/timing'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks'
import { useFiatOnRampAggregatorCountryListQuery } from 'wallet/src/features/fiatOnRamp/api' import { useFiatOnRampAggregatorCountryListQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORSupportedCountry } from 'wallet/src/features/fiatOnRamp/types' import { FORSupportedCountry } from 'wallet/src/features/fiatOnRamp/types'
import { getCountryFlagSvgUrl } from 'wallet/src/features/fiatOnRamp/utils' import { getCountryFlagSvgUrl } from 'wallet/src/features/fiatOnRamp/utils'
import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput' import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
import { isIOS } from 'wallet/src/utils/platform'
const ICON_SIZE = 32 // design prefers a custom value here const ICON_SIZE = 32 // design prefers a custom value here
...@@ -95,7 +95,7 @@ function CountrySelectorContent({ ...@@ -95,7 +95,7 @@ function CountrySelectorContent({
) )
return ( return (
<Flex grow gap="$spacing16" pb={isIOS ? '$spacing16' : '$none'} px="$spacing16"> <Flex grow gap="$spacing16" px="$spacing16">
<Text color="$neutral1" mt="$spacing2" textAlign="center" variant="subheading1"> <Text color="$neutral1" mt="$spacing2" textAlign="center" variant="subheading1">
{t('Select your region')} {t('Select your region')}
</Text> </Text>
...@@ -106,7 +106,6 @@ function CountrySelectorContent({ ...@@ -106,7 +106,6 @@ function CountrySelectorContent({
value={searchText} value={searchText}
onChangeText={setSearchText} onChangeText={setSearchText}
/> />
{true && (
<Flex grow> <Flex grow>
<AnimatedFlex grow entering={FadeIn} exiting={FadeOut}> <AnimatedFlex grow entering={FadeIn} exiting={FadeOut}>
{isLoading ? ( {isLoading ? (
...@@ -117,6 +116,7 @@ function CountrySelectorContent({ ...@@ -117,6 +116,7 @@ function CountrySelectorContent({
bounces={true} bounces={true}
contentContainerStyle={{ paddingBottom: insets.bottom + spacing.spacing12 }} contentContainerStyle={{ paddingBottom: insets.bottom + spacing.spacing12 }}
data={filteredData} data={filteredData}
focusHook={useBottomSheetFocusHook}
keyExtractor={key} keyExtractor={key}
keyboardDismissMode="on-drag" keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always" keyboardShouldPersistTaps="always"
...@@ -127,7 +127,6 @@ function CountrySelectorContent({ ...@@ -127,7 +127,6 @@ function CountrySelectorContent({
)} )}
</AnimatedFlex> </AnimatedFlex>
</Flex> </Flex>
)}
</Flex> </Flex>
) )
} }
...@@ -165,6 +164,7 @@ export function FiatOnRampCountryListModal({ ...@@ -165,6 +164,7 @@ export function FiatOnRampCountryListModal({
fullScreen fullScreen
hideKeyboardOnDismiss hideKeyboardOnDismiss
hideKeyboardOnSwipeDown hideKeyboardOnSwipeDown
renderBehindBottomInset
backgroundColor={colors.surface1.get()} backgroundColor={colors.surface1.get()}
name={ModalName.FiatOnRampCountryList} name={ModalName.FiatOnRampCountryList}
snapPoints={FOR_MODAL_SNAP_POINTS} snapPoints={FOR_MODAL_SNAP_POINTS}
......
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, TextInput } from 'react-native' import { StyleSheet, TextInput } from 'react-native'
import { import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated'
FadeIn,
FadeOut,
FadeOutDown,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated'
import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks' import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks'
import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton' import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton'
import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection' import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection'
...@@ -16,14 +9,13 @@ import { ...@@ -16,14 +9,13 @@ import {
FiatOnRampConnectingView, FiatOnRampConnectingView,
SERVICE_PROVIDER_ICON_SIZE, SERVICE_PROVIDER_ICON_SIZE,
} from 'src/features/fiatOnRamp/FiatOnRampConnecting' } from 'src/features/fiatOnRamp/FiatOnRampConnecting'
import { FiatOnRampTokenSelector } from 'src/features/fiatOnRamp/FiatOnRampTokenSelector' import { useMoonpayFiatOnRamp, useMoonpaySupportedTokens } from 'src/features/fiatOnRamp/hooks'
import { useMoonpayFiatOnRamp } from 'src/features/fiatOnRamp/hooks'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { closeModal } from 'src/features/modals/modalSlice' import { closeModal } from 'src/features/modals/modalSlice'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants' import { MobileEventName } from 'src/features/telemetry/constants'
import { MobileEventProperties } from 'src/features/telemetry/types' import { MobileEventProperties } from 'src/features/telemetry/types'
import { AnimatedFlex, Flex, Text, useDeviceDimensions, useSporeColors } from 'ui/src' import { AnimatedFlex, Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src'
import MoonpayLogo from 'ui/src/assets/logos/svg/moonpay.svg' import MoonpayLogo from 'ui/src/assets/logos/svg/moonpay.svg'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { useTimeout } from 'utilities/src/time/timing' import { useTimeout } from 'utilities/src/time/timing'
...@@ -31,15 +23,16 @@ import { TextInputProps } from 'wallet/src/components/input/TextInput' ...@@ -31,15 +23,16 @@ import { TextInputProps } from 'wallet/src/components/input/TextInput'
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 { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { HandleBar } from 'wallet/src/components/modals/HandleBar'
import { getNativeAddress } from 'wallet/src/constants/addresses' import { getNativeAddress } from 'wallet/src/constants/addresses'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks' import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo'
import { ANIMATE_SPRING_CONFIG } from 'wallet/src/features/transactions/utils'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
import { buildCurrencyId } from 'wallet/src/utils/currencyId' import { buildCurrencyId } from 'wallet/src/utils/currencyId'
import { openUri } from 'wallet/src/utils/linking' import { openUri } from 'wallet/src/utils/linking'
import { FiatOnRampTokenSelectorModal } from './FiatOnRampTokenSelector'
const MOONPAY_UNSUPPORTED_REGION_HELP_URL = const MOONPAY_UNSUPPORTED_REGION_HELP_URL =
'https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-' 'https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-'
...@@ -59,7 +52,9 @@ export function FiatOnRampModal(): JSX.Element { ...@@ -59,7 +52,9 @@ export function FiatOnRampModal(): JSX.Element {
return ( return (
<BottomSheetModal <BottomSheetModal
fullScreen fullScreen
hideHandlebar
hideKeyboardOnDismiss hideKeyboardOnDismiss
renderBehindTopInset
backgroundColor={colors.surface1.get()} backgroundColor={colors.surface1.get()}
name={ModalName.FiatOnRamp} name={ModalName.FiatOnRamp}
onClose={onClose}> onClose={onClose}>
...@@ -70,7 +65,6 @@ export function FiatOnRampModal(): JSX.Element { ...@@ -70,7 +65,6 @@ export function FiatOnRampModal(): JSX.Element {
function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element { function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { fullWidth } = useDeviceDimensions()
const { formatNumberOrString } = useLocalizationContext() const { formatNumberOrString } = useLocalizationContext()
const inputRef = useRef<TextInput>(null) const inputRef = useRef<TextInput>(null)
...@@ -170,25 +164,21 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element { ...@@ -170,25 +164,21 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
} }
}, [showNativeKeyboard, eligible, showTokenSelector]) }, [showNativeKeyboard, eligible, showTokenSelector])
const hideInnerContentRouter = showTokenSelector const selectTokenLoading = quoteCurrencyAmountLoading && !errorText && !!value
const screenXOffset = useSharedValue(hideInnerContentRouter ? -fullWidth : 0)
useEffect(() => {
const screenOffset = showTokenSelector ? 1 : 0
screenXOffset.value = withSpring(-(fullWidth * screenOffset), ANIMATE_SPRING_CONFIG)
}, [screenXOffset, showTokenSelector, fullWidth])
const wrapperStyle = useAnimatedStyle(() => ({ const {
transform: [{ translateX: screenXOffset.value }], list: supportedTokensList,
})) loading: supportedTokensLoading,
error: supportedTokensError,
refetch: supportedTokensRefetch,
} = useMoonpaySupportedTokens()
// we only show loading when there is no error text and value is not empty const insets = useDeviceInsets()
const selectTokenLoading = quoteCurrencyAmountLoading && !errorText && !!value
return ( return (
<> <Flex grow pt={showConnectingToMoonpayScreen ? undefined : insets.top}>
{!showConnectingToMoonpayScreen && ( {!showConnectingToMoonpayScreen && (
<AnimatedFlex row height="100%" pb="$spacing12" style={wrapperStyle}> <AnimatedFlex row height="100%" pb="$spacing12">
{isSheetReady && ( {isSheetReady && (
<AnimatedFlex <AnimatedFlex
entering={FadeIn} entering={FadeIn}
...@@ -197,6 +187,7 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element { ...@@ -197,6 +187,7 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
pb="$spacing16" pb="$spacing16"
px="$spacing24" px="$spacing24"
width="100%"> width="100%">
<HandleBar backgroundColor="none" />
<Text variant="subheading1">{t('Buy')}</Text> <Text variant="subheading1">{t('Buy')}</Text>
<FiatOnRampAmountSection <FiatOnRampAmountSection
appFiatCurrencySupported={appFiatCurrencySupportedInMoonpay} appFiatCurrencySupported={appFiatCurrencySupportedInMoonpay}
...@@ -259,8 +250,12 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element { ...@@ -259,8 +250,12 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
</AnimatedFlex> </AnimatedFlex>
)} )}
{showTokenSelector && ( {showTokenSelector && (
<FiatOnRampTokenSelector <FiatOnRampTokenSelectorModal
onBack={(): void => setShowTokenSelector(false)} error={supportedTokensError}
list={supportedTokensList}
loading={supportedTokensLoading}
onClose={(): void => setShowTokenSelector(false)}
onRetry={supportedTokensRefetch}
onSelectCurrency={(newCurrency: FiatOnRampCurrency): void => { onSelectCurrency={(newCurrency: FiatOnRampCurrency): void => {
setCurrency(newCurrency) setCurrency(newCurrency)
setShowTokenSelector(false) setShowTokenSelector(false)
...@@ -291,7 +286,7 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element { ...@@ -291,7 +286,7 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
serviceProviderName="MoonPay" serviceProviderName="MoonPay"
/> />
)} )}
</> </Flex>
) )
} }
......
import React, { memo, useMemo } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { TokenFiatOnRampList } from 'src/components/TokenSelector/TokenFiatOnRampList' import { TokenFiatOnRampList } from 'src/components/TokenSelector/TokenFiatOnRampList'
import Trace from 'src/components/Trace/Trace' import Trace from 'src/components/Trace/Trace'
import { useFiatOnRampSupportedTokens } from 'src/features/fiatOnRamp/hooks' import { FOR_MODAL_SNAP_POINTS } from 'src/features/fiatOnRamp/constants'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { AnimatedFlex } from 'ui/src' import { AnimatedFlex, Flex, Text, useSporeColors } from 'ui/src'
import { SectionName } from 'wallet/src/telemetry/constants' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ElementName, ModalName, SectionName } from 'wallet/src/telemetry/constants'
import { useAllCommonBaseCurrencies } from 'wallet/src/components/TokenSelector/hooks'
import { fromMoonpayNetwork } from 'wallet/src/features/chains/utils'
import { CurrencyInfo, GqlResult } from 'wallet/src/features/dataApi/types'
import { MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types'
import { ElementName } from 'wallet/src/telemetry/constants'
interface Props { interface Props {
onBack: () => void
onSelectCurrency: (currency: FiatOnRampCurrency) => void onSelectCurrency: (currency: FiatOnRampCurrency) => void
onRetry: () => void
onClose: () => void
error: boolean
loading: boolean
list: FiatOnRampCurrency[] | undefined
} }
const findTokenOptionForMoonpayCurrency = ( export function FiatOnRampTokenSelectorModal({
commonBaseCurrencies: CurrencyInfo[] | undefined, error,
moonpayCurrency: MoonpayCurrency list,
): Maybe<CurrencyInfo> => { loading,
return (commonBaseCurrencies || []).find((item) => { onClose,
const [code, network] = moonpayCurrency.code.split('_') ?? [undefined, undefined] onRetry,
const chainId = fromMoonpayNetwork(network) onSelectCurrency,
return ( }: { onClose: () => void } & Props): JSX.Element {
item && const { t } = useTranslation()
code && const colors = useSporeColors()
code === item.currency.symbol?.toLowerCase() &&
chainId === item.currency.chainId
)
})
}
function useFiatOnRampTokenList(
supportedTokens: MoonpayCurrency[] | undefined
): GqlResult<FiatOnRampCurrency[]> {
const {
data: commonBaseCurrencies,
error: commonBaseCurrenciesError,
loading: commonBaseCurrenciesLoading,
refetch: refetchCommonBaseCurrencies,
} = useAllCommonBaseCurrencies()
const data = useMemo(
() =>
(supportedTokens || [])
.map((moonpayCurrency) => ({
currencyInfo: findTokenOptionForMoonpayCurrency(commonBaseCurrencies, moonpayCurrency),
moonpayCurrencyCode: moonpayCurrency.code,
}))
.filter((item) => !!item.currencyInfo),
[commonBaseCurrencies, supportedTokens]
)
return useMemo(
() => ({
data,
loading: commonBaseCurrenciesLoading,
error: commonBaseCurrenciesError,
refetch: refetchCommonBaseCurrencies,
}),
[commonBaseCurrenciesError, commonBaseCurrenciesLoading, data, refetchCommonBaseCurrencies]
)
}
function _FiatOnRampTokenSelector({ onSelectCurrency, onBack }: Props): JSX.Element {
const {
data: supportedTokens,
isLoading: supportedTokensLoading,
isError: supportedTokensQueryError,
refetch: supportedTokensQueryRefetch,
} = useFiatOnRampSupportedTokens()
const {
data: tokenList,
loading: tokenListLoading,
error: tokenListError,
refetch: tokenListRefetch,
} = useFiatOnRampTokenList(supportedTokens)
const loading = supportedTokensLoading || tokenListLoading
const error = Boolean(supportedTokensQueryError || tokenListError)
const onRetry = (): void => {
if (supportedTokensQueryError) {
supportedTokensQueryRefetch?.()
}
if (tokenListError) {
tokenListRefetch?.()
}
}
return ( return (
<BottomSheetModal
extendOnKeyboardVisible
fullScreen
hideKeyboardOnDismiss
hideKeyboardOnSwipeDown
renderBehindBottomInset
backgroundColor={colors.surface1.get()}
name={ModalName.FiatOnRampCountryList}
snapPoints={FOR_MODAL_SNAP_POINTS}
onClose={onClose}>
<Trace <Trace
logImpression logImpression
element={ElementName.FiatOnRampTokenSelector} element={ElementName.FiatOnRampTokenSelector}
section={SectionName.TokenSelector}> section={SectionName.TokenSelector}>
<AnimatedFlex <Flex grow gap="$spacing16" px="$spacing16">
entering={FadeIn} <Text color="$neutral1" mt="$spacing2" textAlign="center" variant="subheading1">
exiting={FadeOut} {t('Choose a token')}
gap="$spacing12" </Text>
overflow="hidden" <AnimatedFlex grow entering={FadeIn} exiting={FadeOut}>
px="$spacing16"
width="100%">
<TokenFiatOnRampList <TokenFiatOnRampList
error={error} error={error}
list={tokenList} list={list}
loading={loading} loading={loading}
onBack={onBack}
onRetry={onRetry} onRetry={onRetry}
onSelectCurrency={onSelectCurrency} onSelectCurrency={onSelectCurrency}
/> />
</AnimatedFlex> </AnimatedFlex>
</Flex>
</Trace> </Trace>
</BottomSheetModal>
) )
} }
export const FiatOnRampTokenSelector = memo(_FiatOnRampTokenSelector)
...@@ -5,11 +5,14 @@ import { useTranslation } from 'react-i18next' ...@@ -5,11 +5,14 @@ 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
...@@ -61,15 +64,24 @@ function CEXItemWrapper({ ...@@ -61,15 +64,24 @@ function CEXItemWrapper({
) )
} }
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>) => (
......
import { skipToken } from '@reduxjs/toolkit/query/react' import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { useCallback, useRef } from 'react' import { useCallback, useMemo, useRef } 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 { Delay } from 'src/components/layout/Delayed' import { Delay } from 'src/components/layout/Delayed'
import { ColorTokens, useSporeColors } from 'ui/src' import { ColorTokens, useSporeColors } from 'ui/src'
import { useDebounce } from 'utilities/src/time/timing' import { useDebounce } from 'utilities/src/time/timing'
import { useAllCommonBaseCurrencies } from 'wallet/src/components/TokenSelector/hooks'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { uniswapUrls } from 'wallet/src/constants/urls' import { uniswapUrls } from 'wallet/src/constants/urls'
import { fromMoonpayNetwork } from 'wallet/src/features/chains/utils'
import { CurrencyInfo } from 'wallet/src/features/dataApi/types'
import { import {
useFiatOnRampAggregatorSupportedTokensQuery,
useFiatOnRampBuyQuoteQuery, useFiatOnRampBuyQuoteQuery,
useFiatOnRampIpAddressQuery, useFiatOnRampIpAddressQuery,
useFiatOnRampLimitsQuery, useFiatOnRampLimitsQuery,
...@@ -16,7 +20,7 @@ import { ...@@ -16,7 +20,7 @@ import {
useFiatOnRampWidgetUrlQuery, useFiatOnRampWidgetUrlQuery,
} from 'wallet/src/features/fiatOnRamp/api' } from 'wallet/src/features/fiatOnRamp/api'
import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks' import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks'
import { MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types' import { FORSupportedToken, MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types'
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 {
...@@ -29,6 +33,7 @@ import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hoo ...@@ -29,6 +33,7 @@ import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hoo
import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency' import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency'
import { ValueType } from 'wallet/src/utils/getCurrencyAmount' import { ValueType } from 'wallet/src/utils/getCurrencyAmount'
import { isAndroid } from 'wallet/src/utils/platform' import { isAndroid } from 'wallet/src/utils/platform'
import { FiatOnRampCurrency } from './types'
export function useFormatExactCurrencyAmount( export function useFormatExactCurrencyAmount(
currencyAmount: string, currencyAmount: string,
...@@ -250,45 +255,6 @@ export function useMoonpayFiatOnRamp({ ...@@ -250,45 +255,6 @@ export function useMoonpayFiatOnRamp({
} }
} }
// Wrapper hook for useFiatOnRampSupportedTokensQuery with filtering by country and/or state in US
export function useFiatOnRampSupportedTokens(): {
data: MoonpayCurrency[] | undefined
isLoading: boolean
isError: boolean
refetch: () => void
} {
// this should be already cached by the time we need it
const {
data: ipAddressData,
isLoading: isEligibleLoading,
isError: isFiatBuyAllowedQueryError,
refetch: isFiatBuyAllowedQueryRefetch,
} = useFiatOnRampIpAddressQuery()
const {
data: supportedTokens,
isLoading: supportedTokensLoading,
isError: supportedTokensQueryError,
refetch: supportedTokensQueryRefetch,
} = useFiatOnRampSupportedTokensQuery(
{
isUserInUS: ipAddressData?.alpha3 === 'USA' ?? false,
stateInUS: ipAddressData?.state,
},
{ skip: !ipAddressData }
)
return {
data: supportedTokens,
isLoading: isEligibleLoading || supportedTokensLoading,
isError: isFiatBuyAllowedQueryError || supportedTokensQueryError,
refetch: async (): Promise<void> => {
await isFiatBuyAllowedQueryRefetch()
await supportedTokensQueryRefetch()
},
}
}
function useMoonpayError( function useMoonpayError(
hasError: boolean, hasError: boolean,
amountIsTooSmall: boolean, amountIsTooSmall: boolean,
...@@ -316,3 +282,143 @@ function useMoonpayError( ...@@ -316,3 +282,143 @@ function useMoonpayError(
return { errorText, errorColor } return { errorText, errorColor }
} }
function findTokenOptionForFiatOnRampToken(
commonBaseCurrencies: CurrencyInfo[] | undefined = [],
fiatOnRampToken: FORSupportedToken
): Maybe<CurrencyInfo> {
return commonBaseCurrencies.find(
(item) =>
item &&
fiatOnRampToken.cryptoCurrencyCode.toLowerCase() === item.currency.symbol?.toLowerCase() &&
fiatOnRampToken.chainId === item.currency.chainId.toString()
)
}
function findTokenOptionForMoonpayCurrency(
commonBaseCurrencies: CurrencyInfo[] | undefined = [],
moonpayCurrency: MoonpayCurrency
): Maybe<CurrencyInfo> {
return commonBaseCurrencies.find((item) => {
const [code, network] = moonpayCurrency.code.split('_')
const chainId = fromMoonpayNetwork(network)
return (
item &&
code &&
code === item.currency.symbol?.toLowerCase() &&
chainId === item.currency.chainId
)
})
}
export function useFiatOnRampSupportedTokens({
sourceCurrencyCode,
countryCode,
}: {
sourceCurrencyCode: string
countryCode: string
}): {
error: boolean
list: FiatOnRampCurrency[] | undefined
loading: boolean
refetch: () => void
} {
const {
data: supportedTokensResponse,
isLoading: supportedTokensLoading,
error: supportedTokensError,
refetch: refetchSupportedTokens,
} = useFiatOnRampAggregatorSupportedTokensQuery({ fiatCurrency: sourceCurrencyCode, countryCode })
const {
data: commonBaseCurrencies,
error: commonBaseCurrenciesError,
loading: commonBaseCurrenciesLoading,
refetch: refetchCommonBaseCurrencies,
} = useAllCommonBaseCurrencies()
const list = useMemo(
() =>
(supportedTokensResponse?.supportedTokens || [])
.map((fiatOnRampToken) => ({
currencyInfo: findTokenOptionForFiatOnRampToken(commonBaseCurrencies, fiatOnRampToken),
}))
.filter((item) => !!item.currencyInfo),
[commonBaseCurrencies, supportedTokensResponse?.supportedTokens]
)
const loading = supportedTokensLoading || commonBaseCurrenciesLoading
const error = Boolean(supportedTokensError || commonBaseCurrenciesError)
const refetch = async (): Promise<void> => {
if (supportedTokensError) {
await refetchSupportedTokens?.()
}
if (commonBaseCurrenciesError) {
refetchCommonBaseCurrencies?.()
}
}
return { list, loading, error, refetch }
}
export function useMoonpaySupportedTokens(): {
error: boolean
list: FiatOnRampCurrency[] | undefined
loading: boolean
refetch: () => void
} {
// this should be already cached by the time we need it
const {
data: ipAddressData,
isLoading: ipAddressLoading,
isError: ipAddressError,
refetch: refetchIpAddress,
} = useFiatOnRampIpAddressQuery()
const {
data: supportedTokens,
isLoading: supportedTokensLoading,
isError: supportedTokensError,
refetch: refetchSupportedTokens,
} = useFiatOnRampSupportedTokensQuery(
{
isUserInUS: ipAddressData?.alpha3 === 'USA' ?? false,
stateInUS: ipAddressData?.state,
},
{ skip: !ipAddressData }
)
const {
data: commonBaseCurrencies,
error: commonBaseCurrenciesError,
loading: commonBaseCurrenciesLoading,
refetch: refetchCommonBaseCurrencies,
} = useAllCommonBaseCurrencies()
const list = useMemo(
() =>
(supportedTokens || [])
.map((fiatOnRampToken) => ({
currencyInfo: findTokenOptionForMoonpayCurrency(commonBaseCurrencies, fiatOnRampToken),
moonpayCurrencyCode: fiatOnRampToken.code,
}))
.filter((item) => !!item.currencyInfo),
[commonBaseCurrencies, supportedTokens]
)
const loading = ipAddressLoading || supportedTokensLoading || commonBaseCurrenciesLoading
const error = Boolean(ipAddressError || supportedTokensError || commonBaseCurrenciesError)
const refetch = async (): Promise<void> => {
if (ipAddressError) {
await refetchIpAddress()
}
if (supportedTokensError) {
await refetchSupportedTokens()
}
if (commonBaseCurrenciesError) {
refetchCommonBaseCurrencies?.()
}
}
return { list, loading, error, refetch }
}
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
TextInput as NativeTextInput, TextInput as NativeTextInput,
TextInputContentSizeChangeEventData, TextInputContentSizeChangeEventData,
} from 'react-native' } from 'react-native'
import { ColorTokens, Flex, useSporeColors } from 'ui/src' import { ColorTokens, Flex } from 'ui/src'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { TextInput } from 'wallet/src/components/input/TextInput' import { TextInput } from 'wallet/src/components/input/TextInput'
import { isAndroid } from 'wallet/src/utils/platform' import { isAndroid } from 'wallet/src/utils/platform'
...@@ -56,7 +56,6 @@ function Inputs({ ...@@ -56,7 +56,6 @@ function Inputs({
layerType, layerType,
...inputProps ...inputProps
}: Props & { layerType?: 'foreground' | 'background' }): JSX.Element { }: Props & { layerType?: 'foreground' | 'background' }): JSX.Element {
const colors = useSporeColors()
const [isMultiline, setIsMultiline] = useState(false) const [isMultiline, setIsMultiline] = useState(false)
const handleContentSizeChange = useCallback( const handleContentSizeChange = useCallback(
...@@ -120,7 +119,7 @@ function Inputs({ ...@@ -120,7 +119,7 @@ function Inputs({
py="$none" py="$none"
returnKeyType="done" returnKeyType="done"
scrollEnabled={false} scrollEnabled={false}
selectionColor={colors.neutral1.get()} selectionColor="$neutral1"
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,
},
],
} }
} }
> >
......
...@@ -2,6 +2,8 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState' ...@@ -2,6 +2,8 @@ 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 { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
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'
...@@ -12,6 +14,9 @@ export interface AppModalState<T> { ...@@ -12,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>
...@@ -24,7 +29,10 @@ export interface ModalsState { ...@@ -24,7 +29,10 @@ export interface ModalsState {
[ModalName.Scantastic]: AppModalState<ScantasticModalState> [ModalName.Scantastic]: AppModalState<ScantasticModalState>
[ModalName.Send]: AppModalState<TransactionState> [ModalName.Send]: AppModalState<TransactionState>
[ModalName.Swap]: AppModalState<TransactionState> [ModalName.Swap]: AppModalState<TransactionState>
[ModalName.UnitagsIntro]: AppModalState<{ address: Address }> [ModalName.UnitagsIntro]: AppModalState<{
address: Address
entryPoint: Screens.Home | Screens.Settings
}>
[ModalName.ViewOnlyExplainer]: AppModalState<undefined> [ModalName.ViewOnlyExplainer]: AppModalState<undefined>
[ModalName.WalletConnectScan]: AppModalState<ScannerModalState> [ModalName.WalletConnectScan]: AppModalState<ScannerModalState>
} }
...@@ -2,7 +2,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' ...@@ -2,7 +2,9 @@ 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 { getKeys } from 'utilities/src/primitives/objects' import { getKeys } from 'utilities/src/primitives/objects'
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 +15,11 @@ type AccountSwitcherModalParams = { ...@@ -13,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 = {
...@@ -65,7 +72,7 @@ type SendModalParams = { name: typeof ModalName.Send; initialState?: Transaction ...@@ -65,7 +72,7 @@ type SendModalParams = { name: typeof ModalName.Send; initialState?: Transaction
type UnitagsIntroParams = { type UnitagsIntroParams = {
name: typeof ModalName.UnitagsIntro name: typeof ModalName.UnitagsIntro
initialState?: { address: Address } initialState?: { address: Address; entryPoint: Screens.Home | Screens.Settings }
} }
type ViewOnlyExplainerParams = { type ViewOnlyExplainerParams = {
...@@ -75,6 +82,7 @@ type ViewOnlyExplainerParams = { ...@@ -75,6 +82,7 @@ type ViewOnlyExplainerParams = {
export type OpenModalParams = export type OpenModalParams =
| AccountSwitcherModalParams | AccountSwitcherModalParams
| ExchangeTransferModalParams
| ExperimentsModalParams | ExperimentsModalParams
| ExploreModalParams | ExploreModalParams
| FiatCurrencySelectorParams | FiatCurrencySelectorParams
...@@ -94,6 +102,10 @@ export type OpenModalParams = ...@@ -94,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,
},
],
} }
} }
> >
......
import { SharedEventName } from '@uniswap/analytics-events'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types' import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
...@@ -18,7 +19,7 @@ import { ...@@ -18,7 +19,7 @@ import {
} from 'wallet/src/features/wallet/create/pendingAccountsSaga' } from 'wallet/src/features/wallet/create/pendingAccountsSaga'
import { usePendingAccounts } from 'wallet/src/features/wallet/hooks' import { usePendingAccounts } from 'wallet/src/features/wallet/hooks'
import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice'
import { sendWalletAppsFlyerEvent } from 'wallet/src/telemetry' import { sendWalletAnalyticsEvent, sendWalletAppsFlyerEvent } from 'wallet/src/telemetry'
import { WalletAppsFlyerEvents } from 'wallet/src/telemetry/constants' import { WalletAppsFlyerEvents } from 'wallet/src/telemetry/constants'
export type OnboardingCompleteProps = OnboardingStackBaseParams export type OnboardingCompleteProps = OnboardingStackBaseParams
...@@ -58,9 +59,19 @@ export function useCompleteOnboardingCallback({ ...@@ -58,9 +59,19 @@ export function useCompleteOnboardingCallback({
} }
) )
// Log TOS acceptance for new wallets before they are activated
if (entryPoint === OnboardingEntryPoint.FreshInstallOrReplace) {
pendingWalletAddresses.forEach((address: string) => {
sendWalletAnalyticsEvent(SharedEventName.TERMS_OF_SERVICE_ACCEPTED, { address })
})
}
// Claim unitag if there's a claim to process // Claim unitag if there's a claim to process
if (unitagClaim) { if (unitagClaim) {
const { claimError } = await claimUnitag(unitagClaim) const { claimError } = await claimUnitag(unitagClaim, {
source: 'onboarding',
hasENSAddress: false,
})
if (claimError) { if (claimError) {
dispatch( dispatch(
pushNotification({ pushNotification({
......
...@@ -244,7 +244,6 @@ export function ScantasticModal(): JSX.Element | null { ...@@ -244,7 +244,6 @@ export function ScantasticModal(): JSX.Element | null {
borderColor="$surface3" borderColor="$surface3"
borderRadius="$rounded20" borderRadius="$rounded20"
borderWidth={1} borderWidth={1}
flex={1}
gap="$spacing12" gap="$spacing12"
p="$spacing16" p="$spacing16"
width="100%"> width="100%">
......
...@@ -15,6 +15,7 @@ export type MoonpayTransactionEventProperties = TraceProps & ...@@ -15,6 +15,7 @@ export type MoonpayTransactionEventProperties = TraceProps &
export type AssetDetailsBaseProperties = { export type AssetDetailsBaseProperties = {
name?: string name?: string
domain?: string
address: string address: string
chain?: number chain?: number
} }
......
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}
/> />
)} )}
</> </>
......
...@@ -2,24 +2,42 @@ import React, { useState } from 'react' ...@@ -2,24 +2,42 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ActivityIndicator } from 'react-native' import { ActivityIndicator } from 'react-native'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { UnitagStackScreenProp } from 'src/app/navigation/types' import { UnitagEntryPoint, UnitagStackScreenProp } from 'src/app/navigation/types'
import { useAvatarSelectionHandler } from 'src/components/unitags/AvatarSelection' import { useAvatarSelectionHandler } from 'src/components/unitags/AvatarSelection'
import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal' import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal'
import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture'
import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen'
import { UnitagName } from 'src/features/unitags/UnitagName'
import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens'
import { AnimatedFlex, Button, Flex, Icons, Text, useSporeColors } from 'ui/src' import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src'
import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme'
import { ChainId } from 'wallet/src/constants/chains'
import { useENSName } from 'wallet/src/features/ens/api'
import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types'
import { useClaimUnitag } from 'wallet/src/features/unitags/hooks' import { useClaimUnitag } from 'wallet/src/features/unitags/hooks'
import { UnitagClaimSource } from 'wallet/src/features/unitags/types'
function convertEntryPointToAnalyticsSource(entryPoint: UnitagEntryPoint): UnitagClaimSource {
switch (entryPoint) {
case Screens.Home:
return 'home'
case Screens.Settings:
return 'settings'
case OnboardingScreens.Landing:
return 'onboarding'
default:
throw new Error(`unhandled entryPoint for ChooseProfilePictureScreen: ${entryPoint}`)
}
}
export function ChooseProfilePictureScreen({ export function ChooseProfilePictureScreen({
route, route,
}: UnitagStackScreenProp<UnitagScreens.ChooseProfilePicture>): JSX.Element { }: UnitagStackScreenProp<UnitagScreens.ChooseProfilePicture>): JSX.Element {
const { entryPoint, unitag, address } = route.params const { entryPoint, unitag, unitagFontSize, address } = route.params
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
const { data: ensName } = useENSName(address, ChainId.Mainnet)
const claimUnitag = useClaimUnitag() const claimUnitag = useClaimUnitag()
const [imageUri, setImageUri] = useState<string>() const [imageUri, setImageUri] = useState<string>()
...@@ -64,11 +82,18 @@ export function ChooseProfilePictureScreen({ ...@@ -64,11 +82,18 @@ export function ChooseProfilePictureScreen({
const attemptClaimUnitag = async (): Promise<void> => { const attemptClaimUnitag = async (): Promise<void> => {
setIsClaiming(true) setIsClaiming(true)
const { claimError: attemptClaimError } = await claimUnitag({ const source = convertEntryPointToAnalyticsSource(entryPoint)
const { claimError: attemptClaimError } = await claimUnitag(
{
address, address,
username: unitag, username: unitag,
avatarUri: imageUri, avatarUri: imageUri,
}) },
{
source,
hasENSAddress: !!ensName,
}
)
setIsClaiming(false) setIsClaiming(false)
setClaimError(attemptClaimError) setClaimError(attemptClaimError)
...@@ -112,19 +137,7 @@ export function ChooseProfilePictureScreen({ ...@@ -112,19 +137,7 @@ export function ChooseProfilePictureScreen({
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>
<AnimatedFlex <UnitagName fontSize={unitagFontSize} name={unitag} />
row
alignSelf="center"
animation="lazy"
enterStyle={{ opacity: 0 }}
gap="$spacing20">
<Text color="$neutral1" variant="heading2">
{unitag}
</Text>
<Flex row position="absolute" right={-spacing.spacing8} top={-spacing.spacing8}>
<Icons.Unitag size="$icon.28" />
</Flex>
</AnimatedFlex>
{!!claimError && ( {!!claimError && (
<Text color="$statusCritical" variant="body2"> <Text color="$statusCritical" variant="body2">
{claimError} {claimError}
...@@ -168,8 +181,8 @@ function ProfilePicture({ ...@@ -168,8 +181,8 @@ function ProfilePicture({
return ( return (
<UnitagProfilePicture <UnitagProfilePicture
address={address} address={address}
profilePictureUri={imageUri}
size={imageSizes.image100} size={imageSizes.image100}
unitagAvatarUri={imageUri}
/> />
) )
} }
......
...@@ -117,7 +117,7 @@ export function UnitagConfirmationScreen({ ...@@ -117,7 +117,7 @@ export function UnitagConfirmationScreen({
</Text> </Text>
<Text color="$neutral2" textAlign="center" variant="subheading2"> <Text color="$neutral2" textAlign="center" variant="subheading2">
{t( {t(
'{{unitag}}{{unitagSuffix}} is ready to send and receive crypto. Continue to build out your wallet by customizing your profile', '{{unitag}}{{unitagSuffix}} is ready to send and receive crypto. Continue to build out your wallet by customizing your web3 profile.',
{ unitag, unitagSuffix: UNITAG_SUFFIX } { unitag, unitagSuffix: UNITAG_SUFFIX }
)} )}
</Text> </Text>
......
This diff is collapsed.
This diff is collapsed.
...@@ -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>
......
This diff is collapsed.
...@@ -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'
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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