ci(release): publish latest release

parent 4daac4d2
IPFS hash of the deployment:
- CIDv0: `Qmf6XKUvbFPHvKbi1atVQVyMwr4iVxCSpLHffiTio8Hw2h`
- CIDv1: `bafybeihy7ayma2n3ltw6zn2aewegv4eyvxo4f52n2ixpnxgyx47xidce5i`
- CIDv0: `QmeAaU5F67PX1jZemajRJjA6sDvJ1X6ddvgjjqDHfTePx7`
- CIDv1: `bafybeihlezropz7qe2rutxp4solf46m3hgpl7c53fevxscbltlz2fo6exq`
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.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeihy7ayma2n3ltw6zn2aewegv4eyvxo4f52n2ixpnxgyx47xidce5i.ipfs.dweb.link/
- https://bafybeihy7ayma2n3ltw6zn2aewegv4eyvxo4f52n2ixpnxgyx47xidce5i.ipfs.cf-ipfs.com/
- [ipfs://Qmf6XKUvbFPHvKbi1atVQVyMwr4iVxCSpLHffiTio8Hw2h/](ipfs://Qmf6XKUvbFPHvKbi1atVQVyMwr4iVxCSpLHffiTio8Hw2h/)
- https://bafybeihlezropz7qe2rutxp4solf46m3hgpl7c53fevxscbltlz2fo6exq.ipfs.dweb.link/
- https://bafybeihlezropz7qe2rutxp4solf46m3hgpl7c53fevxscbltlz2fo6exq.ipfs.cf-ipfs.com/
- [ipfs://QmeAaU5F67PX1jZemajRJjA6sDvJ1X6ddvgjjqDHfTePx7/](ipfs://QmeAaU5F67PX1jZemajRJjA6sDvJ1X6ddvgjjqDHfTePx7/)
## 5.9.0 (2024-02-12)
## 5.10.0 (2024-02-20)
### Features
* **web:** [info] fix TDP/PDP header mobile responsiveness (#5636) 64edb1a
* **web:** [info] move chart type selector and time selector to below chart (#5880) 524734a
* **web:** add copy tooltip behavior (#5919) aed3633
* **web:** add limit price inversion (#6198) de2d048
* **web:** Allow feature flag overriding through URL parameters (#6182) 01181cf
* **web:** change output currency from limit price panel (#6192) 8651e59
* **web:** deploy v2 everywhere feature flag (#6161) dfa90e7
* **web:** do not reset scroll between tabs (#6090) 4d0d682
* **web:** explore/tokens and /explore should not be interchangeable (#6088) f4ca806
* **web:** Make limit price section first and auto-fill with USDC if no output currency selected (#6013) b8ecb58
* **web:** more dns gateway updates (#5964) 5051029
* **web:** outage banner for arbitrum, optimism, polygon (#6218) 30af199
* **web:** remove increment buttons from Limit Price Input (#6189) b27d874
* **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
* **web:** [info] active liquidity chart (#6237) 1133601
* **web:** [info] add p0 info analytics (#6338) fdaade8
* **web:** [info] dot grids (#6327) e4e8720
* **web:** [uni-tags] add accept / reject to banners (#6309) 34a5aa7
* **web:** [uni-tags] add banner to account drawer (#6209) 995722d
* **web:** [uni-tags] add banner to swap page (#6234) b41f304
* **web:** add tables error states (#6228) 262f8a0
* **web:** fix PDP in-chart price headers (#6211) a6070ae
* **web:** implement TDP TVL chart gql queries (#6190) dabe043
* **web:** implement TDP volumes chart gql queries (#6188) d044d6b
* **web:** pool protocol switcher (#6339) f81cc26
* **web:** replace account drawer header buttons with action tiles (#6240) fba32e9
* **web:** v2 everywhere (#6164) 5c8ec8d
### Bug Fixes
* **web:** [landing-page] add missing translations, improve layout responsiveness, update brand assets (#6166) 38e9e47
* **web:** de-flake swap flow logging test (#6150) 0f7214e
* **web:** de-flake TDP cypress test (#6147) c999070
* **web:** fix and re-enable swap e2e tests (#6143) 6922d5b
* **web:** followup fixes for outage banner (#6229) a6db458
* **web:** left aligned input send (#6144) cd926ae
* **web:** make new landing page enabled by default (#6287) ce952ad
* **web:** re-enable some cypress tests (#6122) 0aaecb1
* **web:** send currency logos in send review are extending too far on safari (#6137) b6eecf2
* **web:** send numbers cutoff on safari (#6136) eed1540
* **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:** [info] fix pool table description ellipsis width (#6148) b83c2a3
* **web:** avoid re-rendering the App constantly (#6296) 45de35c
* **web:** disable statsig metrics (#6368) f554c79
* **web:** fix e2e tests (#6365) 4343ae9
* **web:** fix importing some wallet paths causing errors due to react-native-dotenv (#6255) 6b7fc53
* **web:** import v2 pool goes to add v3 pool (#6273) 8f52943
* **web:** Keep base/quote tokens stable if user has edited the limit price value (#6284) 1dfe744
* **web:** make new landing page enabled by default (#6285) a09d8fe
* **web:** use chainId instead of chainName for analytics (#6390) 81dc15b
* **web:** using correct favicon url (#6298) 46d76a8
web/5.9.0
\ No newline at end of file
web/5.10.0
\ No newline at end of file
......@@ -13,6 +13,7 @@ ignores: [
## React Native Usage
"@amplitude/analytics-react-native",
"@react-native-masked-view/masked-view",
"@react-native-firebase/app-check",
"react-native-image-colors",
# Dependencies that depcheck thinks are missing but are actually present or never used
## Internal packages / workspaces
......
......@@ -125,17 +125,17 @@ android {
dev {
isDefault(true)
applicationIdSuffix ".dev"
versionName "1.21"
versionName "1.22"
dimension "variant"
}
beta {
applicationIdSuffix ".beta"
versionName "1.21"
versionName "1.22"
dimension "variant"
}
prod {
dimension "variant"
versionName "1.21"
versionName "1.22"
}
}
......
......@@ -702,6 +702,9 @@ PODS:
- React-Core (= 0.71.13)
- React-jsi (= 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/CoreOnly
- FirebaseAuth (~> 10.15.0)
......@@ -710,6 +713,10 @@ PODS:
- Firebase/Firestore (10.15.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 10.15.0)
- FirebaseAppCheck (10.15.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseAppCheckInterop (10.15.0)
- FirebaseAuth (10.15.0):
- FirebaseAppCheckInterop (~> 10.0)
......@@ -1263,6 +1270,10 @@ PODS:
- RNFBApp (18.4.0):
- Firebase/CoreOnly (= 10.15.0)
- React-Core
- RNFBAppCheck (18.4.0):
- Firebase/AppCheck (= 10.15.0)
- React-Core
- RNFBApp
- RNFBAuth (18.4.0):
- Firebase/Auth (= 10.15.0)
- React-Core
......@@ -1423,6 +1434,7 @@ DEPENDENCIES:
- RNDeviceInfo (from `../../../node_modules/react-native-device-info`)
- RNFastImage (from `../../../node_modules/react-native-fast-image`)
- "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`)"
- "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)"
- "RNFlashList (from `../../../node_modules/@shopify/flash-list`)"
......@@ -1445,6 +1457,7 @@ SPEC REPOS:
- Argon2Swift
- BoringSSL-GRPC
- Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseCore
......@@ -1629,6 +1642,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-fast-image"
RNFBApp:
:path: "../../../node_modules/@react-native-firebase/app"
RNFBAppCheck:
:path: "../../../node_modules/@react-native-firebase/app-check"
RNFBAuth:
:path: "../../../node_modules/@react-native-firebase/auth"
RNFBFirestore:
......@@ -1686,6 +1701,7 @@ SPEC CHECKSUMS:
FBLazyVector: 24e08bf294faea0abc0278abb2fcad7f3e446f6f
FBReactNativeSpec: cc06081bbc8420e1c0580008ff6d7af324f32f31
Firebase: 66043bd4579e5b73811f96829c694c7af8d67435
FirebaseAppCheck: 66eea1c882cddd1bce9d92a0a7efd596f7204782
FirebaseAppCheckInterop: a8c555b1c2db1d9445e6c3a08a848167ddb7eb51
FirebaseAuth: a55ec5f7f8a5b1c2dd750235c1bb419bfb642445
FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e
......@@ -1758,6 +1774,7 @@ SPEC CHECKSUMS:
RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae
RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660
RNFBApp: a3026bdd951dd7a3a88e8e6518b53ddd2f8b3809
RNFBAppCheck: c5363a0be62f961edfcdf82ed353c69bc37a39f4
RNFBAuth: 553c6e66d3c70e086799104dad4a554c2663c337
RNFBFirestore: a60e6005e071b31360a5bf651eb403b36c7db7de
RNFlashList: 4b4b6b093afc0df60ae08f9cbf6ccd4c836c667a
......
......@@ -2450,7 +2450,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2496,7 +2496,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2542,7 +2542,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
......@@ -2588,7 +2588,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2630,7 +2630,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2673,7 +2673,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2716,7 +2716,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
......@@ -2759,7 +2759,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2795,7 +2795,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2833,7 +2833,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3003,7 +3003,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -3047,7 +3047,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3143,7 +3143,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3214,7 +3214,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......@@ -3310,7 +3310,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3381,7 +3381,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.21;
MARKETING_VERSION = 1.22;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
#import "AppDelegate.h"
#import "RNFBAppCheckModule.h"
#import <Firebase.h>
#import "Uniswap-Swift.h"
......@@ -13,9 +14,11 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// must be first line in startup routine
// Must be first line in startup routine
[ReactNativePerformance onAppStarted];
// Must be before [FIRApp configure], initializes RNFBAppCheckModule
[RNFBAppCheckModule sharedInstance];
[FIRApp configure];
// 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', () => () => ({
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', () => ({
openURL: jest.fn(),
addEventListener: jest.fn(),
......
......@@ -60,6 +60,7 @@
"@react-native-async-storage/async-storage": "1.17.10",
"@react-native-community/netinfo": "9.3.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/firestore": "18.4.0",
"@react-native-masked-view/masked-view": "0.2.9",
......@@ -75,9 +76,9 @@
"@shopify/react-native-performance-navigation": "3.0.0",
"@shopify/react-native-skia": "0.1.187",
"@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/sdk-core": "4.0.7",
"@uniswap/sdk-core": "4.1.2",
"@uniswap/v3-sdk": "3.10.2",
"@walletconnect/core": "2.10.1",
"@walletconnect/react-native-compat": "2.10.1",
......
......@@ -47,6 +47,7 @@ import { useAsyncData } from 'utilities/src/react/hooks'
import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext'
import { config } from 'wallet/src/config'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { initFirebaseAppCheck } from 'wallet/src/features/appCheck'
import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks'
import { EXPERIMENT_NAMES, FEATURE_FLAGS } from 'wallet/src/features/experiments/constants'
import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors'
......@@ -103,6 +104,7 @@ if (!__DEV__) {
initOneSignal()
initAppsFlyer()
initFirebaseAppCheck()
initializeTranslation()
function App(): JSX.Element | null {
......@@ -123,6 +125,8 @@ function App(): JSX.Element | null {
tier: getStatsigEnvironmentTier(),
},
api: uniswapUrls.statsigProxyUrl,
disableAutoMetricsLogging: true,
disableErrorLogging: true,
},
sdkKey: DUMMY_STATSIG_SDK_KEY,
user: deviceId ? { userID: deviceId } : {},
......
import React, { ErrorInfo, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { Image, StyleSheet } from 'react-native'
import RNRestart from 'react-native-restart'
import { useAppDispatch } from 'src/app/hooks'
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 { useAccounts } from 'wallet/src/features/wallet/hooks'
import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice'
......@@ -61,9 +62,15 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
}
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">
<DeadLuni />
<Image source={DEAD_LUNI} style={styles.errorImage} />
<Flex centered gap="$spacing8">
<Text variant="subheading1">{t('Uh oh!')}</Text>
<Text variant="body2">{t('Something crashed.')}</Text>
......@@ -81,3 +88,11 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element {
</Flex>
)
}
const styles = StyleSheet.create({
errorImage: {
height: 150,
resizeMode: 'contain',
width: 150,
},
})
import { PropsWithChildren, useCallback } from 'react'
import { useAppDispatch } from 'src/app/hooks'
import { useAppStackNavigation } from 'src/app/navigation/types'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { Screens } from 'src/screens/Screens'
......@@ -8,11 +9,13 @@ import {
NavigateToSwapFlowArgs,
WalletNavigationProvider,
} from 'wallet/src/contexts/WalletNavigationContext'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import { ModalName } from 'wallet/src/telemetry/constants'
export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element {
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity)
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet()
const navigateToSwapFlow = useNavigateToSwapFlow()
const navigateToTokenDetails = useNavigateToTokenDetails()
......@@ -20,6 +23,7 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren):
<WalletNavigationProvider
navigateToAccountActivityList={navigateToAccountActivityList}
navigateToAccountTokenList={navigateToAccountTokenList}
navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet}
navigateToSwapFlow={navigateToSwapFlow}
navigateToTokenDetails={navigateToTokenDetails}>
{children}
......@@ -59,3 +63,25 @@ function useNavigateToTokenDetails(): (currencyId: string) => void {
[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'
import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal'
import { ExperimentsModal } from 'src/app/modals/ExperimentsModal'
import { ExploreModal } from 'src/app/modals/ExploreModal'
import { FiatOnRampAggregatorModal } from 'src/app/modals/FiatOnRampModalAggregator'
import { SwapModal } from 'src/app/modals/SwapModal'
import { TransferTokenModal } from 'src/app/modals/TransferTokenModal'
import { LazyModalRenderer } from 'src/app/modals/utils'
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 { RestoreWalletModal } from 'src/components/RestoreWalletModal/RestoreWalletModal'
import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal'
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 { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal'
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal'
import { ScantasticModal } from 'src/features/scantastic/ScantasticModal'
import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal'
......@@ -23,6 +24,10 @@ import { ModalName } from 'wallet/src/telemetry/constants'
export function AppModals(): JSX.Element {
return (
<>
<LazyModalRenderer name={ModalName.ExchangeTransferModal}>
<ExchangeTransferModal />
</LazyModalRenderer>
<LazyModalRenderer name={ModalName.Experiments}>
<ExperimentsModal />
</LazyModalRenderer>
......
......@@ -243,7 +243,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
{
"alignItems": "center",
"flexDirection": "row",
"gap": 2,
"justifyContent": "center",
}
}
......@@ -270,37 +269,26 @@ exports[`AccountSwitcher renders correctly 1`] = `
</View>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={16}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
testID="copy"
......@@ -519,45 +507,27 @@ exports[`AccountSwitcher renders correctly 1`] = `
ExpoLinearGradient
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"marginTop": 16,
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -336,7 +336,11 @@ export function UnitagStackNavigator(): JSX.Element {
name={UnitagScreens.UnitagConfirmation}
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.Navigator>
)
......
......@@ -68,14 +68,17 @@ export type OnboardingStackBaseParams = {
unitagClaim?: UnitagClaim
}
export type UnitagEntryPoint = OnboardingScreens.Landing | Screens.Home | Screens.Settings
export type SharedUnitagScreenParams = {
[UnitagScreens.ClaimUnitag]: {
entryPoint: OnboardingScreens.Landing | Screens.Home
entryPoint: UnitagEntryPoint
address?: Address
}
[UnitagScreens.ChooseProfilePicture]: {
entryPoint: OnboardingScreens.Landing | Screens.Home
entryPoint: UnitagEntryPoint
unitag: string
unitagFontSize: number
address: Address
}
}
......
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 { ColorTokens, Flex, Unicon, useSporeColors, useUniconColors } from 'ui/src'
import { borderRadii, opacify } from 'ui/src/theme'
import {
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'
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 = {
address: Address
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H'
......@@ -24,9 +111,9 @@ export const AddressQRCode = ({
safeAreaSize,
safeAreaColor,
}: AddressQRCodeProps): JSX.Element => {
const colors = useSporeColors()
const backgroundColorValue = backgroundColor
const gradientData = useUniconColors(address)
const { gradientProps } = useColorProps(address, color)
const colors = useSporeColors()
const safeAreaProps = useMemo(() => {
let safeAreaPropsObject: {
......@@ -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
logoBackgroundColor: colors.surface1.val,
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
}, [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 (
<QRCode
backgroundColor={backgroundColorValue}
color={color}
ecl={errorCorrectionLevel}
overlayColor={colors.neutral1.val}
{...safeAreaProps}
{...gradientProps}
size={size}
......@@ -88,26 +157,27 @@ type QRCodeDisplayProps = {
size: number
backgroundColor?: ColorTokens
containerBackgroundColor?: ColorTokens
overlayColor?: ColorTokens
safeAreaColor?: ColorTokens
logoSize?: number
overlayOpacityPercent?: number
hideOutline?: boolean
displayShadow?: boolean
color?: string
}
const _QRCodeDisplay = ({
address,
errorCorrectionLevel = 'Q',
errorCorrectionLevel = 'H',
size,
backgroundColor = '$surface1',
containerBackgroundColor,
overlayOpacityPercent,
color,
logoSize = 32,
safeAreaColor,
hideOutline = false,
displayShadow = false,
}: QRCodeDisplayProps): JSX.Element => {
const colors = useSporeColors()
const { avatar } = useAvatar(address)
const { smartColor } = useColorProps(address, color)
return (
<Flex
......@@ -115,37 +185,25 @@ const _QRCodeDisplay = ({
backgroundColor={containerBackgroundColor}
borderColor="$surface3"
borderRadius="$rounded32"
borderWidth={hideOutline ? 0 : 2}
borderWidth={hideOutline ? 0 : 1}
justifyContent="center"
p="$spacing24"
p="$spacing12"
position="relative"
shadowColor="$sporeBlack"
shadowOffset={{ width: 0, height: 16 }}
shadowOpacity={displayShadow ? 0.1 : 0}
shadowRadius={16}>
<Flex>
<AddressQRCode
address={address}
backgroundColor={backgroundColor}
errorCorrectionLevel={errorCorrectionLevel}
safeAreaColor={safeAreaColor}
safeAreaSize={logoSize / 1.5}
size={size}
/>
{overlayOpacityPercent && (
<Flex style={StyleSheet.absoluteFill}>
<Flex alignItems="center">
<AddressQRCode
address={address}
backgroundColor="$transparent"
color={opacify(overlayOpacityPercent, colors.neutral1.val)}
backgroundColor={containerBackgroundColor}
color={smartColor}
errorCorrectionLevel={errorCorrectionLevel}
safeAreaColor={safeAreaColor}
safeAreaSize={logoSize / 1.5}
safeAreaSize={logoSize}
size={size}
/>
</Flex>
)}
</Flex>
<Flex
alignItems="center"
backgroundColor="$transparent"
......@@ -154,10 +212,13 @@ const _QRCodeDisplay = ({
pl="$spacing2"
position="absolute"
pt="$spacing2">
<Unicon
showBorder
<AccountIcon
address={address}
backgroundColor={colors.surface1.val}
avatarUri={avatar}
borderColor="$surface2"
borderWidth={4}
showBackground={true}
showBorder={true}
size={logoSize}
/>
</Flex>
......
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos'
import {
AnimatedFlex,
Flex,
Icons,
Text,
TouchableArea,
useIsDarkMode,
useMedia,
useSporeColors,
useUniconColors,
} from 'ui/src'
import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src'
import { iconSizes, spacing } from 'ui/src/theme'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal'
......@@ -30,15 +18,13 @@ interface Props {
export function WalletQRCode({ address }: Props): JSX.Element | null {
const colors = useSporeColors()
const isDarkMode = useIsDarkMode()
const gradientData = useUniconColors(address)
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const media = useMedia()
const QR_CODE_SIZE = media.short ? 175 : 220
const UNICON_SIZE = QR_CODE_SIZE / 2.8
const QR_CODE_SIZE = media.short ? 220 : 240
const UNICON_SIZE = QR_CODE_SIZE / 4
if (!address) {
return null
......@@ -46,45 +32,34 @@ export function WalletQRCode({ address }: Props): JSX.Element | null {
return (
<>
<GradientBackground>
<UniconThemedGradient
middleOut
borderRadius="$rounded16"
gradientEndColor={colors.surface1.val}
gradientStartColor={gradientData.glow}
opacity={isDarkMode ? 0.24 : 0.2}
/>
</GradientBackground>
<AnimatedFlex
centered
grow
$short={{ mb: spacing.none, mx: spacing.spacing48 }}
entering={FadeIn}
exiting={FadeOut}
gap="$spacing24"
gap="$spacing8"
mb="$spacing8"
mx="$spacing60"
py="$spacing24">
<AddressDisplay
includeUnitagSuffix
showCopy
showCopyWrapperButton
address={address}
captionVariant="body1"
captionVariant="body2"
showAccountIcon={false}
variant="heading3"
/>
<QRCodeDisplay
hideOutline
address={address}
backgroundColor="$surface1"
containerBackgroundColor="$surface1"
displayShadow={true}
containerBackgroundColor={colors.surface1.val}
displayShadow={false}
logoSize={UNICON_SIZE}
overlayOpacityPercent={10}
safeAreaColor="$surface1"
size={QR_CODE_SIZE}
/>
<Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3">
{t('You can send tokens on all of our supported networks to this address.')}
</Text>
......
......@@ -13,6 +13,8 @@ export interface QRCodeProps {
color?: string
/* the color of the background */
backgroundColor?: string
/* the color of the background */
overlayColor?: string
/* an image source object. example {uri: 'base64string'} or {require('pathToImage')} */
logo?: ImageSourcePropType
/* logo size in pixels */
......
......@@ -2,74 +2,52 @@
// Custom matric renderer from: https://github.com/awesomejerry/react-native-qrcode-svg/pull/139/files
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 transformMatrixIntoPath from 'src/components/QRCodeScanner/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js'
import { useMedia } from 'ui/src'
const renderLogo = ({
size,
logo,
logoBackgroundColor,
logoSize,
logoMargin,
logoBorderRadius,
}) => {
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}
const QREyes = ({ x = -1, y = -1, fillColor, size }) => (
<G transform={`scale(${size / 120})`} x={x} y={y}>
<Path
clip-rule="evenodd"
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"
fill={fillColor}
fill-rule="evenodd"
/>
</G>
<G x={logoMargin} y={logoMargin}>
<Image
clipPath="url(#clip-logo)"
height={logoSize}
href={logo}
preserveAspectRatio="xMidYMid slice"
width={logoSize}
<Path
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"
fill={fillColor}
/>
</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>
)
}
)
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 = ({
value = 'this is a QR code',
size = 100,
color = 'sporeBlack',
backgroundColor = 'sporeWhite',
value = 'Wallet QR code',
size = 190,
color,
backgroundColor,
overlayColor = '#FFFFFF',
borderRadius = 24,
logo,
logoSize = size * 0.2,
logoBackgroundColor = 'transparent',
logoMargin = -2,
logoBorderRadius = 0,
quietZone = 4,
quietZone = 8,
enableLinearGradient = false,
gradientDirection = ['0%', '0%', '100%', '100%'],
linearGradient = ['rgb(255,0,0)', 'rgb(0,255,255)'],
ecl = 'M',
linearGradient = ['rgb(255,255,255)', 'rgb(0,255,255)'],
ecl = 'H',
getRef,
onError,
}) => {
......@@ -86,12 +64,15 @@ const QRCode = ({
}
}, [value, size, ecl, onError])
const media = useMedia()
if (!result) {
return null
}
const { path } = result
const eyeSize = media.short ? 126 : 138
return (
<Svg
ref={getRef}
......@@ -104,9 +85,9 @@ const QRCode = ({
id="grad"
x1={gradientDirection[0]}
x2={gradientDirection[2]}
y1={gradientDirection[1]}
y2={gradientDirection[3]}>
<Stop offset="0" stopColor={linearGradient[0]} stopOpacity="1" />
y1={gradientDirection[0]}
y2={gradientDirection[2]}>
<Stop offset="0" stopColor={color} stopOpacity="1" />
<Stop offset="1" stopColor={linearGradient[1]} stopOpacity="1" />
<Stop offset="1" stopColor={linearGradient[2]} stopOpacity="1" />
</LinearGradient>
......@@ -123,16 +104,28 @@ const QRCode = ({
</G>
<G>
<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>
{logo &&
renderLogo({
size,
logo,
logoSize,
logoBackgroundColor,
logoMargin,
logoBorderRadius,
})}
</Svg>
)
}
......
......@@ -199,7 +199,7 @@ export const TokenBalanceListInner = forwardRef<
{!balancesById ? (
isNonPollingRequestInFlight(networkStatus) ? (
<Flex px="$spacing24" style={containerProps?.loadingContainerStyle}>
<Loader.Token repeat={6} />
<Loader.Token withPrice repeat={6} />
</Flex>
) : (
<Flex fill grow justifyContent="center" style={containerProps?.emptyContainerStyle}>
......
......@@ -3,16 +3,15 @@ import React, { memo, useCallback, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { ListRenderItemInfo } from 'react-native'
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 { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem'
import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks'
import { ChainId } from 'wallet/src/constants/chains'
import { ElementName } from 'wallet/src/telemetry/constants'
import { CurrencyId } from 'wallet/src/utils/currencyId'
interface Props {
onSelectCurrency: (currency: FiatOnRampCurrency) => void
onBack: () => void
onRetry: () => void
error: boolean
loading: boolean
......@@ -52,7 +51,6 @@ function TokenOptionItemWrapper({
function _TokenFiatOnRampList({
onSelectCurrency,
onBack,
error,
onRetry,
list,
......@@ -71,8 +69,6 @@ function _TokenFiatOnRampList({
if (error) {
return (
<>
<Header onBack={onBack} />
<Flex centered grow>
<BaseCard.ErrorState
retryButtonLabel="Retry"
......@@ -80,27 +76,20 @@ function _TokenFiatOnRampList({
onRetry={onRetry}
/>
</Flex>
</>
)
}
if (loading) {
return (
<Flex>
<Header onBack={onBack} />
<Loader.Token repeat={5} />
</Flex>
)
return <Loader.Token repeat={5} />
}
return (
<Flex grow>
<Header onBack={onBack} />
<BottomSheetFlatList
ref={flatListRef}
ListEmptyComponent={<Flex />}
ListFooterComponent={<Inset all="$spacing36" />}
data={list}
focusHook={useBottomSheetFocusHook}
keyExtractor={key}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
......@@ -108,20 +97,6 @@ function _TokenFiatOnRampList({
showsVerticalScrollIndicator={false}
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 { 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 { 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 { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { shortenAddress } from 'wallet/src/utils/addresses'
......@@ -15,13 +17,18 @@ const ICON_SIZE = 24
export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Element => {
const colors = useSporeColors()
const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2)
const displayName = useDisplayName(account.address)
return (
<>
<Separator />
<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} />
)}
<Flex shrink alignItems="center" p="$none">
<DisplayNameText
displayName={displayName}
......
......@@ -56,9 +56,9 @@ function PortfolioValue({
return (
<Text color="$neutral2" loading={isLoading} variant="subheading2">
{portfolioValue
? convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance)
: t('N/A')}
{portfolioValue === undefined
? t('N/A')
: convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance)}
</Text>
)
}
......
......@@ -33,39 +33,28 @@ exports[`AccountHeader renders without error 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"alignItems": "center",
"flexDirection": "row",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
testID="manage"
......@@ -334,37 +323,26 @@ exports[`AccountHeader renders without error 1`] = `
</View>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......@@ -440,38 +418,27 @@ exports[`AccountHeader renders without error 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"flexShrink": 1,
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......@@ -561,42 +528,31 @@ exports[`AccountHeader renders without error 1`] = `
}
suppressHighlighting={true}
>
.cantswim.eth
.uni.eth
</Text>
</View>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={20}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"paddingLeft": 8,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -108,48 +108,31 @@ exports[`AccountList renders without error 1`] = `
onPress={[Function]}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 12,
"paddingLeft": 24,
"paddingRight": 24,
"paddingTop": 8,
"transform": [
{
"scale": 1,
},
],
}
}
>
......@@ -403,7 +386,6 @@ exports[`AccountList renders without error 1`] = `
{
"alignItems": "center",
"flexDirection": "row",
"gap": 2,
"justifyContent": "center",
}
}
......
......@@ -42,7 +42,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element {
return (
<Button icon={isCopied ? copiedIcon : copyIcon} theme="tertiary" onPress={onPress}>
{isCopied ? t`Copied` : t`Copy`}
{isCopied ? t`Copied!` : t`Copy`}
</Button>
)
}
......@@ -264,7 +264,7 @@ function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null {
px="$spacing12"
zIndex={1}>
{hasFavoritedTokens && <FavoriteTokensGrid {...props} />}
{hasFavoritedWallets && <FavoriteWalletsGrid showLoading={props.showLoading} />}
{hasFavoritedWallets && <FavoriteWalletsGrid {...props} />}
</Flex>
)
}
......
......@@ -2,18 +2,11 @@ import { ImpactFeedbackStyle } from 'expo-haptics'
import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view'
import {
FadeIn,
SharedValue,
interpolate,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import { FadeIn, SharedValue } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
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 { disableOnPress } from 'src/utils/disableOnPress'
import { usePollOnFocusOnly } from 'src/utils/hooks'
......@@ -55,8 +48,6 @@ function FavoriteTokenCard({
const dispatch = useAppDispatch()
const tokenDetailsNavigation = useTokenDetailsNavigation()
const { convertFiatAmountFormatted } = useLocalizationContext()
const dragAnimationProgress = useSharedValue(0)
const wasTouched = useSharedValue(false)
const { data, networkStatus, startPolling, stopPolling } = useFavoriteTokenCardQuery({
variables: currencyIdToContractInput(currencyId),
......@@ -103,45 +94,14 @@ function FavoriteTokenCard({
tokenDetailsNavigation.navigate(currencyId)
}
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
}
}
}
)
const animatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(dragAnimationProgress.value, [0, 1], [1, 0.5]),
}))
const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress)
if (isNonPollingRequestInFlight(networkStatus)) {
return <Loader.Favorite height={FAVORITE_TOKEN_CARD_LOADER_HEIGHT} />
}
return (
<AnimatedFlex style={animatedStyle}>
<AnimatedFlex style={animatedDragStyle}>
<ContextMenu
actions={menuActions}
disabled={isEditing}
......
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 { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view'
import { SharedValue } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks'
import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks'
import { useAnimatedCardDragStyle } from 'src/components/explore/hooks'
import RemoveButton from 'src/components/explore/RemoveButton'
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 { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
......@@ -19,12 +21,16 @@ import { DisplayNameType } from 'wallet/src/features/wallet/types'
type FavoriteWalletCardProps = {
address: Address
isEditing?: boolean
isTouched: SharedValue<boolean>
dragActivationProgress: SharedValue<number>
setIsEditing: (update: boolean) => void
} & ViewProps
export default function FavoriteWalletCard({
function FavoriteWalletCard({
address,
isEditing,
isTouched,
dragActivationProgress,
setIsEditing,
...rest
}: FavoriteWalletCardProps): JSX.Element {
......@@ -51,7 +57,10 @@ export default function FavoriteWalletCard({
]
}, [t])
const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress)
return (
<AnimatedFlex style={animatedDragStyle}>
<ContextMenu
actions={menuActions}
disabled={isEditing}
......@@ -70,7 +79,10 @@ export default function FavoriteWalletCard({
{...rest}>
<TouchableArea
hapticFeedback
activeOpacity={isEditing ? 1 : undefined}
backgroundColor="$surface2"
borderRadius="$rounded16"
disabled={isEditing}
hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4"
onLongPress={disableOnPress}
......@@ -97,5 +109,8 @@ export default function FavoriteWalletCard({
</BaseCard.Shadow>
</TouchableArea>
</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 { FadeIn } from 'react-native-reanimated'
import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import { useAppSelector } from 'src/app/hooks'
import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow'
import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard'
import { Loader } from 'src/components/loading'
import {
AutoScrollProps,
SortableGrid,
SortableGridChangeEvent,
SortableGridRenderItem,
} from 'src/components/sortableGrid'
import { AnimatedFlex, Flex } from 'ui/src'
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 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 */
export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }): JSX.Element {
export function FavoriteWalletsGrid({
showLoading,
...rest
}: FavoriteWalletsGridProps): JSX.Element {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const [isEditing, setIsEditing] = useState(false)
const isTokenDragged = useSharedValue(false)
const watchedWalletsSet = useAppSelector(selectWatchedAddressSet)
const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet])
......@@ -27,8 +43,33 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }):
}
}, [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 (
<AnimatedFlex entering={FadeIn}>
<AnimatedFlex entering={FadeIn} style={animatedStyle}>
<FavoriteHeaderRow
editingTitle={t('Edit favorite wallets')}
isEditing={isEditing}
......@@ -38,17 +79,21 @@ export function FavoriteWalletsGrid({ showLoading }: { showLoading: boolean }):
{showLoading ? (
<FavoriteWalletsGridLoader />
) : (
<Flex row flexWrap="wrap">
{watchedWalletsList.map((address) => (
<FavoriteWalletCard
key={address}
address={address}
isEditing={isEditing}
setIsEditing={setIsEditing}
style={HALF_WIDTH}
<SortableGrid
{...rest}
activeItemOpacity={1}
data={watchedWalletsList}
editable={isEditing}
numColumns={NUM_COLUMNS}
renderItem={renderItem}
onChange={handleOrderChange}
onDragEnd={(): void => {
isTokenDragged.value = false
}}
onDragStart={(): void => {
isTokenDragged.value = true
}}
/>
))}
</Flex>
)}
</AnimatedFlex>
)
......
import { SharedEventName } from '@uniswap/analytics-events'
import { useCallback, useMemo } from 'react'
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 {
AnimateStyle,
SharedValue,
interpolate,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks'
import { openModal } from 'src/features/modals/modalSlice'
......@@ -148,3 +156,44 @@ export function useExploreTokenContextMenu({
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 {
searchHistory.map((historyItem: SearchResult) => {
if (!unitagFeatureFlagEnabled && historyItem.type === SearchResultType.Unitag) {
return {
type: SearchResultType.ENSAddress,
type: SearchResultType.WalletByAddress,
address: historyItem.address,
searchId: historyItem.searchId,
}
......
......@@ -4,11 +4,13 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader'
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 { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem'
import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnitagItem'
import { SearchWalletByAddressItem } from 'src/components/explore/search/items/SearchWalletByAddressItem'
import {
formatNFTCollectionSearchResults,
formatTokenSearchResults,
......@@ -19,16 +21,12 @@ import { logger } from 'utilities/src/logger/logger'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains'
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 {
NFTCollectionSearchResult,
SearchResultType,
TokenSearchResult,
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 i18n from 'wallet/src/i18n/i18n'
import { getValidAddress } from 'wallet/src/utils/addresses'
import { SEARCH_RESULT_HEADER_KEY } from './constants'
......@@ -66,6 +64,10 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
variables: { searchQuery, nftCollectionsFilter: { nameQuery: searchQuery } },
})
const onRetry = useCallback(async () => {
await refetch()
}, [refetch])
const tokenResults = useMemo<TokenSearchResult[] | undefined>(() => {
if (!searchResultsData || !searchResultsData.searchTokens) {
return
......@@ -74,6 +76,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return formatTokenSearchResults(searchResultsData.searchTokens, searchQuery)
}, [searchQuery, searchResultsData])
// Search for matching NFT collections
const nftCollectionResults = useMemo<NFTCollectionSearchResult[] | undefined>(() => {
if (!searchResultsData || !searchResultsData.nftCollections) {
return
......@@ -82,86 +86,25 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
return formatNFTCollectionSearchResults(searchResultsData.nftCollections)
}, [searchResultsData])
// Search for matching ENS
const {
address: ensAddress,
name: ensName,
loading: ensLoading,
} = useENS(ChainId.Mainnet, searchQuery, true)
// Search for matching wallets
// Search for matching Unitag by name
const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(searchQuery)
const {
wallets: walletSearchResults,
loading: walletsLoading,
exactENSMatch,
exactUnitagMatch,
} = useWalletSearchResults(searchQuery)
const validAddress: Address | undefined = useMemo(
() => getValidAddress(searchQuery, true, false) ?? undefined,
[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 countNftCollectionResults = nftCollectionResults?.length ?? 0
const countWalletResults = walletSearchResults.length
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) =>
isPrefixTokenMatch(res, searchQuery)
)
......@@ -175,8 +118,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified))
const showWalletSectionFirst =
unitagByName || unitagByAddress || (exactENSMatch && !prefixTokenMatch)
const showWalletSectionFirst = exactUnitagMatch || (exactENSMatch && !prefixTokenMatch)
const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults
const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => {
......@@ -294,6 +236,8 @@ export const renderSearchItem = ({
return <SearchENSAddressItem searchContext={searchContext} searchResult={searchResult} />
case SearchResultType.Unitag:
return <SearchUnitagItem searchContext={searchContext} searchResult={searchResult} />
case SearchResultType.WalletByAddress:
return <SearchWalletByAddressItem searchContext={searchContext} searchResult={searchResult} />
case SearchResultType.NFTCollection:
return <SearchNFTCollectionItem collection={searchResult} searchContext={searchContext} />
case SearchResultType.Etherscan:
......
......@@ -49,7 +49,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{
"alignItems": "center",
"flexDirection": "row",
"flexShrink": 1,
"flexGrow": 1,
"gap": 12,
"overflow": "hidden",
}
......@@ -74,7 +74,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{
"alignItems": "flex-start",
"flexDirection": "column",
"flexShrink": 1,
"flexGrow": 1,
}
}
>
......@@ -228,7 +228,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{
"alignItems": "center",
"flexDirection": "row",
"flexShrink": 1,
"flexGrow": 1,
"gap": 12,
"overflow": "hidden",
}
......@@ -253,7 +253,7 @@ exports[`SearchPopularTokens renders without error 1`] = `
{
"alignItems": "flex-start",
"flexDirection": "column",
"flexShrink": 1,
"flexGrow": 1,
}
}
>
......@@ -405,7 +405,7 @@ exports[`SearchPopularTokens renders without error 2`] = `
"name": "Ethereum",
"safetyLevel": "VERIFIED",
"symbol": "ETH",
"type": 2,
"type": 1,
},
{
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
......@@ -414,7 +414,7 @@ exports[`SearchPopularTokens renders without error 2`] = `
"name": "USD Coin",
"safetyLevel": "VERIFIED",
"symbol": "USDC",
"type": 2,
"type": 1,
},
]
}
......@@ -468,44 +468,27 @@ exports[`SearchPopularTokens renders without error 2`] = `
onPress={[Function]}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
testID="search-token-item"
......@@ -667,44 +650,27 @@ exports[`SearchPopularTokens renders without error 2`] = `
onPress={[Function]}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
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({
// 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
const { address, ensName, primaryENSName: savedPrimaryENSName } = searchResult
const { address, ensName, primaryENSName: savedPrimaryENSName, isRawName } = searchResult
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
* ENS different than the `ensName` searched
* 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"
*/
const completedENSName = getCompletedENSName(ensName ?? null)
const { data: fetchedPrimaryENSName, loading: isFetchingPrimaryENSName } = useENSName(
savedPrimaryENSName ? undefined : address
)
const primaryENSName = savedPrimaryENSName ?? fetchedPrimaryENSName
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)
......@@ -55,11 +60,13 @@ export function SearchENSAddressItem({
variant="body1">
{completedENSName || formattedAddress}
</Text>
{showOwnedBy ? (
{showSecondLine ? (
<Text color="$neutral2" ellipsizeMode="tail" numberOfLines={1} variant="subheading2">
{t('Owned by {{owner}}', {
{showOwnedBy &&
t('Owned by {{owner}}', {
owner: primaryENSName || formattedAddress,
})}
{showAddress && formattedAddress}
</Text>
) : null}
</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'
import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors'
import { SearchContext } from 'wallet/src/features/search/SearchContext'
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 = {
searchResult: WalletSearchResult
......@@ -33,31 +37,37 @@ export function SearchWalletItemBase({
const onPress = (): void => {
navigate(address)
if (searchContext) {
const walletName =
type === SearchResultType.Unitag
? searchResult.unitag
: type === SearchResultType.ENSAddress
? searchResult.ensName
: undefined
sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, {
query: searchContext.query,
name:
type === SearchResultType.Unitag ? searchResult.unitag : searchResult.ensName ?? address,
name: walletName,
address,
type: 'address',
domain: walletName ? extractDomain(walletName, type) : undefined,
suggestion_count: searchContext.suggestionCount,
position: searchContext.position,
isHistory: searchContext.isHistory,
})
}
if (type === SearchResultType.Unitag) {
if (type === SearchResultType.ENSAddress) {
dispatch(
addToSearchHistory({
searchResult,
searchResult: {
...searchResult,
primaryENSName: searchResult.primaryENSName,
},
})
)
} else {
dispatch(
addToSearchHistory({
searchResult: {
...searchResult,
primaryENSName: searchResult.primaryENSName ?? undefined,
},
searchResult,
})
)
}
......
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 { TransactionLoader } from 'src/components/loading/TransactionLoader'
import { WalletLoader } from 'src/components/loading/WalletLoader'
import { WaveLoader } from 'src/components/loading/WaveLoader'
import { Flex, FlexLoader, FlexLoaderProps, getToken, Skeleton } from 'ui/src'
......@@ -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({
repeat = 1,
}: {
......@@ -72,7 +57,6 @@ function Favorite({ height, contrast }: { height?: number; contrast?: boolean })
export const Loader = {
Box,
Transaction,
Wallets,
Graph,
Image,
Favorite,
......
......@@ -30,6 +30,9 @@ export function useAnimatedZIndex(renderIndex: number): SharedValue<number> {
previousActiveIndex: previousActiveIndexValue.value,
}),
({ touchedIndex, previousActiveIndex }) => {
if (touchedIndex === null) {
return null
}
if (renderIndex === touchedIndex) {
// Display the currently touched item on top of all other items
zIndexValue.value = 10000
......
......@@ -20,7 +20,8 @@ import { parseUnitagErrorCode } from 'wallet/src/features/unitags/utils'
import { useWalletSigners } from 'wallet/src/features/wallet/context'
import { useAccount } from 'wallet/src/features/wallet/hooks'
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'
export function ChangeUnitagModal({
......@@ -121,6 +122,7 @@ export function ChangeUnitagModal({
// If change succeeded, exit the modal and display a success message
if (changeResponse.success) {
sendWalletAnalyticsEvent(UnitagEventName.UnitagChanged)
triggerRefetchUnitags()
dispatch(
pushNotification({
......@@ -230,7 +232,7 @@ export function ChangeUnitagModal({
returnKeyType="done"
value={newUnitag}
width="100%"
onChangeText={setNewUnitag}
onChangeText={(text: string) => setNewUnitag(text.trim().toLowerCase())}
onSubmitEditing={onFinishEditing}
/>
<Flex position="absolute" right="$spacing20" top="$spacing20">
......@@ -259,7 +261,7 @@ export function ChangeUnitagModal({
width="100%">
<Text color="$neutral2" variant="body3">
{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>
</Flex>
......
......@@ -2,7 +2,7 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { selectPhotoFromLibrary } from 'src/components/unitags/AvatarSelection'
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 { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants'
......@@ -23,7 +23,6 @@ export const ChoosePhotoOptionsModal = ({
showRemoveOption,
}: ChoosePhotoOptionsProps): JSX.Element => {
const colors = useSporeColors()
const { t } = useTranslation()
const [showNftsList, setShowNftsList] = useState(false)
const onPressNftsList = async (): Promise<void> => {
......@@ -42,10 +41,11 @@ export const ChoosePhotoOptionsModal = ({
const onPressCameraRoll = async (): Promise<void> => {
const selectedPhoto = await selectPhotoFromLibrary()
// Close needs to happen before setting the photo, otherwise the handler can get cut short
onClose()
if (selectedPhoto) {
setPhotoUri(selectedPhoto)
}
onClose()
}
const options = [
......@@ -88,16 +88,6 @@ export const ChoosePhotoOptionsModal = ({
</Flex>
))}
</Flex>
<Flex centered row>
<Button
fill
backgroundColor="$surface1"
color="$accent1"
theme="secondary"
onPress={onClose}>
{t('Close')}
</Button>
</Flex>
</Flex>
</BottomSheetModal>
{showNftsList && address && (
......
......@@ -10,7 +10,8 @@ import { useUnitagUpdater } from 'wallet/src/features/unitags/context'
import { useWalletSigners } from 'wallet/src/features/wallet/context'
import { useAccount } from 'wallet/src/features/wallet/hooks'
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({
unitag,
......@@ -51,6 +52,7 @@ export function DeleteUnitagModal({
}
if (deleteResponse?.success) {
sendWalletAnalyticsEvent(UnitagEventName.UnitagRemoved)
triggerRefetchUnitags()
dispatch(
pushNotification({
......
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Keyboard, StyleProp, ViewStyle } from 'react-native'
import { useAppDispatch } from 'src/app/hooks'
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 { borderRadii, iconSizes, spacing } from 'ui/src/theme'
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_SCREEN_WIDTH_PROPORTION = 0.2
const COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION = 0.16
const IMAGE_ASPECT_RATIO = 0.42
const IMAGE_SCREEN_WIDTH_PROPORTION = 0.18
const COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION = 0.15
export function UnitagBanner({
address,
compact,
entryPoint,
}: {
address: Address
compact?: boolean
entryPoint: Screens.Home | Screens.Settings
}): JSX.Element {
const dispatch = useAppDispatch()
const { t } = useTranslation()
const { fullWidth } = useDeviceDimensions()
const isDarkMode = useIsDarkMode()
const colors = useSporeColors()
const imageWidth = compact
? COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth
: IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth
const imageHeight = imageWidth / IMAGE_ASPECT_RATIO
const analyticsEntryPoint = entryPoint === Screens.Home ? 'home' : 'settings'
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 => {
sendWalletAnalyticsEvent(UnitagEventName.UnitagBannerActionTaken, {
action: 'dismiss',
entryPoint: analyticsEntryPoint,
})
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 (
<Flex
grow
row
alignContent="space-between"
backgroundColor={compact ? '$surface2' : '$background'}
borderColor="$surface3"
borderRadius="$rounded16"
borderWidth={compact ? undefined : '$spacing1'}
mt="$spacing12"
overflow="hidden"
pl="$spacing16"
py="$spacing16"
shadowColor="$neutral3"
shadowOpacity={0.4}
shadowRadius="$spacing4">
{compact ? (
<Flex
fill
row
$short={{ mr: '$spacing32' }}
gap="$spacing16"
justifyContent="space-between"
p="$spacing16"
onPress={onPressClaimNow}>
<Flex row gap="$none">
<Text color="$neutral2" variant="subheading2">
<Text color="$accent1" variant="buttonLabel3">
{t('Claim a username ')}
{t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
</Text>
{t('to create a public username and customizable profile.')}
{t(' and build out your customizable profile.')}
</Text>
</Flex>
</Flex>
) : (
<Flex fill gap="$spacing16" justifyContent="space-between" p="$spacing16">
<Flex gap="$spacing8">
<Text variant="subheading2">{t('Claim your Uniswap username')}</Text>
<Flex fill gap="$spacing16" justifyContent="space-between">
<Flex gap="$spacing4">
<Text variant="subheading2">
{t('Claim your {{unitagSuffix}} username', {
unitagSuffix: UNITAG_SUFFIX_NO_LEADING_DOT,
})}
</Text>
<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>
</Flex>
<Flex row gap="$spacing8">
<Button
borderRadius="$rounded24"
fontSize="$small"
<Flex row gap="$spacing2">
{/* TODO: replace with Button when it's extensible enough to accommodate designs */}
<TouchableArea
style={{
...baseButtonStyle,
backgroundColor: colors.accent1.get(),
}}
testID={ElementName.Confirm}
theme="primary"
onPress={onPressClaimNow}>
<Text color="white" variant="buttonLabel4">
{t('Claim now')}
</Button>
<Button
backgroundless
borderRadius="$rounded24"
color="$neutral3"
fontSize="$small"
</Text>
</TouchableArea>
<TouchableArea
style={{
...baseButtonStyle,
backgroundColor: colors.transparent.get(),
}}
testID={ElementName.Cancel}
theme="secondary"
onPress={onPressMaybeLater}>
<Text color="$neutral3" variant="buttonLabel4">
{t('Maybe later')}
</Button>
</Text>
</TouchableArea>
</Flex>
</Flex>
)}
<Flex justifyContent={compact ? 'flex-start' : 'center'} width={imageWidth}>
<Flex mr={compact ? -(imageWidth / 6) : -(imageWidth / 12)} width={imageWidth}>
<Image
alignSelf="center"
position="absolute"
......@@ -105,6 +151,7 @@ export function UnitagBanner({
height: imageHeight,
uri: isDarkMode ? UNITAGS_BANNER_VERTICAL_DARK : UNITAGS_BANNER_VERTICAL_LIGHT,
}}
top={compact ? -(imageHeight * 0.19) : -(imageHeight * 0.22)}
/>
</Flex>
</Flex>
......
......@@ -2,21 +2,23 @@ import React from 'react'
import { Flex, useSporeColors } from 'ui/src'
import { isSVGUri } from 'utilities/src/format/urls'
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 { RemoteSvg } from 'wallet/src/features/images/RemoteSvg'
import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
export function UnitagProfilePicture({
address,
profilePictureUri,
unitagAvatarUri,
size,
}: {
address: Address
size: number
profilePictureUri?: string
unitagAvatarUri?: string
}): JSX.Element {
const colors = useSporeColors()
const { data: ensAvatar } = useENSAvatar(address)
return profilePictureUri ? (
return unitagAvatarUri ? (
<Flex
shrink
backgroundColor="$surface1"
......@@ -27,22 +29,23 @@ export function UnitagProfilePicture({
shadowOpacity={0.4}
shadowRadius="$spacing4"
width={size}>
{isSVGUri(profilePictureUri) ? (
<RemoteSvg
{isSVGUri(unitagAvatarUri) ? (
<RemoteImage
backgroundColor={colors.surface1.val}
height={size}
imageHttpUrl={profilePictureUri}
uri={unitagAvatarUri}
width={size}
/>
) : (
<ImageUri resizeMode="cover" uri={profilePictureUri} />
<ImageUri resizeMode="cover" uri={unitagAvatarUri} />
)}
</Flex>
) : (
<AccountIcon
address={address}
avatarUri={profilePictureUri}
avatarUri={ensAvatar}
showBackground={true}
showBorder={true}
size={size}
/>
)
......
......@@ -16,15 +16,15 @@ export const UnitagWithProfilePicture = ({
<Flex centered gap={-spacing.spacing24}>
<UnitagProfilePicture
address={address}
profilePictureUri={profilePictureUri}
size={imageSizes.image100}
unitagAvatarUri={profilePictureUri}
/>
<Flex
row
backgroundColor="$surface1"
borderRadius="$rounded32"
px="$spacing12"
py="$spacing8"
py="$spacing16"
shadowColor="$neutral3"
shadowOpacity={0.4}
shadowRadius="$spacing4"
......
import { SharedEventName } from '@uniswap/analytics-events'
import React from 'react'
import { useTranslation } from 'react-i18next'
import 'react-native-reanimated'
......@@ -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 { iconSizes } from 'ui/src/theme'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ModalName } from 'wallet/src/telemetry/constants'
export function UnitagsIntroModal(): JSX.Element {
const { t } = useTranslation()
const isDarkMode = useIsDarkMode()
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 => {
appDispatch(closeModal({ name: ModalName.UnitagsIntro }))
}
const onPressClaimOneNow = (): void => {
if (!entryPoint) {
throw new Error('Missing entry point in UnitagsIntroModal')
}
navigate(Screens.UnitagStack, {
screen: UnitagScreens.ClaimUnitag,
params: {
entryPoint: Screens.Home,
entryPoint,
address,
},
})
if (address) {
sendWalletAnalyticsEvent(SharedEventName.TERMS_OF_SERVICE_ACCEPTED, { address })
}
onClose()
}
......@@ -38,10 +48,10 @@ export function UnitagsIntroModal(): JSX.Element {
<BottomSheetModal name={ModalName.UnitagsIntro} onClose={onClose}>
<Flex gap="$spacing24" px="$spacing24" py="$spacing16">
<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">
{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>
</Flex>
......@@ -57,7 +67,7 @@ export function UnitagsIntroModal(): JSX.Element {
<BodyItem Icon={Icons.Ticket} title={t('Free to claim')} />
<BodyItem Icon={Icons.Lightning} title={t('Powered by ENS subdomains')} />
</Flex>
<Flex gap="$spacing8" mt="$spacing16">
<Flex gap="$spacing8">
<Button size="medium" theme="primary" onPress={onPressClaimOneNow}>
{t('Continue')}
</Button>
......
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { Keyboard, TextInput } from 'react-native'
import { PasswordInput } from 'src/components/input/PasswordInput'
import { PasswordError } from 'src/features/onboarding/PasswordError'
import { Button, Flex, Icons, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { useDebounce } from 'utilities/src/time/timing'
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 {
WeakPassword = 'WeakPassword',
......@@ -29,9 +36,17 @@ export function CloudBackupPasswordForm({
const passwordInputRef = useRef<TextInput>(null)
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 => {
if (isConfirmation && newPassword === password) {
......@@ -39,15 +54,15 @@ export function CloudBackupPasswordForm({
}
// always reset error if not confirmation
if (!isConfirmation) {
setPasswordStrength(getPasswordStrength(newPassword))
setError(undefined)
}
setPassword(newPassword)
}
const onPasswordSubmitEditing = (): void => {
const { valid, validationErrorString } = validatePassword(password)
if (!isConfirmation && !valid) {
setError(validationErrorString || PasswordErrors.WeakPassword)
if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return
}
if (isConfirmation && passwordToConfirm !== password) {
......@@ -59,9 +74,8 @@ export function CloudBackupPasswordForm({
}
const onPressNext = (): void => {
const { valid, validationErrorString } = validatePassword(password)
if (!isConfirmation && !valid) {
setError(validationErrorString || PasswordErrors.WeakPassword)
if (!isConfirmation && !isStrongPassword) {
setError(PasswordErrors.WeakPassword)
return
}
if (isConfirmation && passwordToConfirm !== password) {
......@@ -99,6 +113,7 @@ export function CloudBackupPasswordForm({
}}
onSubmitEditing={onPasswordSubmitEditing}
/>
{!isConfirmation && <PasswordStrengthText strength={debouncedPasswordStrength} />}
{error ? <PasswordError errorText={errorText} /> : null}
</Flex>
{!isConfirmation && (
......@@ -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 {
EditAccountAction,
editAccountActions,
} 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 { isAndroid } from 'wallet/src/utils/platform'
......@@ -44,6 +44,11 @@ export function CloudBackupProcessingAnimation({
const colors = useSporeColors()
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)
// Handle finished backing up to Cloud
......@@ -60,7 +65,7 @@ export function CloudBackupProcessingAnimation({
const backup = useCallback(async () => {
try {
// 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(
editAccountActions.trigger({
......@@ -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
......
......@@ -40,10 +40,14 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme
}, [address])
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' } })
)
}, [])
}, [address])
const onPressShare = useCallback(async () => {
if (!address) {
......
......@@ -22,11 +22,14 @@ import {
useIsDarkMode,
useSporeColors,
useUniconColors,
useUniconV2Colors,
} from 'ui/src'
import { ENS_LOGO } from 'ui/src/assets'
import { iconSizes, imageSizes, spacing } from 'ui/src/theme'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
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 { CurrencyField } from 'wallet/src/features/transactions/transactionState/types'
import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks'
......@@ -67,21 +70,29 @@ export const ProfileHeader = memo(function ProfileHeader({
const showENSName = primaryENSName && primaryENSName !== displayName?.name
const { colors: avatarColors } = useExtractedColors(avatar)
const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2)
const hasAvatar = !!avatar && !avatarLoading
// Unicon colors
const { gradientStart: uniconGradientStart, gradientEnd: uniconGradientEnd } =
useUniconColors(address)
// UniconV2 colors
const { color } = useUniconV2Colors(address)
// 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)) {
return [colors.surface1.val, colors.surface1.val]
}
if (hasAvatar && avatarColors && avatarColors.base) {
return [avatarColors.base, avatarColors.base]
}
return [uniconGradientStart, uniconGradientEnd]
return [
isUniconsV2Enabled ? color : uniconGradientStart,
isUniconsV2Enabled ? color : uniconGradientEnd,
]
}, [
avatarColors,
hasAvatar,
......@@ -89,6 +100,8 @@ export const ProfileHeader = memo(function ProfileHeader({
colors.surface1,
uniconGradientEnd,
uniconGradientStart,
color,
isUniconsV2Enabled,
])
const onPressFavorite = useToggleWatchedWalletCallback(address)
......@@ -153,7 +166,11 @@ export const ProfileHeader = memo(function ProfileHeader({
style={StyleSheet.absoluteFill}
/>
</Flex>
{hasAvatar && avatarColors?.primary ? <HeaderRadial color={avatarColors.primary} /> : null}
{hasAvatar && avatarColors?.primary ? (
<HeaderRadial color={avatarColors.primary} />
) : (
<HeaderRadial color={isUniconsV2Enabled ? color : uniconGradientStart} />
)}
</AnimatedFlex>
{/* header row */}
......@@ -270,9 +287,13 @@ export const ProfileHeader = memo(function ProfileHeader({
export const HeaderRadial = memo(function HeaderRadial({
color,
borderRadius,
minOpacity,
maxOpacity,
}: {
color: string
borderRadius?: number
minOpacity?: number
maxOpacity?: number
}): JSX.Element {
return (
<Svg height="100%" width="100%">
......@@ -281,8 +302,8 @@ export const HeaderRadial = memo(function HeaderRadial({
<Rect height="100%" rx={borderRadius} width="100%" />
</ClipPath>
<RadialGradient cy="-0.1" id="background" rx="0.8" ry="1.1">
<Stop offset="0" stopColor={color} stopOpacity="0.6" />
<Stop offset="1" stopColor={color} stopOpacity="0" />
<Stop offset="0" stopColor={color} stopOpacity={maxOpacity ?? '0.6'} />
<Stop offset="1" stopColor={color} stopOpacity={minOpacity ?? '0'} />
</RadialGradient>
</Defs>
<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({
const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2)
return (
<Flex gap="$spacing16" onLayout={onInputPanelLayout}>
<Flex onLayout={onInputPanelLayout}>
<Flex
grow
alignItems="center"
......@@ -137,7 +137,7 @@ export function FiatOnRampAmountSection({
<AnimatedFlex
height={spacing.spacing24}
/* We want to reserve the space here, so when error occurs - layout does not jump */
mt="$spacing48">
mt={appFiatCurrencySupported ? '$spacing48' : '$spacing24'}>
{debouncedErrorText && errorColor && (
<Text color={errorColor} textAlign="center" variant="buttonLabel4">
{debouncedErrorText}
......
......@@ -11,6 +11,7 @@ import {
import { iconSizes } from 'ui/src/theme'
export const SERVICE_PROVIDER_ICON_SIZE = 90
export const SERVICE_PROVIDER_ICON_BORDER_RADIUS = 20
export function FiatOnRampConnectingView({
amount,
......@@ -18,7 +19,7 @@ export function FiatOnRampConnectingView({
serviceProviderName,
serviceProviderLogo,
}: {
amount: string
amount?: string
quoteCurrencyCode?: string
serviceProviderName: string
serviceProviderLogo?: JSX.Element
......@@ -48,7 +49,7 @@ export function FiatOnRampConnectingView({
<Text variant="subheading1">
{t('Connecting you to {{serviceProvider}}', { serviceProvider: serviceProviderName })}
</Text>
{quoteCurrencyCode && (
{quoteCurrencyCode && amount && (
<Text color="$neutral2" variant="body2">
{t('Buying {{amount}} worth of {{quoteCurrencyCode}}', {
amount,
......@@ -73,7 +74,7 @@ const styles = StyleSheet.create({
},
uniswapLogoWrapper: {
backgroundColor: '#FFEFF8', // #FFD8EF with 40% opacity on a white background
borderRadius: 20,
borderRadius: SERVICE_PROVIDER_ICON_BORDER_RADIUS,
height: SERVICE_PROVIDER_ICON_SIZE,
width: SERVICE_PROVIDER_ICON_SIZE,
},
......
......@@ -20,12 +20,12 @@ import { fonts, iconSizes, spacing } from 'ui/src/theme'
import { bubbleToTop } from 'utilities/src/primitives/array'
import { useDebounce } from 'utilities/src/time/timing'
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 { FORSupportedCountry } from 'wallet/src/features/fiatOnRamp/types'
import { getCountryFlagSvgUrl } from 'wallet/src/features/fiatOnRamp/utils'
import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput'
import { ModalName } from 'wallet/src/telemetry/constants'
import { isIOS } from 'wallet/src/utils/platform'
const ICON_SIZE = 32 // design prefers a custom value here
......@@ -95,7 +95,7 @@ function CountrySelectorContent({
)
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">
{t('Select your region')}
</Text>
......@@ -106,7 +106,6 @@ function CountrySelectorContent({
value={searchText}
onChangeText={setSearchText}
/>
{true && (
<Flex grow>
<AnimatedFlex grow entering={FadeIn} exiting={FadeOut}>
{isLoading ? (
......@@ -117,6 +116,7 @@ function CountrySelectorContent({
bounces={true}
contentContainerStyle={{ paddingBottom: insets.bottom + spacing.spacing12 }}
data={filteredData}
focusHook={useBottomSheetFocusHook}
keyExtractor={key}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
......@@ -127,7 +127,6 @@ function CountrySelectorContent({
)}
</AnimatedFlex>
</Flex>
)}
</Flex>
)
}
......@@ -165,6 +164,7 @@ export function FiatOnRampCountryListModal({
fullScreen
hideKeyboardOnDismiss
hideKeyboardOnSwipeDown
renderBehindBottomInset
backgroundColor={colors.surface1.get()}
name={ModalName.FiatOnRampCountryList}
snapPoints={FOR_MODAL_SNAP_POINTS}
......
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, TextInput } from 'react-native'
import {
FadeIn,
FadeOut,
FadeOutDown,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated'
import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated'
import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks'
import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton'
import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection'
......@@ -16,14 +9,13 @@ import {
FiatOnRampConnectingView,
SERVICE_PROVIDER_ICON_SIZE,
} from 'src/features/fiatOnRamp/FiatOnRampConnecting'
import { FiatOnRampTokenSelector } from 'src/features/fiatOnRamp/FiatOnRampTokenSelector'
import { useMoonpayFiatOnRamp } from 'src/features/fiatOnRamp/hooks'
import { useMoonpayFiatOnRamp, useMoonpaySupportedTokens } from 'src/features/fiatOnRamp/hooks'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { closeModal } from 'src/features/modals/modalSlice'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants'
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 { NumberType } from 'utilities/src/format/types'
import { useTimeout } from 'utilities/src/time/timing'
......@@ -31,15 +23,16 @@ import { TextInputProps } from 'wallet/src/components/input/TextInput'
import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy'
import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { HandleBar } from 'wallet/src/components/modals/HandleBar'
import { getNativeAddress } from 'wallet/src/constants/addresses'
import { ChainId } from 'wallet/src/constants/chains'
import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
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 { buildCurrencyId } from 'wallet/src/utils/currencyId'
import { openUri } from 'wallet/src/utils/linking'
import { FiatOnRampTokenSelectorModal } from './FiatOnRampTokenSelector'
const MOONPAY_UNSUPPORTED_REGION_HELP_URL =
'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 {
return (
<BottomSheetModal
fullScreen
hideHandlebar
hideKeyboardOnDismiss
renderBehindTopInset
backgroundColor={colors.surface1.get()}
name={ModalName.FiatOnRamp}
onClose={onClose}>
......@@ -70,7 +65,6 @@ export function FiatOnRampModal(): JSX.Element {
function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
const { t } = useTranslation()
const { fullWidth } = useDeviceDimensions()
const { formatNumberOrString } = useLocalizationContext()
const inputRef = useRef<TextInput>(null)
......@@ -170,25 +164,21 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
}
}, [showNativeKeyboard, eligible, showTokenSelector])
const hideInnerContentRouter = showTokenSelector
const screenXOffset = useSharedValue(hideInnerContentRouter ? -fullWidth : 0)
useEffect(() => {
const screenOffset = showTokenSelector ? 1 : 0
screenXOffset.value = withSpring(-(fullWidth * screenOffset), ANIMATE_SPRING_CONFIG)
}, [screenXOffset, showTokenSelector, fullWidth])
const selectTokenLoading = quoteCurrencyAmountLoading && !errorText && !!value
const wrapperStyle = useAnimatedStyle(() => ({
transform: [{ translateX: screenXOffset.value }],
}))
const {
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 selectTokenLoading = quoteCurrencyAmountLoading && !errorText && !!value
const insets = useDeviceInsets()
return (
<>
<Flex grow pt={showConnectingToMoonpayScreen ? undefined : insets.top}>
{!showConnectingToMoonpayScreen && (
<AnimatedFlex row height="100%" pb="$spacing12" style={wrapperStyle}>
<AnimatedFlex row height="100%" pb="$spacing12">
{isSheetReady && (
<AnimatedFlex
entering={FadeIn}
......@@ -197,6 +187,7 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
pb="$spacing16"
px="$spacing24"
width="100%">
<HandleBar backgroundColor="none" />
<Text variant="subheading1">{t('Buy')}</Text>
<FiatOnRampAmountSection
appFiatCurrencySupported={appFiatCurrencySupportedInMoonpay}
......@@ -259,8 +250,12 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
</AnimatedFlex>
)}
{showTokenSelector && (
<FiatOnRampTokenSelector
onBack={(): void => setShowTokenSelector(false)}
<FiatOnRampTokenSelectorModal
error={supportedTokensError}
list={supportedTokensList}
loading={supportedTokensLoading}
onClose={(): void => setShowTokenSelector(false)}
onRetry={supportedTokensRefetch}
onSelectCurrency={(newCurrency: FiatOnRampCurrency): void => {
setCurrency(newCurrency)
setShowTokenSelector(false)
......@@ -291,7 +286,7 @@ function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element {
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 { TokenFiatOnRampList } from 'src/components/TokenSelector/TokenFiatOnRampList'
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 { AnimatedFlex } from 'ui/src'
import { 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'
import { AnimatedFlex, Flex, Text, useSporeColors } from 'ui/src'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ElementName, ModalName, SectionName } from 'wallet/src/telemetry/constants'
interface Props {
onBack: () => void
onSelectCurrency: (currency: FiatOnRampCurrency) => void
onRetry: () => void
onClose: () => void
error: boolean
loading: boolean
list: FiatOnRampCurrency[] | undefined
}
const findTokenOptionForMoonpayCurrency = (
commonBaseCurrencies: CurrencyInfo[] | undefined,
moonpayCurrency: MoonpayCurrency
): Maybe<CurrencyInfo> => {
return (commonBaseCurrencies || []).find((item) => {
const [code, network] = moonpayCurrency.code.split('_') ?? [undefined, undefined]
const chainId = fromMoonpayNetwork(network)
return (
item &&
code &&
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?.()
}
}
export function FiatOnRampTokenSelectorModal({
error,
list,
loading,
onClose,
onRetry,
onSelectCurrency,
}: { onClose: () => void } & Props): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
return (
<BottomSheetModal
extendOnKeyboardVisible
fullScreen
hideKeyboardOnDismiss
hideKeyboardOnSwipeDown
renderBehindBottomInset
backgroundColor={colors.surface1.get()}
name={ModalName.FiatOnRampCountryList}
snapPoints={FOR_MODAL_SNAP_POINTS}
onClose={onClose}>
<Trace
logImpression
element={ElementName.FiatOnRampTokenSelector}
section={SectionName.TokenSelector}>
<AnimatedFlex
entering={FadeIn}
exiting={FadeOut}
gap="$spacing12"
overflow="hidden"
px="$spacing16"
width="100%">
<Flex grow gap="$spacing16" px="$spacing16">
<Text color="$neutral1" mt="$spacing2" textAlign="center" variant="subheading1">
{t('Choose a token')}
</Text>
<AnimatedFlex grow entering={FadeIn} exiting={FadeOut}>
<TokenFiatOnRampList
error={error}
list={tokenList}
list={list}
loading={loading}
onBack={onBack}
onRetry={onRetry}
onSelectCurrency={onSelectCurrency}
/>
</AnimatedFlex>
</Flex>
</Trace>
</BottomSheetModal>
)
}
export const FiatOnRampTokenSelector = memo(_FiatOnRampTokenSelector)
......@@ -5,11 +5,14 @@ import { useTranslation } from 'react-i18next'
import { ListRenderItemInfo } from 'react-native'
import { getCountry } from 'react-native-localize'
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 { iconSizes } from 'ui/src/theme'
import { useFiatOnRampAggregatorTransferInstitutionsQuery } from 'wallet/src/features/fiatOnRamp/api'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
import { RemoteImage } from 'wallet/src/features/images/RemoteImage'
import { ModalName } from 'wallet/src/telemetry/constants'
function key(item: FORTransferInstitution): string {
return item.id as string
......@@ -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({
countryCode: getCountry(),
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onSelectTransferInstitution = useCallback((transferInstitution: FORTransferInstitution) => {
//TODO(MOB-2603): fetch widget and launch transfer flow
}, [])
const onSelectTransferInstitution = useCallback(
(transferInstitution: FORTransferInstitution) => {
dispatch(
openModal({
name: ModalName.ExchangeTransferModal,
initialState: { serviceProvider: transferInstitution },
})
)
onClose()
},
[dispatch, onClose]
)
const renderItem = useCallback(
({ item: institution }: ListRenderItemInfo<FORTransferInstitution>) => (
......
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency } from '@uniswap/sdk-core'
import { useCallback, useRef } from 'react'
import { useCallback, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppDispatch } from 'src/app/hooks'
import { Delay } from 'src/components/layout/Delayed'
import { ColorTokens, useSporeColors } from 'ui/src'
import { useDebounce } from 'utilities/src/time/timing'
import { useAllCommonBaseCurrencies } from 'wallet/src/components/TokenSelector/hooks'
import { ChainId } from 'wallet/src/constants/chains'
import { uniswapUrls } from 'wallet/src/constants/urls'
import { fromMoonpayNetwork } from 'wallet/src/features/chains/utils'
import { CurrencyInfo } from 'wallet/src/features/dataApi/types'
import {
useFiatOnRampAggregatorSupportedTokensQuery,
useFiatOnRampBuyQuoteQuery,
useFiatOnRampIpAddressQuery,
useFiatOnRampLimitsQuery,
......@@ -16,7 +20,7 @@ import {
useFiatOnRampWidgetUrlQuery,
} from 'wallet/src/features/fiatOnRamp/api'
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 { addTransaction } from 'wallet/src/features/transactions/slice'
import {
......@@ -29,6 +33,7 @@ import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hoo
import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency'
import { ValueType } from 'wallet/src/utils/getCurrencyAmount'
import { isAndroid } from 'wallet/src/utils/platform'
import { FiatOnRampCurrency } from './types'
export function useFormatExactCurrencyAmount(
currencyAmount: string,
......@@ -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(
hasError: boolean,
amountIsTooSmall: boolean,
......@@ -316,3 +282,143 @@ function useMoonpayError(
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 {
TextInput as NativeTextInput,
TextInputContentSizeChangeEventData,
} from 'react-native'
import { ColorTokens, Flex, useSporeColors } from 'ui/src'
import { ColorTokens, Flex } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { TextInput } from 'wallet/src/components/input/TextInput'
import { isAndroid } from 'wallet/src/utils/platform'
......@@ -56,7 +56,6 @@ function Inputs({
layerType,
...inputProps
}: Props & { layerType?: 'foreground' | 'background' }): JSX.Element {
const colors = useSporeColors()
const [isMultiline, setIsMultiline] = useState(false)
const handleContentSizeChange = useCallback(
......@@ -120,7 +119,7 @@ function Inputs({
py="$none"
returnKeyType="done"
scrollEnabled={false}
selectionColor={colors.neutral1.get()}
selectionColor="$neutral1"
spellCheck={false}
testID="import_account_form/input"
textAlign={isInputEmpty ? 'left' : backgroundTextAlignment}
......
......@@ -158,41 +158,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
}
>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"backgroundColor": "#FFEFFF",
......@@ -200,12 +176,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"onPressIn": undefined,
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 8,
"paddingLeft": 8,
"paddingRight": 8,
"paddingTop": 8,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
......@@ -2,6 +2,8 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
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 { ModalName } from 'wallet/src/telemetry/constants'
......@@ -12,6 +14,9 @@ export interface AppModalState<T> {
export interface ModalsState {
[ModalName.AccountSwitcher]: AppModalState<undefined>
[ModalName.ExchangeTransferModal]: AppModalState<{
serviceProvider: FORTransferInstitution
}>
[ModalName.Experiments]: AppModalState<undefined>
[ModalName.Explore]: AppModalState<ExploreModalState>
[ModalName.FiatCurrencySelector]: AppModalState<undefined>
......@@ -24,7 +29,10 @@ export interface ModalsState {
[ModalName.Scantastic]: AppModalState<ScantasticModalState>
[ModalName.Send]: 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.WalletConnectScan]: AppModalState<ScannerModalState>
}
......@@ -2,7 +2,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { Screens } from 'src/screens/Screens'
import { getKeys } from 'utilities/src/primitives/objects'
import { TransactionState } from 'wallet/src/features/transactions/transactionState/types'
import { ModalName } from 'wallet/src/telemetry/constants'
......@@ -13,6 +15,11 @@ type AccountSwitcherModalParams = {
initialState?: undefined
}
type ExchangeTransferModalParams = {
name: typeof ModalName.ExchangeTransferModal
initialState?: ExchangeTransferModalState
}
type ExperimentsModalParams = { name: typeof ModalName.Experiments; initialState?: undefined }
type ExploreModalParams = {
......@@ -65,7 +72,7 @@ type SendModalParams = { name: typeof ModalName.Send; initialState?: Transaction
type UnitagsIntroParams = {
name: typeof ModalName.UnitagsIntro
initialState?: { address: Address }
initialState?: { address: Address; entryPoint: Screens.Home | Screens.Settings }
}
type ViewOnlyExplainerParams = {
......@@ -75,6 +82,7 @@ type ViewOnlyExplainerParams = {
export type OpenModalParams =
| AccountSwitcherModalParams
| ExchangeTransferModalParams
| ExperimentsModalParams
| ExploreModalParams
| FiatCurrencySelectorParams
......@@ -94,6 +102,10 @@ export type OpenModalParams =
export type CloseModalParams = { name: keyof ModalsState }
export const initialModalState: ModalsState = {
[ModalName.ExchangeTransferModal]: {
isOpen: false,
initialState: undefined,
},
[ModalName.FiatOnRamp]: {
isOpen: false,
initialState: undefined,
......
......@@ -2,44 +2,27 @@
exports[`renders collection preview card 1`] = `
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
cancelable={true}
disabled={false}
focusable={true}
hitSlop={
{
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"flexDirection": "column",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
}
}
>
......
import { SharedEventName } from '@uniswap/analytics-events'
import { useAppDispatch } from 'src/app/hooks'
import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
......@@ -18,7 +19,7 @@ import {
} from 'wallet/src/features/wallet/create/pendingAccountsSaga'
import { usePendingAccounts } from 'wallet/src/features/wallet/hooks'
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'
export type OnboardingCompleteProps = OnboardingStackBaseParams
......@@ -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
if (unitagClaim) {
const { claimError } = await claimUnitag(unitagClaim)
const { claimError } = await claimUnitag(unitagClaim, {
source: 'onboarding',
hasENSAddress: false,
})
if (claimError) {
dispatch(
pushNotification({
......
......@@ -244,7 +244,6 @@ export function ScantasticModal(): JSX.Element | null {
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth={1}
flex={1}
gap="$spacing12"
p="$spacing16"
width="100%">
......
......@@ -15,6 +15,7 @@ export type MoonpayTransactionEventProperties = TraceProps &
export type AssetDetailsBaseProperties = {
name?: string
domain?: string
address: string
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'
import Trace from 'src/components/Trace/Trace'
import { Screen } from 'src/components/layout/Screen'
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 { TransferStatus } from 'src/features/transactions/transfer/TransferStatus'
import { useWalletRestore } from 'src/features/wallet/hooks'
......@@ -107,7 +106,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
dispatch,
TokenSelectorFlow.Transfer
)
const onSendEmptyActionPress = useOnSendEmptyActionPress()
// optimization for not rendering InnerContent initially,
// when modal is opened with recipient or token selector presented
......@@ -211,7 +209,6 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS
variation={TokenSelectorVariation.BalancesOnly}
onClose={onHideTokenSelector}
onSelectCurrency={onSelectCurrency}
onSendEmptyActionPress={onSendEmptyActionPress}
/>
)}
</>
......
......@@ -2,24 +2,42 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ActivityIndicator } from 'react-native'
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 { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal'
import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture'
import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen'
import { UnitagName } from 'src/features/unitags/UnitagName'
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 { ChainId } from 'wallet/src/constants/chains'
import { useENSName } from 'wallet/src/features/ens/api'
import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types'
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({
route,
}: UnitagStackScreenProp<UnitagScreens.ChooseProfilePicture>): JSX.Element {
const { entryPoint, unitag, address } = route.params
const { entryPoint, unitag, unitagFontSize, address } = route.params
const { t } = useTranslation()
const colors = useSporeColors()
const { data: ensName } = useENSName(address, ChainId.Mainnet)
const claimUnitag = useClaimUnitag()
const [imageUri, setImageUri] = useState<string>()
......@@ -64,11 +82,18 @@ export function ChooseProfilePictureScreen({
const attemptClaimUnitag = async (): Promise<void> => {
setIsClaiming(true)
const { claimError: attemptClaimError } = await claimUnitag({
const source = convertEntryPointToAnalyticsSource(entryPoint)
const { claimError: attemptClaimError } = await claimUnitag(
{
address,
username: unitag,
avatarUri: imageUri,
})
},
{
source,
hasENSAddress: !!ensName,
}
)
setIsClaiming(false)
setClaimError(attemptClaimError)
......@@ -112,19 +137,7 @@ export function ChooseProfilePictureScreen({
</Flex>
</Flex>
</Flex>
<AnimatedFlex
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>
<UnitagName fontSize={unitagFontSize} name={unitag} />
{!!claimError && (
<Text color="$statusCritical" variant="body2">
{claimError}
......@@ -168,8 +181,8 @@ function ProfilePicture({
return (
<UnitagProfilePicture
address={address}
profilePictureUri={imageUri}
size={imageSizes.image100}
unitagAvatarUri={imageUri}
/>
)
}
......
......@@ -117,7 +117,7 @@ export function UnitagConfirmationScreen({
</Text>
<Text color="$neutral2" textAlign="center" variant="subheading2">
{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 }
)}
</Text>
......
This diff is collapsed.
This diff is collapsed.
......@@ -50,7 +50,7 @@ export function ExploreScreen(): JSX.Element {
const [isSearchMode, setIsSearchMode] = useState<boolean>(false)
const textInputRef = useRef<TextInput>(null)
const onChangeSearchFilter = (newSearchFilter: string): void => {
const onSearchChangeText = (newSearchFilter: string): void => {
setSearchQuery(newSearchFilter)
}
......@@ -83,9 +83,8 @@ export function ExploreScreen(): JSX.Element {
backgroundColor={isSearchMode ? contrastBackgroundColor : searchBarBackgroundColor}
placeholder={t('Search tokens and wallets')}
showShadow={!isSearchMode}
value={searchQuery}
onCancel={onSearchCancel}
onChangeText={onChangeSearchFilter}
onChangeText={onSearchChangeText}
onFocus={onSearchFocus}
/>
</Flex>
......
This diff is collapsed.
......@@ -4,17 +4,16 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppDispatch } from 'src/app/hooks'
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 {
startFetchingCloudStorageBackups,
stopFetchingCloudStorageBackups,
} 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 { OnboardingScreens } from 'src/screens/Screens'
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 { logger } from 'utilities/src/logger/logger'
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