ci(release): publish latest release

parent 9a511c85
* @uniswap/web-admins
Excited to share some new updates! Here’s what’s new: IPFS hash of the deployment:
- CIDv0: `QmUWi3jNeMPBZR4GdHMAGfraizcdpB2szAaPdqRpnEZFSw`
- CIDv1: `bafybeic3xg2e7qmaibklwkbb7ahlubktigmllrc2ucaxtn4xwsflp3idra`
Expanded Fiat On-ramp Providers — We’ve added more provider options to on-ramp to crypto from your wallet, dependent on your geography. The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeic3xg2e7qmaibklwkbb7ahlubktigmllrc2ucaxtn4xwsflp3idra.ipfs.dweb.link/
- https://bafybeic3xg2e7qmaibklwkbb7ahlubktigmllrc2ucaxtn4xwsflp3idra.ipfs.cf-ipfs.com/
- [ipfs://QmUWi3jNeMPBZR4GdHMAGfraizcdpB2szAaPdqRpnEZFSw/](ipfs://QmUWi3jNeMPBZR4GdHMAGfraizcdpB2szAaPdqRpnEZFSw/)
### 5.23.1 (2024-04-10)
Other changes:
- Polish around view-only wallets
- Various bug fixes and performance improvements
mobile/1.24 web/5.23.1
\ 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.26"
dimension "variant" dimension "variant"
} }
beta { beta {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionName "1.24" versionName "1.26"
dimension "variant" dimension "variant"
} }
prod { prod {
dimension "variant" dimension "variant"
versionName "1.24" versionName "1.26"
} }
} }
......
...@@ -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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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;
......
...@@ -126,3 +126,8 @@ jest.mock('wallet/src/features/appearance/hooks', () => { ...@@ -126,3 +126,8 @@ jest.mock('wallet/src/features/appearance/hooks', () => {
useSelectedColorScheme: () => 'light', useSelectedColorScheme: () => 'light',
} }
}) })
jest.mock('wallet/src/features/fiatOnRamp/api', () => ({
...jest.requireActual('wallet/src/features/fiatOnRamp/api'),
useFiatOnRampIpAddressQuery: jest.fn().mockReturnValue({}),
}))
...@@ -4,6 +4,10 @@ const preset = require('../../config/jest-presets/jest/jest-preset') ...@@ -4,6 +4,10 @@ const preset = require('../../config/jest-presets/jest/jest-preset')
module.exports = { module.exports = {
...preset, ...preset,
preset: 'jest-expo', preset: 'jest-expo',
transform: {
...preset.transform,
'^.+\\.jsx?$': 'babel-jest',
},
displayName: 'Mobile Wallet', displayName: 'Mobile Wallet',
collectCoverageFrom: [ collectCoverageFrom: [
'src/**/*.{js,ts,tsx}', 'src/**/*.{js,ts,tsx}',
......
...@@ -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",
...@@ -112,10 +112,9 @@ ...@@ -112,10 +112,9 @@
"i18next": "23.10.0", "i18next": "23.10.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"no-yolo-signatures": "0.0.2", "no-yolo-signatures": "0.0.2",
"qrcode": "1.5.1",
"react": "18.2.0", "react": "18.2.0",
"react-freeze": "1.0.3", "react-freeze": "1.0.3",
"react-i18next": "14.0.5", "react-i18next": "14.1.0",
"react-native": "0.71.13", "react-native": "0.71.13",
"react-native-appsflyer": "6.10.3", "react-native-appsflyer": "6.10.3",
"react-native-context-menu-view": "1.6.0", "react-native-context-menu-view": "1.6.0",
...@@ -133,7 +132,7 @@ ...@@ -133,7 +132,7 @@
"react-native-permissions": "3.6.0", "react-native-permissions": "3.6.0",
"react-native-reanimated": "3.3.0", "react-native-reanimated": "3.3.0",
"react-native-restart": "0.0.27", "react-native-restart": "0.0.27",
"react-native-safe-area-context": "4.5.0", "react-native-safe-area-context": "4.9.0",
"react-native-screens": "3.24.0", "react-native-screens": "3.24.0",
"react-native-splash-screen": "3.3.0", "react-native-splash-screen": "3.3.0",
"react-native-svg": "13.9.0", "react-native-svg": "13.9.0",
...@@ -164,7 +163,7 @@ ...@@ -164,7 +163,7 @@
"@babel/runtime": "7.18.9", "@babel/runtime": "7.18.9",
"@faker-js/faker": "7.6.0", "@faker-js/faker": "7.6.0",
"@storybook/react": "7.0.2", "@storybook/react": "7.0.2",
"@tamagui/babel-plugin": "1.92.0", "@tamagui/babel-plugin": "1.94.3",
"@testing-library/react-hooks": "7.0.2", "@testing-library/react-hooks": "7.0.2",
"@testing-library/react-native": "11.5.0", "@testing-library/react-native": "11.5.0",
"@types/react-native": "0.71.3", "@types/react-native": "0.71.3",
......
...@@ -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,
......
import { PropsWithChildren, useCallback } from 'react' import { PropsWithChildren, useCallback } from 'react'
import { Share } from 'react-native'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { exploreNavigationRef } from 'src/app/navigation/navigation'
import { useAppStackNavigation } from 'src/app/navigation/types' import { useAppStackNavigation } from 'src/app/navigation/types'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { closeModal, openModal } from 'src/features/modals/modalSlice' import { closeModal, openModal } from 'src/features/modals/modalSlice'
import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex'
import { Screens } from 'src/screens/Screens' import { Screens } from 'src/screens/Screens'
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 { logger } from 'utilities/src/logger/logger'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { import {
NavigateToNftItemArgs, NavigateToNftItemArgs,
NavigateToSendArgs,
NavigateToSwapFlowArgs, NavigateToSwapFlowArgs,
ShareNftArgs,
ShareTokenArgs,
WalletNavigationProvider, WalletNavigationProvider,
getNavigateToSwapFlowArgsInitialState, getNavigateToSwapFlowArgsInitialState,
} from 'wallet/src/contexts/WalletNavigationContext' } from 'wallet/src/contexts/WalletNavigationContext'
import { AssetType } from 'wallet/src/entities/assets'
import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api'
import { ModalName } from 'wallet/src/telemetry/constants' import {
CurrencyField,
TransactionState,
} from 'wallet/src/features/transactions/transactionState/types'
import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry'
import { ModalName, ShareableEntity, WalletEventName } from 'wallet/src/telemetry/constants'
import { getNftUrl, getTokenUrl } from 'wallet/src/utils/linking'
export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element { export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element {
const handleShareNft = useHandleShareNft()
const handleShareToken = useHandleShareToken()
const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity) const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity)
const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens) const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens)
const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet() const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet()
const navigateToNftDetails = useNavigateToNftDetails() const navigateToNftDetails = useNavigateToNftDetails()
const navigateToReceive = useNavigateToReceive()
const navigateToSend = useNavigateToSend()
const navigateToSwapFlow = useNavigateToSwapFlow() const navigateToSwapFlow = useNavigateToSwapFlow()
const navigateToTokenDetails = useNavigateToTokenDetails() const navigateToTokenDetails = useNavigateToTokenDetails()
return ( return (
<WalletNavigationProvider <WalletNavigationProvider
handleShareNft={handleShareNft}
handleShareToken={handleShareToken}
navigateToAccountActivityList={navigateToAccountActivityList} navigateToAccountActivityList={navigateToAccountActivityList}
navigateToAccountTokenList={navigateToAccountTokenList} navigateToAccountTokenList={navigateToAccountTokenList}
navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet} navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet}
navigateToNftDetails={navigateToNftDetails} navigateToNftDetails={navigateToNftDetails}
navigateToReceive={navigateToReceive}
navigateToSend={navigateToSend}
navigateToSwapFlow={navigateToSwapFlow} navigateToSwapFlow={navigateToSwapFlow}
navigateToTokenDetails={navigateToTokenDetails}> navigateToTokenDetails={navigateToTokenDetails}>
{children} {children}
...@@ -37,6 +58,52 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren): ...@@ -37,6 +58,52 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren):
) )
} }
function useHandleShareNft(): (args: ShareNftArgs) => Promise<void> {
return useCallback(async ({ contractAddress, tokenId }: ShareNftArgs): Promise<void> => {
try {
const url = getNftUrl(contractAddress, tokenId)
await Share.share({ message: url })
sendWalletAnalyticsEvent(WalletEventName.ShareButtonClicked, {
entity: ShareableEntity.NftItem,
url,
})
} catch (error) {
logger.error(error, {
tags: { file: 'MobileWalletNavigationProvider.tsx', function: 'useHandleShareNft' },
})
}
}, [])
}
function useHandleShareToken(): (args: ShareTokenArgs) => Promise<void> {
return useCallback(async ({ currencyId }: ShareTokenArgs): Promise<void> => {
const url = getTokenUrl(currencyId)
if (!url) {
logger.error(new Error('Failed to get token URL'), {
tags: { file: 'MobileWalletNavigationProvider.tsx', function: 'useHandleShareToken' },
extra: { currencyId },
})
return
}
try {
await Share.share({ message: url })
sendWalletAnalyticsEvent(WalletEventName.ShareButtonClicked, {
entity: ShareableEntity.Token,
url,
})
} catch (error) {
logger.error(error, {
tags: { file: 'MobileWalletNavigationProvider.tsx', function: 'useHandleShareToken' },
})
}
}, [])
}
function useNavigateToHomepageTab(tab: HomeScreenTabIndex): () => void { function useNavigateToHomepageTab(tab: HomeScreenTabIndex): () => void {
const { navigate } = useAppStackNavigation() const { navigate } = useAppStackNavigation()
...@@ -45,6 +112,40 @@ function useNavigateToHomepageTab(tab: HomeScreenTabIndex): () => void { ...@@ -45,6 +112,40 @@ function useNavigateToHomepageTab(tab: HomeScreenTabIndex): () => void {
}, [navigate, tab]) }, [navigate, tab])
} }
function useNavigateToReceive(): () => void {
const dispatch = useAppDispatch()
return useCallback((): void => {
dispatch(
openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })
)
}, [dispatch])
}
function useNavigateToSend(): (args: NavigateToSendArgs) => void {
const dispatch = useAppDispatch()
return useCallback(
(args: NavigateToSendArgs) => {
const initialSendState: TransactionState = {
exactCurrencyField: CurrencyField.INPUT,
exactAmountToken: '',
[CurrencyField.INPUT]: args
? {
address: args.currencyAddress,
chainId: args.chainId,
type: AssetType.Currency,
}
: null,
[CurrencyField.OUTPUT]: null,
showRecipientSelector: true,
}
dispatch(openModal({ name: ModalName.Send, initialState: initialSendState }))
},
[dispatch]
)
}
function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
...@@ -59,14 +160,9 @@ function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { ...@@ -59,14 +160,9 @@ function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void {
} }
function useNavigateToTokenDetails(): (currencyId: string) => void { function useNavigateToTokenDetails(): (currencyId: string) => void {
const navigation = useAppStackNavigation() return useCallback((currencyId: string): void => {
exploreNavigationRef.navigate(Screens.TokenDetails, { currencyId })
return useCallback( }, [])
(currencyId: string): void => {
navigation.navigate(Screens.TokenDetails, { currencyId })
},
[navigation]
)
} }
function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void { function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void {
......
...@@ -68,7 +68,6 @@ import { ...@@ -68,7 +68,6 @@ import {
v9Schema, v9Schema,
} from 'src/app/schema' } from 'src/app/schema'
import { persistConfig } from 'src/app/store' import { persistConfig } from 'src/app/store'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { initialBiometricsSettingsState } from 'src/features/biometrics/slice' import { initialBiometricsSettingsState } from 'src/features/biometrics/slice'
import { initialCloudBackupState } from 'src/features/CloudBackup/cloudBackupSlice' import { initialCloudBackupState } from 'src/features/CloudBackup/cloudBackupSlice'
import { initialPasswordLockoutState } from 'src/features/CloudBackup/passwordLockoutSlice' import { initialPasswordLockoutState } from 'src/features/CloudBackup/passwordLockoutSlice'
...@@ -76,6 +75,7 @@ import { initialModalsState } from 'src/features/modals/modalSlice' ...@@ -76,6 +75,7 @@ import { initialModalsState } from 'src/features/modals/modalSlice'
import { initialTelemetryState } from 'src/features/telemetry/slice' 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 { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { import {
ExtensionOnboardingState, ExtensionOnboardingState,
......
...@@ -418,10 +418,10 @@ exports[`AccountSwitcher renders correctly 1`] = ` ...@@ -418,10 +418,10 @@ exports[`AccountSwitcher renders correctly 1`] = `
style={ style={
{ {
"alignItems": "center", "alignItems": "center",
"backgroundColor": "#F9F9F9", "backgroundColor": "#2222220D",
"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 { NavigationContainer } from '@react-navigation/native' import { createNavigationContainerRef, NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack' import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { createStackNavigator, TransitionPresets } from '@react-navigation/stack' import { createStackNavigator, TransitionPresets } from '@react-navigation/stack'
import React from 'react' import React from 'react'
...@@ -134,11 +134,14 @@ export function WrappedHomeScreen(props: AppStackScreenProp<Screens.Home>): JSX. ...@@ -134,11 +134,14 @@ export function WrappedHomeScreen(props: AppStackScreenProp<Screens.Home>): JSX.
return <HomeScreen key={activeAccount.address} {...props} /> return <HomeScreen key={activeAccount.address} {...props} />
} }
export const exploreNavigationRef = createNavigationContainerRef<ExploreStackParamList>()
export function ExploreStackNavigator(): JSX.Element { export function ExploreStackNavigator(): JSX.Element {
const colors = useSporeColors() const colors = useSporeColors()
return ( return (
<NavigationContainer <NavigationContainer
ref={exploreNavigationRef}
independent={true} independent={true}
theme={{ theme={{
dark: false, dark: false,
......
...@@ -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,
withSpring, withSpring,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { ValueAndFormattedWithAnimation } from 'src/components/PriceExplorer/usePrice'
import { PriceNumberOfDigits } from 'src/components/PriceExplorer/usePriceHistory'
import { useSporeColors } from 'ui/src'
import { TextLoaderWrapper } from 'ui/src/components/text/Text'
import { fonts } from 'ui/src/theme'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { import {
ADDITIONAL_WIDTH_FOR_ANIMATIONS, ADDITIONAL_WIDTH_FOR_ANIMATIONS,
AnimatedCharStyles, AnimatedCharStyles,
...@@ -16,13 +24,7 @@ import { ...@@ -16,13 +24,7 @@ import {
NUMBER_ARRAY, NUMBER_ARRAY,
NUMBER_WIDTH_ARRAY, NUMBER_WIDTH_ARRAY,
TopAndBottomGradient, TopAndBottomGradient,
} from 'src/components/AnimatedNumber' } from 'wallet/src/features/portfolio/AnimatedNumber'
import { ValueAndFormattedWithAnimation } from 'src/components/PriceExplorer/usePrice'
import { PriceNumberOfDigits } from 'src/components/PriceExplorer/usePriceHistory'
import { useSporeColors } from 'ui/src'
import { TextLoaderWrapper } from 'ui/src/components/text/Text'
import { fonts } from 'ui/src/theme'
import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
// if price per token has > 3 numbers before the decimal, start showing decimals in neutral3 // if price per token has > 3 numbers before the decimal, start showing decimals in neutral3
// otherwise, show entire price in neutral1 // otherwise, show entire price in neutral1
...@@ -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>
) )
} }
......
...@@ -55,7 +55,7 @@ describe(useLineChartPrice, () => { ...@@ -55,7 +55,7 @@ describe(useLineChartPrice, () => {
}) })
afterAll(() => { afterAll(() => {
jest.resetAllMocks() jest.clearAllMocks()
}) })
it('returns correct initial values', () => { it('returns correct initial values', () => {
......
...@@ -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
......
import { BarCodeScanner } from 'expo-barcode-scanner' import { BarCodeScanner } from 'expo-barcode-scanner'
import { BarCodeScanningResult, Camera, CameraType } from 'expo-camera' import { BarCodeScanningResult, Camera, CameraType } from 'expo-camera'
import { PermissionStatus } from 'expo-modules-core' import { PermissionStatus } from 'expo-modules-core'
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native' import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native'
import { launchImageLibrary } from 'react-native-image-picker' import { launchImageLibrary } from 'react-native-image-picker'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg'
import { DevelopmentOnly } from 'src/components/DevelopmentOnly/DevelopmentOnly'
import { import {
AnimatedFlex, AnimatedFlex,
Button, Button,
...@@ -20,6 +19,7 @@ import { ...@@ -20,6 +19,7 @@ import {
import CameraScan from 'ui/src/assets/icons/camera-scan.svg' import CameraScan from 'ui/src/assets/icons/camera-scan.svg'
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { Sentry } from 'utilities/src/logger/Sentry' import { Sentry } from 'utilities/src/logger/Sentry'
import { DevelopmentOnly } from 'wallet/src/components/DevelopmentOnly/DevelopmentOnly'
import PasteButton from 'wallet/src/components/buttons/PasteButton' import PasteButton from 'wallet/src/components/buttons/PasteButton'
import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader'
import { openSettings } from 'wallet/src/utils/linking' import { openSettings } from 'wallet/src/utils/linking'
......
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode'
import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos'
import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src'
import { iconSizes, spacing } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal'
import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink'
import { ALL_SUPPORTED_CHAIN_IDS } from 'wallet/src/constants/chains'
import { ModalName } from 'wallet/src/telemetry/constants'
interface Props {
address?: Address
}
export function WalletQRCode({ address }: Props): JSX.Element | null {
const colors = useSporeColors()
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const media = useMedia()
const QR_CODE_SIZE = media.short ? 220 : 240
const UNICON_SIZE = QR_CODE_SIZE / 4
if (!address) {
return null
}
return (
<>
<AnimatedFlex
centered
grow
$short={{ mb: spacing.none, mx: spacing.spacing48 }}
entering={FadeIn}
exiting={FadeOut}
gap="$spacing8"
mb="$spacing8"
mx="$spacing60"
py="$spacing24">
<AddressDisplay
includeUnitagSuffix
showCopy
address={address}
captionVariant="body2"
showAccountIcon={false}
variant="heading3"
/>
<QRCodeDisplay
hideOutline
address={address}
containerBackgroundColor={colors.surface1.val}
displayShadow={false}
logoSize={UNICON_SIZE}
safeAreaColor="$surface1"
size={QR_CODE_SIZE}
/>
<Text color="$neutral2" lineHeight={20} textAlign="center" variant="body3">
{t('qrScanner.wallet.title')}
</Text>
<TouchableArea onPress={(): void => setShowModal(true)}>
<Flex row gap="$spacing4">
<NetworkLogos negativeGap chains={ALL_SUPPORTED_CHAIN_IDS} />
<Icons.RotatableChevron
color="$neutral3"
direction="down"
height={iconSizes.icon20}
width={iconSizes.icon20}
/>
</Flex>
</TouchableArea>
</AnimatedFlex>
{showModal && (
<WarningModal
backgroundIconColor={colors.surface1.val}
caption={t('qrScanner.wallet.networks.description')}
closeText={t('common.button.close')}
icon={
<NetworkLogos
centered
negativeGap
chains={ALL_SUPPORTED_CHAIN_IDS}
size={iconSizes.icon28}
/>
}
modalName={ModalName.QRCodeNetworkInfo}
title={t('qrScanner.wallet.networks.title')}
onClose={(): void => setShowModal(false)}>
<LearnMoreLink url={uniswapUrls.helpArticleUrls.supportedNetworks} />
</WarningModal>
)}
</>
)
}
...@@ -3,15 +3,15 @@ import { useTranslation } from 'react-i18next' ...@@ -3,15 +3,15 @@ import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import 'react-native-reanimated' import 'react-native-reanimated'
import { useAppSelector } from 'src/app/hooks' import { useAppSelector } from 'src/app/hooks'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner'
import { WalletQRCode } from 'src/components/QRCodeScanner/WalletQRCode'
import { getSupportedURI, URIType } from 'src/components/WalletConnect/ScanSheet/util' import { getSupportedURI, URIType } from 'src/components/WalletConnect/ScanSheet/util'
import { Flex, HapticFeedback, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' import { Flex, HapticFeedback, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src'
import Scan from 'ui/src/assets/icons/receive.svg' import Scan from 'ui/src/assets/icons/receive.svg'
import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import ScanQRIcon from 'ui/src/assets/icons/scan.svg'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { WalletQRCode } from 'wallet/src/components/QRCodeScanner/WalletQRCode'
import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { ElementName, ModalName } from 'wallet/src/telemetry/constants'
......
...@@ -131,7 +131,7 @@ export const useModalContent = ({ ...@@ -131,7 +131,7 @@ export const useModalContent = ({
highlight: <Text color="$neutral1" variant="body3" />, highlight: <Text color="$neutral1" variant="body3" />,
}} }}
i18nKey="account.recoveryPhrase.remove.mnemonic.description" i18nKey="account.recoveryPhrase.remove.mnemonic.description"
values={{ walletNames: associatedAccountNames }} values={{ walletName: associatedAccountNames }}
/> />
), ),
Icon: TrashIcon, Icon: TrashIcon,
......
import React, { memo, useMemo } from 'react' 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 { 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'
import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu'
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'
/** /**
......
import React from 'react' import React from 'react'
import { Flex, flexStyles, Text, TouchableArea } from 'ui/src' import { Flex, flexStyles, Text, TouchableArea } from 'ui/src'
import { iconSizes, imageSizes } from 'ui/src/theme'
import { import {
SafetyLevel, SafetyLevel,
TokenDetailsScreenQuery, TokenDetailsScreenQuery,
...@@ -45,10 +44,9 @@ export function TokenDetailsHeader({ ...@@ -45,10 +44,9 @@ export function TokenDetailsHeader({
tokenProject?.safetyLevel === SafetyLevel.Blocked) && ( tokenProject?.safetyLevel === SafetyLevel.Blocked) && (
<TouchableArea onPress={onPressWarningIcon}> <TouchableArea onPress={onPressWarningIcon}>
<WarningIcon <WarningIcon
height={iconSizes.icon20}
safetyLevel={tokenProject?.safetyLevel} safetyLevel={tokenProject?.safetyLevel}
size="$icon.20"
strokeColorOverride="neutral3" strokeColorOverride="neutral3"
width={imageSizes.image20}
/> />
</TouchableArea> </TouchableArea>
)} )}
......
...@@ -121,7 +121,7 @@ describe(useCrossChainBalances, () => { ...@@ -121,7 +121,7 @@ describe(useCrossChainBalances, () => {
describe(useTokenDetailsNavigation, () => { describe(useTokenDetailsNavigation, () => {
afterEach(() => { afterEach(() => {
jest.resetAllMocks() jest.clearAllMocks()
}) })
it('returns correct result', () => { it('returns correct result', () => {
......
...@@ -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
......
...@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next' ...@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next'
import { FlatList, StyleSheet } from 'react-native' import { FlatList, StyleSheet } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { BackButton } from 'src/components/buttons/BackButton'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { DappConnectedNetworkModal } from 'src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal' import { DappConnectedNetworkModal } from 'src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal'
import { DappConnectionItem } from 'src/components/WalletConnect/ConnectedDapps/DappConnectionItem' import { DappConnectionItem } from 'src/components/WalletConnect/ConnectedDapps/DappConnectionItem'
import { BackButton } from 'src/components/buttons/BackButton'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { import {
removePendingSession,
WalletConnectSession, WalletConnectSession,
removePendingSession,
} from 'src/features/walletConnect/walletConnectSlice' } from 'src/features/walletConnect/walletConnectSlice'
import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useDeviceDimensions } from 'ui/src' import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useDeviceDimensions } from 'ui/src'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
type ConnectedDappsProps = { type ConnectedDappsProps = {
......
...@@ -7,7 +7,6 @@ import 'react-native-reanimated' ...@@ -7,7 +7,6 @@ import 'react-native-reanimated'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon'
import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice' import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
...@@ -15,6 +14,7 @@ import { AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text, TouchableArea } ...@@ -15,6 +14,7 @@ import { AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text, TouchableArea }
import { iconSizes, spacing } from 'ui/src/theme' import { iconSizes, spacing } from 'ui/src/theme'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { NetworkLogos } from 'wallet/src/components/network/NetworkLogos'
import { pushNotification } from 'wallet/src/features/notifications/slice' import { pushNotification } from 'wallet/src/features/notifications/slice'
import { AppNotificationType } from 'wallet/src/features/notifications/types' import { AppNotificationType } from 'wallet/src/features/notifications/types'
import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks'
...@@ -127,7 +127,6 @@ export function DappConnectionItem({ ...@@ -127,7 +127,6 @@ export function DappConnectionItem({
onLongPress={disableOnPress} onLongPress={disableOnPress}
onPress={(): void => onPressChangeNetwork(session)}> onPress={(): void => onPressChangeNetwork(session)}>
<NetworkLogos <NetworkLogos
negativeGap
showFirstChainLabel showFirstChainLabel
backgroundColor="$surface2" backgroundColor="$surface2"
borderRadius="$roundedFull" borderRadius="$roundedFull"
......
...@@ -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>
)
}
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',
}
import { useBottomSheetInternal } from '@gorhom/bottom-sheet'
import { getSdkError } from '@walletconnect/utils' import { getSdkError } from '@walletconnect/utils'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { LinkButton } from 'src/components/buttons/LinkButton'
import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon'
import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay'
import { PendingConnectionSwitchAccountModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal' import { PendingConnectionSwitchAccountModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal'
import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util' import { truncateQueryParams } from 'src/components/WalletConnect/ScanSheet/util'
import { LinkButton } from 'src/components/buttons/LinkButton'
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 { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect'
import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { wcWeb3Wallet } from 'src/features/walletConnect/saga'
import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors'
import { getSessionNamespaces } from 'src/features/walletConnect/utils' import { getSessionNamespaces } from 'src/features/walletConnect/utils'
import { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect'
import { import {
WalletConnectPendingSession,
addSession, addSession,
removePendingSession, removePendingSession,
WalletConnectPendingSession,
} from 'src/features/walletConnect/walletConnectSlice' } from 'src/features/walletConnect/walletConnectSlice'
import { AnimatedFlex, Button, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { ONE_SECOND_MS } from 'utilities/src/time/time' import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { NetworkLogos } from 'wallet/src/components/network/NetworkLogos'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
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'
...@@ -33,9 +35,9 @@ import { ...@@ -33,9 +35,9 @@ import {
} from 'wallet/src/features/wallet/hooks' } from 'wallet/src/features/wallet/hooks'
import { setAccountAsActive } from 'wallet/src/features/wallet/slice' import { setAccountAsActive } from 'wallet/src/features/wallet/slice'
import { import {
WalletConnectEvent,
WCEventType, WCEventType,
WCRequestOutcome, WCRequestOutcome,
WalletConnectEvent,
} from 'wallet/src/features/walletConnect/types' } from 'wallet/src/features/walletConnect/types'
import { ElementName, ModalName } from 'wallet/src/telemetry/constants' import { ElementName, ModalName } from 'wallet/src/telemetry/constants'
...@@ -133,7 +135,7 @@ const NetworksRow = ({ chains }: { chains: ChainId[] }): JSX.Element => { ...@@ -133,7 +135,7 @@ const NetworksRow = ({ chains }: { chains: ChainId[] }): JSX.Element => {
variant="body3"> variant="body3">
{t('walletConnect.permissions.networks')} {t('walletConnect.permissions.networks')}
</Text> </Text>
<NetworkLogos negativeGap chains={chains} /> <NetworkLogos chains={chains} />
</Flex> </Flex>
) )
} }
...@@ -176,9 +178,9 @@ const SwitchAccountRow = ({ activeAddress, setModalState }: SwitchAccountProps): ...@@ -176,9 +178,9 @@ const SwitchAccountRow = ({ activeAddress, setModalState }: SwitchAccountProps):
export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.Element => { export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.Element => {
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors()
const activeAddress = useActiveAccountAddressWithThrow()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const activeAddress = useActiveAccountAddressWithThrow()
const activeAccount = useActiveAccountWithThrow() const activeAccount = useActiveAccountWithThrow()
const didOpenFromDeepLink = useAppSelector(selectDidOpenFromDeepLink) const didOpenFromDeepLink = useAppSelector(selectDidOpenFromDeepLink)
...@@ -248,63 +250,26 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. ...@@ -248,63 +250,26 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.
}, },
[activeAddress, dispatch, onClose, pendingSession, didOpenFromDeepLink] [activeAddress, dispatch, onClose, pendingSession, didOpenFromDeepLink]
) )
const dappName = pendingSession.dapp.name || pendingSession.dapp.url || '' const dappName = pendingSession.dapp.name || pendingSession.dapp.url || ''
return ( return (
<BottomSheetModal name={ModalName.WCPendingConnection} onClose={onClose}> <>
<AnimatedFlex <ModalWithOverlay
backgroundColor="$surface1" confirmationButtonText={t('walletConnect.pending.button.connect')}
borderRadius="$rounded12" name={ModalName.WCPendingConnection}
gap="$spacing16" scrollDownButtonText={t('walletConnect.pending.button.scrollDown')}
overflow="hidden" onClose={onClose}
pb="$spacing12" onConfirm={(): Promise<void> => onPressSettleConnection(true)}
pt="$spacing32"> onReject={(): Promise<void> => onPressSettleConnection(false)}>
<Flex alignItems="center" gap="$spacing16" justifyContent="flex-end"> <PendingConnectionModalContent
<DappHeaderIcon dapp={pendingSession.dapp} /> activeAddress={activeAddress}
<Text dappName={dappName}
$short={{ variant: 'subheading2' }} pendingSession={pendingSession}
allowFontScaling={false} setModalState={setModalState}
fontWeight="bold" />
px="$spacing24" </ModalWithOverlay>
textAlign="center"
variant="heading3">
{t('walletConnect.pending.title', {
dappName: truncateDappName(dappName),
})}{' '}
</Text>
<LinkButton
color={colors.accent1.val}
label={pendingSession.dapp.url}
mb="$spacing12"
px="$spacing8"
py="$spacing4"
showIcon={false}
size={iconSizes.icon12}
textVariant="buttonLabel4"
url={pendingSession.dapp.url}
/>
</Flex>
<Flex px="$spacing24">
<SitePermissions />
<NetworksRow chains={pendingSession.chains} />
<SwitchAccountRow activeAddress={activeAddress} setModalState={setModalState} />
</Flex>
<Flex flexDirection="row" gap="$spacing8" justifyContent="space-between" px="$spacing24">
<Button
fill
testID="cancel-pending-connection"
theme="secondary"
onPress={(): Promise<void> => onPressSettleConnection(false)}>
{t('common.button.cancel')}
</Button>
<Button
fill
testID="connect-pending-connection"
onPress={(): Promise<void> => onPressSettleConnection(true)}>
{t('walletConnect.pending.button.connect')}
</Button>
</Flex>
</AnimatedFlex>
{modalState === PendingConnectionModalState.SwitchAccount && ( {modalState === PendingConnectionModalState.SwitchAccount && (
<PendingConnectionSwitchAccountModal <PendingConnectionSwitchAccountModal
activeAccount={activeAccount} activeAccount={activeAccount}
...@@ -315,6 +280,66 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. ...@@ -315,6 +280,66 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.
}} }}
/> />
)} )}
</BottomSheetModal> </>
)
}
type PendingConnectionModalContentProps = {
activeAddress: string
dappName: string
pendingSession: WalletConnectPendingSession
setModalState: (state: PendingConnectionModalState.SwitchAccount) => void
}
function PendingConnectionModalContent({
activeAddress,
dappName,
pendingSession,
setModalState,
}: PendingConnectionModalContentProps): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const { animatedFooterHeight } = useBottomSheetInternal()
const bottomSpacerStyle = useAnimatedStyle(() => ({
height: animatedFooterHeight.value,
}))
return (
<>
<Flex alignItems="center" gap="$spacing16" justifyContent="flex-end">
<DappHeaderIcon dapp={pendingSession.dapp} />
<Text
$short={{ variant: 'subheading2' }}
allowFontScaling={false}
fontWeight="bold"
textAlign="center"
variant="heading3">
{t('walletConnect.pending.title', {
dappName,
})}{' '}
</Text>
<LinkButton
backgroundColor="$surface2"
borderRadius="$rounded16"
color={colors.accent1.val}
iconColor={colors.accent1.val}
label={truncateQueryParams(pendingSession.dapp.url)}
mb="$spacing12"
px="$spacing8"
py="$spacing4"
size={iconSizes.icon12}
textVariant="buttonLabel4"
url={pendingSession.dapp.url}
/>
</Flex>
<Flex gap="$spacing1">
<SitePermissions />
<NetworksRow chains={pendingSession.chains} />
<SwitchAccountRow activeAddress={activeAddress} setModalState={setModalState} />
</Flex>
<Animated.View style={bottomSpacerStyle} />
</>
) )
} }
...@@ -5,15 +5,14 @@ import 'react-native-reanimated' ...@@ -5,15 +5,14 @@ import 'react-native-reanimated'
import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks' import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks'
import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner'
import { WalletQRCode } from 'src/components/QRCodeScanner/WalletQRCode'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import Trace from 'src/components/Trace/Trace' import Trace from 'src/components/Trace/Trace'
import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ConnectedDappsList' import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ConnectedDappsList'
import { import {
URIType, URIType,
UWULINK_PREFIX, UWULINK_PREFIX,
getSupportedURI, getSupportedURI,
isAllowedUwULinkRequest, isAllowedUwuLinkRequest,
useUwuLinkContractAllowlist,
} 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 { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga' import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga'
...@@ -27,6 +26,8 @@ import { iconSizes } from 'ui/src/theme' ...@@ -27,6 +26,8 @@ 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'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
import { WalletQRCode } from 'wallet/src/components/QRCodeScanner/WalletQRCode'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal'
import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
import { EthMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types' import { EthMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types'
...@@ -54,6 +55,8 @@ export function WalletConnectModal({ ...@@ -54,6 +55,8 @@ export function WalletConnectModal({
const isUwULinkEnabled = useFeatureFlag(FeatureFlags.UwULink) const isUwULinkEnabled = useFeatureFlag(FeatureFlags.UwULink)
const isScantasticEnabled = useFeatureFlag(FeatureFlags.Scantastic) const isScantasticEnabled = useFeatureFlag(FeatureFlags.Scantastic)
const uwuLinkContractAllowlist = useUwuLinkContractAllowlist()
// Update QR scanner states when pending session error alert is shown from WCv2 saga event channel // Update QR scanner states when pending session error alert is shown from WCv2 saga event channel
useEffect(() => { useEffect(() => {
if (hasPendingSessionError) { if (hasPendingSessionError) {
...@@ -145,7 +148,7 @@ export function WalletConnectModal({ ...@@ -145,7 +148,7 @@ export function WalletConnectModal({
setShouldFreezeCamera(true) setShouldFreezeCamera(true)
try { try {
const parsedUwulinkRequest: UwULinkRequest = JSON.parse(supportedURI.value) const parsedUwulinkRequest: UwULinkRequest = JSON.parse(supportedURI.value)
const isAllowed = isAllowedUwULinkRequest(parsedUwulinkRequest) const isAllowed = isAllowedUwuLinkRequest(parsedUwulinkRequest, uwuLinkContractAllowlist)
if (!isAllowed) { if (!isAllowed) {
Alert.alert( Alert.alert(
...@@ -204,16 +207,16 @@ export function WalletConnectModal({ ...@@ -204,16 +207,16 @@ export function WalletConnectModal({
}, },
[ [
activeAddress, activeAddress,
navigate,
onClose,
preload,
setShouldFreezeCamera,
shouldFreezeCamera,
hasPendingSessionError, hasPendingSessionError,
shouldFreezeCamera,
isUwULinkEnabled, isUwULinkEnabled,
isScantasticEnabled, isScantasticEnabled,
t, t,
preload,
navigate,
onClose,
dispatch, dispatch,
uwuLinkContractAllowlist,
] ]
) )
......
...@@ -6,6 +6,8 @@ import { ...@@ -6,6 +6,8 @@ import {
UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM,
UNISWAP_WALLETCONNECT_URL, UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants' } from 'src/features/deepLinking/constants'
import { DynamicConfigs } from 'uniswap/src/features/experiments/configs'
import { useDynamicConfig } from 'uniswap/src/features/experiments/hooks'
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'
...@@ -30,19 +32,26 @@ interface EnabledFeatureFlags { ...@@ -30,19 +32,26 @@ interface EnabledFeatureFlags {
isScantasticEnabled: boolean isScantasticEnabled: boolean
} }
const UNISNAP_CONTRACT_ADDRESS = '0xFd2308677A0eb48e2d0c4038c12AA7DCb703e8DC' // This type must match the format in statsig dynamic config for uwulink
const UWULINK_CONTRACT_ALLOWLIST = [UNISNAP_CONTRACT_ADDRESS] // https://console.statsig.com/5HjUux4OvSGzgqWIfKFt8i/dynamic_configs/uwulink_config
type UwuLinkAllowlistItem = {
chainId: number
contractAddress: string
name: string
icon?: string
}
type UwuLinkAllowlist = UwuLinkAllowlistItem[]
const UWULINK_MAX_TXN_VALUE = '0.001' 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(
...@@ -121,6 +130,13 @@ function isUwULink(uri: string): boolean { ...@@ -121,6 +130,13 @@ function isUwULink(uri: string): boolean {
return uri.startsWith(`${UWULINK_PREFIX}{`) return uri.startsWith(`${UWULINK_PREFIX}{`)
} }
// Gets the UWULink contract allow list from statsig dynamic config.
// We can safely cast as long as the statsig config format matches our `UwuLinkAllowlist` type.
export function useUwuLinkContractAllowlist(): UwuLinkAllowlist {
const uwuLinkConfig = useDynamicConfig(DynamicConfigs.UwuLink)
return uwuLinkConfig.getValue('allowlist') as UwuLinkAllowlist
}
/** /**
* Util function to check if a UwULinkRequest is valid. * Util function to check if a UwULinkRequest is valid.
* *
...@@ -131,11 +147,14 @@ function isUwULink(uri: string): boolean { ...@@ -131,11 +147,14 @@ function isUwULink(uri: string): boolean {
* @param request parsed UwULinkRequest * @param request parsed UwULinkRequest
* @returns boolean for whether the UwULinkRequest is allowed * @returns boolean for whether the UwULinkRequest is allowed
*/ */
export function isAllowedUwULinkRequest(request: UwULinkRequest): boolean { export function isAllowedUwuLinkRequest(
request: UwULinkRequest,
allowList: UwuLinkAllowlist
): boolean {
const { to, value } = request.value const { to, value } = request.value
const belowMaximumValue = const belowMaximumValue =
value && parseFloat(value) <= parseEther(UWULINK_MAX_TXN_VALUE).toNumber() value && parseFloat(value) <= parseEther(UWULINK_MAX_TXN_VALUE).toNumber()
const isAllowedContractAddress = to && UWULINK_CONTRACT_ALLOWLIST.includes(to) const isAllowedContractAddress = to && allowList.some((item) => item.contractAddress === to)
if (!belowMaximumValue || !isAllowedContractAddress) { if (!belowMaximumValue || !isAllowedContractAddress) {
return false return false
......
...@@ -106,7 +106,7 @@ export function AccountHeader(): JSX.Element { ...@@ -106,7 +106,7 @@ export function AccountHeader(): JSX.Element {
hitSlop={20} hitSlop={20}
testID="account-header/settings-button" testID="account-header/settings-button"
onPress={onPressSettings}> onPress={onPressSettings}>
<Icons.Settings color="$neutral2" opacity={0.8} size="$icon.28" /> <Icons.Settings color="$neutral2" opacity={0.8} size="$icon.24" />
</TouchableArea> </TouchableArea>
</Flex> </Flex>
{walletHasName ? ( {walletHasName ? (
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
TouchableArea, TouchableArea,
useDeviceDimensions, useDeviceDimensions,
useIsDarkMode, useIsDarkMode,
useIsShortMobileDevice,
useSporeColors, useSporeColors,
} from 'ui/src' } from 'ui/src'
import { EXTENSION_PROMO_BANNER_DARK, EXTENSION_PROMO_BANNER_LIGHT } from 'ui/src/assets' import { EXTENSION_PROMO_BANNER_DARK, EXTENSION_PROMO_BANNER_LIGHT } from 'ui/src/assets'
...@@ -32,6 +33,7 @@ export function ExtensionPromoBanner({ ...@@ -32,6 +33,7 @@ export function ExtensionPromoBanner({
const { fullWidth } = useDeviceDimensions() const { fullWidth } = useDeviceDimensions()
const colors = useSporeColors() const colors = useSporeColors()
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const isShortDevice = useIsShortMobileDevice()
const imageWidth = IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth const imageWidth = IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth
const imageHeight = imageWidth / IMAGE_ASPECT_RATIO const imageHeight = imageWidth / IMAGE_ASPECT_RATIO
...@@ -79,9 +81,11 @@ export function ExtensionPromoBanner({ ...@@ -79,9 +81,11 @@ export function ExtensionPromoBanner({
<Text color="$neutral1" variant="subheading1"> <Text color="$neutral1" variant="subheading1">
{t('home.banner.extension.title')} {t('home.banner.extension.title')}
</Text> </Text>
<Text color="$neutral2" variant="body3"> {!isShortDevice && (
{t('home.banner.extension.message')} <Text color="$neutral2" variant="body3">
</Text> {t('home.banner.extension.message')}
</Text>
)}
</Flex> </Flex>
<Flex grow row gap="$spacing8"> <Flex grow row gap="$spacing8">
<TouchableArea <TouchableArea
......
...@@ -42,7 +42,7 @@ export function LinkButton({ ...@@ -42,7 +42,7 @@ 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>
{showIcon && ( {showIcon && (
......
...@@ -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()
}) })
}) })
import { SharedEventName } from '@uniswap/analytics-events' import { SharedEventName } from '@uniswap/analytics-events'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { NativeSyntheticEvent, Share, ViewStyle } from 'react-native' import { NativeSyntheticEvent, ViewStyle } from 'react-native'
import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view'
import { import {
AnimateStyle, AnimateStyle,
...@@ -11,13 +11,13 @@ import { ...@@ -11,13 +11,13 @@ import {
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks' import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { sendMobileAnalyticsEvent } from 'src/features/telemetry' import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' import { CurrencyId } from 'uniswap/src/types/currency'
import { logger } from 'utilities/src/logger/logger' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { ChainId } from 'wallet/src/constants/chains' import { ChainId } from 'wallet/src/constants/chains'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { AssetType } from 'wallet/src/entities/assets' import { AssetType } from 'wallet/src/entities/assets'
import { import {
CurrencyField, CurrencyField,
...@@ -25,8 +25,7 @@ import { ...@@ -25,8 +25,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'
interface TokenMenuParams { interface TokenMenuParams {
currencyId: CurrencyId currencyId: CurrencyId
...@@ -50,6 +49,8 @@ export function useExploreTokenContextMenu({ ...@@ -50,6 +49,8 @@ export function useExploreTokenContextMenu({
const isFavorited = useSelectHasTokenFavorited(currencyId) const isFavorited = useSelectHasTokenFavorited(currencyId)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { handleShareToken } = useWalletNavigation()
// `address` is undefined for native currencies, so we want to extract it from // `address` is undefined for native currencies, so we want to extract it from
// currencyId, where we have hardcoded addresses for native currencies // currencyId, where we have hardcoded addresses for native currencies
const currencyAddress = currencyIdToAddress(currencyId) const currencyAddress = currencyIdToAddress(currencyId)
...@@ -63,22 +64,8 @@ export function useExploreTokenContextMenu({ ...@@ -63,22 +64,8 @@ export function useExploreTokenContextMenu({
) )
const onPressShare = useCallback(async () => { const onPressShare = useCallback(async () => {
const tokenUrl = getTokenUrl(currencyId) handleShareToken({ currencyId })
if (!tokenUrl) { }, [currencyId, handleShareToken])
return
}
try {
await Share.share({
message: tokenUrl,
})
sendMobileAnalyticsEvent(MobileEventName.ShareButtonClicked, {
entity: ShareableEntity.Token,
url: tokenUrl,
})
} catch (error) {
logger.error(error, { tags: { file: 'balances/hooks.ts', function: 'onPressShare' } })
}
}, [currencyId])
const toggleFavoriteToken = useToggleFavoriteCallback(currencyId, isFavorited) const toggleFavoriteToken = useToggleFavoriteCallback(currencyId, isFavorited)
......
...@@ -17,10 +17,7 @@ import { ...@@ -17,10 +17,7 @@ import {
getSearchResultId, getSearchResultId,
} from 'src/components/explore/search/utils' } from 'src/components/explore/search/utils'
import { AnimatedFlex, Flex, Icons, Text } from 'ui/src' import { AnimatedFlex, Flex, Icons, Text } from 'ui/src'
import { import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
SafetyLevel,
useExploreSearchQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
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 { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard'
...@@ -118,13 +115,6 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -118,13 +115,6 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
isPrefixTokenMatch(res, searchQuery) isPrefixTokenMatch(res, searchQuery)
) )
const hasVerifiedTokenResults = Boolean(
tokenResults?.some(
(res) =>
res.safetyLevel === SafetyLevel.Verified || res.safetyLevel === SafetyLevel.MediumWarning
)
)
const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified)) const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified))
const isUsernameSearch = useMemo(() => { const isUsernameSearch = useMemo(() => {
...@@ -133,7 +123,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ...@@ -133,7 +123,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }):
const showWalletSectionFirst = const showWalletSectionFirst =
isUsernameSearch && (exactUnitagMatch || (exactENSMatch && !prefixTokenMatch)) isUsernameSearch && (exactUnitagMatch || (exactENSMatch && !prefixTokenMatch))
const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !tokenResults?.length
const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => { const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => {
// Format results arrays with header, and handle empty results // Format results arrays with header, and handle empty results
......
...@@ -7,7 +7,6 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry' ...@@ -7,7 +7,6 @@ import { sendMobileAnalyticsEvent } from 'src/features/telemetry'
import { MobileEventName } from 'src/features/telemetry/constants' import { MobileEventName } from 'src/features/telemetry/constants'
import { disableOnPress } from 'src/utils/disableOnPress' import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'wallet/src/components/icons/WarningIcon' import WarningIcon from 'wallet/src/components/icons/WarningIcon'
...@@ -87,10 +86,9 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): ...@@ -87,10 +86,9 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
{(safetyLevel === SafetyLevel.Blocked || {(safetyLevel === SafetyLevel.Blocked ||
safetyLevel === SafetyLevel.StrongWarning) && ( safetyLevel === SafetyLevel.StrongWarning) && (
<WarningIcon <WarningIcon
height={iconSizes.icon16}
safetyLevel={safetyLevel} safetyLevel={safetyLevel}
size="$icon.16"
strokeColorOverride="neutral3" strokeColorOverride="neutral3"
width={iconSizes.icon16}
/> />
)} )}
</Flex> </Flex>
......
...@@ -2,7 +2,6 @@ import { ForwardedRef, forwardRef, memo, useMemo } from 'react' ...@@ -2,7 +2,6 @@ import { ForwardedRef, forwardRef, memo, useMemo } from 'react'
import { FlatList, RefreshControl } from 'react-native' import { FlatList, RefreshControl } from 'react-native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { useAdaptiveFooter } from 'src/components/home/hooks' import { useAdaptiveFooter } from 'src/components/home/hooks'
import { import {
AnimatedBottomSheetFlatList, AnimatedBottomSheetFlatList,
...@@ -15,6 +14,7 @@ import { removePendingSession } from 'src/features/walletConnect/walletConnectSl ...@@ -15,6 +14,7 @@ import { removePendingSession } from 'src/features/walletConnect/walletConnectSl
import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' import { Flex, useDeviceInsets, useSporeColors } 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 { isAndroid } from 'uniswap/src/utils/platform' import { isAndroid } from 'uniswap/src/utils/platform'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { useActivityData } from 'wallet/src/features/activity/useActivityData' import { useActivityData } from 'wallet/src/features/activity/useActivityData'
import { ModalName } from 'wallet/src/telemetry/constants' import { ModalName } from 'wallet/src/telemetry/constants'
......
...@@ -3,18 +3,17 @@ import { useTranslation } from 'react-i18next' ...@@ -3,18 +3,17 @@ import { useTranslation } from 'react-i18next'
import { FlatList, RefreshControl } from 'react-native' import { FlatList, RefreshControl } from 'react-native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { useAppDispatch, useAppSelector } from 'src/app/hooks'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { useAdaptiveFooter } from 'src/components/home/hooks' import { useAdaptiveFooter } from 'src/components/home/hooks'
import { AnimatedFlatList } from 'src/components/layout/AnimatedFlatList' import { AnimatedFlatList } from 'src/components/layout/AnimatedFlatList'
import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers'
import { Loader } from 'src/components/loading' import { Loader } from 'src/components/loading'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src' import { Flex, Icons, Text, useDeviceInsets, useSporeColors } from 'ui/src'
import { NoTransactions } from 'ui/src/components/icons/NoTransactions'
import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries'
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 { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { useFormattedTransactionDataForFeed } from 'wallet/src/features/activity/hooks' import { useFormattedTransactionDataForFeed } from 'wallet/src/features/activity/hooks'
import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors'
import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout'
...@@ -87,7 +86,7 @@ export const FeedTab = memo( ...@@ -87,7 +86,7 @@ export const FeedTab = memo(
<Flex grow style={containerProps?.emptyContainerStyle}> <Flex grow style={containerProps?.emptyContainerStyle}>
<BaseCard.EmptyState <BaseCard.EmptyState
description={t('home.feed.empty.description')} description={t('home.feed.empty.description')}
icon={<NoTransactions />} icon={<Icons.NoTransactions color="$neutral3" size="$icon.70" />}
title={t('home.feed.empty.title')} title={t('home.feed.empty.title')}
onPress={onPressReceive} onPress={onPressReceive}
/> />
......
...@@ -3,20 +3,19 @@ import React, { forwardRef, memo, useCallback, useMemo } from 'react' ...@@ -3,20 +3,19 @@ import React, { forwardRef, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList } from 'react-native' import { FlatList } from 'react-native'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList' import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import { WalletEmptyState } from 'src/components/home/WalletEmptyState' import { WalletEmptyState } from 'src/components/home/WalletEmptyState'
import { NoTokens } from 'src/components/icons/NoTokens'
import { TabContentProps, TabProps } from 'src/components/layout/TabHelpers' import { TabContentProps, TabProps } from 'src/components/layout/TabHelpers'
import { openModal } from 'src/features/modals/modalSlice' 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, Icons } 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 { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
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]
...@@ -72,7 +71,7 @@ export const TokensTab = memo( ...@@ -72,7 +71,7 @@ export const TokensTab = memo(
return isExternalProfile ? ( return isExternalProfile ? (
<BaseCard.EmptyState <BaseCard.EmptyState
description={t('home.tokens.empty.description')} description={t('home.tokens.empty.description')}
icon={<NoTokens />} icon={<Icons.NoTokens color="$neutral3" size="$icon.70" />}
title={t('home.tokens.empty.title')} title={t('home.tokens.empty.title')}
onPress={onPressAction} onPress={onPressAction}
/> />
......
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAppDispatch } from 'src/app/hooks' import { useAppDispatch } from 'src/app/hooks'
import { ScannerModalState } from 'src/components/QRCodeScanner/constants'
import Trace from 'src/components/Trace/Trace' import Trace from 'src/components/Trace/Trace'
import { openModal } from 'src/features/modals/modalSlice' import { openModal } from 'src/features/modals/modalSlice'
import { Flex, Icons, Text, TouchableArea } from 'ui/src' import { Flex, Icons, Text, TouchableArea } from 'ui/src'
...@@ -9,6 +8,7 @@ import PaperStackIcon from 'ui/src/assets/icons/paper-stack.svg' ...@@ -9,6 +8,7 @@ import PaperStackIcon from 'ui/src/assets/icons/paper-stack.svg'
import { iconSizes, colors as rawColors } from 'ui/src/theme' import { iconSizes, colors as rawColors } 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'
import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants'
import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types'
import { useActiveAccount } from 'wallet/src/features/wallet/hooks' import { useActiveAccount } from 'wallet/src/features/wallet/hooks'
import { ElementName, ElementNameType, ModalName } from 'wallet/src/telemetry/constants' import { ElementName, ElementNameType, ModalName } from 'wallet/src/telemetry/constants'
......
import React, { memo } from 'react'
import OverlayIcon from 'src/components/icons/OverlayIcon'
import { Flex, useSporeColors } from 'ui/src'
import NoTokensFgIcon from 'ui/src/assets/icons/empty-state-coin.svg'
import NoTokensBgIcon from 'ui/src/assets/icons/empty-state-tokens.svg'
export const NoTokens = memo(function _NoTokens() {
const colors = useSporeColors()
return (
<Flex>
<OverlayIcon
bottom={0}
icon={<NoTokensBgIcon color={colors.neutral2.get()} />}
overlay={<NoTokensFgIcon color={colors.surface2.get()} fill={colors.accent1.get()} />}
right={0}
/>
</Flex>
)
})
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { NativeSafeAreaViewProps } from 'react-native-safe-area-context' import { Edge, NativeSafeAreaViewProps } from 'react-native-safe-area-context'
import { Flex, FlexProps, useDeviceInsets } from 'ui/src' import { Flex, FlexProps, useDeviceInsets } from 'ui/src'
// Used to determine amount of top padding for short screens // Used to determine amount of top padding for short screens
...@@ -9,7 +9,9 @@ type ScreenProps = FlexProps & ...@@ -9,7 +9,9 @@ type ScreenProps = FlexProps &
// The SafeAreaView from react-native-safe-area-context also supports a `mode` prop which // The SafeAreaView from react-native-safe-area-context also supports a `mode` prop which
// lets you choose if `edges` are added as margin or padding, but we don’t use that so // lets you choose if `edges` are added as margin or padding, but we don’t use that so
// our Screen component doesn't need to support it // our Screen component doesn't need to support it
Omit<NativeSafeAreaViewProps, 'mode'> & { noInsets?: boolean } Omit<NativeSafeAreaViewProps, 'mode' | 'edges'> & { edges?: readonly Edge[] } & {
noInsets?: boolean
}
function SafeAreaWithInsets({ children, edges, noInsets, ...rest }: ScreenProps): JSX.Element { function SafeAreaWithInsets({ children, edges, noInsets, ...rest }: ScreenProps): JSX.Element {
// Safe area insets are wrong (0 when they shouldn't be) when using the <SafeAreaView> // Safe area insets are wrong (0 when they shouldn't be) when using the <SafeAreaView>
......
...@@ -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>
) )
} }
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
TouchableAreaProps, TouchableAreaProps,
useDeviceDimensions, useDeviceDimensions,
useIsDarkMode, useIsDarkMode,
useIsShortMobileDevice,
} from 'ui/src' } from 'ui/src'
import { UNITAGS_BANNER_VERTICAL_DARK, UNITAGS_BANNER_VERTICAL_LIGHT } from 'ui/src/assets' import { UNITAGS_BANNER_VERTICAL_DARK, UNITAGS_BANNER_VERTICAL_LIGHT } from 'ui/src/assets'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
...@@ -41,6 +42,7 @@ export function UnitagBanner({ ...@@ -41,6 +42,7 @@ export function UnitagBanner({
const { fullWidth } = useDeviceDimensions() const { fullWidth } = useDeviceDimensions()
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const hasCompletedUnitagsIntroModal = useAppSelector(selectHasCompletedUnitagsIntroModal) const hasCompletedUnitagsIntroModal = useAppSelector(selectHasCompletedUnitagsIntroModal)
const isShortDevice = useIsShortMobileDevice()
const imageWidth = compact const imageWidth = compact
? COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth ? COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth
...@@ -124,9 +126,11 @@ export function UnitagBanner({ ...@@ -124,9 +126,11 @@ export function UnitagBanner({
unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT, unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT,
})} })}
</Text> </Text>
<Text color="$neutral2" variant="body3"> {!isShortDevice && (
{t('unitags.banner.subtitle')} <Text color="$neutral2" variant="body3">
</Text> {t('unitags.banner.subtitle')}
</Text>
)}
</Flex> </Flex>
<Flex row gap="$spacing2"> <Flex row gap="$spacing2">
{/* TODO: replace with Button when it's extensible enough to accommodate designs */} {/* TODO: replace with Button when it's extensible enough to accommodate designs */}
......
import { tamaguiConfig } from 'ui/src' import { config } from 'ui/src/tamagui.config'
type Conf = typeof tamaguiConfig type Conf = typeof config
declare module 'tamagui' { declare module 'tamagui' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
......
import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types'
const generateRandomId = (): string => {
let randomId = '0x'
for (let i = 0; i < 40; i++) {
randomId += Math.floor(Math.random() * 16).toString(16)
}
return randomId
}
const generateRandomDate = (): number => {
const start = new Date(2023, 4, 12)
const end = new Date()
return Math.floor(
new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).getTime() / 1000
)
}
export const useMockCloudBackups = (numberOfBackups?: number): CloudStorageMnemonicBackup[] => {
const number = numberOfBackups ?? 1
const mockBackups = Array.from({ length: number }, () => ({
mnemonicId: generateRandomId(),
createdAt: generateRandomDate(),
}))
return mockBackups
}
import React from 'react'
import AnimatedNumber from 'src/components/AnimatedNumber'
import { Flex, Shine } from 'ui/src'
import { NumberType } from 'utilities/src/format/types'
import { RelativeChange } from 'wallet/src/components/text/RelativeChange'
import { PollingInterval } from 'wallet/src/constants/misc'
import { isWarmLoadingStatus } from 'wallet/src/data/utils'
import { usePortfolioTotalValue } from 'wallet/src/features/dataApi/balances'
import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants'
import { useAppFiatCurrency, useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks'
import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext'
interface PortfolioBalanceProps {
owner: Address
}
export function PortfolioBalance({ owner }: PortfolioBalanceProps): JSX.Element {
const { data, loading, networkStatus } = usePortfolioTotalValue({
address: owner,
// TransactionHistoryUpdater will refetch this query on new transaction.
// No need to be super aggressive with polling here.
pollInterval: PollingInterval.Normal,
})
const currency = useAppFiatCurrency()
const currencyComponents = useAppFiatCurrencyInfo()
const { convertFiatAmount, convertFiatAmountFormatted } = useLocalizationContext()
const isLoading = loading && !data
const isWarmLoading = !!data && isWarmLoadingStatus(networkStatus)
const { percentChange, absoluteChangeUSD, balanceUSD } = data || {}
const totalBalance = convertFiatAmountFormatted(balanceUSD, NumberType.PortfolioBalance)
const { amount: absoluteChange } = convertFiatAmount(absoluteChangeUSD)
// TODO gary re-enabling this for USD/Euros only, replace with more scalable approach
const shouldFadePortfolioDecimals =
(currency === FiatCurrency.UnitedStatesDollar || currency === FiatCurrency.Euro) &&
currencyComponents.symbolAtFront
return (
<Flex gap="$spacing4">
<AnimatedNumber
colorIndicationDuration={2000}
loading={isLoading}
loadingPlaceholderText="000000.00"
shouldFadeDecimals={shouldFadePortfolioDecimals}
value={totalBalance}
warmLoading={isWarmLoading}
/>
<Shine disabled={!isWarmLoading}>
<RelativeChange
absoluteChange={absoluteChange}
arrowSize="$icon.16"
change={percentChange}
loading={isLoading}
negativeChangeColor={isWarmLoading ? '$neutral2' : '$statusCritical'}
positiveChangeColor={isWarmLoading ? '$neutral2' : '$statusSuccess'}
variant="body3"
/>
</Shine>
</Flex>
)
}
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 {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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