ci(release): publish latest release

parent 09b8467c
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmbYSi6bDw9e8CzLAzN8k6UkcgGYCheJGqsexFZW9HCLnP` - CIDv0: `QmUnNYjRF1siz1YhXyqh2UEoB5AJiTyiYihJCvPVm3FCdJ`
- CIDv1: `bafybeigefvkmjdhesneplmk35lfudrzcycnhtb7awt76obc2ceggoimo5q` - CIDv1: `bafybeic7xtznmrazhjjf2wlmdwzng2qfw3th6skmbj6bjc4i452fvjevsu`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,15 +10,47 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,15 +10,47 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeigefvkmjdhesneplmk35lfudrzcycnhtb7awt76obc2ceggoimo5q.ipfs.dweb.link/ - https://bafybeic7xtznmrazhjjf2wlmdwzng2qfw3th6skmbj6bjc4i452fvjevsu.ipfs.dweb.link/
- https://bafybeigefvkmjdhesneplmk35lfudrzcycnhtb7awt76obc2ceggoimo5q.ipfs.cf-ipfs.com/ - https://bafybeic7xtznmrazhjjf2wlmdwzng2qfw3th6skmbj6bjc4i452fvjevsu.ipfs.cf-ipfs.com/
- [ipfs://QmbYSi6bDw9e8CzLAzN8k6UkcgGYCheJGqsexFZW9HCLnP/](ipfs://QmbYSi6bDw9e8CzLAzN8k6UkcgGYCheJGqsexFZW9HCLnP/) - [ipfs://QmUnNYjRF1siz1YhXyqh2UEoB5AJiTyiYihJCvPVm3FCdJ/](ipfs://QmUnNYjRF1siz1YhXyqh2UEoB5AJiTyiYihJCvPVm3FCdJ/)
### 5.21.1 (2024-03-29) ## 5.22.0 (2024-04-02)
### Features
* **web:** add orderType to uniswapX POST order (#7071) 296a280
* **web:** add warning when pool is out of sync (#7074) 0b4142b
* **web:** alerts for adding liquidity to blast (#7090) 4bb5949
* **web:** blast (#6853) 2e14697
* **web:** Condense SuggestionRow and Remove VE (#6961) ff0aea0
* **web:** fix orderType to backend types (#7093) 516b4a9
* **web:** only search for tokens on the connected chain (#7062) f21bd47
* **web:** rm deprecated providers usage (#6891) c27e170
* **web:** share gql types + query with mobile (#6898) 4f047c7
* **web:** show user added tokens in currency list when using gql tokens (#7095) c21d5bd
* **web:** support parsing X v2 encoded orders (#7157) bbc7791
* **web:** unregister the service worker (#7155) 563c75b
### Bug Fixes ### Bug Fixes
* **web:** [hotfix] use chainId when getting token list currency (#7186) 92ec24b * **web:** auto bade not centered (#7166) 1b11359
* **web:** avoid stacking nested AnimatedDropdowns (#7097) cdee8c0
* **web:** bump redux version to 8 (#7121) b0c0e4e
* **web:** clean up sentry integration (#6945) 7073673
* **web:** fix failing type error on main (#7187) cec328f
* **web:** fix platform checks for mweb breaking uniwallet deeplinking (#7076) a4c6b86
* **web:** fix prepare not generating graphql breaking vercel build (#7049) d6b7e02
* **web:** remove circular dependencies in unit test code (#7057) 769c0b4
* **web:** responsive swap header (#7117) cce2323
* **web:** universal search e2e test (#7153) f767f6b
* **web:** update limit price signage (#7150) 2b303a9
* **web:** use chainId when getting token list currency (#7184) 93e5c4c
### Tests
* **web:** update SwapEventTimestampTracker to not use mocks (#7056) b1c7c61
web/5.21.1 web/5.22.0
\ No newline at end of file \ No newline at end of file
...@@ -131,6 +131,8 @@ Note: The app will likely have limited functionality when running it locally wit ...@@ -131,6 +131,8 @@ Note: The app will likely have limited functionality when running it locally wit
Use the environment variables defined in the `.env.defaults.local` file to run the app locally. Use the environment variables defined in the `.env.defaults.local` file to run the app locally.
You can use the command `yarn mobile env:local:download` if you have the 1password CLI to copy that file to your root folder.
### Compile contract ABI types ### Compile contract ABI types
This is done in bootstrap but good to know about. Before the code will compile you need to generate types for the smart contracts the wallet interacts with. Run `yarn g:prepare` at the top level. Re-run this if the ABIs are ever changed. This is done in bootstrap but good to know about. Before the code will compile you need to generate types for the smart contracts the wallet interacts with. Run `yarn g:prepare` at the top level. Re-run this if the ABIs are ever changed.
......
...@@ -107,9 +107,9 @@ android { ...@@ -107,9 +107,9 @@ android {
include (*reactNativeArchitectures()) include (*reactNativeArchitectures())
} }
} }
lintOptions { lintOptions {
abortOnError false abortOnError false
} }
signingConfigs { signingConfigs {
debug { debug {
storeFile file('debug.keystore') storeFile file('debug.keystore')
...@@ -131,17 +131,17 @@ android { ...@@ -131,17 +131,17 @@ android {
dev { dev {
isDefault(true) isDefault(true)
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionName "1.24" versionName "1.25"
dimension "variant" dimension "variant"
} }
beta { beta {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionName "1.24" versionName "1.25"
dimension "variant" dimension "variant"
} }
prod { prod {
dimension "variant" dimension "variant"
versionName "1.24" versionName "1.25"
} }
} }
......
...@@ -2450,7 +2450,7 @@ ...@@ -2450,7 +2450,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2496,7 +2496,7 @@ ...@@ -2496,7 +2496,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
...@@ -2542,7 +2542,7 @@ ...@@ -2542,7 +2542,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
...@@ -2588,7 +2588,7 @@ ...@@ -2588,7 +2588,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
...@@ -2630,7 +2630,7 @@ ...@@ -2630,7 +2630,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -2673,7 +2673,7 @@ ...@@ -2673,7 +2673,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
...@@ -2716,7 +2716,7 @@ ...@@ -2716,7 +2716,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
...@@ -2759,7 +2759,7 @@ ...@@ -2759,7 +2759,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
...@@ -2795,7 +2795,7 @@ ...@@ -2795,7 +2795,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -2833,7 +2833,7 @@ ...@@ -2833,7 +2833,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3003,7 +3003,7 @@ ...@@ -3003,7 +3003,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
...@@ -3047,7 +3047,7 @@ ...@@ -3047,7 +3047,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
...@@ -3143,7 +3143,7 @@ ...@@ -3143,7 +3143,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3214,7 +3214,7 @@ ...@@ -3214,7 +3214,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
...@@ -3310,7 +3310,7 @@ ...@@ -3310,7 +3310,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
...@@ -3381,7 +3381,7 @@ ...@@ -3381,7 +3381,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.24; MARKETING_VERSION = 1.25;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
...@@ -83,8 +83,8 @@ ...@@ -83,8 +83,8 @@
"@uniswap/analytics": "1.7.0", "@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.32.0", "@uniswap/analytics-events": "2.32.0",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "4.1.2", "@uniswap/sdk-core": "4.2.0",
"@uniswap/v3-sdk": "3.10.2", "@uniswap/v3-sdk": "3.11.0",
"@walletconnect/core": "2.11.2", "@walletconnect/core": "2.11.2",
"@walletconnect/react-native-compat": "2.11.2", "@walletconnect/react-native-compat": "2.11.2",
"@walletconnect/utils": "2.11.2", "@walletconnect/utils": "2.11.2",
......
...@@ -40,7 +40,11 @@ import { ...@@ -40,7 +40,11 @@ import {
setI18NUserDefaults, setI18NUserDefaults,
} from 'src/features/widgets/widgets' } from 'src/features/widgets/widgets'
import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' import { useAppStateTrigger } from 'src/utils/useAppStateTrigger'
import { getSentryEnvironment, getStatsigEnvironmentTier } from 'src/utils/version' import {
getSentryEnvironment,
getSentryTracesSamplingRate,
getStatsigEnvironmentTier,
} from 'src/utils/version'
import { Statsig, StatsigProvider } from 'statsig-react-native' import { Statsig, StatsigProvider } from 'statsig-react-native'
import { flexStyles, useIsDarkMode } from 'ui/src' import { flexStyles, useIsDarkMode } from 'ui/src'
import { config } from 'uniswap/src/config' import { config } from 'uniswap/src/config'
...@@ -52,6 +56,7 @@ import { ...@@ -52,6 +56,7 @@ import {
import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/experiments/flags' import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/experiments/flags'
import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context'
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { CurrencyId } from 'uniswap/src/types/currency'
import { isDetoxBuild } from 'utilities/src/environment' import { isDetoxBuild } from 'utilities/src/environment'
import { registerConsoleOverrides } from 'utilities/src/logger/console' import { registerConsoleOverrides } from 'utilities/src/logger/console'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
...@@ -70,7 +75,6 @@ import { Account } from 'wallet/src/features/wallet/accounts/types' ...@@ -70,7 +75,6 @@ import { Account } from 'wallet/src/features/wallet/accounts/types'
import { WalletContextProvider } from 'wallet/src/features/wallet/context' import { WalletContextProvider } from 'wallet/src/features/wallet/context'
import { useAccounts } from 'wallet/src/features/wallet/hooks' import { useAccounts } from 'wallet/src/features/wallet/hooks'
import { SharedProvider } from 'wallet/src/provider' import { SharedProvider } from 'wallet/src/provider'
import { CurrencyId } from 'wallet/src/utils/currencyId'
import { beforeSend } from 'wallet/src/utils/sentry' import { beforeSend } from 'wallet/src/utils/sentry'
enableFreeze(true) enableFreeze(true)
...@@ -88,9 +92,7 @@ if (!__DEV__ && !isDetoxBuild) { ...@@ -88,9 +92,7 @@ if (!__DEV__ && !isDetoxBuild) {
dsn: config.sentryDsn, dsn: config.sentryDsn,
attachViewHierarchy: true, attachViewHierarchy: true,
enableCaptureFailedRequests: true, enableCaptureFailedRequests: true,
tracesSampler: (_) => { tracesSampleRate: getSentryTracesSamplingRate(),
return 0.2
},
integrations: [ integrations: [
new Sentry.ReactNativeTracing({ new Sentry.ReactNativeTracing({
enableUserInteractionTracing: true, enableUserInteractionTracing: true,
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
NavigateToNftItemArgs, NavigateToNftItemArgs,
NavigateToSwapFlowArgs, NavigateToSwapFlowArgs,
WalletNavigationProvider, WalletNavigationProvider,
getNavigateToSwapFlowArgsInitialState,
} from 'wallet/src/contexts/WalletNavigationContext' } from 'wallet/src/contexts/WalletNavigationContext'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
...@@ -47,8 +48,7 @@ function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { ...@@ -47,8 +48,7 @@ function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void {
return useCallback( return useCallback(
(args: NavigateToSwapFlowArgs): void => { (args: NavigateToSwapFlowArgs): void => {
const initialState = args?.initialState const initialState = getNavigateToSwapFlowArgsInitialState(args)
dispatch(closeModal({ name: ModalName.Swap })) dispatch(closeModal({ name: ModalName.Swap }))
dispatch(openModal({ name: ModalName.Swap, initialState })) dispatch(openModal({ name: ModalName.Swap, initialState }))
}, },
......
...@@ -61,6 +61,7 @@ import { ...@@ -61,6 +61,7 @@ import {
v59Schema, v59Schema,
v5Schema, v5Schema,
v60Schema, v60Schema,
v61Schema,
v6Schema, v6Schema,
v7Schema, v7Schema,
v8Schema, v8Schema,
...@@ -76,7 +77,10 @@ import { initialTelemetryState } from 'src/features/telemetry/slice' ...@@ -76,7 +77,10 @@ import { initialTelemetryState } from 'src/features/telemetry/slice'
import { initialTweaksState } from 'src/features/tweaks/slice' import { initialTweaksState } from 'src/features/tweaks/slice'
import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice' import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice' import {
ExtensionOnboardingState,
initialBehaviorHistoryState,
} from 'wallet/src/features/behaviorHistory/slice'
import { initialFavoritesState } from 'wallet/src/features/favorites/slice' import { initialFavoritesState } from 'wallet/src/features/favorites/slice'
import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice'
import { initialLanguageState } from 'wallet/src/features/language/slice' import { initialLanguageState } from 'wallet/src/features/language/slice'
...@@ -1379,4 +1383,11 @@ describe('Redux state migrations', () => { ...@@ -1379,4 +1383,11 @@ describe('Redux state migrations', () => {
[nftKey4]: { isVisible: false }, [nftKey4]: { isVisible: false },
}) })
}) })
it('migrates from v61 to 62', () => {
const v61Stub = { ...v61Schema }
const v62 = migrations[62](v61Stub)
expect(v62.behaviorHistory.extensionOnboardingState).toBe(ExtensionOnboardingState.Undefined)
})
}) })
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { ExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice'
import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { toSupportedChainId } from 'wallet/src/features/chains/utils'
import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice'
import { initialLanguageState } from 'wallet/src/features/language/slice' import { initialLanguageState } from 'wallet/src/features/language/slice'
...@@ -863,4 +864,15 @@ export const migrations = { ...@@ -863,4 +864,15 @@ export const migrations = {
return newState return newState
}, },
62: function addExtensionOnboardingState(state: any) {
const newState = { ...state }
newState.behaviorHistory = {
...state.behaviorHistory,
extensionOnboardingState: ExtensionOnboardingState.Undefined,
}
return newState
},
} }
...@@ -15,6 +15,7 @@ import { LockScreenModal } from 'src/features/authentication/LockScreenModal' ...@@ -15,6 +15,7 @@ import { LockScreenModal } from 'src/features/authentication/LockScreenModal'
import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal' import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal'
import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal' import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal'
import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal' import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal'
import { ExtensionWaitlistModal } from 'src/features/scantastic/ExtensionWaitlistModal'
import { ScantasticModal } from 'src/features/scantastic/ScantasticModal' import { ScantasticModal } from 'src/features/scantastic/ScantasticModal'
import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal' import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal'
import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal' import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal'
...@@ -48,6 +49,10 @@ export function AppModals(): JSX.Element { ...@@ -48,6 +49,10 @@ export function AppModals(): JSX.Element {
<ExploreModal /> <ExploreModal />
</LazyModalRenderer> </LazyModalRenderer>
<LazyModalRenderer name={ModalName.ExtensionWaitlistModal}>
<ExtensionWaitlistModal />
</LazyModalRenderer>
<ForceUpgradeModal /> <ForceUpgradeModal />
<LockScreenModal /> <LockScreenModal />
......
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import 'react-native-reanimated'
import { Button, Flex, Image, Text, useIsDarkMode } from 'ui/src'
import { EXTENSION_PROMO_MODAL_DARK, EXTENSION_PROMO_MODAL_LIGHT } from 'ui/src/assets'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ModalName } from 'wallet/src/telemetry/constants'
export function ExtensionPromoModal({ onClose }: { onClose: () => void }): JSX.Element {
const { t } = useTranslation()
const isDarkMode = useIsDarkMode()
return (
<BottomSheetModal name={ModalName.ExtensionPromoModal} onClose={onClose}>
<Flex gap="$spacing12" pb="$spacing24" pt="$spacing4" px="$spacing16">
<Image
position="absolute"
resizeMode="contain"
source={{
uri: isDarkMode ? EXTENSION_PROMO_MODAL_DARK : EXTENSION_PROMO_MODAL_LIGHT,
}}
style={ImageStyles.responsiveImage}
/>
<Flex alignItems="center" gap="$spacing8" px="$spacing8" py="$spacing12">
<Text variant="subheading1">{t('home.modal.getExtension.title')}</Text>
<Flex gap="$spacing12" pt="$spacing8">
<Text color="$neutral2" variant="body3">
<Trans
components={{
highlight: <Text color="$accent1" variant="body3" />,
}}
i18nKey="home.modal.getExtension.step1"
/>
</Text>
<Text color="$neutral2" variant="body3">
{t('home.modal.getExtension.step2')}
</Text>
<Text color="$neutral2" variant="body3">
{t('home.modal.getExtension.step3')}
</Text>
</Flex>
</Flex>
<Button theme="secondary" onPress={onClose}>
{t('common.button.close')}
</Button>
</Flex>
</BottomSheetModal>
)
}
const ImageStyles = StyleSheet.create({
responsiveImage: {
aspectRatio: 686 / 430,
height: undefined,
width: '100%',
},
})
...@@ -420,8 +420,8 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -420,8 +420,8 @@ exports[`AccountSwitcher renders correctly 1`] = `
"alignItems": "center", "alignItems": "center",
"backgroundColor": "#F9F9F9", "backgroundColor": "#F9F9F9",
"borderBottomColor": "transparent", "borderBottomColor": "transparent",
"borderBottomLeftRadius": 8, "borderBottomLeftRadius": 12,
"borderBottomRightRadius": 8, "borderBottomRightRadius": 12,
"borderBottomWidth": 1, "borderBottomWidth": 1,
"borderLeftColor": "transparent", "borderLeftColor": "transparent",
"borderLeftWidth": 1, "borderLeftWidth": 1,
...@@ -429,8 +429,8 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -429,8 +429,8 @@ exports[`AccountSwitcher renders correctly 1`] = `
"borderRightWidth": 1, "borderRightWidth": 1,
"borderStyle": "solid", "borderStyle": "solid",
"borderTopColor": "transparent", "borderTopColor": "transparent",
"borderTopLeftRadius": 8, "borderTopLeftRadius": 12,
"borderTopRightRadius": 8, "borderTopRightRadius": 12,
"borderTopWidth": 1, "borderTopWidth": 1,
"flexDirection": "row", "flexDirection": "row",
"gap": 4, "gap": 4,
......
import { ExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice'
import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice'
import { initialLanguageState } from 'wallet/src/features/language/slice' import { initialLanguageState } from 'wallet/src/features/language/slice'
import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice'
...@@ -465,6 +466,15 @@ export const v61Schema = { ...@@ -465,6 +466,15 @@ export const v61Schema = {
nftsVisibility: {}, nftsVisibility: {},
}, },
} }
export const v62Schema = {
...v61Schema,
behaviorHistory: {
...v61Schema.behaviorHistory,
extensionOnboardingState: ExtensionOnboardingState.Undefined,
},
}
// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer // TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema // export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v61Schema => v61Schema export const getSchema = (): typeof v62Schema => v62Schema
...@@ -75,7 +75,7 @@ export const persistConfig = { ...@@ -75,7 +75,7 @@ export const persistConfig = {
key: 'root', key: 'root',
storage: reduxStorage, storage: reduxStorage,
whitelist, whitelist,
version: 61, version: 62,
migrate: createMigrate(migrations), migrate: createMigrate(migrations),
} }
......
...@@ -42,6 +42,7 @@ const RollNumber = ({ ...@@ -42,6 +42,7 @@ const RollNumber = ({
chars, chars,
commonPrefixLength, commonPrefixLength,
shouldFadeDecimals, shouldFadeDecimals,
disableAnimations,
}: { }: {
chars: string[] chars: string[]
digit?: string digit?: string
...@@ -49,6 +50,7 @@ const RollNumber = ({ ...@@ -49,6 +50,7 @@ const RollNumber = ({
index: number index: number
commonPrefixLength: number commonPrefixLength: number
shouldFadeDecimals: boolean shouldFadeDecimals: boolean
disableAnimations?: boolean
}): JSX.Element => { }): JSX.Element => {
const colors = useSporeColors() const colors = useSporeColors()
const fontColor = useSharedValue( const fontColor = useSharedValue(
...@@ -60,6 +62,10 @@ const RollNumber = ({ ...@@ -60,6 +62,10 @@ const RollNumber = ({
useEffect(() => { useEffect(() => {
const finishColor = const finishColor =
shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val
if (disableAnimations) {
fontColor.value = finishColor
return
}
if (nextColor && index > commonPrefixLength - 1) { if (nextColor && index > commonPrefixLength - 1) {
fontColor.value = withSequence( fontColor.value = withSequence(
withTiming(nextColor, { duration: 250 }), withTiming(nextColor, { duration: 250 }),
...@@ -78,6 +84,7 @@ const RollNumber = ({ ...@@ -78,6 +84,7 @@ const RollNumber = ({
commonPrefixLength, commonPrefixLength,
fontColor, fontColor,
shouldFadeDecimals, shouldFadeDecimals,
disableAnimations,
]) ])
const animatedFontStyle = useAnimatedStyle(() => { const animatedFontStyle = useAnimatedStyle(() => {
...@@ -99,7 +106,8 @@ const RollNumber = ({ ...@@ -99,7 +106,8 @@ const RollNumber = ({
useEffect(() => { useEffect(() => {
if (digit && Number(digit) >= 0) { if (digit && Number(digit) >= 0) {
yOffset.value = withTiming(DIGIT_HEIGHT * -digit) const newOffset = DIGIT_HEIGHT * -digit
yOffset.value = disableAnimations ? newOffset : withTiming(newOffset)
} }
}) })
...@@ -139,23 +147,26 @@ const Char = ({ ...@@ -139,23 +147,26 @@ const Char = ({
nextColor, nextColor,
commonPrefixLength, commonPrefixLength,
shouldFadeDecimals, shouldFadeDecimals,
disableAnimations,
}: { }: {
index: number index: number
chars: string[] chars: string[]
nextColor?: string nextColor?: string
commonPrefixLength: number commonPrefixLength: number
shouldFadeDecimals: boolean shouldFadeDecimals: boolean
disableAnimations?: boolean
}): JSX.Element => { }): JSX.Element => {
return ( return (
<Animated.View <Animated.View
entering={nextColor ? FadeIn : undefined} entering={nextColor && !disableAnimations ? FadeIn : undefined}
exiting={FadeOut} exiting={disableAnimations ? undefined : FadeOut}
layout={Layout} layout={disableAnimations ? undefined : Layout}
style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}> style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}>
<RollNumber <RollNumber
chars={chars} chars={chars}
commonPrefixLength={commonPrefixLength} commonPrefixLength={commonPrefixLength}
digit={chars[index]} digit={chars[index]}
disableAnimations={disableAnimations}
index={index} index={index}
nextColor={nextColor} nextColor={nextColor}
shouldFadeDecimals={shouldFadeDecimals} shouldFadeDecimals={shouldFadeDecimals}
...@@ -205,6 +216,7 @@ const AnimatedNumber = ({ ...@@ -205,6 +216,7 @@ const AnimatedNumber = ({
colorIndicationDuration, colorIndicationDuration,
shouldFadeDecimals, shouldFadeDecimals,
warmLoading, warmLoading,
disableAnimations,
}: { }: {
loadingPlaceholderText: string loadingPlaceholderText: string
loading: boolean | 'no-shimmer' loading: boolean | 'no-shimmer'
...@@ -212,6 +224,7 @@ const AnimatedNumber = ({ ...@@ -212,6 +224,7 @@ const AnimatedNumber = ({
colorIndicationDuration: number colorIndicationDuration: number
shouldFadeDecimals: boolean shouldFadeDecimals: boolean
warmLoading: boolean warmLoading: boolean
disableAnimations?: boolean
}): JSX.Element => { }): JSX.Element => {
const prevValue = usePrevious(value) const prevValue = usePrevious(value)
const [chars, setChars] = useState<string[]>() const [chars, setChars] = useState<string[]>()
...@@ -237,11 +250,11 @@ const AnimatedNumber = ({ ...@@ -237,11 +250,11 @@ const AnimatedNumber = ({
if (newScale < 1) { if (newScale < 1) {
const newOffset = (e.nativeEvent.layout.width - e.nativeEvent.layout.width * newScale) / 2 const newOffset = (e.nativeEvent.layout.width - e.nativeEvent.layout.width * newScale) / 2
scale.value = withTiming(newScale) scale.value = disableAnimations ? newScale : withTiming(newScale)
offset.value = withTiming(-newOffset) offset.value = disableAnimations ? -newOffset : withTiming(-newOffset)
} else if (scale.value < 1) { } else if (scale.value < 1) {
scale.value = withTiming(1) scale.value = disableAnimations ? 1 : withTiming(1)
offset.value = withTiming(0) offset.value = disableAnimations ? 0 : withTiming(0)
} }
} }
...@@ -278,6 +291,7 @@ const AnimatedNumber = ({ ...@@ -278,6 +291,7 @@ const AnimatedNumber = ({
} }
chars={placeholderChars} chars={placeholderChars}
commonPrefixLength={commonPrefixLength} commonPrefixLength={commonPrefixLength}
disableAnimations={disableAnimations}
index={index} index={index}
nextColor={nextColor} nextColor={nextColor}
shouldFadeDecimals={shouldFadeDecimals} shouldFadeDecimals={shouldFadeDecimals}
...@@ -306,6 +320,7 @@ const AnimatedNumber = ({ ...@@ -306,6 +320,7 @@ const AnimatedNumber = ({
} }
chars={chars} chars={chars}
commonPrefixLength={commonPrefixLength} commonPrefixLength={commonPrefixLength}
disableAnimations={disableAnimations}
index={index} index={index}
nextColor={nextColor} nextColor={nextColor}
shouldFadeDecimals={shouldFadeDecimals} shouldFadeDecimals={shouldFadeDecimals}
......
...@@ -13,9 +13,9 @@ import { Loader } from 'src/components/loading' ...@@ -13,9 +13,9 @@ import { Loader } from 'src/components/loading'
import { Flex, HapticFeedback } from 'ui/src' import { Flex, HapticFeedback } from 'ui/src'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { CurrencyId } from 'uniswap/src/types/currency'
import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { CurrencyId } from 'wallet/src/utils/currencyId'
import { PriceNumberOfDigits, TokenSpotData, useTokenPriceHistory } from './usePriceHistory' import { PriceNumberOfDigits, TokenSpotData, useTokenPriceHistory } from './usePriceHistory'
type PriceTextProps = { type PriceTextProps = {
......
import { SCREEN_WIDTH } from '@gorhom/bottom-sheet'
import _ from 'lodash' import _ from 'lodash'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
import Animated, { import Animated, {
SharedValue, SharedValue,
useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
useDerivedValue, useDerivedValue,
useSharedValue, useSharedValue,
...@@ -294,6 +296,8 @@ const LoadingWrapper = (): JSX.Element | null => { ...@@ -294,6 +296,8 @@ const LoadingWrapper = (): JSX.Element | null => {
) )
} }
const SCREEN_WIDTH_BUFFER = 50
const PriceExplorerAnimatedNumber = ({ const PriceExplorerAnimatedNumber = ({
price, price,
numberOfDigits, numberOfDigits,
...@@ -305,6 +309,8 @@ const PriceExplorerAnimatedNumber = ({ ...@@ -305,6 +309,8 @@ const PriceExplorerAnimatedNumber = ({
}): JSX.Element => { }): JSX.Element => {
const colors = useSporeColors() const colors = useSporeColors()
const hideShimmer = useSharedValue(false) const hideShimmer = useSharedValue(false)
const scale = useSharedValue(1)
const offset = useSharedValue(0)
const animatedWrapperStyle = useAnimatedStyle(() => { const animatedWrapperStyle = useAnimatedStyle(() => {
return { return {
opacity: price.value.value > 0 && hideShimmer.value ? 0 : 1, opacity: price.value.value > 0 && hideShimmer.value ? 0 : 1,
...@@ -320,6 +326,31 @@ const PriceExplorerAnimatedNumber = ({ ...@@ -320,6 +326,31 @@ const PriceExplorerAnimatedNumber = ({
} }
}) })
useAnimatedReaction(
() => {
return Number(
[0, ...price.formatted.value.split('')].reduce((accumulator, currentValue) => {
if (NUMBER_WIDTH_ARRAY[Number(currentValue)]) {
return Number(accumulator) + Number(NUMBER_WIDTH_ARRAY[Number(currentValue)])
}
return accumulator
})
)
},
(priceWidth: number) => {
const newScale = (SCREEN_WIDTH - SCREEN_WIDTH_BUFFER) / priceWidth
if (newScale < 1) {
const newOffset = (priceWidth - priceWidth * newScale) / 2
scale.value = withTiming(newScale)
offset.value = withTiming(-newOffset)
} else if (scale.value < 1) {
scale.value = withTiming(1)
offset.value = withTiming(0)
}
}
)
const hidePlaceholder = (): void => { const hidePlaceholder = (): void => {
hideShimmer.value = true hideShimmer.value = true
} }
...@@ -344,8 +375,18 @@ const PriceExplorerAnimatedNumber = ({ ...@@ -344,8 +375,18 @@ const PriceExplorerAnimatedNumber = ({
</Animated.Text> </Animated.Text>
) )
const scaleWraper = useAnimatedStyle(() => {
return {
transform: [
{ translateX: -SCREEN_WIDTH / 2 },
{ scale: scale.value },
{ translateX: SCREEN_WIDTH / 2 },
],
}
})
return ( return (
<> <Animated.View style={scaleWraper}>
<Animated.View style={animatedWrapperStyle}> <Animated.View style={animatedWrapperStyle}>
<LoadingWrapper /> <LoadingWrapper />
</Animated.View> </Animated.View>
...@@ -356,7 +397,7 @@ const PriceExplorerAnimatedNumber = ({ ...@@ -356,7 +397,7 @@ const PriceExplorerAnimatedNumber = ({
{Numbers({ price, hidePlaceholder, numberOfDigits, currency })} {Numbers({ price, hidePlaceholder, numberOfDigits, currency })}
{!currency.symbolAtFront && currencySymbol} {!currency.symbolAtFront && currencySymbol}
</View> </View>
</> </Animated.View>
) )
} }
......
...@@ -115,7 +115,7 @@ describe(useTokenPriceHistory, () => { ...@@ -115,7 +115,7 @@ describe(useTokenPriceHistory, () => {
expect(result.current.numberOfDigits).toEqual({ expect(result.current.numberOfDigits).toEqual({
left: 1, left: 1,
right: 10, right: 16,
}) })
}) })
......
...@@ -100,12 +100,13 @@ export function useTokenPriceHistory( ...@@ -100,12 +100,13 @@ export function useTokenPriceHistory(
if (!maxPriceInHistory && price === undefined) { if (!maxPriceInHistory && price === undefined) {
return lastNumberOfDigits.current return lastNumberOfDigits.current
} }
const maxPrice = Math.max(maxPriceInHistory || 0, price || 0) const maxPrice = Math.max(maxPriceInHistory || 0, price || 0)
const convertedMaxValue = convertFiatAmount(maxPrice).amount const convertedMaxValue = convertFiatAmount(maxPrice).amount
const newNumberOfDigits = { const newNumberOfDigits = {
left: String(convertedMaxValue).split('.')[0]?.length || 10, left: String(convertedMaxValue).split('.')[0]?.length || 10,
right: Number(String(convertedMaxValue.toFixed(10)).split('.')[0]) > 0 ? 2 : 10, right: Number(String(convertedMaxValue.toFixed(16)).split('.')[0]) > 0 ? 2 : 16,
} }
lastNumberOfDigits.current = newNumberOfDigits lastNumberOfDigits.current = newNumberOfDigits
......
...@@ -5,6 +5,8 @@ import { ...@@ -5,6 +5,8 @@ import {
ColorTokens, ColorTokens,
Flex, Flex,
getUniconV2Colors, getUniconV2Colors,
passesContrast,
useExtractedColors,
useIsDarkMode, useIsDarkMode,
useSporeColors, useSporeColors,
useUniconColors, useUniconColors,
...@@ -16,7 +18,6 @@ import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' ...@@ -16,7 +18,6 @@ import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { isAndroid } from 'uniswap/src/utils/platform' import { isAndroid } from 'uniswap/src/utils/platform'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { useAvatar } from 'wallet/src/features/wallet/hooks' import { useAvatar } from 'wallet/src/features/wallet/hooks'
import { passesContrast, useExtractedColors } from 'wallet/src/utils/colors'
type AvatarColors = { type AvatarColors = {
primary: string primary: string
......
...@@ -2,7 +2,7 @@ import React, { memo, useMemo } from 'react' ...@@ -2,7 +2,7 @@ import React, { memo, useMemo } from 'react'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { useTokenContextMenu } from 'src/features/balances/hooks' import { useTokenContextMenu } from 'src/features/balances/hooks'
import { borderRadii } from 'ui/src/theme' import { borderRadii } from 'ui/src/theme'
import { PortfolioBalance } from 'wallet/src/features/dataApi/types' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({ export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({
portfolioBalance, portfolioBalance,
......
...@@ -23,6 +23,7 @@ import { ...@@ -23,6 +23,7 @@ import {
useSporeColors, useSporeColors,
} from 'ui/src' } from 'ui/src'
import { zIndices } from 'ui/src/theme' import { zIndices } from 'ui/src/theme'
import { CurrencyId } from 'uniswap/src/types/currency'
import { isAndroid } from 'uniswap/src/utils/platform' import { isAndroid } from 'uniswap/src/utils/platform'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils'
...@@ -34,7 +35,6 @@ import { ...@@ -34,7 +35,6 @@ import {
TokenBalanceListRow, TokenBalanceListRow,
useTokenBalanceListContext, useTokenBalanceListContext,
} from 'wallet/src/features/portfolio/TokenBalanceListContext' } from 'wallet/src/features/portfolio/TokenBalanceListContext'
import { CurrencyId } from 'wallet/src/utils/currencyId'
type TokenBalanceListProps = TabProps & { type TokenBalanceListProps = TabProps & {
empty?: JSX.Element | null empty?: JSX.Element | null
......
...@@ -5,15 +5,15 @@ import Trace from 'src/components/Trace/Trace' ...@@ -5,15 +5,15 @@ import Trace from 'src/components/Trace/Trace'
import { MobileEventName } from 'src/features/telemetry/constants' import { MobileEventName } from 'src/features/telemetry/constants'
import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { CurrencyId } from 'uniswap/src/types/currency'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill' import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill'
import { PortfolioBalance } from 'wallet/src/features/dataApi/types'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccount, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useActiveAccount, useDisplayName } from 'wallet/src/features/wallet/hooks'
import { getSymbolDisplayText } from 'wallet/src/utils/currency' import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import { CurrencyId } from 'wallet/src/utils/currencyId'
import { SendButton } from './SendButton' import { SendButton } from './SendButton'
/** /**
......
...@@ -6,11 +6,11 @@ import { ...@@ -6,11 +6,11 @@ import {
Chain, Chain,
useTokenDetailsScreenLazyQuery, useTokenDetailsScreenLazyQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { CurrencyId } from 'uniswap/src/types/currency'
import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { fromGraphQLChain } from 'wallet/src/features/chains/utils'
import { PortfolioBalance } from 'wallet/src/features/dataApi/types'
import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils'
import { import {
CurrencyId,
buildCurrencyId, buildCurrencyId,
buildNativeCurrencyId, buildNativeCurrencyId,
currencyIdToChain, currencyIdToChain,
......
...@@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next' ...@@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next'
import { ListRenderItemInfo } from 'react-native' import { ListRenderItemInfo } from 'react-native'
import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types'
import { Flex, Inset, Loader } from 'ui/src' import { Flex, Inset, Loader } from 'ui/src'
import { CurrencyId } from 'uniswap/src/types/currency'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem' import { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem'
import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { CurrencyId } from 'wallet/src/utils/currencyId'
interface Props { interface Props {
onSelectCurrency: (currency: FiatOnRampCurrency) => void onSelectCurrency: (currency: FiatOnRampCurrency) => void
......
...@@ -2,9 +2,9 @@ import React from 'react' ...@@ -2,9 +2,9 @@ import React from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { borderRadii, iconSizes } from 'ui/src/theme' import { borderRadii, iconSizes } from 'ui/src/theme'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo' import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo'
import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder'
import { CurrencyInfo } from 'wallet/src/features/dataApi/types'
import { ImageUri } from 'wallet/src/features/images/ImageUri' import { ImageUri } from 'wallet/src/features/images/ImageUri'
import { DappInfo } from 'wallet/src/features/walletConnect/types' import { DappInfo } from 'wallet/src/features/walletConnect/types'
......
import {
BottomSheetFooter,
BottomSheetScrollView,
useBottomSheetInternal,
} from '@gorhom/bottom-sheet'
import { PropsWithChildren, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
LayoutChangeEvent,
MeasureLayoutOnSuccessCallback,
NativeScrollEvent,
NativeSyntheticEvent,
ScrollView,
View,
} from 'react-native'
import { useDerivedValue } from 'react-native-reanimated'
import { ScrollDownOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ScrollDownOverlay'
import { Button, Flex, useDeviceInsets } from 'ui/src'
import { spacing } from 'ui/src/theme'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { BottomSheetModalProps } from 'wallet/src/components/modals/BottomSheetModalProps'
import { ElementName } from 'wallet/src/telemetry/constants'
const MEASURE_LAYOUT_TIMEOUT = 100
type ModalWithOverlayProps = PropsWithChildren<
BottomSheetModalProps & {
confirmationButtonText?: string
scrollDownButtonText?: string
onReject: () => void
onConfirm: () => void
}
>
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}: NativeScrollEvent): boolean => {
return layoutMeasurement.height + contentOffset.y >= contentSize.height - spacing.spacing24
}
export function ModalWithOverlay({
children,
confirmationButtonText,
scrollDownButtonText,
onReject,
onConfirm,
...bottomSheetModalProps
}: ModalWithOverlayProps): JSX.Element {
const scrollViewRef = useRef<ScrollView>(null)
const contentViewRef = useRef<View>(null)
const measureLayoutTimeoutRef = useRef<NodeJS.Timeout>()
const startedScrollingRef = useRef(false)
const [showOverlay, setShowOverlay] = useState(false)
const [confirmationEnabled, setConfirmationEnabled] = useState(false)
const handleScroll = useCallback(
({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
startedScrollingRef.current = true
if (showOverlay) {
setShowOverlay(false)
}
if (isCloseToBottom(nativeEvent)) {
setConfirmationEnabled(true)
}
},
[showOverlay]
)
const handleScrollDown = useCallback(() => {
scrollViewRef.current?.scrollToEnd()
}, [])
const measureContent = useCallback((parentHeight: number) => {
const onSuccess: MeasureLayoutOnSuccessCallback = (x, y, w, h) => {
if (h > parentHeight) {
setShowOverlay(!startedScrollingRef.current)
} else {
setConfirmationEnabled(true)
}
}
const contentNode = contentViewRef.current
if (contentNode) {
contentNode.measure(onSuccess)
} else {
setConfirmationEnabled(true)
}
}, [])
const handleScrollViewLayout = useCallback(
(e: LayoutChangeEvent) => {
const parentHeight = e.nativeEvent.layout.height
if (measureLayoutTimeoutRef.current) {
clearTimeout(measureLayoutTimeoutRef.current)
}
// BottomSheetScrollView calls onLayout multiple times with different
// height values. In order to make a correct measurement, we have to
// ignore all measurements except the last one, thus we add the timeout
// to cancel measurements when onLayout is called within a small interval
measureLayoutTimeoutRef.current = setTimeout(() => {
measureContent(parentHeight)
}, MEASURE_LAYOUT_TIMEOUT)
},
[measureContent]
)
return (
<BottomSheetModal overrideInnerContainer {...bottomSheetModalProps}>
<BottomSheetScrollView
ref={scrollViewRef}
contentContainerStyle={{
paddingHorizontal: spacing.spacing24,
paddingTop: spacing.spacing36,
}}
showsVerticalScrollIndicator={false}
onLayout={handleScrollViewLayout}
onScroll={handleScroll}>
<Flex ref={contentViewRef}>{children}</Flex>
</BottomSheetScrollView>
<ModalFooter
confirmationButtonText={confirmationButtonText}
confirmationEnabled={confirmationEnabled}
scrollDownButtonText={scrollDownButtonText}
showScrollDownOverlay={showOverlay}
onConfirm={onConfirm}
onReject={onReject}
onScrollDownPress={handleScrollDown}
/>
</BottomSheetModal>
)
}
type ModalFooterProps = {
confirmationEnabled: boolean
showScrollDownOverlay: boolean
confirmationButtonText?: string
scrollDownButtonText?: string
onScrollDownPress: () => void
onReject: () => void
onConfirm: () => void
}
function ModalFooter({
confirmationEnabled,
showScrollDownOverlay,
scrollDownButtonText,
confirmationButtonText,
onScrollDownPress,
onReject,
onConfirm,
}: ModalFooterProps): JSX.Element {
const { t } = useTranslation()
const insets = useDeviceInsets()
const { animatedPosition, animatedHandleHeight, animatedFooterHeight, animatedContainerHeight } =
useBottomSheetInternal()
// Calculate position of the modal footer to ensure it stays at the bottom of the screen
// when the modal content is scrolled
const animatedFooterPosition = useDerivedValue(
() =>
Math.max(0, animatedContainerHeight.value - animatedPosition.value) -
animatedFooterHeight.value -
animatedHandleHeight.value
)
return (
<BottomSheetFooter animatedFooterPosition={animatedFooterPosition}>
{showScrollDownOverlay && (
<ScrollDownOverlay
scrollDownButonText={scrollDownButtonText}
onScrollDownPress={onScrollDownPress}
/>
)}
<Flex
row
backgroundColor="$surface1"
gap="$spacing8"
pb={insets.bottom + spacing.spacing12}
pt="$spacing12"
px="$spacing24">
<Button fill size="medium" testID={ElementName.Cancel} theme="tertiary" onPress={onReject}>
{t('common.button.cancel')}
</Button>
<Button
fill
disabled={!confirmationEnabled}
size="medium"
testID={ElementName.Confirm}
onPress={onConfirm}>
{confirmationButtonText ?? t('common.button.accept')}
</Button>
</Flex>
</BottomSheetFooter>
)
}
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import { FadeInDown, FadeOut } from 'react-native-reanimated'
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg'
import {
AnimatedFlex,
Flex,
Text,
TouchableArea,
useDeviceDimensions,
useSporeColors,
} from 'ui/src'
import { ArrowDown } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
type ScrollDownOverlayProps = {
scrollDownButonText?: string
onScrollDownPress: () => void
}
export function ScrollDownOverlay({
onScrollDownPress,
scrollDownButonText,
}: ScrollDownOverlayProps): JSX.Element {
const { t } = useTranslation()
const { fullHeight, fullWidth } = useDeviceDimensions()
const colors = useSporeColors()
return (
<AnimatedFlex
alignItems="center"
bottom={100}
entering={FadeInDown}
exiting={FadeOut}
height={0.25 * fullHeight}
justifyContent="flex-end"
pb="$spacing24"
pointerEvents="box-none"
position="absolute"
width="100%">
<Flex pointerEvents="none" style={StyleSheet.absoluteFill}>
<Svg height="100%" width={fullWidth}>
<Defs>
<LinearGradient id="scroll-button-fadeout" x1="0" x2="0" y1="0" y2="1">
<Stop offset="0" stopColor={colors.surface1.val} stopOpacity="0" />
<Stop offset="0.75" stopColor={colors.surface1.val} stopOpacity="1" />
</LinearGradient>
</Defs>
<Rect fill="url(#scroll-button-fadeout)" height="100%" width="100%" x={0} y={0} />
</Svg>
</Flex>
<TouchableArea alignItems="center" onPress={onScrollDownPress}>
<Text color="$accent1" variant="buttonLabel3">
{scrollDownButonText ?? t('common.button.scrollDown')}
</Text>
<ArrowDown color="$accent1" size={iconSizes.icon16} />
</TouchableArea>
</AnimatedFlex>
)
}
...@@ -35,10 +35,11 @@ export function NetworkLogos({ ...@@ -35,10 +35,11 @@ export function NetworkLogos({
<Flex centered row gap={negativeGap ? -spacing.spacing8 : '$spacing4'}> <Flex centered row gap={negativeGap ? -spacing.spacing8 : '$spacing4'}>
{chains.map((chainId) => ( {chains.map((chainId) => (
<Flex <Flex
key={chainId}
backgroundColor="$surface2" backgroundColor="$surface2"
borderRadius="$rounded8" borderRadius="$rounded8"
p={negativeGap ? '$spacing2' : '$none'}> p={negativeGap ? '$spacing2' : '$none'}>
<NetworkLogo key={chainId} chainId={chainId} size={size} /> <NetworkLogo chainId={chainId} size={size} />
</Flex> </Flex>
))} ))}
</Flex> </Flex>
......
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import React from 'react' import React from 'react'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util'
import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice' import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice'
import { Text } from 'ui/src' import { Text } from 'ui/src'
import { EthMethod } from 'wallet/src/features/walletConnect/types' import { EthMethod } from 'wallet/src/features/walletConnect/types'
...@@ -70,7 +69,7 @@ export function HeaderText({ ...@@ -70,7 +69,7 @@ export function HeaderText({
return ( return (
<Text textAlign="center" variant="heading3"> <Text textAlign="center" variant="heading3">
{getReadableMethodName(method, truncateDappName(dapp.name || dapp.url))} {getReadableMethodName(method, dapp.name || dapp.url)}
</Text> </Text>
) )
} }
import { useBottomSheetInternal } from '@gorhom/bottom-sheet'
import { useNetInfo } from '@react-native-community/netinfo'
import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency'
import React, { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleProp, ViewStyle } from 'react-native'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { ClientDetails, PermitInfo } from 'src/components/WalletConnect/RequestModal/ClientDetails'
import { RequestDetails } from 'src/components/WalletConnect/RequestModal/RequestDetails'
import {
TransactionRequest,
WalletConnectRequest,
isTransactionRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, useSporeColors } from 'ui/src'
import AlertTriangle from 'ui/src/assets/icons/alert-triangle.svg'
import { iconSizes } from 'ui/src/theme'
import { logger } from 'utilities/src/logger/logger'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails'
import { NetworkFee } from 'wallet/src/components/network/NetworkFee'
import { NetworkPill } from 'wallet/src/components/network/NetworkPill'
import { GasFeeResult } from 'wallet/src/features/gas/types'
import { BlockedAddressWarning } from 'wallet/src/features/trm/BlockedAddressWarning'
import { EthMethod, isPrimaryTypePermit } from 'wallet/src/features/walletConnect/types'
import { buildCurrencyId } from 'wallet/src/utils/currencyId'
const MAX_MODAL_MESSAGE_HEIGHT = 200
const isPotentiallyUnsafe = (request: WalletConnectRequest): boolean =>
request.type !== EthMethod.PersonalSign
export const methodCostsGas = (request: WalletConnectRequest): request is TransactionRequest =>
request.type === EthMethod.EthSendTransaction
/** If the request is a permit then parse the relevant information otherwise return undefined. */
const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined => {
if (request.type !== EthMethod.SignTypedDataV4) {
return undefined
}
try {
const message = JSON.parse(request.rawMessage)
if (!isPrimaryTypePermit(message)) {
return undefined
}
const { domain, message: permitPayload } = message
const currencyId = buildCurrencyId(domain.chainId, domain.verifyingContract)
const amount = permitPayload.value
return { currencyId, amount }
} catch (error) {
logger.error(error, { tags: { file: 'WalletConnectRequestModal', function: 'getPermitInfo' } })
return undefined
}
}
type WalletConnectRequestModalContentProps = {
gasFee: GasFeeResult
hasSufficientFunds: boolean
request: WalletConnectRequest
isBlocked: boolean
}
export function WalletConnectRequestModalContent({
request,
hasSufficientFunds,
isBlocked,
gasFee,
}: WalletConnectRequestModalContentProps): JSX.Element {
const chainId = request.chainId
const permitInfo = getPermitInfo(request)
const nativeCurrency = chainId && NativeCurrency.onChain(chainId)
const { t } = useTranslation()
const colors = useSporeColors()
const { animatedFooterHeight } = useBottomSheetInternal()
const netInfo = useNetInfo()
const bottomSpacerStyle = useAnimatedStyle(() => ({
height: animatedFooterHeight.value,
}))
return (
<>
<ClientDetails permitInfo={permitInfo} request={request} />
<Flex gap="$spacing12">
<Flex
backgroundColor="$surface2"
borderBottomColor="$surface2"
borderBottomWidth={1}
borderRadius="$rounded16">
{!permitInfo && (
<SectionContainer style={requestMessageStyle}>
<Flex gap="$spacing12">
<RequestDetails request={request} />
</Flex>
</SectionContainer>
)}
<Flex px="$spacing16" py="$spacing8">
{methodCostsGas(request) ? (
<NetworkFee chainId={chainId} gasFee={gasFee} />
) : (
<Flex row alignItems="center" justifyContent="space-between">
<Text color="$neutral1" variant="subheading2">
{t('walletConnect.request.label.network')}
</Text>
<NetworkPill
showIcon
chainId={chainId}
gap="$spacing4"
pl="$spacing4"
pr="$spacing8"
py="$spacing2"
textVariant="subheading2"
/>
</Flex>
)}
</Flex>
<SectionContainer>
<AccountDetails address={request.account} />
{!hasSufficientFunds && (
<Text color="$DEP_accentWarning" pt="$spacing8" variant="body2">
{t('walletConnect.request.error.insufficientFunds', {
currencySymbol: nativeCurrency?.symbol,
})}
</Text>
)}
</SectionContainer>
</Flex>
{!netInfo.isInternetReachable ? (
<BaseCard.InlineErrorState
backgroundColor="$DEP_accentWarningSoft"
icon={
<AlertTriangle
color={colors.DEP_accentWarning.val}
height={iconSizes.icon16}
width={iconSizes.icon16}
/>
}
textColor="$DEP_accentWarning"
title={t('walletConnect.request.error.network')}
/>
) : (
<WarningSection
isBlockedAddress={isBlocked}
request={request}
showUnsafeWarning={isPotentiallyUnsafe(request)}
/>
)}
</Flex>
<Animated.View style={bottomSpacerStyle} />
</>
)
}
function SectionContainer({
children,
style,
}: PropsWithChildren<{ style?: StyleProp<ViewStyle> }>): JSX.Element | null {
return children ? (
<Flex p="$spacing16" style={style}>
{children}
</Flex>
) : null
}
function WarningSection({
request,
showUnsafeWarning,
isBlockedAddress,
}: {
request: WalletConnectRequest
showUnsafeWarning: boolean
isBlockedAddress: boolean
}): JSX.Element | null {
const colors = useSporeColors()
const { t } = useTranslation()
if (!showUnsafeWarning && !isBlockedAddress) {
return null
}
if (isBlockedAddress) {
return <BlockedAddressWarning centered row alignSelf="center" />
}
return (
<Flex centered row alignSelf="center" gap="$spacing8">
<AlertTriangle
color={colors.DEP_accentWarning.val}
height={iconSizes.icon16}
width={iconSizes.icon16}
/>
<Text color="$neutral2" fontStyle="italic" variant="body3">
{isTransactionRequest(request)
? t('walletConnect.request.warning.general.transaction')
: t('walletConnect.request.warning.general.message')}
</Text>
</Flex>
)
}
const requestMessageStyle: StyleProp<ViewStyle> = {
// need a fixed height here or else modal gets confused about total height
maxHeight: MAX_MODAL_MESSAGE_HEIGHT,
overflow: 'hidden',
}
...@@ -14,10 +14,9 @@ import { ...@@ -14,10 +14,9 @@ import {
UWULINK_PREFIX, UWULINK_PREFIX,
getSupportedURI, getSupportedURI,
isAllowedUwULinkRequest, isAllowedUwULinkRequest,
parseScantasticParams,
} from 'src/components/WalletConnect/ScanSheet/util' } from 'src/components/WalletConnect/ScanSheet/util'
import { BackButtonView } from 'src/components/layout/BackButtonView' import { BackButtonView } from 'src/components/layout/BackButtonView'
import { closeAllModals, openModal } from 'src/features/modals/modalSlice' import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga'
import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect'
import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils' import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils'
import { addRequest } from 'src/features/walletConnect/walletConnectSlice' import { addRequest } from 'src/features/walletConnect/walletConnectSlice'
...@@ -136,36 +135,9 @@ export function WalletConnectModal({ ...@@ -136,36 +135,9 @@ export function WalletConnectModal({
} }
if (supportedURI.type === URIType.Scantastic) { if (supportedURI.type === URIType.Scantastic) {
const params = parseScantasticParams(supportedURI.value)
if (!params) {
setShouldFreezeCamera(true)
Alert.alert(
t('walletConnect.error.scantastic.title'),
t('walletConnect.error.scantastic.message'),
[
{
text: t('common.button.ok'),
onPress: (): void => {
setShouldFreezeCamera(false)
},
},
]
)
return
}
setShouldFreezeCamera(true) setShouldFreezeCamera(true)
dispatch(closeAllModals()) dispatch(openDeepLink({ url: uri, coldStart: false }))
dispatch( onClose()
openModal({
name: ModalName.Scantastic,
initialState: {
params,
},
})
)
return return
} }
......
...@@ -2,9 +2,10 @@ import { parseUri } from '@walletconnect/utils' ...@@ -2,9 +2,10 @@ import { parseUri } from '@walletconnect/utils'
import { parseEther } from 'ethers/lib/utils' import { parseEther } from 'ethers/lib/utils'
import { import {
UNISWAP_URL_SCHEME, UNISWAP_URL_SCHEME,
UNISWAP_URL_SCHEME_SCANTASTIC,
UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM,
UNISWAP_WALLETCONNECT_URL, UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/handleDeepLinkSaga' } from 'src/features/deepLinking/constants'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types' import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types'
import { UwULinkRequest } from 'wallet/src/features/walletConnect/types' import { UwULinkRequest } from 'wallet/src/features/walletConnect/types'
...@@ -36,12 +37,11 @@ const UWULINK_MAX_TXN_VALUE = '0.001' ...@@ -36,12 +37,11 @@ const UWULINK_MAX_TXN_VALUE = '0.001'
const EASTER_EGG_QR_CODE = 'DO_NOT_SCAN_OR_ELSE_YOU_WILL_GO_TO_MOBILE_TEAM_JAIL' const EASTER_EGG_QR_CODE = 'DO_NOT_SCAN_OR_ELSE_YOU_WILL_GO_TO_MOBILE_TEAM_JAIL'
export const CUSTOM_UNI_QR_CODE_PREFIX = 'hello_uniwallet:' export const CUSTOM_UNI_QR_CODE_PREFIX = 'hello_uniwallet:'
export const UWULINK_PREFIX = 'uwulink' export const UWULINK_PREFIX = 'uwulink'
const MAX_DAPP_NAME_LENGTH = 60
export function truncateDappName(name: string): string { export const truncateQueryParams = (url: string): string => {
return name && name.length > MAX_DAPP_NAME_LENGTH // In fact, the first element will be always returned below. url is
? `${name.slice(0, MAX_DAPP_NAME_LENGTH)}...` // added as a fallback just to satisfy TypeScript.
: name return url.split('?')[0] ?? url
} }
export async function getSupportedURI( export async function getSupportedURI(
...@@ -62,9 +62,9 @@ export async function getSupportedURI( ...@@ -62,9 +62,9 @@ export async function getSupportedURI(
return { type: URIType.Address, value: maybeMetamaskAddress } return { type: URIType.Address, value: maybeMetamaskAddress }
} }
const maybeScantasticAddress = getScantasticAddress(uri) const maybeScantasticQueryParams = getScantasticQueryParams(uri)
if (enabledFeatureFlags?.isScantasticEnabled && maybeScantasticAddress) { if (enabledFeatureFlags?.isScantasticEnabled && maybeScantasticQueryParams) {
return { type: URIType.Scantastic, value: maybeScantasticAddress } return { type: URIType.Scantastic, value: maybeScantasticQueryParams }
} }
// The check for custom prefixes must be before the parseUri version 2 check because // The check for custom prefixes must be before the parseUri version 2 check because
...@@ -153,13 +153,13 @@ function getMetamaskAddress(uri: string): Nullable<string> { ...@@ -153,13 +153,13 @@ function getMetamaskAddress(uri: string): Nullable<string> {
return getValidAddress(uriParts[1], /*withChecksum=*/ true, /*log=*/ false) return getValidAddress(uriParts[1], /*withChecksum=*/ true, /*log=*/ false)
} }
// format is scantastic://<uri> // format is uniswap://scantastic?<params>
function getScantasticAddress(uri: string): Nullable<string> { export function getScantasticQueryParams(uri: string): Nullable<string> {
if (!uri.startsWith('scantastic://')) { if (!uri.startsWith(UNISWAP_URL_SCHEME_SCANTASTIC)) {
return null return null
} }
const uriParts = uri.split('://') const uriParts = uri.split('://scantastic?')
if (uriParts.length < 2) { if (uriParts.length < 2) {
return null return null
......
...@@ -69,7 +69,13 @@ export function AccountHeader(): JSX.Element { ...@@ -69,7 +69,13 @@ export function AccountHeader(): JSX.Element {
const iconSize = 52 const iconSize = 52
return ( return (
<Flex gap="$spacing12" overflow="scroll" pt="$spacing8" testID="account-header" width="100%"> <Flex
gap="$spacing12"
overflow="scroll"
pt="$spacing8"
px="$spacing12"
testID="account-header"
width="100%">
{activeAddress && ( {activeAddress && (
<Flex alignItems="flex-start" gap="$spacing12" width="100%"> <Flex alignItems="flex-start" gap="$spacing12" width="100%">
<Flex row justifyContent="space-between" width="100%"> <Flex row justifyContent="space-between" width="100%">
......
...@@ -7,6 +7,8 @@ exports[`AccountHeader renders correctly 1`] = ` ...@@ -7,6 +7,8 @@ exports[`AccountHeader renders correctly 1`] = `
"flexDirection": "column", "flexDirection": "column",
"gap": 12, "gap": 12,
"overflow": "scroll", "overflow": "scroll",
"paddingLeft": 12,
"paddingRight": 12,
"paddingTop": 8, "paddingTop": 8,
"width": "100%", "width": "100%",
} }
...@@ -395,7 +397,7 @@ exports[`AccountHeader renders correctly 1`] = ` ...@@ -395,7 +397,7 @@ exports[`AccountHeader renders correctly 1`] = `
strokeWidth="8" strokeWidth="8"
> >
<RNSVGPath <RNSVGPath
d="M20.83 14.6C19.9 14.06 19.33 13.07 19.33 12C19.33 10.93 19.9 9.93999 20.83 9.39999C20.99 9.29999 21.05 9.1 20.95 8.94L19.28 6.06C19.22 5.95 19.11 5.89001 19 5.89001C18.94 5.89001 18.88 5.91 18.83 5.94C18.37 6.2 17.85 6.34 17.33 6.34C16.8 6.34 16.28 6.19999 15.81 5.92999C14.88 5.38999 14.31 4.41 14.31 3.34C14.31 3.15 14.16 3 13.98 3H10.02C9.83999 3 9.69 3.15 9.69 3.34C9.69 4.41 9.12 5.38999 8.19 5.92999C7.72 6.19999 7.20001 6.34 6.67001 6.34C6.15001 6.34 5.63001 6.2 5.17001 5.94C5.01001 5.84 4.81 5.9 4.72 6.06L3.04001 8.94C3.01001 8.99 3 9.05001 3 9.10001C3 9.22001 3.06001 9.32999 3.17001 9.39999C4.10001 9.93999 4.67001 10.92 4.67001 11.99C4.67001 13.07 4.09999 14.06 3.17999 14.6H3.17001C3.01001 14.7 2.94999 14.9 3.04999 15.06L4.72 17.94C4.78 18.05 4.89 18.11 5 18.11C5.06 18.11 5.12001 18.09 5.17001 18.06C6.11001 17.53 7.26 17.53 8.19 18.07C9.11 18.61 9.67999 19.59 9.67999 20.66C9.67999 20.85 9.82999 21 10.02 21H13.98C14.16 21 14.31 20.85 14.31 20.66C14.31 19.59 14.88 18.61 15.81 18.07C16.28 17.8 16.8 17.66 17.33 17.66C17.85 17.66 18.37 17.8 18.83 18.06C18.99 18.16 19.19 18.1 19.28 17.94L20.96 15.06C20.99 15.01 21 14.95 21 14.9C21 14.78 20.94 14.67 20.83 14.6ZM12 15C10.34 15 9 13.66 9 12C9 10.34 10.34 9 12 9C13.66 9 15 10.34 15 12C15 13.66 13.66 15 12 15Z" d="M22.7922 15.1778C21.6555 14.5178 20.9589 13.3078 20.9589 12C20.9589 10.6922 21.6555 9.48221 22.7922 8.82221C22.9878 8.69999 23.0611 8.45556 22.9389 8.26L20.8978 4.74C20.8244 4.60555 20.69 4.53224 20.5556 4.53224C20.4822 4.53224 20.4089 4.55667 20.3478 4.59334C19.7855 4.91111 19.15 5.08222 18.5144 5.08222C17.8667 5.08222 17.2311 4.9111 16.6567 4.5811C15.52 3.9211 14.8233 2.72333 14.8233 1.41555C14.8233 1.18333 14.64 1 14.42 1H9.57999C9.35999 1 9.17667 1.18333 9.17667 1.41555C9.17667 2.72333 8.48 3.9211 7.34334 4.5811C6.76889 4.9111 6.13335 5.08222 5.48557 5.08222C4.85002 5.08222 4.21446 4.91111 3.65224 4.59334C3.45668 4.47111 3.21222 4.54444 3.10222 4.74L1.0489 8.26C1.01223 8.32111 1 8.39445 1 8.45556C1 8.60223 1.07335 8.73666 1.20779 8.82221C2.34446 9.48221 3.04113 10.68 3.04113 11.9878C3.04113 13.3078 2.34444 14.5178 1.21999 15.1778H1.20779C1.01224 15.3 0.938874 15.5444 1.0611 15.74L3.10222 19.26C3.17556 19.3944 3.31 19.4678 3.44444 19.4678C3.51778 19.4678 3.59113 19.4433 3.65224 19.4067C4.80113 18.7589 6.20667 18.7589 7.34334 19.4189C8.46778 20.0789 9.16444 21.2767 9.16444 22.5844C9.16444 22.8167 9.34776 23 9.57999 23H14.42C14.64 23 14.8233 22.8167 14.8233 22.5844C14.8233 21.2767 15.52 20.0789 16.6567 19.4189C17.2311 19.0889 17.8667 18.9178 18.5144 18.9178C19.15 18.9178 19.7855 19.0889 20.3478 19.4067C20.5433 19.5289 20.7878 19.4556 20.8978 19.26L22.9511 15.74C22.9878 15.6789 23 15.6055 23 15.5444C23 15.3978 22.9267 15.2633 22.7922 15.1778ZM12 15.6667C9.97111 15.6667 8.33333 14.0289 8.33333 12C8.33333 9.97111 9.97111 8.33333 12 8.33333C14.0289 8.33333 15.6667 9.97111 15.6667 12C15.6667 14.0289 14.0289 15.6667 12 15.6667Z"
fill={ fill={
{ {
"type": 2, "type": 2,
......
import { useTranslation } from 'react-i18next'
import { Keyboard, StyleProp, ViewStyle } from 'react-native'
import { useAppDispatch } from 'src/app/hooks'
import {
Flex,
Image,
Text,
TouchableArea,
useDeviceDimensions,
useIsDarkMode,
useSporeColors,
} from 'ui/src'
import { EXTENSION_PROMO_BANNER_DARK, EXTENSION_PROMO_BANNER_LIGHT } from 'ui/src/assets'
import { borderRadii, iconSizes, spacing } from 'ui/src/theme'
import {
ExtensionOnboardingState,
setExtensionOnboardingState,
} from 'wallet/src/features/behaviorHistory/slice'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ExtensionOnboardingEventName } from 'wallet/src/telemetry/constants'
const IMAGE_ASPECT_RATIO = 0.69
const IMAGE_SCREEN_WIDTH_PROPORTION = 0.3
export function ExtensionPromoBanner({
onShowExtensionPromoModal,
}: {
onShowExtensionPromoModal: () => void
}): JSX.Element {
const dispatch = useAppDispatch()
const { t } = useTranslation()
const { fullWidth } = useDeviceDimensions()
const colors = useSporeColors()
const isDarkMode = useIsDarkMode()
const imageWidth = IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth
const imageHeight = imageWidth / IMAGE_ASPECT_RATIO
const onPressClaimNow = (): void => {
Keyboard.dismiss()
sendWalletAnalyticsEvent(ExtensionOnboardingEventName.PromoBannerActionTaken, {
action: 'join',
})
onShowExtensionPromoModal()
}
const onPressMaybeLater = (): void => {
sendWalletAnalyticsEvent(ExtensionOnboardingEventName.PromoBannerActionTaken, {
action: 'dismiss',
})
dispatch(setExtensionOnboardingState(ExtensionOnboardingState.Completed))
}
const baseButtonStyle: StyleProp<ViewStyle> = {
borderRadius: borderRadii.rounded12,
justifyContent: 'center',
height: iconSizes.icon36,
paddingVertical: spacing.spacing8,
paddingHorizontal: spacing.spacing12,
}
return (
<Flex
grow
row
alignContent="space-between"
backgroundColor="$background"
borderColor="$surface3"
borderRadius="$rounded20"
borderWidth="$spacing1"
mt="$spacing4"
overflow="hidden"
pl="$spacing16"
shadowColor="$neutral3"
shadowOpacity={0.4}
shadowRadius="$spacing4">
<Flex fill gap="$spacing16" justifyContent="space-between" mr="$spacing12" py="$spacing16">
<Flex gap="$spacing4">
<Text color="$neutral1" variant="subheading1">
{t('home.banner.extension.title')}
</Text>
<Text color="$neutral2" variant="body3">
{t('home.banner.extension.message')}
</Text>
</Flex>
<Flex grow row gap="$spacing8">
<TouchableArea
flexGrow={1}
style={{
...baseButtonStyle,
backgroundColor: colors.neutral1.get(),
}}
onPress={onPressClaimNow}>
<Text color={isDarkMode ? 'black' : 'white'} textAlign="center" variant="buttonLabel4">
{t('home.banner.extension.confirm')}
</Text>
</TouchableArea>
<TouchableArea
flexGrow={1}
style={{
...baseButtonStyle,
backgroundColor: colors.transparent.get(),
}}
onPress={onPressMaybeLater}>
<Text color="$neutral2" textAlign="center" variant="buttonLabel4">
{t('common.button.later')}
</Text>
</TouchableArea>
</Flex>
</Flex>
<Flex width={imageWidth}>
<Image
position="absolute"
resizeMode="contain"
right={-4}
source={{
width: imageWidth,
height: imageHeight,
uri: isDarkMode ? EXTENSION_PROMO_BANNER_DARK : EXTENSION_PROMO_BANNER_LIGHT,
}}
top={-2}
/>
</Flex>
</Flex>
)
}
...@@ -11,6 +11,7 @@ interface LinkButtonProps extends Omit<TouchableAreaProps, 'onPress'> { ...@@ -11,6 +11,7 @@ interface LinkButtonProps extends Omit<TouchableAreaProps, 'onPress'> {
isSafeUri?: boolean isSafeUri?: boolean
color?: string color?: string
iconColor?: string iconColor?: string
showIcon?: boolean
size?: number size?: number
textVariant?: TextVariantTokens textVariant?: TextVariantTokens
} }
...@@ -21,6 +22,7 @@ export function LinkButton({ ...@@ -21,6 +22,7 @@ export function LinkButton({
textVariant, textVariant,
color, color,
iconColor, iconColor,
showIcon = true,
openExternalBrowser = false, openExternalBrowser = false,
isSafeUri = false, isSafeUri = false,
size = iconSizes.icon20, size = iconSizes.icon20,
...@@ -40,15 +42,17 @@ export function LinkButton({ ...@@ -40,15 +42,17 @@ export function LinkButton({
onPress={(): Promise<void> => openUri(url, openExternalBrowser, isSafeUri)} onPress={(): Promise<void> => openUri(url, openExternalBrowser, isSafeUri)}
{...rest}> {...rest}>
<Flex row alignItems="center" gap="$spacing4" justifyContent={justifyContent}> <Flex row alignItems="center" gap="$spacing4" justifyContent={justifyContent}>
<Text {...colorStyles} variant={textVariant}> <Text {...colorStyles} flexShrink={1} variant={textVariant}>
{label} {label}
</Text> </Text>
<ExternalLinkIcon {showIcon && (
color={iconColor ?? color ?? colors.accent1.get()} <ExternalLinkIcon
height={size} color={iconColor ?? color ?? colors.accent1.get()}
strokeWidth={1.5} height={size}
width={size} strokeWidth={1.5}
/> width={size}
/>
)}
</Flex> </Flex>
</TouchableArea> </TouchableArea>
) )
......
...@@ -41,6 +41,7 @@ exports[`LinkButton renders without error 1`] = ` ...@@ -41,6 +41,7 @@ exports[`LinkButton renders without error 1`] = `
style={ style={
{ {
"color": "#222222", "color": "#222222",
"flexShrink": 1,
"fontFamily": "Basel-Book", "fontFamily": "Basel-Book",
} }
} }
......
import { fireEvent, render } from 'src/test/test-utils'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures'
import { FavoriteHeaderRow } from './FavoriteHeaderRow'
const defaultProps = {
title: 'Title',
editingTitle: 'Editing Title',
isEditing: false,
onPress: jest.fn(),
}
describe(FavoriteHeaderRow, () => {
describe('when not editing', () => {
it('renders without error', () => {
const tree = render(<FavoriteHeaderRow {...defaultProps} />)
expect(tree.toJSON()).toMatchSnapshot()
})
it('renders title', () => {
const { queryByText } = render(<FavoriteHeaderRow {...defaultProps} />)
expect(queryByText(defaultProps.title)).toBeTruthy()
expect(queryByText(defaultProps.editingTitle)).toBeFalsy()
})
it('renders favorite button', () => {
const { queryByTestId } = render(<FavoriteHeaderRow {...defaultProps} />)
const favoriteButton = queryByTestId('favorite-header-row/favorite-button')
const doneButton = queryByTestId('favorite-header-row/done-button')
expect(favoriteButton).toBeTruthy()
expect(doneButton).toBeFalsy()
})
it('calls onPress when favorite icon pressed', () => {
const { getByTestId } = render(<FavoriteHeaderRow {...defaultProps} />)
const favoriteButton = getByTestId('favorite-header-row/favorite-button')
fireEvent.press(favoriteButton, ON_PRESS_EVENT_PAYLOAD)
expect(defaultProps.onPress).toHaveBeenCalledTimes(1)
})
})
describe('when editing', () => {
it('renders without error', () => {
const tree = render(<FavoriteHeaderRow {...defaultProps} isEditing />)
expect(tree.toJSON()).toMatchSnapshot()
})
it('renders editingTitle', () => {
const { queryByText } = render(<FavoriteHeaderRow {...defaultProps} isEditing />)
expect(queryByText(defaultProps.editingTitle)).toBeTruthy()
expect(queryByText(defaultProps.title)).toBeFalsy()
})
it('renders done button', () => {
const { queryByTestId } = render(<FavoriteHeaderRow {...defaultProps} isEditing />)
const favoriteButton = queryByTestId('favorite-header-row/favorite-button')
const doneButton = queryByTestId('favorite-header-row/done-button')
expect(favoriteButton).toBeFalsy()
expect(doneButton).toBeTruthy()
})
it('calls onPress when done button pressed', () => {
const { getByTestId } = render(<FavoriteHeaderRow {...defaultProps} isEditing />)
const doneButton = getByTestId('favorite-header-row/done-button')
fireEvent.press(doneButton, ON_PRESS_EVENT_PAYLOAD)
expect(defaultProps.onPress).toHaveBeenCalledTimes(1)
})
})
})
...@@ -2,7 +2,6 @@ import { default as React } from 'react' ...@@ -2,7 +2,6 @@ import { default as React } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { Flex, Icons, Text, TouchableArea } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { ElementName } from 'wallet/src/telemetry/constants'
export function FavoriteHeaderRow({ export function FavoriteHeaderRow({
title, title,
...@@ -28,7 +27,11 @@ export function FavoriteHeaderRow({ ...@@ -28,7 +27,11 @@ export function FavoriteHeaderRow({
{isEditing ? editingTitle : title} {isEditing ? editingTitle : title}
</Text> </Text>
{!isEditing ? ( {!isEditing ? (
<TouchableArea hapticFeedback hitSlop={16} testID={ElementName.Edit} onPress={onPress}> <TouchableArea
hapticFeedback
hitSlop={16}
testID="favorite-header-row/favorite-button"
onPress={onPress}>
<Icons.TripleDots <Icons.TripleDots
color="$neutral2" color="$neutral2"
size={iconSizes.icon20} size={iconSizes.icon20}
...@@ -38,7 +41,7 @@ export function FavoriteHeaderRow({ ...@@ -38,7 +41,7 @@ export function FavoriteHeaderRow({
</TouchableArea> </TouchableArea>
) : ( ) : (
<TouchableArea hitSlop={16} onPress={onPress}> <TouchableArea hitSlop={16} onPress={onPress}>
<Text color="$accent1" variant="buttonLabel3"> <Text color="$accent1" testID="favorite-header-row/done-button" variant="buttonLabel3">
{t('common.button.done')} {t('common.button.done')}
</Text> </Text>
</TouchableArea> </TouchableArea>
......
import { makeMutable } from 'react-native-reanimated'
import configureMockStore from 'redux-mock-store'
import { act, cleanup, fireEvent, render, waitFor } from 'src/test/test-utils'
import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants'
import { Language } from 'wallet/src/features/language/constants'
import {
ON_PRESS_EVENT_PAYLOAD,
SAMPLE_CURRENCY_ID_1,
amount,
ethToken,
tokenProject,
tokenProjectMarket,
} from 'wallet/src/test/fixtures'
import { queryResolvers } from 'wallet/src/test/utils'
import { getSymbolDisplayText } from 'wallet/src/utils/currency'
import FavoriteTokenCard, { FavoriteTokenCardProps } from './FavoriteTokenCard'
const mockedNavigation = {
navigate: jest.fn(),
}
jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native')
return {
...actualNav,
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
useNavigation: () => mockedNavigation,
}
})
const mockStore = configureMockStore()
const favoriteToken = ethToken({
project: tokenProject({
markets: [
tokenProjectMarket({
price: amount({ value: 12345.67 }),
pricePercentChange24h: amount({ value: 4.56 }),
}),
],
}),
})
const touchableId = `token-box-${favoriteToken.symbol}`
const defaultProps: FavoriteTokenCardProps = {
currencyId: SAMPLE_CURRENCY_ID_1,
isTouched: makeMutable(false),
dragActivationProgress: makeMutable(0),
setIsEditing: jest.fn(),
isEditing: false,
}
const { resolvers } = queryResolvers({
token: () => favoriteToken,
})
describe('FavoriteTokenCard', () => {
it('renders without error', async () => {
const tree = render(<FavoriteTokenCard {...defaultProps} />)
expect(tree).toMatchSnapshot()
cleanup()
})
describe('when token data is being fetched', () => {
it('renders loader', async () => {
const { queryByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
const loader = queryByTestId('loader/favorite')
// loading
expect(loader).toBeTruthy()
// loading finished
await waitFor(() => {
expect(queryByTestId(touchableId)).toBeTruthy()
})
})
})
describe('when token data is available', () => {
const cases = [
{ test: 'symbol', value: getSymbolDisplayText(favoriteToken.symbol)! },
{ test: 'price', value: '$12,345.67' },
{ test: 'relative price change', value: '4.56%' },
]
it.each(cases)('renders correct $test', async ({ value }) => {
const { queryByText } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
await waitFor(() => {
expect(queryByText(value)).toBeTruthy()
})
})
it('navigates to the token details screen when pressed', async () => {
const { findByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
const touchable = await findByTestId(`token-box-${favoriteToken.symbol}`)
await act(() => {
fireEvent.press(touchable, ON_PRESS_EVENT_PAYLOAD)
})
expect(mockedNavigation.navigate).toHaveBeenCalledTimes(1)
expect(mockedNavigation.navigate).toHaveBeenCalledWith('TokenDetails', {
currencyId: SAMPLE_CURRENCY_ID_1, // passed in component props
})
})
it('does not show remove button when not in edit mode', async () => {
const { findByTestId } = render(<FavoriteTokenCard {...defaultProps} />, { resolvers })
const removeButton = await findByTestId('explore/remove-button')
expect(removeButton).toHaveAnimatedStyle({ opacity: 0 })
})
})
describe('edit mode', () => {
it('shows remove button when in edit mode', async () => {
const { findByTestId } = render(<FavoriteTokenCard {...defaultProps} isEditing />, {
resolvers,
})
const removeButton = await findByTestId('explore/remove-button')
expect(removeButton).toHaveAnimatedStyle({ opacity: 1 })
})
it('dispatches removeFavoriteToken action when remove button is pressed', async () => {
const store = mockStore({
favorites: { tokens: [] },
fiatCurrencySettings: { currentCurrency: FiatCurrency.UnitedStatesDollar },
languageSettings: { currentLanguage: Language.English },
})
const { findByTestId } = render(<FavoriteTokenCard {...defaultProps} isEditing />, {
resolvers,
store,
})
const removeButton = await findByTestId('explore/remove-button')
await act(() => {
fireEvent.press(removeButton, ON_PRESS_EVENT_PAYLOAD)
})
const actions = store.getActions()
expect(actions).toEqual([
{ type: 'favorites/removeFavoriteToken', payload: { currencyId: SAMPLE_CURRENCY_ID_1 } },
])
})
})
})
...@@ -28,7 +28,7 @@ import { getSymbolDisplayText } from 'wallet/src/utils/currency' ...@@ -28,7 +28,7 @@ import { getSymbolDisplayText } from 'wallet/src/utils/currency'
export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114 export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114
type FavoriteTokenCardProps = { export type FavoriteTokenCardProps = {
currencyId: string currencyId: string
isEditing?: boolean isEditing?: boolean
isTouched: SharedValue<boolean> isTouched: SharedValue<boolean>
......
import { makeMutable } from 'react-native-reanimated'
import configureMockStore from 'redux-mock-store'
import { Screens } from 'src/screens/Screens'
import { preloadedMobileState } from 'src/test/fixtures'
import { fireEvent, render } from 'src/test/test-utils'
import * as ensHooks from 'wallet/src/features/ens/api'
import * as unitagHooks from 'wallet/src/features/unitags/hooks'
import {
ON_PRESS_EVENT_PAYLOAD,
SAMPLE_SEED_ADDRESS_1,
preloadedWalletState,
signerMnemonicAccount,
} from 'wallet/src/test/fixtures'
import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses'
import FavoriteWalletCard, { FavoriteWalletCardProps } from './FavoriteWalletCard'
const mockedNavigation = {
navigate: jest.fn(),
}
jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native')
return {
...actualNav,
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
useNavigation: () => mockedNavigation,
}
})
const mockStore = configureMockStore()
const defaultProps: FavoriteWalletCardProps = {
address: SAMPLE_SEED_ADDRESS_1,
isTouched: makeMutable(false),
isEditing: false,
dragActivationProgress: makeMutable(0),
setIsEditing: jest.fn(),
}
describe('FavoriteWalletCard', () => {
it('renders without error', () => {
const tree = render(<FavoriteWalletCard {...defaultProps} />)
expect(tree).toMatchSnapshot()
})
describe('displayName', () => {
afterEach(() => {
jest.restoreAllMocks()
})
it('renders unitag name if available', () => {
jest.spyOn(unitagHooks, 'useUnitagByAddress').mockReturnValue({
unitag: { username: 'unitagname' },
loading: false,
})
const { queryByText } = render(<FavoriteWalletCard {...defaultProps} />)
expect(queryByText('unitagname')).toBeTruthy()
})
it('renders ens name if available', () => {
jest.spyOn(ensHooks, 'useENSName').mockReturnValue({
data: 'ensname.eth',
loading: false,
error: undefined,
})
const { queryByText } = render(<FavoriteWalletCard {...defaultProps} />)
expect(queryByText('ensname.eth')).toBeTruthy()
})
it('renders local name if wallet name is set locally', () => {
const { queryByText } = render(<FavoriteWalletCard {...defaultProps} />, {
preloadedState: preloadedMobileState({
wallet: preloadedWalletState({
account: signerMnemonicAccount({
address: defaultProps.address,
name: 'Local account',
}),
}),
}),
})
expect(queryByText('Local account')).toBeTruthy()
})
it('renders wallet address in other cases', () => {
const { queryByText } = render(<FavoriteWalletCard {...defaultProps} />)
const displayedAddress = sanitizeAddressText(shortenAddress(defaultProps.address))!
expect(queryByText(displayedAddress)).toBeTruthy()
})
})
describe('when not editing', () => {
it('navigates to the wallet details screen when pressed', () => {
const { getByTestId } = render(<FavoriteWalletCard {...defaultProps} />)
const touchable = getByTestId('favorite-wallet-card')
fireEvent.press(touchable, ON_PRESS_EVENT_PAYLOAD)
expect(mockedNavigation.navigate).toHaveBeenCalledWith(Screens.ExternalProfile, {
address: defaultProps.address,
})
})
it('does not display the remove button', () => {
const { getByTestId } = render(<FavoriteWalletCard {...defaultProps} />)
const removeButton = getByTestId('explore/remove-button')
expect(removeButton).toHaveAnimatedStyle({ opacity: 0 })
})
})
describe('when editing', () => {
it('displays the remove button', () => {
const { getByTestId } = render(<FavoriteWalletCard {...defaultProps} isEditing />)
const removeButton = getByTestId('explore/remove-button')
expect(removeButton).toHaveAnimatedStyle({ opacity: 1 })
})
it('dispatches removeWatchedAddress when remove button is pressed', () => {
const store = mockStore({
favorites: { tokens: [] },
wallet: {
accounts: {
[defaultProps.address]: signerMnemonicAccount({ address: defaultProps.address }),
},
},
})
const { getByTestId } = render(<FavoriteWalletCard {...defaultProps} isEditing />, {
store,
})
const removeButton = getByTestId('explore/remove-button')
fireEvent.press(removeButton, ON_PRESS_EVENT_PAYLOAD)
expect(store.getActions()).toEqual([
{
type: 'favorites/removeWatchedAddress',
payload: { address: defaultProps.address },
},
])
})
})
})
...@@ -17,7 +17,7 @@ import { removeWatchedAddress } from 'wallet/src/features/favorites/slice' ...@@ -17,7 +17,7 @@ import { removeWatchedAddress } from 'wallet/src/features/favorites/slice'
import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
type FavoriteWalletCardProps = { export type FavoriteWalletCardProps = {
address: Address address: Address
isEditing?: boolean isEditing?: boolean
isTouched: SharedValue<boolean> isTouched: SharedValue<boolean>
...@@ -84,6 +84,7 @@ function FavoriteWalletCard({ ...@@ -84,6 +84,7 @@ function FavoriteWalletCard({
disabled={isEditing} disabled={isEditing}
hapticStyle={ImpactFeedbackStyle.Light} hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4" m="$spacing4"
testID="favorite-wallet-card"
onLongPress={disableOnPress} onLongPress={disableOnPress}
onPress={(): void => { onPress={(): void => {
navigate(address) navigate(address)
......
import { fireEvent, render } from 'src/test/test-utils'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures'
import RemoveButton from './RemoveButton'
describe(RemoveButton, () => {
it('renders without error', () => {
const tree = render(<RemoveButton />)
expect(tree.toJSON()).toMatchSnapshot()
})
it('calls onPress when pressed', () => {
const onPress = jest.fn()
const { getByTestId } = render(<RemoveButton onPress={onPress} />)
const button = getByTestId('explore/remove-button')
fireEvent.press(button, ON_PRESS_EVENT_PAYLOAD)
expect(onPress).toHaveBeenCalledTimes(1)
})
describe('visibility', () => {
it('renders with opacity 1 when visible', () => {
const { getByTestId } = render(<RemoveButton visible />)
const button = getByTestId('explore/remove-button')
expect(button).toHaveAnimatedStyle({ opacity: 1 })
})
it('renders with opacity 0 when not visible', () => {
const { getByTestId } = render(<RemoveButton visible={false} />)
const button = getByTestId('explore/remove-button')
expect(button).toHaveAnimatedStyle({ opacity: 0 })
})
})
})
...@@ -20,6 +20,7 @@ export default function RemoveButton({ visible = true, ...rest }: RemoveButtonPr ...@@ -20,6 +20,7 @@ export default function RemoveButton({ visible = true, ...rest }: RemoveButtonPr
height={imageSizes.image24} height={imageSizes.image24}
justifyContent="center" justifyContent="center"
style={animatedVisibilityStyle} style={animatedVisibilityStyle}
testID="explore/remove-button"
width={imageSizes.image24} width={imageSizes.image24}
zIndex="$tooltip" zIndex="$tooltip"
{...rest}> {...rest}>
......
import ContextMenu from 'react-native-context-menu-view'
import { render } from 'src/test/test-utils'
import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ClientTokensOrderBy } from 'wallet/src/features/wallet/types'
import { SortButton } from './SortButton'
jest.mock('react-native-context-menu-view', () => {
// Use the actual implementation of `react-native-context-menu-view` as the mock implementation
// (we use mock just to get the props of the component in test)
return jest.fn(jest.requireActual('react-native-context-menu-view').default)
})
describe('SortButton', () => {
it('renders without error', () => {
const tree = render(<SortButton orderBy={TokenSortableField.Volume} />)
expect(tree).toMatchSnapshot()
})
const cases = [
{ test: 'volume', orderBy: TokenSortableField.Volume, label: 'Volume' },
{ test: 'total value locked', orderBy: TokenSortableField.TotalValueLocked, label: 'TVL' },
{ test: 'market cap', orderBy: TokenSortableField.MarketCap, label: 'Market cap' },
{
test: 'price increase',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hDesc,
label: 'Price increase',
},
{
test: 'price decrease',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hAsc,
label: 'Price decrease',
},
]
describe.each(cases)('when ordering by $test', ({ orderBy, label }) => {
it(`renders ${label} as the selected option`, () => {
const { queryByText } = render(<SortButton orderBy={orderBy} />)
const selectedOption = queryByText(label)
expect(selectedOption).toBeTruthy()
})
it(`returns correct context menu actions with checmark near the ${label} option`, () => {
jest.clearAllMocks()
render(<SortButton orderBy={orderBy} />)
expect((ContextMenu as unknown as jest.Mock).mock.calls[0][0]).toEqual(
expect.objectContaining({
actions: [
{
title: 'Uniswap volume (24H)',
systemIcon: orderBy === TokenSortableField.Volume ? 'checkmark' : '',
orderBy: TokenSortableField.Volume,
},
{
title: 'Uniswap TVL',
systemIcon: orderBy === TokenSortableField.TotalValueLocked ? 'checkmark' : '',
orderBy: TokenSortableField.TotalValueLocked,
},
{
title: 'Market cap',
systemIcon: orderBy === TokenSortableField.MarketCap ? 'checkmark' : '',
orderBy: TokenSortableField.MarketCap,
},
{
title: 'Price increase (24H)',
systemIcon:
orderBy === ClientTokensOrderBy.PriceChangePercentage24hDesc ? 'checkmark' : '',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hDesc,
},
{
title: 'Price decrease (24H)',
systemIcon:
orderBy === ClientTokensOrderBy.PriceChangePercentage24hAsc ? 'checkmark' : '',
orderBy: ClientTokensOrderBy.PriceChangePercentage24hAsc,
},
],
})
)
})
})
})
import * as tokenDetailsHooks from 'src/components/TokenDetails/hooks'
import { TOKEN_ITEM_DATA, tokenItemData } from 'src/test/fixtures'
import { fireEvent, render, within } from 'src/test/test-utils'
import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types'
import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures'
import { buildCurrencyId } from 'wallet/src/utils/currencyId'
import { TokenItem } from './TokenItem'
import * as exploreHooks from './hooks'
describe('TokenItem', () => {
const mockedTokenDetailsNavigation = {
navigate: jest.fn(),
navigateWithPop: jest.fn(),
preload: jest.fn(),
}
beforeAll(() => {
jest
.spyOn(tokenDetailsHooks, 'useTokenDetailsNavigation')
.mockReturnValue(mockedTokenDetailsNavigation)
jest.spyOn(exploreHooks, 'useExploreTokenContextMenu').mockReturnValue({
menuActions: [],
onContextMenuPress: jest.fn(),
})
})
it('renders without error', () => {
const tree = render(<TokenItem index={0} tokenItemData={TOKEN_ITEM_DATA} />)
expect(tree).toMatchSnapshot()
})
it('renders correct token number based on index', () => {
const data = tokenItemData()
const { queryByText } = render(<TokenItem index={1} tokenItemData={data} />)
expect(queryByText('2')).toBeTruthy()
})
it('renders proper token name', () => {
const data = tokenItemData()
const { queryByText } = render(<TokenItem index={0} tokenItemData={data} />)
expect(queryByText(data.name)).toBeTruthy()
})
it('navigates to the token details screen when pressed', () => {
const data = tokenItemData()
const { getByTestId } = render(<TokenItem index={0} tokenItemData={data} />)
fireEvent.press(getByTestId(`token-item-${data.name}`), ON_PRESS_EVENT_PAYLOAD)
expect(mockedTokenDetailsNavigation.navigate).toHaveBeenCalledWith(
buildCurrencyId(data.chainId, data.address)
)
})
describe('token price', () => {
it('renders token price if it is provided', () => {
const data = tokenItemData({ price: 123.45 })
const { getByTestId } = render(<TokenItem index={0} tokenItemData={data} />)
const tokenPrice = getByTestId('token-item/price')
expect(within(tokenPrice).queryByText('$123.45')).toBeTruthy()
expect(within(tokenPrice).queryByText('-')).toBeFalsy()
})
it('renders price placeholder if token price is not provided', () => {
const data = tokenItemData({ price: undefined })
const { getByTestId } = render(<TokenItem index={0} tokenItemData={data} />)
const tokenPrice = getByTestId('token-item/price')
expect(within(tokenPrice).queryByText('-')).toBeTruthy()
})
})
describe('token price change', () => {
it('renders token price change if it is provided', () => {
const data = tokenItemData({ pricePercentChange24h: 12.34 })
const { getByTestId } = render(<TokenItem index={0} tokenItemData={data} />)
const relativeChange = getByTestId('relative-change')
expect(within(relativeChange).queryByText('12.34%')).toBeTruthy()
})
it('renders price change placeholder if token price change is not provided', () => {
const data = tokenItemData({ pricePercentChange24h: undefined })
const { getByTestId } = render(<TokenItem index={0} tokenItemData={data} />)
const relativeChange = getByTestId('relative-change')
expect(within(relativeChange).queryByText('-')).toBeTruthy()
})
})
describe('metadata subtitle', () => {
const data = tokenItemData({
marketCap: 123.45,
volume24h: 234.56,
totalValueLocked: 345.67,
})
const cases = [
{ test: 'market cap', type: TokenMetadataDisplayType.MarketCap, expected: '$123.45 MCap' },
{ test: 'volume', type: TokenMetadataDisplayType.Volume, expected: '$234.56 Vol' },
{ test: 'total value locked', type: TokenMetadataDisplayType.TVL, expected: '$345.67 TVL' },
{ test: 'symbol', type: TokenMetadataDisplayType.Symbol, expected: data.symbol },
]
it.each(cases)('renders $test metadata subtitle', ({ type, expected }) => {
const { getByTestId } = render(
<TokenItem index={0} metadataDisplayType={type} tokenItemData={data} />
)
const metadataSubtitle = getByTestId('token-item/metadata-subtitle')
expect(within(metadataSubtitle).queryByText(expected)).toBeTruthy()
})
})
})
...@@ -125,13 +125,17 @@ export const TokenItem = memo(function _TokenItem({ ...@@ -125,13 +125,17 @@ export const TokenItem = memo(function _TokenItem({
<Text numberOfLines={1} variant="body1"> <Text numberOfLines={1} variant="body1">
{name} {name}
</Text> </Text>
<Text color="$neutral2" numberOfLines={1} variant="subheading2"> <Text
color="$neutral2"
numberOfLines={1}
testID="token-item/metadata-subtitle"
variant="subheading2">
{getMetadataSubtitle()} {getMetadataSubtitle()}
</Text> </Text>
</Flex> </Flex>
<Flex grow row alignItems="center" justifyContent="flex-end"> <Flex grow row alignItems="center" justifyContent="flex-end">
<TokenMetadata> <TokenMetadata>
<Text lineHeight={24} variant="body1"> <Text lineHeight={24} testID="token-item/price" variant="body1">
{convertFiatAmountFormatted(price, NumberType.FiatTokenPrice)} {convertFiatAmountFormatted(price, NumberType.FiatTokenPrice)}
</Text> </Text>
<RelativeChange change={pricePercentChange24h} variant="body2" /> <RelativeChange change={pricePercentChange24h} variant="body2" />
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FavoriteHeaderRow when editing renders without error 1`] = `
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"gap": 16,
"justifyContent": "space-between",
"marginBottom": 8,
"marginLeft": 8,
"marginRight": 8,
}
}
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "#7D7D7D",
"fontFamily": "Basel-Book",
"fontSize": 17,
"lineHeight": 24,
}
}
suppressHighlighting={true}
>
Editing Title
</Text>
<View
cancelable={true}
disabled={false}
focusable={true}
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,
},
],
}
}
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.2}
style={
{
"color": "#FC72FF",
"fontFamily": "Basel-Medium",
"fontSize": 17,
"fontWeight": "500",
"lineHeight": 24,
}
}
suppressHighlighting={true}
testID="favorite-header-row/done-button"
>
Done
</Text>
</View>
</View>
`;
exports[`FavoriteHeaderRow when not editing renders without error 1`] = `
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"gap": 16,
"justifyContent": "space-between",
"marginBottom": 8,
"marginLeft": 8,
"marginRight": 8,
}
}
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "#7D7D7D",
"fontFamily": "Basel-Book",
"fontSize": 17,
"lineHeight": 24,
}
}
suppressHighlighting={true}
>
Title
</Text>
<View
cancelable={true}
disabled={false}
focusable={true}
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="favorite-header-row/favorite-button"
>
<RNSVGSvgView
align="xMidYMid"
bbHeight="20"
bbWidth="20"
fill="currentColor"
focusable={false}
meetOrSlice={0}
minX={0}
minY={0}
stroke="currentColor"
strokeLinecap="round"
strokeWidth={1}
style={
[
{
"backgroundColor": "transparent",
"borderWidth": 0,
},
{
"color": "#7D7D7D",
"height": 20,
"width": 20,
},
{
"flex": 0,
"height": 20,
"width": 20,
},
]
}
tintColor="#7D7D7D"
vbHeight={4}
vbWidth={18}
>
<RNSVGGroup
fill={
{
"type": 2,
}
}
propList={
[
"fill",
"stroke",
"strokeWidth",
"strokeLinecap",
]
}
stroke={
{
"type": 2,
}
}
strokeLinecap={1}
strokeWidth="1"
>
<RNSVGPath
d="M9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
fill={
{
"payload": 4278190080,
"type": 0,
}
}
propList={
[
"stroke",
"strokeWidth",
"strokeLinecap",
"strokeLinejoin",
]
}
stroke={
{
"type": 2,
}
}
strokeLinecap={1}
strokeLinejoin={1}
strokeWidth="2"
/>
<RNSVGPath
d="M16 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
fill={
{
"payload": 4278190080,
"type": 0,
}
}
propList={
[
"stroke",
"strokeWidth",
"strokeLinecap",
"strokeLinejoin",
]
}
stroke={
{
"type": 2,
}
}
strokeLinecap={1}
strokeLinejoin={1}
strokeWidth="2"
/>
<RNSVGPath
d="M2 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
fill={
{
"payload": 4278190080,
"type": 0,
}
}
propList={
[
"stroke",
"strokeWidth",
"strokeLinecap",
"strokeLinejoin",
]
}
stroke={
{
"type": 2,
}
}
strokeLinecap={1}
strokeLinejoin={1}
strokeWidth="2"
/>
</RNSVGGroup>
</RNSVGSvgView>
</View>
</View>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FavoriteTokenCard renders without error 1`] = `
<View
onLayout={[Function]}
style={
{
"flexDirection": "column",
"opacity": 0,
}
}
testID="shimmer-placeholder"
>
<View
sentry-label="FlexLoader"
style={
{
"flexDirection": "column",
}
}
>
<View
style={
{
"backgroundColor": "#2222220D",
"borderBottomLeftRadius": 16,
"borderBottomRightRadius": 16,
"borderTopLeftRadius": 16,
"borderTopRightRadius": 16,
"flexDirection": "column",
"height": 114,
"width": "100%",
}
}
testID="loader/favorite"
/>
</View>
</View>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RemoveButton renders without error 1`] = `
<View
animatedStyle={
{
"value": {
"opacity": 1,
},
}
}
cancelable={true}
collapsable={false}
disabled={false}
focusable={true}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"alignItems": "center",
"backgroundColor": "#CECECE",
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
"borderTopRightRadius": 999999,
"flexDirection": "column",
"height": 24,
"justifyContent": "center",
"opacity": 1,
"transform": [
{
"scale": 1,
},
],
"width": 24,
"zIndex": 1080,
}
}
testID="explore/remove-button"
>
<View
style={
{
"backgroundColor": "#FFFFFF",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"height": 2,
"width": 10,
}
}
/>
</View>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SortButton renders without error 1`] = `
<ContextMenu
actions={
[
{
"orderBy": "VOLUME",
"systemIcon": "checkmark",
"title": "Uniswap volume (24H)",
},
{
"orderBy": "TOTAL_VALUE_LOCKED",
"systemIcon": "",
"title": "Uniswap TVL",
},
{
"orderBy": "MARKET_CAP",
"systemIcon": "",
"title": "Market cap",
},
{
"orderBy": "PriceChangePercentage24hDesc",
"systemIcon": "",
"title": "Price increase (24H)",
},
{
"orderBy": "PriceChangePercentage24hAsc",
"systemIcon": "",
"title": "Price decrease (24H)",
},
]
}
dropdownMenuMode={true}
onPress={[Function]}
>
<View
cancelable={true}
disabled={false}
focusable={true}
hitSlop={[Function]}
minPressDuration={0}
onBlur={[Function]}
onFocus={[Function]}
onLongPress={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onPress={[Function]}
onPressIn={[Function]}
onPressOut={[Function]}
style={
{
"alignItems": "center",
"backgroundColor": "#FFFFFF",
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
"borderTopRightRadius": 999999,
"flexDirection": "row",
"opacity": 1,
"paddingBottom": 8,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 8,
"transform": [
{
"scale": 1,
},
],
}
}
>
<View
style={
{
"flexDirection": "row",
"gap": 4,
}
}
>
<Text
allowFontScaling={true}
lineBreakMode="clip"
maxFontSizeMultiplier={1.2}
numberOfLines={1}
style={
{
"color": "#7D7D7D",
"flexShrink": 1,
"fontFamily": "Basel-Medium",
"fontSize": 17,
"fontWeight": "500",
"lineHeight": 24,
}
}
suppressHighlighting={true}
>
Volume
</Text>
<View
style={
{
"alignItems": "center",
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
"borderTopRightRadius": 999999,
"flexDirection": "column",
"justifyContent": "center",
"transform": [
{
"rotate": "270deg",
},
],
}
}
>
<RNSVGSvgView
align="xMidYMid"
bbHeight="20"
bbWidth="20"
fill="none"
focusable={false}
meetOrSlice={0}
minX={0}
minY={0}
stroke="currentColor"
strokeWidth={8}
style={
[
{
"backgroundColor": "transparent",
"borderWidth": 0,
},
{
"color": "#7D7D7D",
"height": 20,
"width": 20,
},
{
"flex": 0,
"height": 20,
"width": 20,
},
]
}
tintColor="#7D7D7D"
vbHeight={24}
vbWidth={24}
>
<RNSVGGroup
fill={null}
propList={
[
"fill",
"stroke",
"strokeWidth",
]
}
stroke={
{
"type": 2,
}
}
strokeWidth="8"
>
<RNSVGPath
d="M15 6L9 12L15 18"
fill={
{
"payload": 4278190080,
"type": 0,
}
}
propList={
[
"stroke",
"strokeWidth",
"strokeLinecap",
"strokeLinejoin",
]
}
stroke={
{
"type": 2,
}
}
strokeLinecap={1}
strokeLinejoin={1}
strokeWidth="3"
/>
</RNSVGGroup>
</RNSVGSvgView>
</View>
</View>
</View>
</ContextMenu>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TokenItem renders without error 1`] = `
<View
actions={[]}
onPress={[MockFunction]}
>
<View
cancelable={true}
disabled={false}
focusable={true}
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="token-item-cum"
>
<View
animatedStyle={
{
"value": {},
}
}
collapsable={false}
style={
{
"flexDirection": "row",
"flexGrow": 1,
"gap": 12,
"paddingBottom": 8,
"paddingLeft": 24,
"paddingRight": 24,
"paddingTop": 8,
}
}
>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"gap": 4,
"justifyContent": "center",
"overflow": "hidden",
}
}
>
<View
style={
{
"flexDirection": "column",
"minWidth": 16,
}
}
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.2}
style={
{
"color": "#7D7D7D",
"fontFamily": "Basel-Medium",
"fontSize": 15,
"fontWeight": "500",
"lineHeight": 16,
}
}
suppressHighlighting={true}
>
1
</Text>
</View>
<View
style={
{
"alignItems": "center",
"flexDirection": "column",
"height": 40,
"justifyContent": "center",
"width": 40,
}
}
>
<Image
source={
{
"uri": "https://loremflickr.com/640/480",
}
}
style={
[
{
"resizeMode": "contain",
},
{
"backgroundColor": "#2222220D",
"borderColor": "#2222220D",
"borderRadius": 20,
"borderWidth": 0.5,
"height": 40,
"width": 40,
},
]
}
/>
</View>
</View>
<View
style={
{
"flexDirection": "column",
"flexShrink": 1,
"gap": 2,
}
}
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
style={
{
"color": "#222222",
"fontFamily": "Basel-Book",
"fontSize": 19,
"lineHeight": 24,
}
}
suppressHighlighting={true}
>
cum
</Text>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
style={
{
"color": "#7D7D7D",
"fontFamily": "Basel-Book",
"fontSize": 17,
"lineHeight": 24,
}
}
suppressHighlighting={true}
testID="token-item/metadata-subtitle"
/>
</View>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"flexGrow": 1,
"justifyContent": "flex-end",
}
}
>
<View
style={
{
"flexDirection": "row",
}
}
>
<View
style={
{
"alignItems": "flex-end",
"flexDirection": "column",
"gap": 4,
"minWidth": 70,
}
}
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "#222222",
"fontFamily": "Basel-Book",
"fontSize": 19,
"lineHeight": 24,
}
}
suppressHighlighting={true}
testID="token-item/price"
>
-
</Text>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
"gap": 2,
"justifyContent": "flex-start",
}
}
testID="relative-change"
>
<View
style={
{
"flexDirection": "column",
}
}
>
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
style={
{
"color": "#7D7D7D",
"fontFamily": "Basel-Book",
"fontSize": 17,
"lineHeight": 24,
}
}
suppressHighlighting={true}
>
-
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
</View>
`;
import { NativeSyntheticEvent, Share } from 'react-native' import { NativeSyntheticEvent, Share } from 'react-native'
import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view'
import { act } from 'react-test-renderer'
import configureMockStore from 'redux-mock-store' import configureMockStore from 'redux-mock-store'
import { useExploreTokenContextMenu } from 'src/components/explore/hooks' import { useExploreTokenContextMenu } from 'src/components/explore/hooks'
import { renderHookWithProviders } from 'src/test/render' import { renderHookWithProviders } from 'src/test/render'
...@@ -9,6 +8,7 @@ import { FavoritesState } from 'wallet/src/features/favorites/slice' ...@@ -9,6 +8,7 @@ import { FavoritesState } from 'wallet/src/features/favorites/slice'
import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types'
import { SectionName } from 'wallet/src/telemetry/constants' import { SectionName } from 'wallet/src/telemetry/constants'
import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures/constants' import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures/constants'
import { cleanup } from 'wallet/src/test/test-utils'
const tokenId = SAMPLE_SEED_ADDRESS_1 const tokenId = SAMPLE_SEED_ADDRESS_1
const currencyId = `1-${tokenId}` const currencyId = `1-${tokenId}`
...@@ -35,10 +35,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -35,10 +35,6 @@ describe(useExploreTokenContextMenu, () => {
{ resolvers } { resolvers }
) )
await act(async () => {
// Wait for the token query to resolve
})
expect(result.current.menuActions).toEqual([ expect(result.current.menuActions).toEqual([
expect.objectContaining({ expect.objectContaining({
title: 'Favorite token', title: 'Favorite token',
...@@ -57,6 +53,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -57,6 +53,7 @@ describe(useExploreTokenContextMenu, () => {
onPress: expect.any(Function), onPress: expect.any(Function),
}), }),
]) ])
cleanup()
}) })
it('renders proper context menu items when onEditFavorites is provided', async () => { it('renders proper context menu items when onEditFavorites is provided', async () => {
...@@ -66,10 +63,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -66,10 +63,6 @@ describe(useExploreTokenContextMenu, () => {
{ resolvers } { resolvers }
) )
await act(async () => {
// Wait for the token query to resolve
})
expect(result.current.menuActions).toEqual([ expect(result.current.menuActions).toEqual([
expect.objectContaining({ expect.objectContaining({
title: 'Favorite token', title: 'Favorite token',
...@@ -88,6 +81,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -88,6 +81,7 @@ describe(useExploreTokenContextMenu, () => {
onPress: expect.any(Function), onPress: expect.any(Function),
}), }),
]) ])
cleanup()
}) })
it('calls onEditFavorites when edit favorites is pressed', async () => { it('calls onEditFavorites when edit favorites is pressed', async () => {
...@@ -97,10 +91,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -97,10 +91,6 @@ describe(useExploreTokenContextMenu, () => {
{ resolvers } { resolvers }
) )
await act(async () => {
// Wait for the token query to resolve
})
const editFavoritesActionIndex = result.current.menuActions.findIndex( const editFavoritesActionIndex = result.current.menuActions.findIndex(
(action: ContextMenuAction) => action.title === 'Edit favorites' (action: ContextMenuAction) => action.title === 'Edit favorites'
) )
...@@ -109,6 +99,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -109,6 +99,7 @@ describe(useExploreTokenContextMenu, () => {
} as NativeSyntheticEvent<ContextMenuOnPressNativeEvent>) } as NativeSyntheticEvent<ContextMenuOnPressNativeEvent>)
expect(onEditFavorites).toHaveBeenCalledTimes(1) expect(onEditFavorites).toHaveBeenCalledTimes(1)
cleanup()
}) })
}) })
...@@ -124,10 +115,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -124,10 +115,6 @@ describe(useExploreTokenContextMenu, () => {
} }
) )
await act(async () => {
// Wait for the token query to resolve
})
expect(result.current.menuActions).toEqual([ expect(result.current.menuActions).toEqual([
expect.objectContaining({ expect.objectContaining({
title: 'Remove favorite', title: 'Remove favorite',
...@@ -146,6 +133,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -146,6 +133,7 @@ describe(useExploreTokenContextMenu, () => {
onPress: expect.any(Function), onPress: expect.any(Function),
}), }),
]) ])
cleanup()
}) })
it("dispatches add to favorites redux action when 'Favorite token' is pressed", async () => { it("dispatches add to favorites redux action when 'Favorite token' is pressed", async () => {
...@@ -155,10 +143,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -155,10 +143,6 @@ describe(useExploreTokenContextMenu, () => {
{ resolvers, store } { resolvers, store }
) )
await act(async () => {
// Wait for the token query to resolve
})
const favoriteTokenActionIndex = result.current.menuActions.findIndex( const favoriteTokenActionIndex = result.current.menuActions.findIndex(
(action: ContextMenuAction) => action.title === 'Favorite token' (action: ContextMenuAction) => action.title === 'Favorite token'
) )
...@@ -173,6 +157,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -173,6 +157,7 @@ describe(useExploreTokenContextMenu, () => {
payload: { currencyId: tokenMenuParams.currencyId }, payload: { currencyId: tokenMenuParams.currencyId },
}, },
]) ])
cleanup()
}) })
it("dispatches remove from favorites redux action when 'Remove favorite' is pressed", async () => { it("dispatches remove from favorites redux action when 'Remove favorite' is pressed", async () => {
...@@ -185,10 +170,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -185,10 +170,6 @@ describe(useExploreTokenContextMenu, () => {
{ resolvers, store } { resolvers, store }
) )
await act(async () => {
// Wait for the token query to resolve
})
const removeFavoriteTokenActionIndex = result.current.menuActions.findIndex( const removeFavoriteTokenActionIndex = result.current.menuActions.findIndex(
(action: ContextMenuAction) => action.title === 'Remove favorite' (action: ContextMenuAction) => action.title === 'Remove favorite'
) )
...@@ -203,6 +184,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -203,6 +184,7 @@ describe(useExploreTokenContextMenu, () => {
payload: { currencyId: tokenMenuParams.currencyId }, payload: { currencyId: tokenMenuParams.currencyId },
}, },
]) ])
cleanup()
}) })
}) })
...@@ -216,10 +198,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -216,10 +198,6 @@ describe(useExploreTokenContextMenu, () => {
resolvers, resolvers,
}) })
await act(async () => {
// Wait for the token query to resolve
})
const swapActionIndex = result.current.menuActions.findIndex( const swapActionIndex = result.current.menuActions.findIndex(
(action: ContextMenuAction) => action.title === 'Swap' (action: ContextMenuAction) => action.title === 'Swap'
) )
...@@ -246,6 +224,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -246,6 +224,7 @@ describe(useExploreTokenContextMenu, () => {
}, },
}, },
]) ])
cleanup()
}) })
it('opens share modal when share is pressed', async () => { it('opens share modal when share is pressed', async () => {
...@@ -253,10 +232,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -253,10 +232,6 @@ describe(useExploreTokenContextMenu, () => {
resolvers, resolvers,
}) })
await act(async () => {
// Wait for the token query to resolve
})
jest.spyOn(Share, 'share') jest.spyOn(Share, 'share')
const shareActionIndex = result.current.menuActions.findIndex( const shareActionIndex = result.current.menuActions.findIndex(
...@@ -267,5 +242,6 @@ describe(useExploreTokenContextMenu, () => { ...@@ -267,5 +242,6 @@ describe(useExploreTokenContextMenu, () => {
} as NativeSyntheticEvent<ContextMenuOnPressNativeEvent>) } as NativeSyntheticEvent<ContextMenuOnPressNativeEvent>)
expect(Share.share).toHaveBeenCalledTimes(1) expect(Share.share).toHaveBeenCalledTimes(1)
cleanup()
}) })
}) })
...@@ -16,6 +16,7 @@ import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/featu ...@@ -16,6 +16,7 @@ import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/featu
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants'
import { CurrencyId } from 'uniswap/src/types/currency'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { AssetType } from 'wallet/src/entities/assets' import { AssetType } from 'wallet/src/entities/assets'
...@@ -25,7 +26,7 @@ import { ...@@ -25,7 +26,7 @@ import {
} from 'wallet/src/features/transactions/transactionState/types' } from 'wallet/src/features/transactions/transactionState/types'
import { useAppDispatch } from 'wallet/src/state' import { useAppDispatch } from 'wallet/src/state'
import { ElementName, ModalName, SectionNameType } from 'wallet/src/telemetry/constants' import { ElementName, ModalName, SectionNameType } from 'wallet/src/telemetry/constants'
import { CurrencyId, currencyIdToAddress } from 'wallet/src/utils/currencyId' import { currencyIdToAddress } from 'wallet/src/utils/currencyId'
import { getTokenUrl } from 'wallet/src/utils/linking' import { getTokenUrl } from 'wallet/src/utils/linking'
interface TokenMenuParams { interface TokenMenuParams {
......
...@@ -7,9 +7,8 @@ import { SearchPopularNFTCollections } from 'src/components/explore/search/Searc ...@@ -7,9 +7,8 @@ import { SearchPopularNFTCollections } from 'src/components/explore/search/Searc
import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens' import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens'
import { renderSearchItem } from 'src/components/explore/search/SearchResultsSection' import { renderSearchItem } from 'src/components/explore/search/SearchResultsSection'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { AnimatedFlex, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src'
import ClockIcon from 'ui/src/assets/icons/clock.svg' import ClockIcon from 'ui/src/assets/icons/clock.svg'
import TrendArrowIcon from 'ui/src/assets/icons/trend-up.svg'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags' import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
...@@ -21,6 +20,8 @@ import { ...@@ -21,6 +20,8 @@ import {
} from 'wallet/src/features/search/SearchResult' } from 'wallet/src/features/search/SearchResult'
import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory' import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory'
const TrendUpIcon = <Icons.TrendUp color="$neutral2" size="$icon.24" />
export const SUGGESTED_WALLETS: WalletSearchResult[] = [ export const SUGGESTED_WALLETS: WalletSearchResult[] = [
{ {
type: SearchResultType.ENSAddress, type: SearchResultType.ENSAddress,
...@@ -62,7 +63,7 @@ export function SearchEmptySection(): JSX.Element { ...@@ -62,7 +63,7 @@ export function SearchEmptySection(): JSX.Element {
// Show search history (if applicable), trending tokens, and wallets // Show search history (if applicable), trending tokens, and wallets
return ( return (
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing12"> <AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing12" pb="$spacing36">
{searchHistory.length > 0 && ( {searchHistory.length > 0 && (
<AnimatedFlex entering={FadeIn} exiting={FadeOut}> <AnimatedFlex entering={FadeIn} exiting={FadeOut}>
<FlatList <FlatList
...@@ -92,17 +93,17 @@ export function SearchEmptySection(): JSX.Element { ...@@ -92,17 +93,17 @@ export function SearchEmptySection(): JSX.Element {
</AnimatedFlex> </AnimatedFlex>
)} )}
<Flex gap="$spacing4"> <Flex gap="$spacing4">
<SectionHeaderText icon={<TrendIcon />} title={t('explore.search.section.popularTokens')} /> <SectionHeaderText icon={TrendUpIcon} title={t('explore.search.section.popularTokens')} />
<SearchPopularTokens /> <SearchPopularTokens />
</Flex> </Flex>
<Flex gap="$spacing4"> <Flex gap="$spacing4">
<SectionHeaderText icon={<TrendIcon />} title={t('explore.search.section.popularNFT')} /> <SectionHeaderText icon={TrendUpIcon} title={t('explore.search.section.popularNFT')} />
<SearchPopularNFTCollections /> <SearchPopularNFTCollections />
</Flex> </Flex>
<FlatList <FlatList
ListHeaderComponent={ ListHeaderComponent={
<SectionHeaderText <SectionHeaderText
icon={<TrendIcon />} icon={TrendUpIcon}
title={t('explore.search.section.suggestedWallets')} title={t('explore.search.section.suggestedWallets')}
/> />
} }
...@@ -118,11 +119,6 @@ const walletKey = (wallet: WalletSearchResult): string => { ...@@ -118,11 +119,6 @@ const walletKey = (wallet: WalletSearchResult): string => {
return wallet.address return wallet.address
} }
export const TrendIcon = (): JSX.Element => {
const colors = useSporeColors()
return <TrendArrowIcon color={colors.neutral2.get()} width={iconSizes.icon20} />
}
export const RecentIcon = (): JSX.Element => { export const RecentIcon = (): JSX.Element => {
const colors = useSporeColors() const colors = useSporeColors()
return ( return (
......
...@@ -2,26 +2,35 @@ import React from 'react' ...@@ -2,26 +2,35 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { AnimatedFlex, Flex, Loader } from 'ui/src' import { AnimatedFlex, Flex, Icons, Loader } from 'ui/src'
export const SearchResultsLoader = (): JSX.Element => { export const SearchResultsLoader = (): JSX.Element => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Flex gap="$spacing16"> <Flex gap="$spacing16">
<Flex gap="$spacing12"> <Flex gap="$spacing12">
<SectionHeaderText title={t('explore.search.section.tokens')} /> <SectionHeaderText
icon={<Icons.Coin color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.tokens')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8"> <AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token repeat={2} /> <Loader.Token repeat={2} />
</AnimatedFlex> </AnimatedFlex>
</Flex> </Flex>
<Flex gap="$spacing12"> <Flex gap="$spacing12">
<SectionHeaderText title={t('explore.search.section.nft')} /> <SectionHeaderText
icon={<Icons.Gallery color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.nft')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8"> <AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token repeat={2} /> <Loader.Token repeat={2} />
</AnimatedFlex> </AnimatedFlex>
</Flex> </Flex>
<Flex gap="$spacing12"> <Flex gap="$spacing12">
<SectionHeaderText title={t('explore.search.section.wallets')} /> <SectionHeaderText
icon={<Icons.Person color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.wallets')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8"> <AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing8">
<Loader.Token /> <Loader.Token />
</AnimatedFlex> </AnimatedFlex>
......
...@@ -16,7 +16,7 @@ import { ...@@ -16,7 +16,7 @@ import {
formatTokenSearchResults, formatTokenSearchResults,
getSearchResultId, getSearchResultId,
} from 'src/components/explore/search/utils' } from 'src/components/explore/search/utils'
import { AnimatedFlex, Flex, Text } from 'ui/src' import { AnimatedFlex, Flex, Icons, Text } from 'ui/src'
import { import {
SafetyLevel, SafetyLevel,
useExploreSearchQuery, useExploreSearchQuery,
...@@ -35,15 +35,21 @@ import { getValidAddress } from 'wallet/src/utils/addresses' ...@@ -35,15 +35,21 @@ import { getValidAddress } from 'wallet/src/utils/addresses'
import { SEARCH_RESULT_HEADER_KEY } from './constants' import { SEARCH_RESULT_HEADER_KEY } from './constants'
import { SearchResultOrHeader } from './types' import { SearchResultOrHeader } from './types'
const ICON_SIZE = '$icon.24'
const ICON_COLOR = '$neutral2'
const WalletHeaderItem: SearchResultOrHeader = { const WalletHeaderItem: SearchResultOrHeader = {
icon: <Icons.Person color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY, type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.wallets'), title: i18n.t('explore.search.section.wallets'),
} }
const TokenHeaderItem: SearchResultOrHeader = { const TokenHeaderItem: SearchResultOrHeader = {
icon: <Icons.Coin color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY, type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.tokens'), title: i18n.t('explore.search.section.tokens'),
} }
const NFTHeaderItem: SearchResultOrHeader = { const NFTHeaderItem: SearchResultOrHeader = {
icon: <Icons.Gallery color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY, type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.nft'), title: i18n.t('explore.search.section.nft'),
} }
...@@ -121,7 +127,12 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -121,7 +127,12 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified)) const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified))
const showWalletSectionFirst = exactUnitagMatch || (exactENSMatch && !prefixTokenMatch) const isUsernameSearch = useMemo(() => {
return searchQuery.includes('.')
}, [searchQuery])
const showWalletSectionFirst =
isUsernameSearch && (exactUnitagMatch || (exactENSMatch && !prefixTokenMatch))
const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults
const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => { const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => {
...@@ -133,18 +144,17 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -133,18 +144,17 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
const walletsWithHeader = const walletsWithHeader =
walletSearchResults.length > 0 ? [WalletHeaderItem, ...walletSearchResults] : [] walletSearchResults.length > 0 ? [WalletHeaderItem, ...walletSearchResults] : []
// Rank token and nft results let searchResultItems: SearchResultOrHeader[] = []
const searchResultItems: SearchResultOrHeader[] = showNftCollectionsBeforeTokens
? [...nftsWithHeader, ...tokensWithHeader]
: [...tokensWithHeader, ...nftsWithHeader]
// Add wallet results at beginning or end if (showWalletSectionFirst) {
if (walletsWithHeader.length > 0) { // Wallets first, then tokens, then NFTs
if (showWalletSectionFirst) { searchResultItems = [...walletsWithHeader, ...tokensWithHeader, ...nftsWithHeader]
searchResultItems.unshift(...walletsWithHeader) } else if (showNftCollectionsBeforeTokens) {
} else { // NFTs, then wallets, then tokens
searchResultItems.push(...walletsWithHeader) searchResultItems = [...nftsWithHeader, ...walletsWithHeader, ...tokensWithHeader]
} } else {
// Tokens, then NFTs, then wallets
searchResultItems = [...tokensWithHeader, ...nftsWithHeader, ...walletsWithHeader]
} }
// Add etherscan items at end // Add etherscan items at end
...@@ -182,7 +192,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -182,7 +192,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
} }
return ( return (
<Flex grow gap="$spacing8"> <Flex grow gap="$spacing8" pb="$spacing36">
<FlatList <FlatList
ListEmptyComponent={ ListEmptyComponent={
<AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing8"> <AnimatedFlex entering={FadeIn} exiting={FadeOut} gap="$spacing8" mx="$spacing8">
...@@ -222,7 +232,6 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -222,7 +232,6 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
} }
// Render function for FlatList of SearchResult items // Render function for FlatList of SearchResult items
export const renderSearchItem = ({ export const renderSearchItem = ({
item: searchResult, item: searchResult,
searchContext, searchContext,
...@@ -233,7 +242,11 @@ export const renderSearchItem = ({ ...@@ -233,7 +242,11 @@ export const renderSearchItem = ({
switch (searchResult.type) { switch (searchResult.type) {
case SEARCH_RESULT_HEADER_KEY: case SEARCH_RESULT_HEADER_KEY:
return ( return (
<SectionHeaderText mt={index === 0 ? '$none' : '$spacing8'} title={searchResult.title} /> <SectionHeaderText
icon={searchResult.icon}
mt={index === 0 ? '$none' : '$spacing8'}
title={searchResult.title}
/>
) )
case SearchResultType.Token: case SearchResultType.Token:
return <SearchTokenItem searchContext={searchContext} token={searchResult} /> return <SearchTokenItem searchContext={searchContext} token={searchResult} />
......
...@@ -12,9 +12,9 @@ export const SectionHeaderText = ({ ...@@ -12,9 +12,9 @@ export const SectionHeaderText = ({
...rest ...rest
}: SectionHeaderTextProps & TextProps): JSX.Element => { }: SectionHeaderTextProps & TextProps): JSX.Element => {
return ( return (
<Flex row alignItems="center" gap="$spacing12" mb="$spacing4" mx="$spacing4"> <Flex row alignItems="center" gap="$spacing8" mb="$spacing4" mx="$spacing4" {...rest}>
{icon && icon} {icon && icon}
<Text color="$neutral2" variant="subheading2" {...rest}> <Text color="$neutral2" variant="subheading2">
{title} {title}
</Text> </Text>
</Flex> </Flex>
......
import React from 'react' import React from 'react'
import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase'
import { Flex } from 'ui/src' import { Flex, Text } from 'ui/src'
import { imageSizes } from 'ui/src/theme' import { imageSizes } from 'ui/src/theme'
import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
...@@ -8,6 +8,7 @@ import { SearchContext } from 'wallet/src/features/search/SearchContext' ...@@ -8,6 +8,7 @@ import { SearchContext } from 'wallet/src/features/search/SearchContext'
import { UnitagSearchResult } from 'wallet/src/features/search/SearchResult' import { UnitagSearchResult } from 'wallet/src/features/search/SearchResult'
import { useAvatar } from 'wallet/src/features/wallet/hooks' import { useAvatar } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses'
type SearchUnitagItemProps = { type SearchUnitagItemProps = {
searchResult: UnitagSearchResult searchResult: UnitagSearchResult
...@@ -27,7 +28,16 @@ export function SearchUnitagItem({ ...@@ -27,7 +28,16 @@ export function SearchUnitagItem({
<SearchWalletItemBase searchContext={searchContext} searchResult={searchResult}> <SearchWalletItemBase searchContext={searchContext} searchResult={searchResult}>
<Flex row alignItems="center" gap="$spacing12" px="$spacing8" py="$spacing12"> <Flex row alignItems="center" gap="$spacing12" px="$spacing8" py="$spacing12">
<AccountIcon address={address} avatarUri={avatar} size={imageSizes.image40} /> <AccountIcon address={address} avatarUri={avatar} size={imageSizes.image40} />
<DisplayNameText displayName={displayName} textProps={{ variant: 'body1' }} /> <Flex alignItems="flex-start" justifyContent="center">
<DisplayNameText
includeUnitagSuffix
displayName={displayName}
textProps={{ variant: 'body1' }}
/>
<Text color="$neutral2" variant="body2">
{sanitizeAddressText(shortenAddress(address))}
</Text>
</Flex>
</Flex> </Flex>
</SearchWalletItemBase> </SearchWalletItemBase>
) )
......
...@@ -5,4 +5,4 @@ import { SEARCH_RESULT_HEADER_KEY } from './constants' ...@@ -5,4 +5,4 @@ import { SEARCH_RESULT_HEADER_KEY } from './constants'
export type SearchResultOrHeader = export type SearchResultOrHeader =
| SearchResult | SearchResult
| { type: typeof SEARCH_RESULT_HEADER_KEY; title: string } | { type: typeof SEARCH_RESULT_HEADER_KEY; title: string; icon?: JSX.Element }
...@@ -13,10 +13,10 @@ import { openModal } from 'src/features/modals/modalSlice' ...@@ -13,10 +13,10 @@ import { openModal } from 'src/features/modals/modalSlice'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
import { CurrencyId } from 'uniswap/src/types/currency'
import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
import { CurrencyId } from 'wallet/src/utils/currencyId'
export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances] export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances]
......
...@@ -49,7 +49,12 @@ function Favorite({ height, contrast }: { height?: number; contrast?: boolean }) ...@@ -49,7 +49,12 @@ function Favorite({ height, contrast }: { height?: number; contrast?: boolean })
return ( return (
<Skeleton contrast={contrast}> <Skeleton contrast={contrast}>
{/* surface3 because these only show up on explore modal which has a blurred bg that makes neutral3 look weird */} {/* surface3 because these only show up on explore modal which has a blurred bg that makes neutral3 look weird */}
<FlexLoader backgroundColor="$surface3" borderRadius="$rounded16" height={height ?? 50} /> <FlexLoader
backgroundColor="$surface3"
borderRadius="$rounded16"
height={height ?? 50}
testID="loader/favorite"
/>
</Skeleton> </Skeleton>
) )
} }
......
...@@ -6,14 +6,15 @@ import { useAppDispatch } from 'src/app/hooks' ...@@ -6,14 +6,15 @@ import { useAppDispatch } from 'src/app/hooks'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { useNavigateToSend } from 'src/features/send/hooks' import { useNavigateToSend } from 'src/features/send/hooks'
import { useNavigateToSwap } from 'src/features/swap/hooks'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { CurrencyId } from 'uniswap/src/types/currency'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { usePortfolioCacheUpdater } from 'wallet/src/features/dataApi/balances' import { usePortfolioCacheUpdater } from 'wallet/src/features/dataApi/balances'
import { PortfolioBalance } from 'wallet/src/features/dataApi/types'
import { toggleTokenVisibility } from 'wallet/src/features/favorites/slice' import { toggleTokenVisibility } from 'wallet/src/features/favorites/slice'
import { pushNotification } from 'wallet/src/features/notifications/slice' import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types' import { AppNotificationType } from 'wallet/src/features/notifications/types'
...@@ -21,7 +22,6 @@ import { CurrencyField } from 'wallet/src/features/transactions/transactionState ...@@ -21,7 +22,6 @@ import { CurrencyField } from 'wallet/src/features/transactions/transactionState
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
import { import {
CurrencyId,
areCurrencyIdsEqual, areCurrencyIdsEqual,
currencyIdToAddress, currencyIdToAddress,
currencyIdToChain, currencyIdToChain,
...@@ -47,7 +47,8 @@ export function useTokenContextMenu({ ...@@ -47,7 +47,8 @@ export function useTokenContextMenu({
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const activeAccountAddress = useActiveAccountAddressWithThrow() const activeAccountAddress = useActiveAccountAddressWithThrow()
const navigateToSwap = useNavigateToSwap()
const { navigateToSwapFlow } = useWalletNavigation()
const navigateToSend = useNavigateToSend() const navigateToSend = useNavigateToSend()
const activeAccountHoldsToken = const activeAccountHoldsToken =
...@@ -73,9 +74,9 @@ export function useTokenContextMenu({ ...@@ -73,9 +74,9 @@ export function useTokenContextMenu({
const onPressSwap = useCallback( const onPressSwap = useCallback(
(currencyField: CurrencyField) => { (currencyField: CurrencyField) => {
// Do not show warning modal speed-bump if user is trying to swap tokens they own // Do not show warning modal speed-bump if user is trying to swap tokens they own
navigateToSwap(currencyField, currencyAddress, currencyChainId) navigateToSwapFlow({ currencyField, currencyAddress, currencyChainId })
}, },
[currencyAddress, currencyChainId, navigateToSwap] [currencyAddress, currencyChainId, navigateToSwapFlow]
) )
const onPressShare = useCallback(async () => { const onPressShare = useCallback(async () => {
...@@ -124,13 +125,8 @@ export function useTokenContextMenu({ ...@@ -124,13 +125,8 @@ export function useTokenContextMenu({
const menuActions = useMemo( const menuActions = useMemo(
(): MenuAction[] => [ (): MenuAction[] => [
{ {
title: t('common.button.buy'), title: t('common.button.swap'),
systemIcon: 'arrow.down', systemIcon: 'rectangle.2.swap',
onPress: () => onPressSwap(CurrencyField.OUTPUT),
},
{
title: t('common.button.sell'),
systemIcon: 'arrow.up',
onPress: () => onPressSwap(CurrencyField.INPUT), onPress: () => onPressSwap(CurrencyField.INPUT),
}, },
{ {
......
import { useMemo } from 'react' import { useMemo } from 'react'
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { CurrencyId } from 'uniswap/src/types/currency'
import { usePortfolioBalances } from 'wallet/src/features/dataApi/balances' import { usePortfolioBalances } from 'wallet/src/features/dataApi/balances'
import { PortfolioBalance } from 'wallet/src/features/dataApi/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
import { CurrencyId } from 'wallet/src/utils/currencyId'
/** Helper hook to retrieve balances for a set of currencies for the active account. */ /** Helper hook to retrieve balances for a set of currencies for the active account. */
export function useBalances(currencies: CurrencyId[] | undefined): PortfolioBalance[] | null { export function useBalances(currencies: CurrencyId[] | undefined): PortfolioBalance[] | null {
......
import { uniswapUrls } from 'uniswap/src/constants/urls'
export const UNISWAP_URL_SCHEME = 'uniswap://'
export const UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM = 'uniswap://wc?uri='
export const UNISWAP_URL_SCHEME_SCANTASTIC = 'uniswap://scantastic?'
export const UNISWAP_WALLETCONNECT_URL = uniswapUrls.appBaseUrl + '/wc?uri='
...@@ -4,10 +4,19 @@ import { Alert } from 'react-native' ...@@ -4,10 +4,19 @@ import { Alert } from 'react-native'
import { URL } from 'react-native-url-polyfill' import { URL } from 'react-native-url-polyfill'
import { appSelect } from 'src/app/hooks' import { appSelect } from 'src/app/hooks'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import {
getScantasticQueryParams,
parseScantasticParams,
} from 'src/components/WalletConnect/ScanSheet/util'
import {
UNISWAP_URL_SCHEME,
UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM,
UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants'
import { handleMoonpayReturnLink } from 'src/features/deepLinking/handleMoonpayReturnLinkSaga' import { handleMoonpayReturnLink } from 'src/features/deepLinking/handleMoonpayReturnLinkSaga'
import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga'
import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga'
import { openModal } from 'src/features/modals/modalSlice' import { closeAllModals, openModal } from 'src/features/modals/modalSlice'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants'
import { waitForWcWeb3WalletIsReady } from 'src/features/walletConnect/saga' import { waitForWcWeb3WalletIsReady } from 'src/features/walletConnect/saga'
...@@ -15,16 +24,26 @@ import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils' ...@@ -15,16 +24,26 @@ import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils'
import { setDidOpenFromDeepLink } from 'src/features/walletConnect/walletConnectSlice' import { setDidOpenFromDeepLink } from 'src/features/walletConnect/walletConnectSlice'
import { WidgetType } from 'src/features/widgets/widgets' import { WidgetType } from 'src/features/widgets/widgets'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
import { Statsig } from 'statsig-react-native'
import { call, put, takeLatest } from 'typed-redux-saga' import { call, put, takeLatest } from 'typed-redux-saga'
import { UNISWAP_APP_HOSTNAME, uniswapUrls } from 'uniswap/src/constants/urls' import { UNISWAP_APP_HOSTNAME } from 'uniswap/src/constants/urls'
import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/experiments/flags'
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { selectExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/selectors'
import { ExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice'
import { fromUniswapWebAppLink } from 'wallet/src/features/chains/utils' import { fromUniswapWebAppLink } from 'wallet/src/features/chains/utils'
import { ScantasticParams } from 'wallet/src/features/scantastic/types'
import {
fetchExtensionEligibityByAddresses,
fetchUnitagByAddresses,
} from 'wallet/src/features/unitags/api'
import { import {
selectAccounts, selectAccounts,
selectActiveAccount, selectActiveAccount,
selectActiveAccountAddress, selectActiveAccountAddress,
selectNonPendingAccounts, selectNonPendingAccounts,
selectSignerMnemonicAccounts,
} from 'wallet/src/features/wallet/selectors' } from 'wallet/src/features/wallet/selectors'
import { setAccountAsActive } from 'wallet/src/features/wallet/slice' import { setAccountAsActive } from 'wallet/src/features/wallet/slice'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
...@@ -41,10 +60,7 @@ export enum LinkSource { ...@@ -41,10 +60,7 @@ export enum LinkSource {
Share = 'Share', Share = 'Share',
} }
export const UNISWAP_URL_SCHEME = 'uniswap://'
export const UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM = 'uniswap://wc?uri='
const UNISWAP_URL_SCHEME_WIDGET = 'uniswap://widget/' const UNISWAP_URL_SCHEME_WIDGET = 'uniswap://widget/'
export const UNISWAP_WALLETCONNECT_URL = uniswapUrls.appBaseUrl + '/wc?uri='
const WALLETCONNECT_URI_SCHEME = 'wc:' // https://eips.ethereum.org/EIPS/eip-1328 const WALLETCONNECT_URI_SCHEME = 'wc:' // https://eips.ethereum.org/EIPS/eip-1328
const NFT_ITEM_SHARE_LINK_HASH_REGEX = /^(#\/)?nfts\/asset\/(0x[a-fA-F0-9]{40})\/(\d+)$/ const NFT_ITEM_SHARE_LINK_HASH_REGEX = /^(#\/)?nfts\/asset\/(0x[a-fA-F0-9]{40})\/(\d+)$/
...@@ -240,6 +256,13 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) { ...@@ -240,6 +256,13 @@ export function* handleDeepLink(action: ReturnType<typeof openDeepLink>) {
return return
} }
// Handle scantastic deep links in the format uniswap://scantastic?${PARAMS}
const maybeScantasticQueryParams = getScantasticQueryParams(action.payload.url)
if (maybeScantasticQueryParams) {
yield* call(handleScantasticDeepLink, maybeScantasticQueryParams)
return
}
// Skip handling any non-WalletConnect uniswap:// URL scheme deep links for now for security reasons // Skip handling any non-WalletConnect uniswap:// URL scheme deep links for now for security reasons
// Currently only used on WalletConnect Universal Link web page fallback button (https://uniswap.org/app/wc) // Currently only used on WalletConnect Universal Link web page fallback button (https://uniswap.org/app/wc)
if (action.payload.url.startsWith(UNISWAP_URL_SCHEME)) { if (action.payload.url.startsWith(UNISWAP_URL_SCHEME)) {
...@@ -359,3 +382,71 @@ export function* parseAndValidateUserAddress(userAddress: string | null) { ...@@ -359,3 +382,71 @@ export function* parseAndValidateUserAddress(userAddress: string | null) {
return matchingAccount.address return matchingAccount.address
} }
export function* handleScantasticDeepLink(scantasticQueryParams: string): Generator {
const params = parseScantasticParams(scantasticQueryParams)
const extensionOnboardingEnabled = Statsig.checkGate(
getFeatureFlagName(FeatureFlags.ExtensionOnboarding)
)
const scantasticEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Scantastic))
if (!params || !scantasticEnabled) {
Alert.alert(
i18n.t('walletConnect.error.scantastic.title'),
i18n.t('walletConnect.error.scantastic.message'),
[{ text: i18n.t('common.button.ok') }]
)
return
}
const extensionOnboardingState = yield* appSelect(selectExtensionOnboardingState)
if (
extensionOnboardingEnabled &&
extensionOnboardingState !== ExtensionOnboardingState.Undefined
) {
// User has already passed extension onboarding eligibility check
yield* call(launchScantastic, params)
return
}
const signerAccounts = yield* appSelect(selectSignerMnemonicAccounts)
const waitlistPositionResponse = (yield* call(
fetchExtensionEligibityByAddresses,
signerAccounts.map((account) => account.address)
)).data
if (extensionOnboardingEnabled && waitlistPositionResponse?.isAccepted) {
yield* call(launchScantastic, params)
} else {
const activeAccount = yield* appSelect(selectActiveAccount)
const activeAccountUnitag = activeAccount
? (yield* call(fetchUnitagByAddresses, [activeAccount.address])).data?.[activeAccount.address]
?.username
: undefined
yield* put(closeAllModals())
yield* put(
openModal({
name: ModalName.ExtensionWaitlistModal,
initialState: {
isUserOnWaitlist: activeAccountUnitag !== undefined,
},
})
)
}
}
function* launchScantastic(params: ScantasticParams): Generator {
yield* put(closeAllModals())
yield* put(
openModal({
name: ModalName.Scantastic,
initialState: {
params,
},
})
)
}
...@@ -20,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ import {
Text, Text,
TouchableArea, TouchableArea,
getUniconV2Colors, getUniconV2Colors,
useExtractedColors,
useIsDarkMode, useIsDarkMode,
useSporeColors, useSporeColors,
useUniconColors, useUniconColors,
...@@ -35,7 +36,6 @@ import { CurrencyField } from 'wallet/src/features/transactions/transactionState ...@@ -35,7 +36,6 @@ import { CurrencyField } from 'wallet/src/features/transactions/transactionState
import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { ElementName, ModalName } from 'wallet/src/telemetry/constants'
import { useExtractedColors } from 'wallet/src/utils/colors'
import { openUri } from 'wallet/src/utils/linking' import { openUri } from 'wallet/src/utils/linking'
const HEADER_GRADIENT_HEIGHT = 144 const HEADER_GRADIENT_HEIGHT = 144
......
...@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react' ...@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants' import { MobileEventName } from 'src/features/telemetry/constants'
import { CurrencyId } from 'uniswap/src/types/currency'
import { import {
makeSelectHasTokenFavorited, makeSelectHasTokenFavorited,
selectWatchedAddressSet, selectWatchedAddressSet,
...@@ -14,7 +15,7 @@ import { ...@@ -14,7 +15,7 @@ import {
} from 'wallet/src/features/favorites/slice' } from 'wallet/src/features/favorites/slice'
import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo'
import { useDisplayName } from 'wallet/src/features/wallet/hooks' import { useDisplayName } from 'wallet/src/features/wallet/hooks'
import { CurrencyId, currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' import { currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId'
export function useToggleFavoriteCallback(id: CurrencyId, isFavoriteToken: boolean): () => void { export function useToggleFavoriteCallback(id: CurrencyId, isFavoriteToken: boolean): () => void {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
......
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
useSporeColors, useSporeColors,
} from 'ui/src' } from 'ui/src'
import { fonts, iconSizes, spacing } from 'ui/src/theme' import { fonts, iconSizes, spacing } from 'ui/src/theme'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { usePrevious } from 'utilities/src/react/hooks' import { usePrevious } from 'utilities/src/react/hooks'
import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing' import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing'
...@@ -29,7 +30,6 @@ import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo' ...@@ -29,7 +30,6 @@ import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo'
import { AmountInput } from 'wallet/src/components/input/AmountInput' import { AmountInput } from 'wallet/src/components/input/AmountInput'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { Pill } from 'wallet/src/components/text/Pill' import { Pill } from 'wallet/src/components/text/Pill'
import { CurrencyInfo } from 'wallet/src/features/dataApi/types'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { ElementName } from 'wallet/src/telemetry/constants' import { ElementName } from 'wallet/src/telemetry/constants'
......
...@@ -19,6 +19,8 @@ interface FiatOnRampContextType { ...@@ -19,6 +19,8 @@ interface FiatOnRampContextType {
setSelectedQuote: (quote: FORQuote | undefined) => void setSelectedQuote: (quote: FORQuote | undefined) => void
countryCode: string countryCode: string
setCountryCode: (countryCode: string) => void setCountryCode: (countryCode: string) => void
countryState: string | undefined
setCountryState: (countryCode: string | undefined) => void
baseCurrencyInfo?: FiatCurrencyInfo baseCurrencyInfo?: FiatCurrencyInfo
setBaseCurrencyInfo: (baseCurrency: FiatCurrencyInfo | undefined) => void setBaseCurrencyInfo: (baseCurrency: FiatCurrencyInfo | undefined) => void
quoteCurrency: FiatOnRampCurrency quoteCurrency: FiatOnRampCurrency
...@@ -33,11 +35,13 @@ const initialState: FiatOnRampContextType = { ...@@ -33,11 +35,13 @@ const initialState: FiatOnRampContextType = {
setQuotesSections: () => undefined, setQuotesSections: () => undefined,
setSelectedQuote: () => undefined, setSelectedQuote: () => undefined,
setCountryCode: () => undefined, setCountryCode: () => undefined,
setCountryState: () => undefined,
setBaseCurrencyInfo: () => undefined, setBaseCurrencyInfo: () => undefined,
setQuoteCurrency: () => undefined, setQuoteCurrency: () => undefined,
setAmount: () => undefined, setAmount: () => undefined,
setServiceProviders: () => undefined, setServiceProviders: () => undefined,
countryCode: '', countryCode: '',
countryState: undefined,
quoteCurrency: { currencyInfo: undefined }, quoteCurrency: { currencyInfo: undefined },
} }
...@@ -51,6 +55,7 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): ...@@ -51,6 +55,7 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }):
const [quotesSections, setQuotesSections] = useState<FiatOnRampContextType['quotesSections']>() const [quotesSections, setQuotesSections] = useState<FiatOnRampContextType['quotesSections']>()
const [selectedQuote, setSelectedQuote] = useState<FORQuote | undefined>() const [selectedQuote, setSelectedQuote] = useState<FORQuote | undefined>()
const [countryCode, setCountryCode] = useState<string>(getCountry()) const [countryCode, setCountryCode] = useState<string>(getCountry())
const [countryState, setCountryState] = useState<string | undefined>()
const [baseCurrencyInfo, setBaseCurrencyInfo] = useState<FiatCurrencyInfo>() const [baseCurrencyInfo, setBaseCurrencyInfo] = useState<FiatCurrencyInfo>()
const [amount, setAmount] = useState<number>() const [amount, setAmount] = useState<number>()
const [serviceProviders, setServiceProviders] = useState<FORServiceProvider[]>() const [serviceProviders, setServiceProviders] = useState<FORServiceProvider[]>()
...@@ -72,6 +77,8 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): ...@@ -72,6 +77,8 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }):
setQuotesSections, setQuotesSections,
countryCode, countryCode,
setCountryCode, setCountryCode,
countryState,
setCountryState,
baseCurrencyInfo, baseCurrencyInfo,
setBaseCurrencyInfo, setBaseCurrencyInfo,
quoteCurrency, quoteCurrency,
......
import { CurrencyInfo } from 'wallet/src/features/dataApi/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
export type FiatOnRampCurrency = { export type FiatOnRampCurrency = {
currencyInfo: Maybe<CurrencyInfo> currencyInfo: Maybe<CurrencyInfo>
......
import { ExploreModalState } from 'src/app/modals/ExploreModalState' import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ExtensionWaitlistModalState } from 'src/features/scantastic/ExtensionWaitlistModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types'
...@@ -27,6 +28,7 @@ export interface ModalsState { ...@@ -27,6 +28,7 @@ export interface ModalsState {
[ModalName.RemoveWallet]: AppModalState<RemoveWalletModalState> [ModalName.RemoveWallet]: AppModalState<RemoveWalletModalState>
[ModalName.RestoreWallet]: AppModalState<undefined> [ModalName.RestoreWallet]: AppModalState<undefined>
[ModalName.Scantastic]: AppModalState<ScantasticModalState> [ModalName.Scantastic]: AppModalState<ScantasticModalState>
[ModalName.ExtensionWaitlistModal]: AppModalState<ExtensionWaitlistModalState>
[ModalName.Send]: AppModalState<TransactionState> [ModalName.Send]: AppModalState<TransactionState>
[ModalName.Swap]: AppModalState<TransactionState> [ModalName.Swap]: AppModalState<TransactionState>
[ModalName.UnitagsIntro]: AppModalState<{ [ModalName.UnitagsIntro]: AppModalState<{
......
...@@ -3,6 +3,7 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState' ...@@ -3,6 +3,7 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants' import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState' import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState'
import { ExtensionWaitlistModalState } from 'src/features/scantastic/ExtensionWaitlistModalState'
import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
import { getKeys } from 'utilities/src/primitives/objects' import { getKeys } from 'utilities/src/primitives/objects'
...@@ -27,6 +28,11 @@ type ExploreModalParams = { ...@@ -27,6 +28,11 @@ type ExploreModalParams = {
initialState?: ExploreModalState initialState?: ExploreModalState
} }
type ExtensionWaitlistModalParams = {
name: typeof ModalName.ExtensionWaitlistModal
initialState: ExtensionWaitlistModalState
}
type FiatCurrencySelectorParams = { type FiatCurrencySelectorParams = {
name: typeof ModalName.FiatCurrencySelector name: typeof ModalName.FiatCurrencySelector
initialState?: undefined initialState?: undefined
...@@ -85,6 +91,7 @@ export type OpenModalParams = ...@@ -85,6 +91,7 @@ export type OpenModalParams =
| ExchangeTransferModalParams | ExchangeTransferModalParams
| ExperimentsModalParams | ExperimentsModalParams
| ExploreModalParams | ExploreModalParams
| ExtensionWaitlistModalParams
| FiatCurrencySelectorParams | FiatCurrencySelectorParams
| FiatOnRampModalParams | FiatOnRampModalParams
| FiatOnRampAggregatorModalParams | FiatOnRampAggregatorModalParams
...@@ -126,6 +133,10 @@ export const initialModalsState: ModalsState = { ...@@ -126,6 +133,10 @@ export const initialModalsState: ModalsState = {
isOpen: false, isOpen: false,
initialState: undefined, initialState: undefined,
}, },
[ModalName.ExtensionWaitlistModal]: {
isOpen: false,
initialState: undefined,
},
[ModalName.Swap]: { [ModalName.Swap]: {
isOpen: false, isOpen: false,
initialState: undefined, initialState: undefined,
......
...@@ -5,14 +5,21 @@ import { BackButton } from 'src/components/buttons/BackButton' ...@@ -5,14 +5,21 @@ import { BackButton } from 'src/components/buttons/BackButton'
import { Loader } from 'src/components/loading' import { Loader } from 'src/components/loading'
import { LongMarkdownText } from 'src/components/text/LongMarkdownText' import { LongMarkdownText } from 'src/components/text/LongMarkdownText'
import { NFTCollectionContextMenu } from 'src/features/nfts/collection/NFTCollectionContextMenu' import { NFTCollectionContextMenu } from 'src/features/nfts/collection/NFTCollectionContextMenu'
import { Flex, FlexProps, Logos, Text, useDeviceInsets, useSporeColors } from 'ui/src' import {
Flex,
FlexProps,
Logos,
Text,
useDeviceInsets,
useExtractedColors,
useSporeColors,
} from 'ui/src'
import VerifiedIcon from 'ui/src/assets/icons/verified.svg' import VerifiedIcon from 'ui/src/assets/icons/verified.svg'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { ImageUri } from 'wallet/src/features/images/ImageUri' import { ImageUri } from 'wallet/src/features/images/ImageUri'
import { NFTViewer } from 'wallet/src/features/images/NFTViewer' import { NFTViewer } from 'wallet/src/features/images/NFTViewer'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
import { useExtractedColors } from 'wallet/src/utils/colors'
import { NFTCollectionData } from './types' import { NFTCollectionData } from './types'
const PROFILE_IMAGE_SIZE = 72 const PROFILE_IMAGE_SIZE = 72
......
This diff is collapsed.
export interface ExtensionWaitlistModalState {
isUserOnWaitlist: boolean
}
This diff is collapsed.
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
useUniconColors, useUniconColors,
} from 'ui/src' } from 'ui/src'
import { borderRadii, fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' import { borderRadii, fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme'
import { useExtractedColors } from 'ui/src/utils/colors'
import { FeatureFlags } from 'uniswap/src/features/experiments/flags' import { FeatureFlags } from 'uniswap/src/features/experiments/flags'
import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks' import { useFeatureFlag } from 'uniswap/src/features/experiments/hooks'
import { useUnitagUpdater } from 'uniswap/src/features/unitags/context' import { useUnitagUpdater } from 'uniswap/src/features/unitags/context'
...@@ -53,7 +54,6 @@ import { useAppDispatch } from 'wallet/src/state' ...@@ -53,7 +54,6 @@ import { useAppDispatch } from 'wallet/src/state'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { UnitagEventName } from 'wallet/src/telemetry/constants' import { UnitagEventName } from 'wallet/src/telemetry/constants'
import { shortenAddress } from 'wallet/src/utils/addresses' import { shortenAddress } from 'wallet/src/utils/addresses'
import { useExtractedColors } from 'wallet/src/utils/colors'
const BIO_TEXT_INPUT_LINES = 6 const BIO_TEXT_INPUT_LINES = 6
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
export * from './explore'
export * from './redux' export * from './redux'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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