diff --git a/RELEASE b/RELEASE index 8d1fcf0694c..7f37cee44f6 100644 --- a/RELEASE +++ b/RELEASE @@ -1,6 +1,6 @@ IPFS hash of the deployment: -- CIDv0: `QmRCrXbMR4EUz9dAJvL4pBfVtVGkj6sB7zNZo6H8V65jDb` -- CIDv1: `bafybeibkspdr2mrrvuxfqo2kh7aaasnzbjlxixfic2dpqtbnsq4qw25o6i` +- CIDv0: `QmReiE84jPE7A2a52oNhxkyR1S9hZDgR9ENHdBpsqxM4vA` +- CIDv1: `bafybeibrgn7bxrw5utokbemws52rnuwiexakexjw5n4fq35fp66lnuqla4` The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). @@ -10,15 +10,70 @@ You can also access the Uniswap Interface from an IPFS gateway. Your Uniswap settings are never remembered across different URLs. IPFS gateways: -- https://bafybeibkspdr2mrrvuxfqo2kh7aaasnzbjlxixfic2dpqtbnsq4qw25o6i.ipfs.dweb.link/ -- https://bafybeibkspdr2mrrvuxfqo2kh7aaasnzbjlxixfic2dpqtbnsq4qw25o6i.ipfs.cf-ipfs.com/ -- [ipfs://QmRCrXbMR4EUz9dAJvL4pBfVtVGkj6sB7zNZo6H8V65jDb/](ipfs://QmRCrXbMR4EUz9dAJvL4pBfVtVGkj6sB7zNZo6H8V65jDb/) +- https://bafybeibrgn7bxrw5utokbemws52rnuwiexakexjw5n4fq35fp66lnuqla4.ipfs.dweb.link/ +- https://bafybeibrgn7bxrw5utokbemws52rnuwiexakexjw5n4fq35fp66lnuqla4.ipfs.cf-ipfs.com/ +- [ipfs://QmReiE84jPE7A2a52oNhxkyR1S9hZDgR9ENHdBpsqxM4vA/](ipfs://QmReiE84jPE7A2a52oNhxkyR1S9hZDgR9ENHdBpsqxM4vA/) -### 5.59.3 (2024-11-21) +## 5.60.0 (2024-11-26) + + +### Features + +* **web:** add an error state for trading api errors for create flow (#13954) 155c273 +* **web:** add deadline settings to create flow (#13876) 2ef89c1 +* **web:** add error / no data state to liq range input component (#13847) 6f3180f +* **web:** add loading state to mini price charts (#13826) c0656c1 +* **web:** add migrate to position dropdown (#13829) 48b4d13 +* **web:** add settings to add/remove liq (#13864) d178851 +* **web:** gate v4 features behind feature flag (#13877) 7c59740 +* **web:** integrate LiquidityRangeInput to create flow (#13804) f41b2c6 +* **web:** mvp of new price range input (#13803) 5bb8579 +* **web:** refactor flags into redesign flag and v4data flag (#13867) bf39790 +* **web:** set max height for create position bottomsheet (#13979) 21b486c ### Bug Fixes -* **web:** only pad preference menu instead of all nav [prod] (#13994) 1809d8c +* **web:** add current page to pool position breadcrumbs (#13737) 6124764 +* **web:** add trading api error to all the flows (#13961) 81f5359 +* **web:** add wrapper to fix info icon alignment (#13740) 700a749 +* **web:** aligns icon colors and fix info buttons (#13957) b97d130 +* **web:** button sizing on mweb TDP (#13942) 89f83fc +* **web:** create swap settings context (#13929) b3bc846 +* **web:** DEATH TO THE HORIZONTAL SCROLLBAR (#14026) a27e5a6 +* **web:** fix closed positions cta container (#13988) 6d27f16 +* **web:** fix explore chart colors (#13902) 1168e7e +* **web:** fix incorrect pairs on v2 (#13985) da75ba5 +* **web:** fix limits form button text color (#13828) 4edb362 +* **web:** fix overflow in unconnected menu (#13939) 34c15af +* **web:** fix price chart range calculations (#13970) 20fe90b +* **web:** improve autoscaling and zooming in price range input (#13940) efea0bb +* **web:** increase + create ui fixes (#13943) 4a9e536 +* **web:** initialize uniswap wc modal on click (#13975) 944900e +* **web:** LP create form - fix text overflow and design change (#13830) 12988b7 +* **web:** mock datadog in jest tests (#13913) 81bb64a +* **web:** more improvements to price range input (#14000) 19d7fb3 +* **web:** only pad preference menu instead of all nav (#13995) 279e5c7 +* **web:** overflow bug on empty positions page (#13842) 5d7b242 +* **web:** part 1 of polling uniswap x orders (#14032) 594f090 +* **web:** polyfill roundRect calls (#13971) 42bfa61 +* **web:** reduce nft/swap test flakiness (#13924) 8739af5 +* **web:** remove hover behavior on mweb (#13888) 12c97dc +* **web:** reset chain id and multichain context (#14014) fef3f96 +* **web:** reset to default state when testnet mode is toggled (#14011) 6086f4d +* **web:** revert pr 12277 (#13851) f562f7a +* **web:** scroll to top of posdp (#13860) 2d47c65 +* **web:** single step creation review treatment (#13993) dc09d73 +* **web:** small UI nits (#13879) 62282b1 +* **web:** tdp e2e test fixes (#13861) 566a721 +* **web:** update create modal padding (#14019) 6321752 +* **web:** use sepolia eth as default token when in testnet mode (#13856) 6e4f6b8 +* **web:** v4 mobile web fixes (#13831) ff816b5 +* **web:** v4 ui nits (#13927) f3efa1c + + +### Continuous Integration + +* **web:** update sitemaps de8bd02 diff --git a/VERSION b/VERSION index 0df6f7fbf23..3ed104b36fe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -web/5.59.3 \ No newline at end of file +web/5.60.0 \ No newline at end of file diff --git a/apps/extension/src/app/UnitagClaimApp.tsx b/apps/extension/src/app/UnitagClaimApp.tsx index d5a35563bfc..a3d35d65711 100644 --- a/apps/extension/src/app/UnitagClaimApp.tsx +++ b/apps/extension/src/app/UnitagClaimApp.tsx @@ -27,6 +27,7 @@ import { SentryAppNameTag, initializeSentry, sentryCreateHashRouter } from 'src/ import { initExtensionAnalytics } from 'src/app/utils/analytics' import { getReduxPersistor, getReduxStore } from 'src/store/store' import { Flex } from 'ui/src' +import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import Trace from 'uniswap/src/features/telemetry/Trace' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' @@ -166,12 +167,14 @@ export default function UnitagClaimApp(): JSX.Element { - - - - - - + + + + + + + + diff --git a/apps/extension/src/app/components/Trace/TraceUserProperties.tsx b/apps/extension/src/app/components/Trace/TraceUserProperties.tsx index 6f22bc88469..005f75cb546 100644 --- a/apps/extension/src/app/components/Trace/TraceUserProperties.tsx +++ b/apps/extension/src/app/components/Trace/TraceUserProperties.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useColorScheme } from 'react-native' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' import { useCurrentLanguage } from 'uniswap/src/features/language/hooks' import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' diff --git a/apps/extension/src/app/features/accounts/CreateWalletModal.tsx b/apps/extension/src/app/features/accounts/CreateWalletModal.tsx index ce74e9ecf68..641abb6e7ec 100644 --- a/apps/extension/src/app/features/accounts/CreateWalletModal.tsx +++ b/apps/extension/src/app/features/accounts/CreateWalletModal.tsx @@ -6,7 +6,7 @@ import { iconSizes, opacify } from 'ui/src/theme' import { TextInput } from 'uniswap/src/components/input/TextInput' import { Modal } from 'uniswap/src/components/modals/Modal' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { shortenAddress } from 'uniswap/src/utils/addresses' +import { shortenAddress } from 'utilities/src/addresses' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' diff --git a/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap b/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap index 4fda80ff628..63df7abd4dc 100644 --- a/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap +++ b/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap @@ -150,7 +150,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` class="font_body _display-inline _boxSizing-border-box _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _color-843135005 _fontFamily-299667014 _wordWrap-break-word _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202" data-disable-theme="true" > - 0x​9eb67f...d9a2ca + 0x​9EB67f...D9A2Ca - 0x​9eb67f...d9a2ca + 0x​9EB67f...D9A2Ca { const dappInfo = dappStore.getDappInfo(dappUrl) const name = extractNameFromUrl(dappUrl) + + const hostName = extractUrlHost(dappUrl) + const title = dappInfo?.displayName || hostName + + const DeleteDappButton = ( + + + + ) + + const DappIcon = ( + } + size={{ + height: iconSizes.icon32, + width: iconSizes.icon32, + }} + style={{ image: { borderRadius: borderRadii.rounded8 } }} + uri={dappInfo?.iconUrl} + /> + ) + + /** + * TEXT AREA; TITLE/SUBTITLE + * + * we only need to set the text area height because it is the only section with optional fields + */ + const Title = ( + + + {title} + + {hostName !== title && ( + + {hostName} + + )} + + ) + return ( - - - - - } - size={{ - height: iconSizes.icon32, - width: iconSizes.icon32, - }} - style={{ image: { borderRadius: borderRadii.rounded8 } }} - uri={dappInfo?.iconUrl} - /> - - {dappInfo?.displayName || name} - - {extractUrlHost(dappUrl)} - - + {DeleteDappButton} + {DappIcon} + {Title} ) }), diff --git a/apps/extension/src/app/features/settings/SettingsScreen.tsx b/apps/extension/src/app/features/settings/SettingsScreen.tsx index 22fbb358585..a4af469e930 100644 --- a/apps/extension/src/app/features/settings/SettingsScreen.tsx +++ b/apps/extension/src/app/features/settings/SettingsScreen.tsx @@ -37,7 +37,7 @@ import { import { iconSizes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' import { resetUniswapBehaviorHistory } from 'uniswap/src/features/behaviorHistory/slice' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' import { getFiatCurrencyName, useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' diff --git a/apps/extension/src/app/features/swap/SwapFlowScreen.tsx b/apps/extension/src/app/features/swap/SwapFlowScreen.tsx index f3d93c11acd..02a490b5f85 100644 --- a/apps/extension/src/app/features/swap/SwapFlowScreen.tsx +++ b/apps/extension/src/app/features/swap/SwapFlowScreen.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useExtensionNavigation } from 'src/app/navigation/utils' import { Flex } from 'ui/src' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances' import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/hooks/useSwapPrefilledState' import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState' diff --git a/apps/extension/src/app/navigation/SideBarNavigationProvider.tsx b/apps/extension/src/app/navigation/SideBarNavigationProvider.tsx index 98d796211d9..92e516e9902 100644 --- a/apps/extension/src/app/navigation/SideBarNavigationProvider.tsx +++ b/apps/extension/src/app/navigation/SideBarNavigationProvider.tsx @@ -5,7 +5,7 @@ import { useCopyToClipboard } from 'src/app/hooks/useOnCopyToClipboard' import { AppRoutes, HomeQueryParams, HomeTabs } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/state' import { SidebarLocationState, focusOrCreateTokensExploreTab } from 'src/app/navigation/utils' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { CopyNotificationType } from 'uniswap/src/features/notifications/types' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/extension/src/manifest.json b/apps/extension/src/manifest.json index 2bde52d401e..e4bbd1b8be4 100644 --- a/apps/extension/src/manifest.json +++ b/apps/extension/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Uniswap Extension", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", - "version": "1.10.0", + "version": "1.11.0", "minimum_chrome_version": "116", "icons": { "16": "assets/icon16.png", diff --git a/apps/mobile/README.md b/apps/mobile/README.md index d4b08b71d76..215ce86968d 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -218,6 +218,20 @@ You can also run the app from Xcode, which is necessary for any Swift related ch Hopefully you now (after a few minutes) see the Uniswap Wallet running in the iOS Simulator! +### Using Radon IDE (VSCode/Cursor Extension) + +[Radon IDE](https://marketplace.visualstudio.com/items?itemName=swmansion.react-native-ide&ssr=false#review-details) is a relatively new VSCode extension build by Software Mansion. TLDR; its tagline is + +> A better developer experience for React Native developers + +It's not perfect, but it's great to have in the toolbox. One noteworthy feature is the ability to click on any piece of UI and be able to inspect the component hierarchy + jump straight into the relevant code. There's also support for breakpoints in VSCode/Cursor, better logging, instant replay of your session, and the ability to adjust common device settings on the fly. + +To get started, you should already be able to build the iOS app (either in XCode or via the cli). Install the extension, open it, and follow the onboarding instructions. + +One you have a device configured, it will start to build. If/when successful, you'll see the device simulator/emulator in the sidebar. + +In `.vscode/launch.json`, you will see configurations for each platform. This is where you can specify the fingerprint command. The fingerprint is a hash of the build environment, and Radon uses it to determine if the build has changed so that it knows when to re-run the build process (i.e. only on native code changes). There are more complex implementations of this, but this is a simple first step. + #### Running on a Physical iOS Device 1. Follow all steps listed above. diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 4a212560146..d19d4c6b01c 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) { apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" } -def devVersionName = "1.40" -def betaVersionName = "1.40" -def prodVersionName = "1.40" +def devVersionName = "1.41" +def betaVersionName = "1.41" +def prodVersionName = "1.41" android { ndkVersion rootProject.ext.ndkVersion diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js index 105551bb077..07c24c03c3d 100644 --- a/apps/mobile/babel.config.js +++ b/apps/mobile/babel.config.js @@ -4,7 +4,11 @@ const inProduction = NODE_ENV === 'production' module.exports = function (api) { api.cache.using(() => process.env.NODE_ENV) - var plugins = [ + + let plugins = inProduction ? ['transform-remove-console'] : [] + + plugins = [ + ...plugins, // disable for now as its causing ci to hang // process.env.NODE_ENV === 'test' // ? null @@ -33,26 +37,23 @@ module.exports = function (api) { allowUndefined: false, }, ], + 'transform-inline-environment-variables', + // TypeScript compiles this, but in production builds, metro doesn't use tsc + '@babel/plugin-proposal-logical-assignment-operators', + // metro doesn't like these + '@babel/plugin-proposal-numeric-separator', // https://github.com/software-mansion/react-native-reanimated/issues/3364#issuecomment-1268591867 '@babel/plugin-proposal-export-namespace-from', + // 'react-native-reanimated/plugin' must be listed last + // https://arc.net/l/quote/plrvpkad [ 'react-native-reanimated/plugin', { globals: ['__scanCodes', '__scanOCR'], }, ], - 'transform-inline-environment-variables', - // TypeScript compiles this, but in production builds, metro doesn't use tsc - '@babel/plugin-proposal-logical-assignment-operators', - // metro doesn't like these - '@babel/plugin-proposal-numeric-separator', ].filter(Boolean) - if (inProduction) { - // Remove all console statements in production - plugins = [...plugins, 'transform-remove-console'] - } - return { ignore: [ // speeds up compile diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj index c54391fc520..4e5592d2926 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -2205,7 +2205,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2258,7 +2258,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -2311,7 +2311,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -2364,7 +2364,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -2402,7 +2402,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2438,7 +2438,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -2473,7 +2473,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -2508,7 +2508,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -2555,7 +2555,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2601,7 +2601,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; @@ -2647,7 +2647,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; @@ -2693,7 +2693,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; @@ -2735,7 +2735,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2778,7 +2778,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; @@ -2821,7 +2821,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; @@ -2864,7 +2864,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; @@ -2900,7 +2900,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2938,7 +2938,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3138,7 +3138,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3182,7 +3182,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; @@ -3293,7 +3293,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3364,7 +3364,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; @@ -3475,7 +3475,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3546,7 +3546,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.40; + MARKETING_VERSION = 1.41; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index dd9531fd558..f5294ed9567 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -104,6 +104,7 @@ "expo-blur": "12.9.2", "expo-camera": "14.1.2", "expo-clipboard": "5.0.1", + "expo-haptics": "12.8.1", "expo-linear-gradient": "12.7.2", "expo-linking": "6.2.2", "expo-local-authentication": "13.8.0", diff --git a/apps/mobile/scripts/getFingerprintForRadonIDE.js b/apps/mobile/scripts/getFingerprintForRadonIDE.js new file mode 100644 index 00000000000..1a1359fbf81 --- /dev/null +++ b/apps/mobile/scripts/getFingerprintForRadonIDE.js @@ -0,0 +1,5 @@ +// This file is run by Radon IDE to get the fingerprint for the current build +// Change the string to update the fingerprint for the current build, forcing Radon to re-run the build process +// Usually, this should only be necessary when there are native code changes relative to the most recent RadonIDE build + +console.log('2024-11-13') diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index 0ddbbea28ae..3c831c9d9af 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -46,7 +46,7 @@ import { } from 'src/features/widgets/widgets' import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' import { getDatadogEnvironment, getStatsigEnvironmentTier } from 'src/utils/version' -import { flexStyles, useHapticFeedback, useIsDarkMode } from 'ui/src' +import { flexStyles, useIsDarkMode } from 'ui/src' import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner' import { config } from 'uniswap/src/config' import { uniswapUrls } from 'uniswap/src/constants/urls' @@ -83,7 +83,6 @@ import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testne import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient' import { initFirebaseAppCheck } from 'wallet/src/features/appCheck/appCheck' import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' -import { selectHapticsEnabled } from 'wallet/src/features/appearance/slice' import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater' import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext' import { Account } from 'wallet/src/features/wallet/accounts/types' @@ -294,8 +293,6 @@ function AppInner(): JSX.Element { const isDarkMode = useIsDarkMode() const themeSetting = useCurrentAppearanceSetting() const allowAnalytics = useSelector(selectAllowAnalytics) - const hapticsUserEnabled = useSelector(selectHapticsEnabled) - const { setHapticsEnabled } = useHapticFeedback() useTestnetModeForLoggingAndAnalytics() @@ -315,11 +312,6 @@ function AppInner(): JSX.Element { } }, [allowAnalytics]) - // Sets haptics for the UI library based on the user redux setting - useEffect(() => { - setHapticsEnabled(hapticsUserEnabled) - }, [hapticsUserEnabled, setHapticsEnabled]) - useEffect(() => { dispatch(clearNotificationQueue()) // clear all in-app toasts on app start dispatch(syncAppWithDeviceLanguage()) diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx index da4dbcbeed0..333b6baa9f5 100644 --- a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx +++ b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx @@ -5,13 +5,14 @@ import { exploreNavigationRef } from 'src/app/navigation/navigation' import { useAppStackNavigation } from 'src/app/navigation/types' import { closeModal, openModal } from 'src/features/modals/modalSlice' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { ShareableEntity } from 'uniswap/src/types/sharing' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { @@ -26,6 +27,7 @@ import { WalletNavigationProvider, getNavigateToSendFlowArgsInitialState, getNavigateToSwapFlowArgsInitialState, + isNavigateToSwapFlowArgsPartialState, } from 'wallet/src/contexts/WalletNavigationContext' import { getNftUrl, getTokenUrl } from 'wallet/src/utils/linking' @@ -141,13 +143,34 @@ function useNavigateToSend(): (args: NavigateToSendFlowArgs) => void { function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { const dispatch = useDispatch() - const { defaultChainId } = useEnabledChains() + return useCallback( (args: NavigateToSwapFlowArgs): void => { const initialState = getNavigateToSwapFlowArgsInitialState(args, defaultChainId) - dispatch(closeModal({ name: ModalName.Swap })) - dispatch(openModal({ name: ModalName.Swap, initialState })) + + // If no prefilled token, go directly to swap + if (!isNavigateToSwapFlowArgsPartialState(args)) { + dispatch(closeModal({ name: ModalName.Swap })) + dispatch(openModal({ name: ModalName.Swap, initialState })) + return + } + + // Show warning modal for prefilled tokens, which will handle token safety checks + const currencyId = buildCurrencyId(args.currencyChainId, args.currencyAddress) + dispatch( + openModal({ + name: ModalName.TokenWarning, + initialState: { + currencyId, + onAcknowledge: () => { + dispatch(closeModal({ name: ModalName.TokenWarning })) + dispatch(closeModal({ name: ModalName.Swap })) + dispatch(openModal({ name: ModalName.Swap, initialState })) + }, + }, + }), + ) }, [dispatch, defaultChainId], ) diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx index a028e5687d8..5669d6f6929 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -276,7 +276,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme - + diff --git a/apps/mobile/src/app/modals/AppModals.tsx b/apps/mobile/src/app/modals/AppModals.tsx index b7b36041b10..8f07874a1be 100644 --- a/apps/mobile/src/app/modals/AppModals.tsx +++ b/apps/mobile/src/app/modals/AppModals.tsx @@ -9,6 +9,7 @@ import { KoreaCexTransferInfoModal } from 'src/app/modals/KoreaCexTransferInfoMo import { LazyModalRenderer } from 'src/app/modals/LazyModalRenderer' import { SendTokenModal } from 'src/app/modals/SendTokenModal' import { SwapModal } from 'src/app/modals/SwapModal' +import { TokenWarningModalWrapper } from 'src/app/modals/TokenWarningModalWrapper' import { ViewOnlyExplainerModal } from 'src/app/modals/ViewOnlyExplainerModal' import { RemoveWalletModal } from 'src/components/RemoveWallet/RemoveWalletModal' import { WalletConnectModals } from 'src/components/Requests/WalletConnectModals' @@ -114,6 +115,10 @@ export function AppModals(): JSX.Element { + + + + ) } diff --git a/apps/mobile/src/app/modals/SwapModal.tsx b/apps/mobile/src/app/modals/SwapModal.tsx index aace3789088..f23e70d113f 100644 --- a/apps/mobile/src/app/modals/SwapModal.tsx +++ b/apps/mobile/src/app/modals/SwapModal.tsx @@ -5,6 +5,7 @@ import { useBiometricAppSettings, useBiometricPrompt, useOsBiometricAuthEnabled import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' import { useWalletRestore } from 'src/features/wallet/hooks' +import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { updateSwapStartTimestamp } from 'uniswap/src/features/timing/slice' import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/hooks/useSwapPrefilledState' @@ -13,6 +14,7 @@ import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwap export function SwapModal(): JSX.Element { const appDispatch = useDispatch() const { initialState } = useSelector(selectModalState(ModalName.Swap)) + const { hapticFeedback } = useHapticFeedback() const onClose = useCallback((): void => { appDispatch(closeModal({ name: ModalName.Swap })) @@ -38,6 +40,7 @@ export function SwapModal(): JSX.Element { openWalletRestoreModal={openWalletRestoreModal} prefilledState={swapPrefilledState} walletNeedsRestore={Boolean(walletNeedsRestore)} + onSubmitSwap={hapticFeedback.success} onClose={onClose} /> ) diff --git a/apps/mobile/src/app/modals/TokenWarningModalState.ts b/apps/mobile/src/app/modals/TokenWarningModalState.ts new file mode 100644 index 00000000000..9385d9cda0c --- /dev/null +++ b/apps/mobile/src/app/modals/TokenWarningModalState.ts @@ -0,0 +1,4 @@ +export interface TokenWarningModalState { + currencyId: string + onAcknowledge: () => void +} diff --git a/apps/mobile/src/app/modals/TokenWarningModalWrapper.tsx b/apps/mobile/src/app/modals/TokenWarningModalWrapper.tsx new file mode 100644 index 00000000000..e9117ba81e9 --- /dev/null +++ b/apps/mobile/src/app/modals/TokenWarningModalWrapper.tsx @@ -0,0 +1,65 @@ +import { useCallback } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { closeModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { TokenList } from 'uniswap/src/features/dataApi/types' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' +import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/slice/hooks' +import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' +import { currencyIdToAddress, currencyIdToChain, isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' + +export function TokenWarningModalWrapper(): JSX.Element | null { + const dispatch = useDispatch() + const { defaultChainId } = useEnabledChains() + const modalState = useSelector(selectModalState(ModalName.TokenWarning)) + + const { currencyId, onAcknowledge } = modalState.initialState ?? {} + const currencyChainId = (currencyId && currencyIdToChain(currencyId)) || defaultChainId + const currencyAddress = currencyId ? currencyIdToAddress(currencyId) : undefined + const currencyInfo = useCurrencyInfo(currencyId) + + // Get the token info only if we have a valid non-native currency + const isNativeCurrency = isNativeCurrencyAddress(currencyChainId, currencyAddress) + const { tokenWarningDismissed } = useDismissedTokenWarnings( + isNativeCurrency || !currencyAddress ? undefined : { chainId: currencyChainId, address: currencyAddress }, + ) + + const onClose = useCallback(() => { + dispatch(closeModal({ name: ModalName.TokenWarning })) + }, [dispatch]) + + // Return null if modal state is malformed + if (!modalState.isOpen || !modalState.initialState) { + return null + } + + // If no currency info found, skip warning and proceed to SwapFlow + if (!currencyInfo) { + onAcknowledge?.() + onClose() + return null + } + + const safetyLevel = currencyInfo.safetyLevel + const isBlocked = safetyLevel === SafetyLevel.Blocked || currencyInfo.safetyInfo?.tokenList === TokenList.Blocked + + // If token is verified or warning was dismissed and not blocked, skip warning and proceed to SwapFlow + if (!isBlocked && (safetyLevel === SafetyLevel.Verified || tokenWarningDismissed)) { + onAcknowledge?.() + onClose() + return null + } + + return ( + + ) +} diff --git a/apps/mobile/src/app/navigation/NavBar.tsx b/apps/mobile/src/app/navigation/NavBar.tsx index 10fda1e1c38..631af29e3d6 100644 --- a/apps/mobile/src/app/navigation/NavBar.tsx +++ b/apps/mobile/src/app/navigation/NavBar.tsx @@ -15,20 +15,12 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context' import { useDispatch } from 'react-redux' import { pulseAnimation } from 'src/components/buttons/utils' import { openModal } from 'src/features/modals/modalSlice' -import { - Flex, - FlexProps, - LinearGradient, - Text, - TouchableArea, - useHapticFeedback, - useIsDarkMode, - useSporeColors, -} from 'ui/src' +import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback' +import { Flex, FlexProps, LinearGradient, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' import { Search } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { borderRadii, fonts, opacify } from 'ui/src/theme' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -134,8 +126,8 @@ type SwapTabBarButtonProps = { const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96, onSwapLayout }: SwapTabBarButtonProps) { const { t } = useTranslation() const dispatch = useDispatch() - const { hapticFeedback } = useHapticFeedback() const { defaultChainId } = useEnabledChains() + const { hapticFeedback } = useHapticFeedback() const isDarkMode = useIsDarkMode() const activeAccountAddress = useActiveAccountAddressWithThrow() @@ -149,8 +141,8 @@ const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96, onSwapLayout }: Swa }), ) - await hapticFeedback.impact() - }, [dispatch, inputCurrencyId, hapticFeedback, defaultChainId]) + await hapticFeedback.light() + }, [dispatch, inputCurrencyId, defaultChainId, hapticFeedback]) const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }), [scale]) @@ -265,11 +257,7 @@ function ExploreTabBarButton({ activeScale = 0.98, onLayout, isNarrow }: Explore } return ( - + { - const priceHistory = data?.priceHistory?.map((point) => { - return { ...point, value: point.value * conversionRate } - }) + const priceHistory = + data?.priceHistory?.map((point) => { + return { ...point, value: point.value * conversionRate } + }) ?? [] const lastPoint = priceHistory ? priceHistory.length - 1 : 0 diff --git a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx index d8b6fc3bbc3..1a61949a8a7 100644 --- a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx +++ b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx @@ -8,7 +8,7 @@ import { launchImageLibrary } from 'react-native-image-picker' import { FadeIn, FadeOut } from 'react-native-reanimated' import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' -import { Button, Flex, SpinningLoader, Text, TouchableArea, useSporeColors } from 'ui/src' +import { Button, Flex, SpinningLoader, Text, ThemeName, TouchableArea, useSporeColors } from 'ui/src' import CameraScan from 'ui/src/assets/icons/camera-scan.svg' import { Global, PhotoStacked } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' @@ -21,6 +21,7 @@ import { openSettings } from 'wallet/src/utils/linking' type QRCodeScannerProps = { onScanCode: (data: string) => void shouldFreezeCamera: boolean + theme?: ThemeName } interface WCScannerProps extends QRCodeScannerProps { numConnections: number @@ -38,7 +39,7 @@ const SCAN_ICON_MASK_OFFSET_RATIO = 0.02 // used for mask to match spacing in Ca const LOADER_SIZE = iconSizes.icon40 export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.Element { - const { onScanCode, shouldFreezeCamera } = props + const { onScanCode, shouldFreezeCamera, theme } = props const isWalletConnectModal = isWalletConnect(props) const { t } = useTranslation() @@ -120,6 +121,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E const cameraHeight = CAMERA_ASPECT_RATIO * cameraWidth const scannerSize = Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO + const photoSelectBackgroundColor = useSporeColors(theme).surface1 + /** * Resets the camera auto focus to force the camera to refocus by toggling * the auto focus off and on. This allows us to manually let the user refocus @@ -140,7 +143,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E } return ( - + {permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && ( @@ -245,7 +248,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E > (ScannerModalState.ScanQr) const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false) - const { hapticFeedback } = useHapticFeedback() + + const isScanningQr = currentScreenState === ScannerModalState.ScanQr + + const colors = useSporeColors(isScanningQr ? 'dark' : undefined) const onScanCode = async (uri: string): Promise => { - // don't scan any QR codes if camera is frozen if (shouldFreezeCamera) { return } - await hapticFeedback.selection() setShouldFreezeCamera(true) const supportedURI = await getSupportedURI(uri) @@ -60,23 +62,31 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E setCurrentScreenState(ScannerModalState.ScanQr) } } - const isDarkMode = useIsDarkMode() return ( - + {currentScreenState === ScannerModalState.ScanQr && ( - + )} {currentScreenState === ScannerModalState.WalletQr && activeAddress && } @@ -86,7 +96,7 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E ) : ( )} - + {currentScreenState === ScannerModalState.ScanQr ? t('qrScanner.recipient.action.show') : t('qrScanner.recipient.action.scan')} diff --git a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx index 3fbf2b34d21..b2839d6e0ae 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx @@ -29,7 +29,7 @@ function QRScannerIconButton({ onPress }: { onPress: () => void }): JSX.Element const colors = useSporeColors() return ( - + ) diff --git a/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx index 953a211f8ca..a2afdb74344 100644 --- a/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx +++ b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx @@ -55,7 +55,6 @@ export function DappConnectionItem({ > {isEditing ? ( (initialScreenState) @@ -55,13 +55,17 @@ export function WalletConnectModal({ const dispatch = useDispatch() const isUwULinkEnabled = useFeatureFlag(FeatureFlags.UwULink) const isScantasticEnabled = useFeatureFlag(FeatureFlags.Scantastic) - const { hapticFeedback } = useHapticFeedback() const uwuLinkContractAllowlist = useUwuLinkContractAllowlist() const providerManager = useProviderManager() const contractManager = useContractManager() + const isScanningQr = currentScreenState === ScannerModalState.ScanQr + + // We want to always show the QR Code Scanner in "dark mode" + const colors = useSporeColors(isScanningQr ? 'dark' : undefined) + // Update QR scanner states when pending session error alert is shown from WCv2 saga event channel useEffect(() => { if (hasPendingSessionError) { @@ -76,7 +80,6 @@ export function WalletConnectModal({ if (!activeAccount || hasPendingSessionError || shouldFreezeCamera) { return } - await hapticFeedback.selection() const supportedURI = await getSupportedURI(uri, { isUwULinkEnabled, @@ -216,12 +219,11 @@ export function WalletConnectModal({ uwuLinkContractAllowlist, providerManager, contractManager, - hapticFeedback, ], ) const onPressBottomToggle = (): void => { - if (currentScreenState === ScannerModalState.ScanQr) { + if (isScanningQr) { setCurrentScreenState(ScannerModalState.WalletQr) } else { setCurrentScreenState(ScannerModalState.ScanQr) @@ -241,21 +243,28 @@ export function WalletConnectModal({ } return ( - + <> {currentScreenState === ScannerModalState.ConnectedDapps && ( + } sessions={sessions} /> )} - {currentScreenState === ScannerModalState.ScanQr && ( + {isScanningQr && ( - {currentScreenState === ScannerModalState.ScanQr ? ( - + {isScanningQr ? ( + ) : ( - + )} - - {currentScreenState === ScannerModalState.ScanQr - ? t('qrScanner.recipient.action.show') - : t('qrScanner.recipient.action.scan')} + + {isScanningQr ? t('qrScanner.recipient.action.show') : t('qrScanner.recipient.action.scan')} diff --git a/apps/mobile/src/components/TokenDetails/LinkButton.tsx b/apps/mobile/src/components/TokenDetails/LinkButton.tsx index f48e2000966..346aca6bae6 100644 --- a/apps/mobile/src/components/TokenDetails/LinkButton.tsx +++ b/apps/mobile/src/components/TokenDetails/LinkButton.tsx @@ -67,7 +67,6 @@ export function LinkButton({ return ( + diff --git a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx index 8414f18348c..d9209f8886e 100644 --- a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx @@ -7,7 +7,7 @@ import { iconSizes } from 'ui/src/theme' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { InlineNetworkPill } from 'uniswap/src/components/network/NetworkPill' import { AccountType } from 'uniswap/src/features/accounts/types' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -137,7 +137,7 @@ function OtherChainBalance({ return ( - navigate(balance.currencyInfo.currencyId)}> + navigate(balance.currencyInfo.currencyId)}> {currentLanguage !== Language.English && !!translatedDescription && ( - setShowTranslation(!showTranslation)}> + setShowTranslation(!showTranslation)}> {showTranslation ? ( diff --git a/apps/mobile/src/components/Trace/TraceUserProperties.tsx b/apps/mobile/src/components/Trace/TraceUserProperties.tsx index 90ff2e35e55..86d80a272bc 100644 --- a/apps/mobile/src/components/Trace/TraceUserProperties.tsx +++ b/apps/mobile/src/components/Trace/TraceUserProperties.tsx @@ -5,7 +5,7 @@ import { useBiometricAppSettings, useDeviceSupportsBiometricAuth } from 'src/fea import { getAuthMethod } from 'src/features/telemetry/utils' import { getFullAppVersion } from 'src/utils/version' import { useIsDarkMode } from 'ui/src' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' diff --git a/apps/mobile/src/components/accounts/AccountCardItem.tsx b/apps/mobile/src/components/accounts/AccountCardItem.tsx index c7389def1e1..ed6e1aa6884 100644 --- a/apps/mobile/src/components/accounts/AccountCardItem.tsx +++ b/apps/mobile/src/components/accounts/AccountCardItem.tsx @@ -7,7 +7,7 @@ import { navigate } from 'src/app/navigation/rootNavigation' import { NotificationBadge } from 'src/components/notifications/Badge' import { closeModal, openModal } from 'src/features/modals/modalSlice' import { disableOnPress } from 'src/utils/disableOnPress' -import { Flex, Text, TouchableArea, useHapticFeedback } from 'ui/src' +import { Flex, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { pushNotification } from 'uniswap/src/features/notifications/slice' @@ -73,10 +73,8 @@ export function AccountCardItem({ }: AccountCardItemProps): JSX.Element { const { t } = useTranslation() const dispatch = useDispatch() - const { hapticFeedback } = useHapticFeedback() const onPressCopyAddress = async (): Promise => { - await hapticFeedback.impact() await setClipboard(address) dispatch( pushNotification({ @@ -131,7 +129,6 @@ export function AccountCardItem({ }} > => { if (activeAddress) { - await hapticFeedback.impact() await setClipboard(activeAddress) dispatch( pushNotification({ @@ -123,15 +122,12 @@ export function AccountHeader(): JSX.Element { => { if (isDevEnv()) { - await hapticFeedback.selection() dispatch(openModal({ name: ModalName.Experiments })) } }} @@ -155,17 +151,12 @@ export function AccountHeader(): JSX.Element { justifyContent="space-between" testID="account-header/display-name" > - + ) : ( - + {sanitizeAddressText(shortenAddress(activeAddress))} diff --git a/apps/mobile/src/components/accounts/AccountList.test.tsx b/apps/mobile/src/components/accounts/AccountList.test.tsx index 5b4a988ad07..02fea2883d1 100644 --- a/apps/mobile/src/components/accounts/AccountList.test.tsx +++ b/apps/mobile/src/components/accounts/AccountList.test.tsx @@ -4,7 +4,8 @@ import { Locale } from 'uniswap/src/features/language/constants' import { ON_PRESS_EVENT_PAYLOAD, amounts, portfolio } from 'uniswap/src/test/fixtures' import { mockLocalizedFormatter } from 'uniswap/src/test/mocks' import { createArray, queryResolvers } from 'uniswap/src/test/utils' -import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' +import { sanitizeAddressText } from 'uniswap/src/utils/addresses' +import { shortenAddress } from 'utilities/src/addresses' import { NumberType } from 'utilities/src/format/types' import { ACCOUNT, readOnlyAccount, signerMnemonicAccount } from 'wallet/src/test/fixtures' diff --git a/apps/mobile/src/components/buttons/BackButton.tsx b/apps/mobile/src/components/buttons/BackButton.tsx index 9dfb19c7053..74ed7900b14 100644 --- a/apps/mobile/src/components/buttons/BackButton.tsx +++ b/apps/mobile/src/components/buttons/BackButton.tsx @@ -20,7 +20,7 @@ export function BackButton({ onPressBack, size, color, showButtonLabel, ...rest navigation.goBack() } return ( - + ) diff --git a/apps/mobile/src/components/explore/ExploreSections.tsx b/apps/mobile/src/components/explore/ExploreSections.tsx index 4211180848a..99e4ebc6a9b 100644 --- a/apps/mobile/src/components/explore/ExploreSections.tsx +++ b/apps/mobile/src/components/explore/ExploreSections.tsx @@ -20,7 +20,7 @@ import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { NetworkPill } from 'uniswap/src/components/network/NetworkPill' import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base' import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { selectHasFavoriteTokens, selectHasWatchedWallets } from 'uniswap/src/features/favorites/selectors' diff --git a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx index a2b19bab43a..9900cda2381 100644 --- a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx +++ b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx @@ -25,7 +25,7 @@ export function FavoriteHeaderRow({ {isEditing ? editingTitle : title} {!isEditing ? ( - + ) : ( diff --git a/apps/mobile/src/components/explore/FavoriteTokenCard.tsx b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx index c5575cdb297..5fe9c99e5db 100644 --- a/apps/mobile/src/components/explore/FavoriteTokenCard.tsx +++ b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx @@ -9,21 +9,13 @@ import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/compon import { Loader } from 'src/components/loading/loaders' import { disableOnPress } from 'src/utils/disableOnPress' import { usePollOnFocusOnly } from 'src/utils/hooks' -import { - AnimatedTouchableArea, - Flex, - ImpactFeedbackStyle, - Text, - useIsDarkMode, - useShadowPropsShort, - useSporeColors, -} from 'ui/src' +import { AnimatedTouchableArea, Flex, Text, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { borderRadii, imageSizes, opacify } from 'ui/src/theme' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { PollingInterval } from 'uniswap/src/constants/misc' import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import { removeFavoriteToken } from 'uniswap/src/features/favorites/slice' @@ -125,8 +117,6 @@ function FavoriteTokenCard({ borderColor={opacify(0.05, colors.surface3.val)} borderRadius="$rounded16" borderWidth={isDarkMode ? '$none' : '$spacing1'} - hapticFeedback={!isEditing} - hapticStyle={ImpactFeedbackStyle.Light} m="$spacing4" testID={`token-box-${token?.symbol}`} onLongPress={disableOnPress} diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx index 7141ace77e0..7d113b9311f 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx @@ -7,7 +7,8 @@ import * as ensHooks from 'uniswap/src/features/ens/api' import * as unitagHooks from 'uniswap/src/features/unitags/hooks' import { ON_PRESS_EVENT_PAYLOAD, SAMPLE_SEED_ADDRESS_1 } from 'uniswap/src/test/fixtures' import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' +import { sanitizeAddressText } from 'uniswap/src/utils/addresses' +import { shortenAddress } from 'utilities/src/addresses' import { preloadedWalletReducerState, signerMnemonicAccount } from 'wallet/src/test/fixtures' const mockedNavigation = { diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx index 3adc9a69cd3..789e2dd78e7 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx @@ -8,7 +8,7 @@ import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' import RemoveButton from 'src/components/explore/RemoveButton' import { useAnimatedCardDragStyle } from 'src/components/explore/hooks' import { disableOnPress } from 'src/utils/disableOnPress' -import { Flex, ImpactFeedbackStyle, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' +import { Flex, TouchableArea, useIsDarkMode, useShadowPropsShort, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { borderRadii, iconSizes, opacify } from 'ui/src/theme' import { useAvatar } from 'uniswap/src/features/address/avatar' @@ -84,14 +84,12 @@ function FavoriteWalletCard({ {...rest} > - + {overlay} diff --git a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx index d6ad6abc31d..ffcf95732d9 100644 --- a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx +++ b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx @@ -29,7 +29,7 @@ import { AnimatedBottomSheetFlashList } from 'ui/src/components/AnimatedFlashLis import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { diff --git a/apps/mobile/src/components/explore/search/hooks.ts b/apps/mobile/src/components/explore/search/hooks.ts index 5b0fb67b8fd..f03ca673db0 100644 --- a/apps/mobile/src/components/explore/search/hooks.ts +++ b/apps/mobile/src/components/explore/search/hooks.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { ENS_SUFFIX } from 'uniswap/src/features/ens/constants' import { useENS } from 'uniswap/src/features/ens/useENS' diff --git a/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx index dfa5349e225..af560b6ee8e 100644 --- a/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx @@ -7,7 +7,8 @@ import { useENSAvatar, useENSName } from 'uniswap/src/features/ens/api' import { getCompletedENSName } from 'uniswap/src/features/ens/useENS' import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { ENSAddressSearchResult } from 'uniswap/src/features/search/SearchResult' -import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' +import { sanitizeAddressText } from 'uniswap/src/utils/addresses' +import { shortenAddress } from 'utilities/src/addresses' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' type SearchENSAddressItemProps = { diff --git a/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx index cca3e29fb5d..5087f40de5d 100644 --- a/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx @@ -1,15 +1,15 @@ import { default as React } from 'react' import { useDispatch } from 'react-redux' import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' -import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useSporeColors } from 'ui/src' +import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { Arrow } from 'ui/src/components/arrow/Arrow' import { iconSizes } from 'ui/src/theme' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { EtherscanSearchResult } from 'uniswap/src/features/search/SearchResult' import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' import { TestID } from 'uniswap/src/test/fixtures/testIDs' -import { shortenAddress } from 'uniswap/src/utils/addresses' import { ExplorerDataType, getExplorerLink, openUri } from 'uniswap/src/utils/linking' +import { shortenAddress } from 'utilities/src/addresses' type SearchEtherscanItemProps = { etherscanResult: EtherscanSearchResult @@ -35,12 +35,7 @@ export function SearchEtherscanItem({ etherscanResult }: SearchEtherscanItemProp const EtherscanIcon = getBlockExplorerIcon(defaultChainId) return ( - + diff --git a/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx index 6cafa1537d1..42a7146e84c 100644 --- a/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx @@ -1,7 +1,7 @@ import { default as React } from 'react' import { useDispatch } from 'react-redux' import { useAppStackNavigation } from 'src/app/navigation/types' -import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' +import { Flex, Text, TouchableArea } from 'ui/src' import { Verified } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { SearchContext } from 'uniswap/src/features/search/SearchContext' @@ -56,12 +56,7 @@ export function SearchNFTCollectionItem({ collection, searchContext }: NFTCollec } return ( - + = AutoScrollProps & numColumns?: number editable?: boolean animateContainerHeight?: boolean - hapticFeedback?: boolean keyExtractor?: (item: I, index: number) => string onChange?: (e: SortableGridChangeEvent) => void onDragStart?: (e: SortableGridDragStartEvent) => void @@ -33,7 +32,6 @@ export function SortableGrid({ data, renderItem, numColumns = 1, - hapticFeedback, keyExtractor = defaultKeyExtractor, containerRef, ...rest @@ -43,7 +41,6 @@ export function SortableGrid({ const sharedProps = { data, numColumns, - hapticFeedback, keyExtractor: stableKeyExtractor, } @@ -56,7 +53,7 @@ export function SortableGrid({ type SortableGridInnerProps = Pick< SortableGridProps, - 'data' | 'renderItem' | 'numColumns' | 'keyExtractor' | 'hapticFeedback' | 'containerRef' + 'data' | 'renderItem' | 'numColumns' | 'keyExtractor' | 'containerRef' > function SortableGridInner({ @@ -64,13 +61,12 @@ function SortableGridInner({ renderItem, containerRef, numColumns = 1, - hapticFeedback = true, keyExtractor = defaultKeyExtractor, }: SortableGridInnerProps): JSX.Element { const { containerHeight, containerWidth, appliedContainerHeight } = useLayoutContext() const { gridContainerRef, containerStartOffset } = useAutoScrollContext() - useItemOrderUpdater(numColumns, hapticFeedback) + useItemOrderUpdater(numColumns) const handleGridMeasurement = ({ nativeEvent: { diff --git a/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx b/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx index 8d6f19abcdb..f2de8f9d038 100644 --- a/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx +++ b/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx @@ -3,7 +3,6 @@ import { runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'r import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider' import { useStableCallback } from 'src/components/sortableGrid/internal/utils' import { DragContextProviderProps, DragContextType, Vector } from 'src/components/sortableGrid/types' -import { ImpactFeedbackStyle, useHapticFeedback } from 'ui/src' const DragContext = createContext(null) @@ -21,7 +20,6 @@ export function DragContextProvider({ data, itemKeys, editable = true, - hapticFeedback: triggerHapticFeedback = true, activeItemScale: activeItemScaleProp = 1.1, activeItemOpacity: activeItemOpacityProp = 0.7, activeItemShadowOpacity: activeItemShadowOpacityProp = 0.5, @@ -31,7 +29,6 @@ export function DragContextProvider({ keyExtractor, children, }: DragContextProviderProps): JSX.Element { - const { hapticFeedback } = useHapticFeedback() const { keyToIndex } = useLayoutContext() /** * VARIABLES @@ -59,9 +56,6 @@ export function DragContextProvider({ return } const item = data[index] - if (triggerHapticFeedback) { - await hapticFeedback.impact(ImpactFeedbackStyle.Heavy) - } if (!onDragStart || !item) { return } @@ -92,10 +86,6 @@ export function DragContextProvider({ const reorderedData: I[] = [] - if (triggerHapticFeedback) { - await hapticFeedback.impact(ImpactFeedbackStyle.Medium) - } - for (let i = 0; i < data.length; i++) { const item = data[i] if (!item) { diff --git a/apps/mobile/src/components/sortableGrid/internal/SortableGirdProvider.tsx b/apps/mobile/src/components/sortableGrid/internal/SortableGirdProvider.tsx index ed69b775616..5a8f9e56a8e 100644 --- a/apps/mobile/src/components/sortableGrid/internal/SortableGirdProvider.tsx +++ b/apps/mobile/src/components/sortableGrid/internal/SortableGirdProvider.tsx @@ -19,7 +19,6 @@ export function SortableGridProvider({ data, numColumns, editable, - hapticFeedback, animateContainerHeight, activeItemScale, activeItemOpacity, @@ -48,7 +47,6 @@ export function SortableGridProvider({ activeItemShadowOpacity={activeItemShadowOpacity} data={data} editable={editable} - hapticFeedback={hapticFeedback} keyExtractor={keyExtractor} onChange={onChange} onDragStart={onDragStart} diff --git a/apps/mobile/src/components/sortableGrid/internal/hooks.ts b/apps/mobile/src/components/sortableGrid/internal/hooks.ts index 88288214539..6ef911f704d 100644 --- a/apps/mobile/src/components/sortableGrid/internal/hooks.ts +++ b/apps/mobile/src/components/sortableGrid/internal/hooks.ts @@ -1,10 +1,8 @@ -import { useCallback } from 'react' -import { SharedValue, runOnJS, useAnimatedReaction, useSharedValue, withTiming } from 'react-native-reanimated' +import { SharedValue, useAnimatedReaction, useSharedValue, withTiming } from 'react-native-reanimated' import { useAutoScrollContext } from 'src/components/sortableGrid/contexts/AutoScrollContextProvider' import { useDragContext } from 'src/components/sortableGrid/contexts/DragContextProvider' import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider' import { getColumnIndex, getRowIndex } from 'src/components/sortableGrid/internal/utils' -import { ImpactFeedbackStyle, useHapticFeedback } from 'ui/src' export function useItemPosition(key: string): { x: SharedValue @@ -48,15 +46,10 @@ export function useItemPosition(key: string): { return { x, y } } -export function useItemOrderUpdater(numColumns: number, triggerHapticFeedback: boolean): void { +export function useItemOrderUpdater(numColumns: number): void { const { keyToIndex, indexToKey, rowOffsets, targetContainerHeight, itemDimensions } = useLayoutContext() const { activeItemKey, activeItemPosition } = useDragContext() const { scrollOffsetDiff } = useAutoScrollContext() - const { hapticFeedback } = useHapticFeedback() - - const vibrate = useCallback(async () => { - await hapticFeedback.impact(ImpactFeedbackStyle.Light) - }, [hapticFeedback]) useAnimatedReaction( () => ({ @@ -133,11 +126,7 @@ export function useItemOrderUpdater(numColumns: number, triggerHapticFeedback: b ...indexToKey.value.slice(newIndex + 1), ] } - - if (triggerHapticFeedback) { - runOnJS(vibrate)() - } }, - [triggerHapticFeedback], + [], ) } diff --git a/apps/mobile/src/components/sortableGrid/types.ts b/apps/mobile/src/components/sortableGrid/types.ts index f219d67a52f..2a3b535c2ca 100644 --- a/apps/mobile/src/components/sortableGrid/types.ts +++ b/apps/mobile/src/components/sortableGrid/types.ts @@ -73,7 +73,6 @@ export type DragContextProviderProps = PropsWithChildren< data: I[] itemKeys: string[] editable?: boolean - hapticFeedback?: boolean onChange?: (e: SortableGridChangeEvent) => void onDragStart?: (e: SortableGridDragStartEvent) => void onDrop?: (e: SortableGridDropEvent) => void diff --git a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx index dca4771ba76..f34677d1b64 100644 --- a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx @@ -6,11 +6,11 @@ import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context import { useDispatch } from 'react-redux' import { TripleDot } from 'src/components/icons/TripleDot' import { disableOnPress } from 'src/utils/disableOnPress' -import { Flex, TouchableArea, useHapticFeedback } from 'ui/src' +import { Flex, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { pushNotification } from 'uniswap/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types' import { ElementName, WalletEventName } from 'uniswap/src/features/telemetry/constants' @@ -33,21 +33,20 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme const { t } = useTranslation() const dispatch = useDispatch() const { unitag } = useUnitagByAddress(address) - const { hapticFeedback } = useHapticFeedback() const { defaultChainId } = useEnabledChains() const onPressCopyAddress = useCallback(async () => { if (!address) { return } - await hapticFeedback.impact() + await setClipboard(address) dispatch(pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address })) sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { element: ElementName.CopyAddress, screen: MobileScreens.ExternalProfile, }) - }, [address, dispatch, hapticFeedback]) + }, [address, dispatch]) const openExplorerLink = useCallback(async () => { await openUri(getExplorerLink(defaultChainId, address, ExplorerDataType.ADDRESS)) diff --git a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx index 37b0177ca7d..a808c3717d8 100644 --- a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx @@ -213,7 +213,6 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea + void +type OnChangeAmount = (amount: string, newIsTokenInputMode?: boolean) => void function OnRampError({ errorText, color }: { errorText?: string; color: ColorTokens }): JSX.Element { return ( @@ -53,7 +47,9 @@ interface FiatOnRampAmountSectionProps { currency: FiatOnRampCurrency onEnterAmount: OnChangeAmount onChoosePredifendAmount: OnChangeAmount + onToggleIsTokenInputMode: () => void quoteAmount: number + sourceAmount: number quoteCurrencyAmountReady: boolean selectTokenLoading: boolean onTokenSelectorPress: () => void @@ -78,11 +74,13 @@ export const FiatOnRampAmountSection = forwardRef(null) @@ -137,12 +136,11 @@ export const FiatOnRampAmountSection = forwardRef { async function shake(): Promise { triggerShakeAnimation() - await hapticFeedback.impact() } if (errorText && prevErrorText !== errorText) { shake().catch(() => undefined) } - }, [errorText, inputShakeX, prevErrorText, triggerShakeAnimation, hapticFeedback]) + }, [errorText, inputShakeX, prevErrorText, triggerShakeAnimation]) // Design has asked to make it around 100ms and DEFAULT_DELAY is 200ms const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2) @@ -157,14 +155,17 @@ export const FiatOnRampAmountSection = forwardRef - + - {fiatCurrencyInfo.symbol} + {isTokenInputMode ? currency.currencyInfo?.currency.symbol : fiatCurrencyInfo.symbol} ) : ( - + - {formattedCurrencyAmount} - {currency.currencyInfo?.currency.symbol} + {formattedDerivedAmount} - {/* TODO: support switching from fiat to token amounts */} - {/* */} + )} @@ -311,7 +314,6 @@ function PredefinedAmount({ currencyCode: fiatCurrencyInfo.code, currencySymbol: fiatCurrencyInfo.symbol, }) - const { hapticFeedback } = useHapticFeedback() const highlighted = currentAmount === amount.toString() @@ -319,7 +321,6 @@ function PredefinedAmount({ => { - await hapticFeedback.impact() onPress(amount.toString()) }} > diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampContext.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampContext.tsx index 2a716943874..44996395c0f 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampContext.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampContext.tsx @@ -27,10 +27,14 @@ interface FiatOnRampContextType { quoteCurrency: FiatOnRampCurrency defaultCurrency: FiatOnRampCurrency setQuoteCurrency: (quoteCurrency: FiatOnRampCurrency) => void - amount?: number - setAmount: (amount: number | undefined) => void + fiatAmount: number | undefined + tokenAmount: number | undefined + setFiatAmount: (fiatAmount: number | undefined) => void + setTokenAmount: (tokenAmount: number | undefined) => void isOffRamp: boolean setIsOffRamp: (isOffRamp: boolean) => void + isTokenInputMode: boolean + setIsTokenInputMode: React.Dispatch> } const initialState: FiatOnRampContextType = { @@ -40,13 +44,18 @@ const initialState: FiatOnRampContextType = { setCountryState: () => undefined, setBaseCurrencyInfo: () => undefined, setQuoteCurrency: () => undefined, - setAmount: () => undefined, + setFiatAmount: () => undefined, + setTokenAmount: () => undefined, + fiatAmount: undefined, + tokenAmount: undefined, countryCode: '', countryState: undefined, quoteCurrency: { currencyInfo: undefined }, defaultCurrency: { currencyInfo: undefined }, isOffRamp: false, setIsOffRamp: () => undefined, + isTokenInputMode: false, + setIsTokenInputMode: () => undefined, } const FiatOnRampContext = createContext(initialState) @@ -61,8 +70,10 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): const [countryCode, setCountryCode] = useState(getCountry()) const [countryState, setCountryState] = useState() const [baseCurrencyInfo, setBaseCurrencyInfo] = useState() - const [amount, setAmount] = useState() const [isOffRamp, setIsOffRamp] = useState(false) + const [isTokenInputMode, setIsTokenInputMode] = useState(false) + const [fiatAmount, setFiatAmount] = useState() + const [tokenAmount, setTokenAmount] = useState() const { initialState: initialModalState } = useSelector(selectModalState(ModalName.FiatOnRampAggregator)) const prefilledCurrency = initialModalState?.prefilledCurrency @@ -93,10 +104,14 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): quoteCurrency, defaultCurrency, setQuoteCurrency, - amount, - setAmount, + fiatAmount, + setFiatAmount, + tokenAmount, + setTokenAmount, isOffRamp, setIsOffRamp, + isTokenInputMode, + setIsTokenInputMode, }} > {children} diff --git a/apps/mobile/src/features/modals/ModalsState.ts b/apps/mobile/src/features/modals/ModalsState.ts index b13b4d7dabe..3b2414a7013 100644 --- a/apps/mobile/src/features/modals/ModalsState.ts +++ b/apps/mobile/src/features/modals/ModalsState.ts @@ -1,4 +1,5 @@ import { ExploreModalState } from 'src/app/modals/ExploreModalState' +import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { FiatOnRampModalState } from 'src/screens/FiatOnRampModalState' @@ -13,6 +14,7 @@ export interface AppModalState { isOpen: boolean initialState?: T } + export interface ModalsState { [ModalName.AccountSwitcher]: AppModalState [ModalName.BackupReminder]: AppModalState @@ -39,4 +41,5 @@ export interface ModalsState { }> [ModalName.ViewOnlyExplainer]: AppModalState [ModalName.WalletConnectScan]: AppModalState + [ModalName.TokenWarning]: AppModalState } diff --git a/apps/mobile/src/features/modals/modalSlice.ts b/apps/mobile/src/features/modals/modalSlice.ts index 28790687edc..4015fe3bdab 100644 --- a/apps/mobile/src/features/modals/modalSlice.ts +++ b/apps/mobile/src/features/modals/modalSlice.ts @@ -1,5 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { ExploreModalState } from 'src/app/modals/ExploreModalState' +import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState' import { ModalsState } from 'src/features/modals/ModalsState' @@ -95,6 +96,11 @@ type BackupWarningParams = { initialState?: undefined } +type TokenWarningParams = { + name: typeof ModalName.TokenWarning + initialState?: TokenWarningModalState +} + export type OpenModalParams = | AccountSwitcherModalParams | BackupReminderParams @@ -115,6 +121,7 @@ export type OpenModalParams = | RestoreWalletModalParams | UnitagsIntroParams | ViewOnlyExplainerParams + | TokenWarningParams export type CloseModalParams = { name: keyof ModalsState } diff --git a/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx index 8a83ff92dae..b928a0e47f9 100644 --- a/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx +++ b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx @@ -106,7 +106,6 @@ export function NFTCollectionContextMenu({ }} > + { }) } -export const promptPushPermission = (successCallback?: () => void, failureCallback?: () => void): void => { - OneSignal.promptForPushNotificationsWithUserResponse((response) => { - logger.debug('Onesignal', 'promptForPushNotificationsWithUserResponse', `Prompt response: ${response}`) - if (response) { - successCallback?.() - } else { - failureCallback?.() - } +export const promptPushPermission = async (): Promise => { + return new Promise((resolve) => { + OneSignal.promptForPushNotificationsWithUserResponse((response) => { + logger.debug('Onesignal', 'promptForPushNotificationsWithUserResponse', `Prompt response: ${response}`) + resolve(response) + }) }) } diff --git a/apps/mobile/src/features/onboarding/OptionCard.tsx b/apps/mobile/src/features/onboarding/OptionCard.tsx index 3c23b69235c..6c64140a6ef 100644 --- a/apps/mobile/src/features/onboarding/OptionCard.tsx +++ b/apps/mobile/src/features/onboarding/OptionCard.tsx @@ -14,7 +14,6 @@ export function OptionCard({ disabled, opacity, badgeText, - hapticFeedback, testID, }: { title: string @@ -26,7 +25,6 @@ export function OptionCard({ disabled?: boolean opacity?: number badgeText?: string | undefined - hapticFeedback?: boolean | undefined }): JSX.Element { const isDarkMode = useIsDarkMode() @@ -38,7 +36,6 @@ export function OptionCard({ borderRadius="$rounded20" borderWidth={1} disabled={disabled} - hapticFeedback={hapticFeedback} opacity={disabled ? 0.5 : opacity} p="$spacing16" testID={testID} diff --git a/apps/mobile/src/features/openai/OpenAIContext.tsx b/apps/mobile/src/features/openai/OpenAIContext.tsx index 378141e206b..bf338d4e04c 100644 --- a/apps/mobile/src/features/openai/OpenAIContext.tsx +++ b/apps/mobile/src/features/openai/OpenAIContext.tsx @@ -16,7 +16,7 @@ import { } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/features/chains/chainInfo' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getChainLabel, toSupportedChainId } from 'uniswap/src/features/chains/utils' import { usePortfolioBalances, useTokenBalancesGroupedByVisibility } from 'uniswap/src/features/dataApi/balances' diff --git a/apps/mobile/src/features/send/SendReviewScreen.tsx b/apps/mobile/src/features/send/SendReviewScreen.tsx index 8b50c774b2e..f1e6937ba06 100644 --- a/apps/mobile/src/features/send/SendReviewScreen.tsx +++ b/apps/mobile/src/features/send/SendReviewScreen.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react' import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants' +import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback' import { Flex } from 'ui/src/components/layout/Flex' import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/TransactionModal/TransactionModal' import { useTransactionModalContext } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext' @@ -18,6 +19,8 @@ export function SendReviewScreen(): JSX.Element { function SendReviewScreenContent({ hideContent }: { hideContent: boolean }): JSX.Element { const { bottomSheetViewStyles, renderBiometricsIcon, onClose, authTrigger } = useTransactionModalContext() + const { hapticFeedback } = useHapticFeedback() + // Same logic we apply in `SwapReviewScreen` // We forcefully hide the content via `hideContent` to allow the bottom sheet to animate faster while still allowing all API requests to trigger ASAP. // The value of `height + mb` must be equal to the height of the fully rendered component to avoid the modal jumping on open. @@ -31,6 +34,7 @@ function SendReviewScreenContent({ hideContent }: { hideContent: boolean }): JSX ButtonAuthIcon={renderBiometricsIcon?.({ color: 'white' })} authTrigger={authTrigger} onCloseModal={onClose} + onSubmitSend={hapticFeedback.success} /> ) diff --git a/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx index 361ab7b1dc6..821ae8410ee 100644 --- a/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx +++ b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx @@ -104,7 +104,6 @@ export function UnitagConfirmationScreen({ {elementsToAnimate.map(({ element, coordinates }, index) => ( ))} - + diff --git a/apps/mobile/src/features/walletConnect/utils.ts b/apps/mobile/src/features/walletConnect/utils.ts index fb40349715b..9a60431af92 100644 --- a/apps/mobile/src/features/walletConnect/utils.ts +++ b/apps/mobile/src/features/walletConnect/utils.ts @@ -159,11 +159,22 @@ export const parseTransactionRequest = ( } } -export function decodeMessage(value: string): string { - if (utils.isHexString(value)) { - return utils.toUtf8String(value) +function isUtf8(str: string): boolean { + try { + new TextDecoder('utf-8', { fatal: true }).decode(new TextEncoder().encode(str)) + return true + } catch { + return false } +} +export function decodeMessage(value: string): string { + if (utils.isHexString(value) && isUtf8(value)) { + const decoded = utils.toUtf8String(value) + if (decoded?.trim()) { + return decoded + } + } return value } diff --git a/apps/mobile/src/screens/ExploreScreen.tsx b/apps/mobile/src/screens/ExploreScreen.tsx index 8624b87dbc3..710ed9d5ccf 100644 --- a/apps/mobile/src/screens/ExploreScreen.tsx +++ b/apps/mobile/src/screens/ExploreScreen.tsx @@ -15,7 +15,7 @@ import { Flex, flexStyles } from 'ui/src' import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext' import { HandleBar } from 'uniswap/src/components/modals/HandleBar' import { NetworkFilter } from 'uniswap/src/components/network/NetworkFilter' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CancelBehaviorType, SearchTextInput } from 'uniswap/src/features/search/SearchTextInput' import { MobileEventName, ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' diff --git a/apps/mobile/src/screens/FiatOnRampConnecting.tsx b/apps/mobile/src/screens/FiatOnRampConnecting.tsx index 28e4bada0ec..0d7cd604d44 100644 --- a/apps/mobile/src/screens/FiatOnRampConnecting.tsx +++ b/apps/mobile/src/screens/FiatOnRampConnecting.tsx @@ -49,7 +49,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | countryState, baseCurrencyInfo, quoteCurrency, - amount, + fiatAmount, } = useFiatOnRampContext() const serviceProvider = selectedQuote?.serviceProviderDetails @@ -75,12 +75,12 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | isLoading: widgetLoading, error: widgetError, } = useFiatOnRampAggregatorWidgetQuery( - !isOffRamp && serviceProvider && quoteCurrency.meldCurrencyCode && baseCurrencyInfo && amount + !isOffRamp && serviceProvider && quoteCurrency.meldCurrencyCode && baseCurrencyInfo && fiatAmount ? { serviceProvider: serviceProvider.serviceProvider, countryCode, destinationCurrencyCode: quoteCurrency.meldCurrencyCode, - sourceAmount: amount, + sourceAmount: fiatAmount, sourceCurrencyCode: baseCurrencyInfo.code, walletAddress: activeAccountAddress, externalSessionId: externalTransactionId, @@ -95,12 +95,12 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | isLoading: offRampWidgetLoading, error: offRampWidgetError, } = useFiatOnRampAggregatorOffRampWidgetQuery( - isOffRamp && serviceProvider && quoteCurrency.meldCurrencyCode && baseCurrencyInfo && amount + isOffRamp && serviceProvider && quoteCurrency.meldCurrencyCode && baseCurrencyInfo && fiatAmount ? { serviceProvider: serviceProvider.serviceProvider, countryCode, baseCurrencyCode: quoteCurrency.meldCurrencyCode, - sourceAmount: amount, + sourceAmount: fiatAmount, quoteCurrencyCode: baseCurrencyInfo.code, refundWalletAddress: activeAccountAddress, externalCustomerId: activeAccountAddress, @@ -177,7 +177,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | <> { + if (!isOffRamp) { + return false + } + + if (isTokenInputMode) { + if (tokenAmount && tokenAmount > (portfolioBalance?.quantity || 0)) { + return true + } + } else { + if (fiatAmount && fiatAmount > (portfolioBalance?.balanceUSD || 0)) { + return true + } + } + + return false + }, [fiatAmount, tokenAmount, isOffRamp, portfolioBalance, isTokenInputMode]) + + useUSDTokenUpdater({ + isFiatInput: !isTokenInputMode, + exactAmountToken: tokenAmount ? tokenAmount.toString() : '', + exactAmountFiat: fiatAmount ? fiatAmount.toString() : '', + onFiatAmountUpdated: (amount: string) => { + setFiatAmount(parseFloat(amount)) + }, + onTokenAmountUpdated: (amount: string) => { + setTokenAmount(parseFloat(amount)) + }, + currency: quoteCurrency.currencyInfo?.currency, + }) + const { error: quotesError, loading: quotesLoading, quotes, } = useFiatOnRampQuotes({ - baseCurrencyAmount: debouncedAmount, + baseCurrencyAmount: isOffRamp ? debouncedTokenAmount : debouncedFiatAmount, baseCurrencyCode: meldSupportedFiatCurrency.code, quoteCurrencyCode: quoteCurrency.meldCurrencyCode, countryCode, countryState, rampDirection: isOffRamp ? RampDirection.OFFRAMP : RampDirection.ONRAMP, + balanceError: exceedsBalanceError, }) useEffect(() => { @@ -160,7 +208,14 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { } }, [quotesLoading, setSelectedQuote]) - const selectTokenLoading = quotesLoading || amount !== debouncedAmount || (!selectedQuote && !quotesError) + const debouncedAmountsMatch = isTokenInputMode + ? tokenAmount === debouncedTokenAmount + : fiatAmount === debouncedFiatAmount + + // always eforce the amount used in the request to backend service + const hasValidAmount = isOffRamp ? !!tokenAmount : !!fiatAmount + + const selectTokenLoading = hasValidAmount && (quotesLoading || !debouncedAmountsMatch) && !exceedsBalanceError const { useFiatOnRampAggregatorGetCountryQuery } = getFiatOnRampAggregatorApi() const { currentData: ipCountryData } = useFiatOnRampAggregatorGetCountryQuery() @@ -200,11 +255,11 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { }, [prevQuotes, quotes, selectedQuote, setQuotesSections, setSelectedQuote, t]) useEffect(() => { - if (!quotes && (quotesError || !amount)) { + if (!quotes && (quotesError || !fiatAmount)) { setQuotesSections(undefined) setSelectedQuote(undefined) } - }, [amount, quotesError, quotes, setQuotesSections, setSelectedQuote]) + }, [quotesError, quotes, setQuotesSections, setSelectedQuote, fiatAmount]) const onSelectCountry: ComponentProps['onSelectCountry'] = (country): void => { dispatch( @@ -222,32 +277,76 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { const fiatToUSDConverter = useLocalFiatToUSDConverter() - const onChangeValue = - (source: FORAmountEnteredProperties['source']) => - (newAmount: string): void => { - amountUpdatedTimeRef.current = Date.now() - sendAnalyticsEvent( - isOffRamp ? FiatOffRampEventName.FiatOffRampAmountEntered : FiatOnRampEventName.FiatOnRampAmountEntered, - { - source, - amountUSD: fiatToUSDConverter(parseFloat(newAmount)), - }, - ) - const truncatedValue = truncateToMaxDecimals({ - value: newAmount, - maxDecimals: MAX_FIAT_INPUT_DECIMALS, - }) + const tokenMaxDecimals = quoteCurrency.currencyInfo?.currency.decimals + + const onChangeValue = ( + newAmount: string, + source: FORAmountEnteredProperties['source'], + newIsTokenInputMode?: boolean, + ): void => { + amountUpdatedTimeRef.current = Date.now() + sendAnalyticsEvent( + isOffRamp ? FiatOffRampEventName.FiatOffRampAmountEntered : FiatOnRampEventName.FiatOnRampAmountEntered, + { + source, + amountUSD: fiatToUSDConverter(parseFloat(newAmount)), + }, + ) - valueRef.current = truncatedValue - setValue(truncatedValue) - setAmount(truncatedValue ? parseFloat(truncatedValue) : 0) - // if user did not use Decimal Pad to enter value - if (source !== 'textInput') { - resetSelection({ start: valueRef.current.length, end: valueRef.current.length }) - } - decimalPadRef.current?.updateDisabledKeys() + const currentIsTokenInputMode = newIsTokenInputMode !== undefined ? newIsTokenInputMode : isTokenInputMode + + const maxDecimals = currentIsTokenInputMode ? tokenMaxDecimals : MAX_FIAT_INPUT_DECIMALS + + // Appease TS this should not happen + if (maxDecimals === undefined) { + return } + const truncatedValue = truncateToMaxDecimals({ + value: newAmount, + maxDecimals, + }) + + valueRef.current = truncatedValue + setValue(truncatedValue) + + if (currentIsTokenInputMode) { + setTokenAmount(truncatedValue ? parseFloat(truncatedValue) : 0) + } else { + setFiatAmount(truncatedValue ? parseFloat(truncatedValue) : 0) + } + + // if user did not use Decimal Pad to enter value + if (source !== 'textInput') { + resetSelection({ start: valueRef.current.length, end: valueRef.current.length }) + } + decimalPadRef.current?.updateDisabledKeys() + + if (newIsTokenInputMode !== undefined && newIsTokenInputMode !== isTokenInputMode) { + setIsTokenInputMode(newIsTokenInputMode) + } + } + + const onToggleIsTokenInputMode = useCallback(() => { + const { sourceAmount, destinationAmount } = selectedQuote ?? {} + + // Use the exact amounts from the backend so that the newly populated amount is exactly what the quote returns + const fiatAmountFromQuote = isOffRamp ? destinationAmount : sourceAmount + const tokenAmountFromQuote = isOffRamp ? sourceAmount : destinationAmount + const newAmount = (isTokenInputMode ? fiatAmountFromQuote : tokenAmountFromQuote)?.toString() ?? '' + + // update values + valueRef.current = newAmount + setValue(newAmount) + + // update cursor position and decimal pad disabled keys + resetSelection({ start: valueRef.current.length, end: valueRef.current.length }) + decimalPadRef.current?.updateDisabledKeys() + + // toggle input mode + setIsTokenInputMode((prev) => !prev) + }, [isOffRamp, isTokenInputMode, resetSelection, selectedQuote, setIsTokenInputMode]) + const onContinue = (): void => { if (quotes && quoteCurrency?.currencyInfo?.currency) { setBaseCurrencyInfo(meldSupportedFiatCurrency) @@ -296,6 +395,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { const { errorText } = useParseFiatOnRampError( !notAvailableInThisRegion && quotesError, meldSupportedFiatCurrency.code, + exceedsBalanceError, ) const onSelectionChange = useCallback( @@ -314,14 +414,6 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { [amountUpdatedTimeRef], ) - const activeAccount = useActiveAccountWithThrow() - const { data: balancesById } = usePortfolioBalances({ address: activeAccount.address }) - const portfolioBalance = quoteCurrency.currencyInfo && balancesById?.[quoteCurrency.currencyInfo.currencyId] - const formattedAmount = useFormatExactCurrencyAmount( - portfolioBalance?.quantity.toString() || '0', - quoteCurrency.currencyInfo?.currency, - ) - const { navigateToSwapFlow } = useWalletNavigation() const onAcceptUnsupportedTokenSwap = useCallback(() => { setShowUnsupportedTokenModal(false) @@ -339,7 +431,8 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { setIsOffRamp(option === RampToggle.SELL) setValue('') - setAmount(0) + setFiatAmount(0) + setTokenAmount(0) valueRef.current = '' resetSelection({ start: 0 }) setQuoteCurrency(defaultCurrency) @@ -351,7 +444,11 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { // we only show loading when there are no errors and quote value is not empty const buttonDisabled = - notAvailableInThisRegion || selectTokenLoading || !!quotesError || !selectedQuote?.destinationAmount + notAvailableInThisRegion || + selectTokenLoading || + !!quotesError || + !selectedQuote?.destinationAmount || + exceedsBalanceError return ( @@ -402,11 +499,17 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { notAvailableInThisRegion={notAvailableInThisRegion} predefinedAmountsSupported={predefinedAmountsSupported} quoteAmount={selectedQuote?.destinationAmount ?? 0} - quoteCurrencyAmountReady={Boolean(amount && selectedQuote)} + sourceAmount={selectedQuote?.sourceAmount ?? 0} + quoteCurrencyAmountReady={Boolean(fiatAmount && selectedQuote)} selectTokenLoading={selectTokenLoading} value={value} - onChoosePredifendAmount={onChangeValue('chip')} - onEnterAmount={onChangeValue('textInput')} + onChoosePredifendAmount={(amount: string): void => { + onChangeValue(amount, 'chip', false) + }} + onEnterAmount={(amount: string, newIsTokenInputMode?: boolean): void => { + onChangeValue(amount, 'textInput', newIsTokenInputMode) + }} + onToggleIsTokenInputMode={onToggleIsTokenInputMode} onSelectionChange={onSelectionChange} onTokenSelectorPress={(): void => { setShowTokenSelector(true) @@ -440,10 +543,12 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { { + onChangeValue(newValue, 'textInput') + }} valueRef={valueRef} onReady={onDecimalPadReady} onTriggerInputShakeAnimation={onDecimalPadTriggerInputShake} diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index f59ce9367e5..52691d9e663 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -47,9 +47,10 @@ import { AIAssistantOverlay } from 'src/features/openai/AIAssistantOverlay' import { useWalletRestore } from 'src/features/wallet/hooks' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback' import { hideSplashScreen } from 'src/utils/splashScreen' import { useOpenBackupReminderModal } from 'src/utils/useOpenBackupReminderModal' -import { Flex, Text, TouchableArea, useHapticFeedback, useMedia, useSporeColors } from 'ui/src' +import { Flex, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src' import ReceiveIcon from 'ui/src/assets/icons/arrow-down-circle.svg' import BuyIcon from 'ui/src/assets/icons/buy.svg' import ScanIcon from 'ui/src/assets/icons/scan-home.svg' @@ -58,7 +59,7 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { iconSizes, spacing } from 'ui/src/theme' import { AccountType } from 'uniswap/src/features/accounts/types' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { usePortfolioBalances } from 'uniswap/src/features/dataApi/balances' import { useCexTransferProviders } from 'uniswap/src/features/fiatOnRamp/useCexTransferProviders' import { FeatureFlags } from 'uniswap/src/features/gating/flags' @@ -108,7 +109,6 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. const isFocused = useIsFocused() const isModalOpen = useSelector(selectSomeModalOpen) const isHomeScreenBlur = !isFocused || isModalOpen - const { hapticFeedback } = useHapticFeedback() const showFeedTab = useFeatureFlag(FeatureFlags.FeedTab) @@ -347,14 +347,23 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. const cexTransferProviders = useCexTransferProviders() const { isTestnetModeEnabled } = useEnabledChains() + const { hapticFeedback } = useHapticFeedback() + + const triggerHaptics = useCallback(async () => { + await hapticFeedback.light() + }, [hapticFeedback]) - const onPressScan = useCallback(() => { + const onPressScan = useCallback(async () => { // in case we received a pending session from a previous scan after closing modal dispatch(removePendingSession()) dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr })) - }, [dispatch]) - const onPressSend = useCallback(() => dispatch(openModal({ name: ModalName.Send })), [dispatch]) - const onPressReceive = useCallback(() => { + await triggerHaptics() + }, [dispatch, triggerHaptics]) + const onPressSend = useCallback(async () => { + dispatch(openModal({ name: ModalName.Send })) + await triggerHaptics() + }, [dispatch, triggerHaptics]) + const onPressReceive = useCallback(async () => { dispatch( openModal( cexTransferProviders.length > 0 @@ -362,7 +371,8 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. : { name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }, ), ) - }, [dispatch, cexTransferProviders]) + await triggerHaptics() + }, [dispatch, cexTransferProviders, triggerHaptics]) const onPressViewOnlyLabel = useCallback(() => dispatch(openModal({ name: ModalName.ViewOnlyExplainer })), [dispatch]) // Hide actions when active account isn't a signer account. @@ -379,7 +389,8 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. setIsTestnetWarningModalOpen(false) }, []) - const onPressBuy = useCallback((): void => { + const onPressBuy = useCallback(async (): Promise => { + await triggerHaptics() if (isTestnetModeEnabled) { setIsTestnetWarningModalOpen(true) return @@ -389,7 +400,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. name: disableForKorea ? ModalName.KoreaCexTransferInfoModal : ModalName.FiatOnRampAggregator, }), ) - }, [dispatch, isTestnetModeEnabled, disableForKorea]) + }, [dispatch, isTestnetModeEnabled, disableForKorea, triggerHaptics]) const actions = useMemo( (): QuickAction[] => [ @@ -464,7 +475,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. {isSignerAccount ? ( ) : ( - + {viewOnlyLabel} @@ -580,9 +591,6 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. }, ]} tabStyle={style} - onTabPress={async (): Promise => { - await hapticFeedback.impact() - }} /> )} @@ -600,7 +608,6 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. routes, tabBarStyle, tabIndex, - hapticFeedback, ], ) @@ -820,7 +827,7 @@ function ActionButton({ return ( - + ( diff --git a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx index 7d3ba4d246f..9742822695d 100644 --- a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx @@ -78,7 +78,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS {importableAccounts?.map((account, i) => { const { address, balance } = account return ( - + ) : ( - onPressItem(item)} - > + onPressItem(item)}> => { await setClipboardImage(imageUrl) - await hapticFeedback.impact() + dispatch( pushNotification({ type: AppNotificationType.Copied, diff --git a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx index 3538577e10c..717e54e1640 100644 --- a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx @@ -9,9 +9,9 @@ import { Screen } from 'src/components/layout/Screen' import { openModal } from 'src/features/modals/modalSlice' import { TermsOfService } from 'src/screens/Onboarding/TermsOfService' import { hideSplashScreen } from 'src/utils/splashScreen' -import { Flex, Text, TouchableArea, useHapticFeedback } from 'ui/src' +import { Flex, Text, TouchableArea } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' @@ -32,7 +32,6 @@ type Props = NativeStackScreenProps => { if (isDevEnv()) { - await hapticFeedback.selection() dispatch(openModal({ name: ModalName.Experiments })) } }} diff --git a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx index dd26e1b5dac..6411551af9f 100644 --- a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx @@ -58,9 +58,13 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop }, [deviceSupportsBiometrics, hasSeedPhrase, isBiometricAuthEnabled, navigation, onCompleteOnboarding, params]) const onPressEnableNotifications = useCallback(async () => { - promptPushPermission(() => { + const arePushNotificationsEnabled = await promptPushPermission() + + if (arePushNotificationsEnabled) { enableNotifications() - }, showNotificationSettingsAlert) + } else { + showNotificationSettingsAlert() + } await navigateToNextScreen() }, [enableNotifications, navigateToNextScreen]) diff --git a/apps/mobile/src/screens/ReceiveCryptoModal.tsx b/apps/mobile/src/screens/ReceiveCryptoModal.tsx index 2f933f8900f..8c16f25e586 100644 --- a/apps/mobile/src/screens/ReceiveCryptoModal.tsx +++ b/apps/mobile/src/screens/ReceiveCryptoModal.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import { ServiceProviderSelector } from 'src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector' import { closeModal, openModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' -import { Flex, ImpactFeedbackStyle, Separator, Text, TouchableArea, useHapticFeedback, useSporeColors } from 'ui/src' +import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' import { CopySheets, QrCode } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { Modal } from 'uniswap/src/components/modals/Modal' @@ -24,10 +24,8 @@ const ICON_BORDER_RADIUS = 100 function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element { const dispatch = useDispatch() const activeAccountAddress = useActiveAccountAddressWithThrow() - const { hapticFeedback } = useHapticFeedback() const onPressCopyAddress = async (): Promise => { - await hapticFeedback.impact() await setClipboard(activeAccountAddress) dispatch( pushNotification({ @@ -47,7 +45,7 @@ function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element { } return ( - + void }): JSX.Element { /> - + { setTimeout(() => { - dispatch(setHapticsUserSettingEnabled(!hapticsUserEnabled)) + setHapticsEnabled(!hapticsEnabled) }, AVOID_RENDER_DURING_ANIMATION_MS) - }, [dispatch, hapticsUserEnabled]) + }, [setHapticsEnabled, hapticsEnabled]) const [isTestnetModalOpen, setIsTestnetModalOpen] = useState(false) const { isTestnetModeEnabled } = useEnabledChains() @@ -195,7 +197,7 @@ export function SettingsScreen(): JSX.Element { { text: t('settings.setting.hapticTouch.title'), icon: , - isToggleEnabled: hapticsUserEnabled, + isToggleEnabled: hapticsEnabled, onToggle: onToggleEnableHaptics, }, { @@ -325,7 +327,7 @@ export function SettingsScreen(): JSX.Element { onToggleHideSmallBalances, hideSpamTokens, onToggleHideSpamTokens, - hapticsUserEnabled, + hapticsEnabled, onToggleEnableHaptics, noSignerAccountImported, deviceSupportsBiometrics, diff --git a/apps/mobile/src/screens/SettingsWallet.tsx b/apps/mobile/src/screens/SettingsWallet.tsx index 941afc19cbc..f277caad24a 100644 --- a/apps/mobile/src/screens/SettingsWallet.tsx +++ b/apps/mobile/src/screens/SettingsWallet.tsx @@ -32,7 +32,7 @@ import GlobalIcon from 'ui/src/assets/icons/global.svg' import TextEditIcon from 'ui/src/assets/icons/textEdit.svg' import { iconSizes, spacing } from 'ui/src/theme' import { AccountType } from 'uniswap/src/features/accounts/types' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useENS } from 'uniswap/src/features/ens/useENS' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -88,8 +88,9 @@ export function SettingsWallet({ ), ) - const onChangeNotificationSettings = (enabled: boolean): void => { + const onChangeNotificationSettings = async (enabled: boolean): Promise => { sendAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled }) + if (notificationOSPermission === NotificationPermission.Enabled) { dispatch( editAccountActions.trigger({ @@ -100,7 +101,9 @@ export function SettingsWallet({ ) setNotificationSwitchEnabled(enabled) } else { - promptPushPermission(() => { + const arePushNotificationsEnabled = await promptPushPermission() + + if (arePushNotificationsEnabled) { dispatch( editAccountActions.trigger({ type: EditAccountAction.TogglePushNotification, @@ -109,7 +112,9 @@ export function SettingsWallet({ }), ) setNotificationSwitchEnabled(enabled) - }, showNotificationSettingsAlert) + } else { + showNotificationSettingsAlert() + } } } diff --git a/apps/mobile/src/screens/SettingsWalletEdit.tsx b/apps/mobile/src/screens/SettingsWalletEdit.tsx index 164af3e5b7b..55a2c581863 100644 --- a/apps/mobile/src/screens/SettingsWalletEdit.tsx +++ b/apps/mobile/src/screens/SettingsWalletEdit.tsx @@ -132,7 +132,6 @@ export function SettingsWalletEdit({ {showUnitagBanner && } + + + + + + ) +} diff --git a/apps/web/src/components/Charts/LoadingState.tsx b/apps/web/src/components/Charts/LoadingState.tsx index d5398a04ff7..083a36ca61f 100644 --- a/apps/web/src/components/Charts/LoadingState.tsx +++ b/apps/web/src/components/Charts/LoadingState.tsx @@ -24,12 +24,13 @@ const ChartErrorContainer = styled(Row)` padding: 12px 20px 12px 12px; gap: 12px; ${textFadeIn}; + z-index: 1; ` const ErrorTextColumn = styled(Column)` white-space: normal; ` -function ChartErrorView({ children }: PropsWithChildren) { +export function ChartErrorView({ children }: PropsWithChildren) { return (
diff --git a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts index ef270c9168c..b621580ea11 100644 --- a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts +++ b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts @@ -73,14 +73,14 @@ export class StackedAreaSeriesRenderer implements }) const zeroY = priceToCoordinate(0) ?? 0 const colorsCount = options.colors.length - const isV4EverywhereEnabled = options.colors.length === 3 + const isV4DataEnabled = options.colors.length === 3 const { linesMeshed, hoverInfo } = this._createLinePaths( bars, this._data.visibleRange, renderingScope, zeroY * renderingScope.verticalPixelRatio, options.hoveredLogicalIndex, - isV4EverywhereEnabled, + isV4DataEnabled, ) const fullLinesMeshed = linesMeshed.slice(0, colorsCount + 1) @@ -176,7 +176,7 @@ export class StackedAreaSeriesRenderer implements renderingScope: BitmapCoordinatesRenderingScope, zeroY: number, hoveredIndex?: number | null, - isV4EverywhereEnabled?: boolean, + isV4DataEnabled?: boolean, ) { const { horizontalPixelRatio, verticalPixelRatio } = renderingScope const v2Lines: LinePathData[] = [] @@ -191,7 +191,7 @@ export class StackedAreaSeriesRenderer implements // Modification: tracks and returns coordinates of where a glyph should be rendered for each line when a crosshair is drawn const hoverInfo = { points: new Array(), x: 0 } - const numLines = isV4EverywhereEnabled ? 3 : 2 + const numLines = isV4DataEnabled ? 3 : 2 // Modification: updated loop to include one point above and below the visible range to ensure the line is drawn to edges of chart for (let i = visibleRange.from - 1; i < visibleRange.to + 1; i++) { if (i >= bars.length || i < 0) { @@ -302,7 +302,7 @@ export class StackedAreaSeriesRenderer implements const stack = bars[i] let lineIndex = 0 stack.ys.forEach((yMedia, index) => { - if (index % numLines !== 2 || !isV4EverywhereEnabled) { + if (index % numLines !== 2 || !isV4DataEnabled) { return } @@ -358,13 +358,13 @@ export class StackedAreaSeriesRenderer implements if (i < v3Lines.length) { linesMeshed.push(v3Lines[i]) } - if (i < v4Lines.length && isV4EverywhereEnabled) { + if (i < v4Lines.length && isV4DataEnabled) { linesMeshed.push(v4Lines[i]) } if (hoveredIndex) { linesMeshed.push(v2HighlightLines[i]) linesMeshed.push(v3HighlightLines[i]) - isV4EverywhereEnabled && linesMeshed.push(v4HighlightLines[i]) + isV4DataEnabled && linesMeshed.push(v4HighlightLines[i]) } } diff --git a/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx b/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx index df171ed5924..9fc40134462 100644 --- a/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx +++ b/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx @@ -16,7 +16,7 @@ export type CustomVolumeChartModelParams = { // Extensible to other volume charts (i.e. see VolumeChartModel for single-histogram volume chart implementation) export class CustomVolumeChartModel extends ChartModel { protected series: ISeriesApi<'Custom'> - private highlightBarPrimitive: CrosshairHighlightPrimitive + private highlightBarPrimitive?: CrosshairHighlightPrimitive private hoveredXPos: number | undefined constructor(chartDiv: HTMLDivElement, params: ChartModelParams & CustomVolumeChartModelParams) { diff --git a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx index af155620189..3cdff94acb3 100644 --- a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx +++ b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx @@ -10,6 +10,7 @@ import { LimitsExpiry } from 'uniswap/src/types/limits' import { SwapTab } from 'uniswap/src/types/screens/interface' const mockMultichainContextValue = { + reset: jest.fn(), setSelectedChainId: jest.fn(), setIsUserSelectedToken: jest.fn(), isSwapAndLimitContext: true, diff --git a/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx b/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx index d739f753674..9c6e6059b93 100644 --- a/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx +++ b/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx @@ -25,7 +25,7 @@ import { useMultichainContext } from 'state/multichain/useMultichainContext' import { ThemedText } from 'theme/components' import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles' import { AnimatePresence, Flex, Text } from 'ui/src' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import Trace from 'uniswap/src/features/telemetry/Trace' import { Trans } from 'uniswap/src/i18n' import { CurrencyField } from 'uniswap/src/types/currency' diff --git a/apps/web/src/components/CurrencyInputPanel/index.tsx b/apps/web/src/components/CurrencyInputPanel/index.tsx index 2e53de4b695..68244467443 100644 --- a/apps/web/src/components/CurrencyInputPanel/index.tsx +++ b/apps/web/src/components/CurrencyInputPanel/index.tsx @@ -19,7 +19,7 @@ import { useCurrencyBalance } from 'state/connection/hooks' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import Trace from 'uniswap/src/features/telemetry/Trace' import { Trans, useTranslation } from 'uniswap/src/i18n' import { CurrencyField } from 'uniswap/src/types/currency' diff --git a/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx index cfa19ddc5ab..72e2c96807e 100644 --- a/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx +++ b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx @@ -242,13 +242,16 @@ export default function FeatureFlagModal() { label="Enable EIP-6963: Multi Injected Provider Discovery" /> - + + + + diff --git a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx index 7102ff09bb5..9b235edad63 100644 --- a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx +++ b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx @@ -14,6 +14,8 @@ import { IncreasePositionTxAndGasInfo, LiquidityTransactionType, } from 'uniswap/src/features/transactions/liquidity/types' +import { getTradeSettingsDeadline } from 'uniswap/src/features/transactions/swap/form/utils' +import { useSwapSettingsContext } from 'uniswap/src/features/transactions/swap/settings/contexts/SwapSettingsContext' import { validatePermit, validateTransactionRequest } from 'uniswap/src/features/transactions/swap/utils/trade' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useAccount } from 'wagmi' @@ -21,6 +23,8 @@ import { useAccount } from 'wagmi' interface IncreasePositionContextType { txInfo?: IncreasePositionTxAndGasInfo gasFeeEstimateUSD?: CurrencyAmount + error?: boolean + refetch?: () => void } const IncreaseLiquidityTxContext = createContext(undefined) @@ -28,6 +32,7 @@ const IncreaseLiquidityTxContext = createContext{children} diff --git a/apps/web/src/components/Liquidity/HookModal.tsx b/apps/web/src/components/Liquidity/HookModal.tsx index 6507cbb15b0..41c69f0b3b3 100644 --- a/apps/web/src/components/Liquidity/HookModal.tsx +++ b/apps/web/src/components/Liquidity/HookModal.tsx @@ -14,7 +14,7 @@ import { uniswapUrls } from 'uniswap/src/constants/urls' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { useTranslation } from 'uniswap/src/i18n' -import { shortenAddress } from 'uniswap/src/utils/addresses' +import { shortenAddress } from 'utilities/src/addresses' function HookWarnings({ flags, hasDangerous }: { flags: FlagWarning[]; hasDangerous: boolean }) { const { t } = useTranslation() diff --git a/apps/web/src/components/Liquidity/LiquidityModalDetailRows.tsx b/apps/web/src/components/Liquidity/LiquidityModalDetailRows.tsx index 1ae7a3a96c5..11faa59a290 100644 --- a/apps/web/src/components/Liquidity/LiquidityModalDetailRows.tsx +++ b/apps/web/src/components/Liquidity/LiquidityModalDetailRows.tsx @@ -48,20 +48,22 @@ export function LiquidityModalDetailRows({ ), }} /> - ( - - - - ), - Value: () => ( - - {formatCurrencyAmount({ value: networkCost, type: NumberType.FiatGasPrice })} - - ), - }} - /> + {Boolean(networkCost) && ( + ( + + + + ), + Value: () => ( + + {formatCurrencyAmount({ value: networkCost, type: NumberType.FiatGasPrice })} + + ), + }} + /> + )} ) } diff --git a/apps/web/src/components/Liquidity/LiquidityModalHeader.test.tsx b/apps/web/src/components/Liquidity/LiquidityModalHeader.test.tsx index d624f20d1a2..c4533762a85 100644 --- a/apps/web/src/components/Liquidity/LiquidityModalHeader.test.tsx +++ b/apps/web/src/components/Liquidity/LiquidityModalHeader.test.tsx @@ -1,10 +1,19 @@ import { LiquidityModalHeader } from 'components/Liquidity/LiquidityModalHeader' +import { WebUniswapProvider } from 'components/Web3Provider/WebUniswapContext' import { act, fireEvent, render } from 'test-utils/render' +import { SwapSettingsContextProvider } from 'uniswap/src/features/transactions/swap/settings/contexts/SwapSettingsContext' describe('LiquidityModalHeader', () => { it('should render with given title and call close callback', () => { const onClose = jest.fn() - const { getByText, getByTestId } = render() + const { getByText, getByTestId } = render( + + + + + , + , + ) expect(getByText('Test Title')).toBeInTheDocument() expect(onClose).not.toHaveBeenCalled() act(() => { diff --git a/apps/web/src/components/Liquidity/LiquidityModalHeader.tsx b/apps/web/src/components/Liquidity/LiquidityModalHeader.tsx index e1d7bdda381..5b257482b10 100644 --- a/apps/web/src/components/Liquidity/LiquidityModalHeader.tsx +++ b/apps/web/src/components/Liquidity/LiquidityModalHeader.tsx @@ -3,6 +3,10 @@ import { CloseIcon } from 'theme/components' import { Flex, Text, TouchableArea } from 'ui/src' import { BackArrow } from 'ui/src/components/icons/BackArrow' import { iconSizes } from 'ui/src/theme' +import { SwapFormSettings } from 'uniswap/src/features/transactions/swap/form/SwapFormSettings' +import { Deadline } from 'uniswap/src/features/transactions/swap/settings/configs/Deadline' +import { Slippage } from 'uniswap/src/features/transactions/swap/settings/configs/Slippage' +import { useTranslation } from 'uniswap/src/i18n' export function LiquidityModalHeader({ title, @@ -13,6 +17,8 @@ export function LiquidityModalHeader({ closeModal: () => void goBack?: () => void }) { + const { t } = useTranslation() + const CloseIconComponent = useMemo( () => , [closeModal], @@ -30,7 +36,15 @@ export function LiquidityModalHeader({ {title} - {!!goBack && CloseIconComponent} + {!goBack ? ( + + ) : ( + CloseIconComponent + )} ) } diff --git a/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx b/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx index 8e67d42fdb4..6b0973d34d5 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx @@ -1,20 +1,22 @@ // eslint-disable-next-line no-restricted-imports -import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { LiquidityPositionRangeChart } from 'components/Charts/LiquidityPositionRangeChart/LiquidityPositionRangeChart' import { LiquidityPositionFeeStats } from 'components/Liquidity/LiquidityPositionFeeStats' import { LiquidityPositionInfo } from 'components/Liquidity/LiquidityPositionInfo' -import { LiquidityPositionRangeChart } from 'components/Liquidity/LiquidityPositionRangeChart' import { useGetRangeDisplay, useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks' import { PositionInfo } from 'components/Liquidity/types' import { PriceOrdering } from 'components/PositionListItem' import { MouseoverTooltip } from 'components/Tooltip' import { getPoolDetailsURL } from 'graphql/data/util' +import { useSwitchChain } from 'hooks/useSwitchChain' import { useMemo, useState } from 'react' import { MoreHorizontal } from 'react-feather' import { useNavigate } from 'react-router-dom' import { setOpenModal } from 'state/application/reducer' import { useAppDispatch } from 'state/hooks' import { ClickableTamaguiStyle } from 'theme/components' -import { Button, Flex, GeneratedIcon, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' +import { Button, Flex, GeneratedIcon, Separator, Text, TouchableArea, useIsTouchDevice, useSporeColors } from 'ui/src' +import { ArrowRight } from 'ui/src/components/icons/ArrowRight' import { ArrowsLeftRight } from 'ui/src/components/icons/ArrowsLeftRight' import { Dollar } from 'ui/src/components/icons/Dollar' import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled' @@ -22,12 +24,14 @@ import { Minus } from 'ui/src/components/icons/Minus' import { Plus } from 'ui/src/components/icons/Plus' import { iconSizes } from 'ui/src/theme' import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice' import { useTranslation } from 'uniswap/src/i18n/useTranslation' import { NumberType } from 'utilities/src/format/types' +import { useAccount } from 'wagmi' function DropdownOptionRender({ children, Icon }: { children: React.ReactNode; Icon: GeneratedIcon }) { return ( @@ -52,8 +56,12 @@ export function LiquidityPositionCard({ const { formatCurrencyAmount } = useLocalizationContext() const { t } = useTranslation() const colors = useSporeColors() + const isTouchDevice = useIsTouchDevice() + const dispatch = useAppDispatch() const navigate = useNavigate() + const account = useAccount() + const switchChain = useSwitchChain() const { fiatFeeValue0, fiatFeeValue1, fiatValue0, fiatValue1, priceOrdering, apr } = useV3OrV4PositionDerivedInfo(liquidityPosition) @@ -76,8 +84,7 @@ export function LiquidityPositionCard({ fiatFeeValue0 && fiatFeeValue1 ? formatCurrencyAmount({ value: fiatFeeValue0.add(fiatFeeValue1), - type: - liquidityPosition.status === PositionStatus.CLOSED ? NumberType.FiatStandard : NumberType.FiatTokenPrice, + type: NumberType.FiatStandard, }) : undefined @@ -99,8 +106,29 @@ export function LiquidityPositionCard({ }, ] + const chainInfo = getChainInfo(liquidityPosition.chainId) + + const migrateV2Option = { + key: 'position-card-migrate', + onPress: async () => { + if (chainInfo.id !== account.chainId) { + await switchChain(chainInfo.id) + } + navigate(`/migrate/v2/${liquidityPosition.liquidityToken?.address ?? ''}`) + }, + render: () => {t('pool.migrateLiquidity')}, + } + if (liquidityPosition.version === ProtocolVersion.V2) { - return v2Options + return [...v2Options, migrateV2Option] + } + + const migrateV3Option = { + key: 'position-card-migrate', + onPress: () => { + navigate(`/migrate/v3/${chainInfo.urlParam}/${liquidityPosition.tokenId}`) + }, + render: () => {t('pool.migrateLiquidity')}, } return [ @@ -114,6 +142,7 @@ export function LiquidityPositionCard({ render: () => {t('pool.collectFees')}, }, ...v2Options, + migrateV3Option, { key: 'position-card-separator', onPress: () => null, @@ -131,7 +160,7 @@ export function LiquidityPositionCard({ render: () => {t('pool.info')}, }, ] - }, [liquidityPosition, dispatch, t, navigate]) + }, [liquidityPosition, dispatch, t, account.chainId, navigate, switchChain]) if (isMiniVersion) { return ( @@ -159,14 +188,9 @@ export function LiquidityPositionCard({ borderColor="$surface3" width="100%" overflow="hidden" - hoverStyle={ - isClickableStyle - ? { - backgroundColor: '$surface1Hovered', - borderColor: '$surface3Hovered', - } - : {} - } + $md={{ gap: '$gap20' }} + hoverStyle={isClickableStyle ? { backgroundColor: '$surface1Hovered', borderColor: '$surface3Hovered' } : {}} + pressStyle={isClickableStyle ? { backgroundColor: '$surface1Pressed', borderColor: '$surface3Pressed' } : {}} > - - { - event.preventDefault() - event.stopPropagation() - }} - styles={{ - dropdownMinWidth: 200, - buttonPaddingX: '$spacing8', - buttonPaddingY: '$spacing8', - dropdownGap: 2, - alignment: 'right', - }} - options={dropdownOptions} + {!isTouchDevice && ( + - - - + { + event.preventDefault() + event.stopPropagation() + }} + styles={{ + dropdownMinWidth: 200, + buttonPaddingX: '$spacing8', + buttonPaddingY: '$spacing8', + dropdownGap: 2, + alignment: 'right', + }} + options={dropdownOptions} + > + + + + )} ) } @@ -258,14 +285,8 @@ function MiniPositionCard({ borderColor="$surface3" borderWidth={1} m="$spacing16" - hoverStyle={ - isClickableStyle - ? { - backgroundColor: '$surface1Hovered', - borderColor: '$surface3Hovered', - } - : {} - } + hoverStyle={isClickableStyle ? { backgroundColor: '$surface1Hovered', borderColor: '$surface3Hovered' } : {}} + pressStyle={isClickableStyle ? { backgroundColor: '$surface1Pressed', borderColor: '$surface3Pressed' } : {}} > diff --git a/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx b/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx index 1d3ca6240c2..3027f5a5948 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx @@ -1,5 +1,6 @@ // eslint-disable-next-line no-restricted-imports import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { CHART_WIDTH } from 'components/Charts/LiquidityPositionRangeChart/LiquidityPositionRangeChart' import { useGetRangeDisplay } from 'components/Liquidity/hooks' import { PriceOrdering } from 'components/PositionListItem' import { MouseoverTooltip } from 'components/Tooltip' @@ -33,8 +34,34 @@ const PrimaryText = styled(Text, { const SecondaryText = styled(Text, { color: '$neutral2', variant: 'body3', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', }) +function WrapChildrenForMediaSize({ children }: { children: React.ReactNode }) { + const isScreenSize = useScreenSize() + const isMobile = !isScreenSize['navDropdownMobileDrawer'] + + if (isMobile) { + return ( + + {children} + + ) + } + + return <>{children} +} + +function FeeStat({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + export function LiquidityPositionFeeStats({ formattedUsdValue, formattedUsdFees, @@ -48,7 +75,6 @@ export function LiquidityPositionFeeStats({ const { t } = useTranslation() const { formatPercent } = useLocalizationContext() const [pricesInverted, setPricesInverted] = useState(false) - const screenSize = useScreenSize() const { maxPrice, minPrice, tokenASymbol, tokenBSymbol, isFullRange } = useGetRangeDisplay({ priceOrdering, @@ -59,20 +85,25 @@ export function LiquidityPositionFeeStats({ }) return ( - - - {formattedUsdValue ? ( - {formattedUsdValue} - ) : ( - } placement="top"> - - - - )} - {t('pool.position')} - - - {version === ProtocolVersion.V2 || !!formattedUsdFees ? ( - <> + + + + + {formattedUsdValue ? ( + {formattedUsdValue} + ) : ( + } placement="top"> + - + + )} + {t('pool.position')} + + {version === ProtocolVersion.V2 ? ( @@ -85,43 +116,33 @@ export function LiquidityPositionFeeStats({ ) : ( - {formattedUsdFees} + {formattedUsdFees ?? '-'} )} {t('common.fees')} - - ) : null} + + + + {apr ? formatPercent(apr) : '-'} + + {t('pool.apr')} + + - - {!!apr && ( - <> - {formatPercent(apr)} - - {t('pool.apr')} - - - )} - - + {priceOrdering.priceLower && priceOrdering.priceUpper && !isFullRange ? ( - + {minPrice} {tokenASymbol} / {tokenBSymbol} - - + + @@ -135,8 +156,11 @@ export function LiquidityPositionFeeStats({ e.stopPropagation() setPricesInverted((prevInverted) => !prevInverted) }} + {...ClickableTamaguiStyle} + display="none" + $group-item-hover={{ display: 'flex' }} > - + diff --git a/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx b/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx index 63dfebe3ca1..04083cac890 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx @@ -25,8 +25,8 @@ export function LiquidityPositionInfo({ currencies={[currency0Amount?.currency, currency1Amount?.currency]} size={currencyLogoSize} /> - - + + {currency0Amount?.currency.symbol} / {currency1Amount?.currency.symbol} diff --git a/apps/web/src/components/Liquidity/LiquidityPositionRangeChart.tsx b/apps/web/src/components/Liquidity/LiquidityPositionRangeChart.tsx deleted file mode 100644 index 1952ef4afd8..00000000000 --- a/apps/web/src/components/Liquidity/LiquidityPositionRangeChart.tsx +++ /dev/null @@ -1,277 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' -import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core' -import { BandsIndicator } from 'components/Charts/BandsIndicator/bands-indicator' -import { cloneReadonly } from 'components/Charts/BandsIndicator/helpers/simple-clone' -import { Chart, ChartModel, ChartModelParams } from 'components/Charts/ChartModel' -import { PriceChartData } from 'components/Charts/PriceChart' -import { PriceChartType } from 'components/Charts/utils' -import { useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks' -import { PositionInfo } from 'components/Liquidity/types' -import { DataQuality } from 'components/Tokens/TokenDetails/ChartSection/util' -import { usePoolPriceChartData } from 'hooks/usePoolPriceChartData' -import { useTheme } from 'lib/styled-components' -import { CrosshairMode, ISeriesApi, LineStyle, LineType, UTCTimestamp } from 'lightweight-charts' -import { getCurrencyAddressWithWrap, getSortedCurrenciesTupleWithWrap } from 'pages/Pool/Positions/create/utils' -import { useMemo, useState } from 'react' -import { opacify } from 'theme/utils' -import { Flex, FlexProps } from 'ui/src' -import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' - -const CHART_HEIGHT = 52 -const CHART_WIDTH = 224 - -const pulseKeyframe = ` - @keyframes pulse { - 0% { - transform: scale(1); - opacity: 1; - } - 100% { - transform: scale(5); - opacity: 0; - } - } -` - -function getCrosshairProps(color: any, yCoordinate: number): FlexProps { - // The chart extends by a constant amount horizontally past the price data. - return { - position: 'absolute', - right: 19, - top: yCoordinate - 3, // Center the crosshair vertically on the price line. - width: 6, - height: 6, - borderRadius: '$roundedFull', - backgroundColor: color, - } -} - -interface LPPriceChartModelParams extends ChartModelParams { - type: PriceChartType.LINE - positionInfo: PositionInfo - positionPriceLower?: Price - positionPriceUpper?: Price - setCrosshairYCoordinate: (xCoordinate: number) => void -} - -class LPPriceChartModel extends ChartModel { - protected series: ISeriesApi<'Area'> - private rangeBandSeries: ISeriesApi<'Line'> - private extendedData: PriceChartData[] - private positionRangeMin: number - private positionRangeMax: number - - constructor(chartDiv: HTMLDivElement, params: LPPriceChartModelParams) { - super(chartDiv, params) - - this.positionRangeMin = Number( - params.positionPriceLower - ?.quote( - CurrencyAmount.fromRawAmount( - params.positionPriceLower.baseCurrency, - Math.pow(10, params.positionPriceLower.baseCurrency.decimals), - ), - ) - ?.toSignificant(params.positionPriceLower.baseCurrency.decimals) ?? 0, - ) - this.positionRangeMax = Number( - params.positionPriceUpper - ?.quote( - CurrencyAmount.fromRawAmount( - params.positionPriceUpper.baseCurrency, - Math.pow(10, params.positionPriceUpper.baseCurrency.decimals), - ), - ) - ?.toSignificant(params.positionPriceUpper.baseCurrency.decimals) ?? 0, - ) - - // Price history (primary series) - this.series = this.api.addAreaSeries() - this.series.setData(this.data) - - this.extendedData = LPPriceChartModel.generateExtendedData(this.data) - this.rangeBandSeries = this.api.addLineSeries() - // The price values in the data are ignored by this Series, - // it only uses the time values to make the BandsIndicator work. - this.rangeBandSeries.setData(this.extendedData) - - this.rangeBandSeries.applyOptions({ - priceLineVisible: false, - color: 'transparent', - }) - - const bandIndicator = new BandsIndicator({ - lineColor: opacify(10, params.theme.neutral1), - fillColor: params.theme.surface3, - lineWidth: 1, - upperValue: this.positionRangeMax, - lowerValue: this.positionRangeMin, - }) - this.rangeBandSeries.attachPrimitive(bandIndicator) - - this.updateOptions(params) - this.fitContent() - this.overrideCrosshair(params) - } - - updateOptions(params: LPPriceChartModelParams): void { - // Handle changes in data - if (this.data !== params.data) { - this.data = params.data - this.series.setData(this.data) - this.extendedData = LPPriceChartModel.generateExtendedData(this.data) - this.rangeBandSeries.setData(this.extendedData) - this.fitContent() - this.overrideCrosshair(params) - } - - super.updateOptions(params, { - rightPriceScale: { - visible: false, - }, - leftPriceScale: { - visible: false, - }, - timeScale: { - visible: false, - }, - handleScroll: false, - handleScale: false, - crosshair: { - mode: CrosshairMode.Hidden, - }, - }) - - // Re-set options that depend on data. - const priceLineColor = LPPriceChartModel.getPriceLineColor(params) - this.series.applyOptions({ - priceLineVisible: true, - priceLineStyle: LineStyle.SparseDotted, - lineType: this.data.length < 20 ? LineType.WithSteps : LineType.Curved, - lineWidth: 2, - lineColor: priceLineColor, - topColor: 'transparent', - bottomColor: 'transparent', - }) - } - - public static getPriceLineColor(params: Pick): string { - switch (params.positionInfo.status) { - case PositionStatus.OUT_OF_RANGE: - return params.theme.critical - case PositionStatus.IN_RANGE: - return params.theme.success - case PositionStatus.CLOSED: - default: - return params.theme.neutral2 - } - } - - private overrideCrosshair(params: LPPriceChartModelParams): void { - const lastDataPoint = this.data[this.data.length - 1] - if (!lastDataPoint) { - return - } - - const yCoordinate = this.series.priceToCoordinate(lastDataPoint.value) - params.setCrosshairYCoordinate(Number(yCoordinate)) - } - - private static generateExtendedData(data: PriceChartData[]): PriceChartData[] { - const lastTime = data[data.length - 1]?.time - if (!lastTime) { - return data - } - const timeDelta = lastTime - data[0]?.time - const timeIncrement = timeDelta / data.length - - if (timeIncrement === 0) { - return data - } - - const newData = cloneReadonly(data) - const lastData = newData[newData.length - 1] - - for (let i = 1; i <= Math.floor(data.length / 10); i++) { - const time = lastTime + timeIncrement * i - newData.push({ - ...lastData, - time: time as UTCTimestamp, - }) - } - return newData - } -} - -interface LiquidityPositionRangeChartProps { - positionInfo: PositionInfo -} - -export function LiquidityPositionRangeChart({ positionInfo }: LiquidityPositionRangeChartProps) { - const { priceOrdering } = useV3OrV4PositionDerivedInfo(positionInfo) - const theme = useTheme() - const isV2 = positionInfo.version === ProtocolVersion.V2 - const isV3 = positionInfo.version === ProtocolVersion.V3 - const isV4 = positionInfo.version === ProtocolVersion.V4 - const chainInfo = getChainInfo(positionInfo.currency0Amount.currency.chainId) - const poolAddressOrId = isV2 ? positionInfo.pair?.liquidityToken.address : positionInfo.poolId - const variables = poolAddressOrId - ? { - addressOrId: poolAddressOrId, - chain: chainInfo.backendChain.chain, - duration: HistoryDuration.Month, - isV4, - isV3, - isV2, - } - : undefined - const sortedCurrencies = getSortedCurrenciesTupleWithWrap( - positionInfo.currency0Amount.currency, - positionInfo.currency1Amount.currency, - positionInfo.version, - ) - const priceData = usePoolPriceChartData( - variables, - positionInfo.currency0Amount.currency, - positionInfo.currency0Amount.currency, - positionInfo.version, - getCurrencyAddressWithWrap(sortedCurrencies[0], positionInfo.version), - priceOrdering.base?.equals(sortedCurrencies[0]) ?? false /* isReversed */, - ) - - const [crosshairYCoordinate, setCrosshairYCoordinate] = useState() - - const chartParams = useMemo(() => { - return { - data: priceData.entries, - stale: priceData.dataQuality === DataQuality.STALE, - type: PriceChartType.LINE, - positionInfo, - positionPriceLower: priceOrdering.priceLower, - positionPriceUpper: priceOrdering.priceUpper, - setCrosshairYCoordinate, - } as const - }, [priceData.dataQuality, priceData.entries, positionInfo, priceOrdering.priceLower, priceOrdering.priceUpper]) - - return ( - - - - {crosshairYCoordinate && crosshairYCoordinate > 5 && ( - <> - - - - )} - - ) -} diff --git a/apps/web/src/components/Liquidity/hooks.ts b/apps/web/src/components/Liquidity/hooks.ts index 260b32eb44a..dac7f6ed9e4 100644 --- a/apps/web/src/components/Liquidity/hooks.ts +++ b/apps/web/src/components/Liquidity/hooks.ts @@ -79,6 +79,7 @@ export function useAllFeeTierPoolData({ formattedFee: formatPercent(new Percent(pool.fee, 1000000)), totalLiquidityUsd: totalLiquidityUsdTruncated, percentage, + tvl: pool.totalLiquidityUsd, created: true, } satisfies FeeTierData } diff --git a/apps/web/src/components/Liquidity/types.ts b/apps/web/src/components/Liquidity/types.ts index 9f759f1e836..4ee35edbb83 100644 --- a/apps/web/src/components/Liquidity/types.ts +++ b/apps/web/src/components/Liquidity/types.ts @@ -15,6 +15,7 @@ export interface DepositState { } export type DepositContextType = { + reset: () => void depositState: DepositState setDepositState: Dispatch> derivedDepositInfo: DepositInfo @@ -82,5 +83,6 @@ export type FeeTierData = { formattedFee: string totalLiquidityUsd: number percentage: Percent + tvl: string created: boolean } diff --git a/apps/web/src/components/Liquidity/utils.tsx b/apps/web/src/components/Liquidity/utils.tsx index 352b7e3475e..1a9c4ac3930 100644 --- a/apps/web/src/components/Liquidity/utils.tsx +++ b/apps/web/src/components/Liquidity/utils.tsx @@ -201,7 +201,7 @@ function parseRestToken(token: RestToken | undefined): T | u return new Token(token.chainId, token.address, token.decimals, token.symbol) as T } -export function getPairFromRest({ +function getPairFromRest({ pair, token0, token1, @@ -371,16 +371,6 @@ export function calculateTickSpacingFromFeeAmount(feeAmount: number): number { return (2 * feeAmount) / 100 } -export function calculateInvertedPrice({ price, invert }: { price?: Price; invert: boolean }) { - const currentPrice = invert ? price?.invert() : price - - return { - price: currentPrice, - quote: currentPrice?.quoteCurrency, - base: currentPrice?.baseCurrency, - } -} - export enum HookFlag { BeforeAddLiquidity = 'before-add-liquidity', AfterAddLiquidity = 'after-add-liquidity', @@ -484,6 +474,7 @@ export function mergeFeeTiers( totalLiquidityUsd: 0, percentage: new Percent(0, 100), created: false, + tvl: '0', } satisfies FeeTierData } @@ -537,42 +528,49 @@ export function getDefaultFeeTiersWithData({ value: defaultFeeTiersForChain[FeeAmount.LOWEST], title: t(`fee.bestForVeryStable`), selectionPercent: feeTierData[FeeAmount.LOWEST]?.percentage, + tvl: feeTierData[FeeAmount.LOWEST]?.tvl, }, { tier: FeeAmount.LOW_200, value: defaultFeeTiersForChain[FeeAmount.LOW_200], title: '', selectionPercent: feeTierData[FeeAmount.LOW_200]?.percentage, + tvl: feeTierData[FeeAmount.LOW_200]?.tvl, }, { tier: FeeAmount.LOW_300, value: defaultFeeTiersForChain[FeeAmount.LOW_300], title: '', selectionPercent: feeTierData[FeeAmount.LOW_300]?.percentage, + tvl: feeTierData[FeeAmount.LOW_300]?.tvl, }, { tier: FeeAmount.LOW_400, value: defaultFeeTiersForChain[FeeAmount.LOW_400], title: '', selectionPercent: feeTierData[FeeAmount.LOW_400]?.percentage, + tvl: feeTierData[FeeAmount.LOW_400]?.tvl, }, { tier: FeeAmount.LOW, value: defaultFeeTiersForChain[FeeAmount.LOW], title: t(`fee.bestForStablePairs`), selectionPercent: feeTierData[FeeAmount.LOW]?.percentage, + tvl: feeTierData[FeeAmount.LOW]?.tvl, }, { tier: FeeAmount.MEDIUM, value: defaultFeeTiersForChain[FeeAmount.MEDIUM], title: t(`fee.bestForMost`), selectionPercent: feeTierData[FeeAmount.MEDIUM]?.percentage, + tvl: feeTierData[FeeAmount.MEDIUM]?.tvl, }, { tier: FeeAmount.HIGH, value: defaultFeeTiersForChain[FeeAmount.HIGH], title: t(`fee.bestForExotic`), selectionPercent: feeTierData[FeeAmount.HIGH]?.percentage, + tvl: feeTierData[FeeAmount.HIGH]?.tvl, }, ] as const diff --git a/apps/web/src/components/LiquidityChartRangeInput/Chart.tsx b/apps/web/src/components/LiquidityChartRangeInput/Chart.tsx index 19282eb48d1..8d9c55f526e 100644 --- a/apps/web/src/components/LiquidityChartRangeInput/Chart.tsx +++ b/apps/web/src/components/LiquidityChartRangeInput/Chart.tsx @@ -49,7 +49,7 @@ export function Chart({ } return scales - }, [current, zoomLevels.initialMin, zoomLevels.initialMax, innerWidth, series, innerHeight, zoom]) + }, [zoomLevels, current, innerWidth, series, innerHeight, zoom]) useEffect(() => { // reset zoom as necessary diff --git a/apps/web/src/components/LiquidityChartRangeInput/hooks.ts b/apps/web/src/components/LiquidityChartRangeInput/hooks.ts index 5c4eaed2cc9..a55c0b1e700 100644 --- a/apps/web/src/components/LiquidityChartRangeInput/hooks.ts +++ b/apps/web/src/components/LiquidityChartRangeInput/hooks.ts @@ -1,17 +1,23 @@ import { Currency } from '@uniswap/sdk-core' -import { FeeAmount } from '@uniswap/v3-sdk' import { ChartEntry } from 'components/LiquidityChartRangeInput/types' -import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData' +import { usePoolActiveLiquidity } from 'hooks/usePoolTickData' import { useCallback, useMemo } from 'react' +import { TickProcessed } from 'utils/computeSurroundingTicks' +/** + * Currency A and B should be sorted to get accurate data, but you can pass invertPrices = true + * to get inverted prices. + */ export function useDensityChartData({ currencyA, currencyB, feeAmount, + invertPrices, }: { currencyA?: Currency currencyB?: Currency - feeAmount?: FeeAmount + feeAmount?: number + invertPrices?: boolean }) { const { isLoading, error, data } = usePoolActiveLiquidity(currencyA, currencyB, feeAmount) @@ -25,9 +31,12 @@ export function useDensityChartData({ for (let i = 0; i < data.length; i++) { const t: TickProcessed = data[i] + const price0 = invertPrices ? t.sdkPrice.invert().toSignificant(8) : t.sdkPrice.toSignificant(8) + const chartEntry = { activeLiquidity: parseFloat(t.liquidityActive.toString()), - price0: parseFloat(t.price0), + price0: parseFloat(price0), + tick: t.tick, } if (chartEntry.activeLiquidity > 0) { @@ -36,7 +45,7 @@ export function useDensityChartData({ } return newData - }, [data]) + }, [data, invertPrices]) return useMemo(() => { return { diff --git a/apps/web/src/components/LiquidityChartRangeInput/svg.tsx b/apps/web/src/components/LiquidityChartRangeInput/svg.tsx index f8227b5c547..210c0e2f516 100644 --- a/apps/web/src/components/LiquidityChartRangeInput/svg.tsx +++ b/apps/web/src/components/LiquidityChartRangeInput/svg.tsx @@ -31,6 +31,13 @@ export const brushHandlePath = (height: number) => `z`, // close path ].join(' ') +// Handle - straight horizontal line only +export const brushHandlePathV2 = (width: number) => + [ + `M 0 0`, // move to origin + `h ${width}`, // horizontal line with specified width + ].join(' ') + export const brushHandleAccentPath = () => [ 'm 5 7', // move to first accent @@ -41,6 +48,15 @@ export const brushHandleAccentPath = () => 'z', ].join(' ') +export const brushHandleAccentPathV2 = (width: number) => { + const lineStart = width / 2 - 15 + return [ + 'M 0 0', // move to origin + `m ${lineStart} 8`, // move to start of accent line + `h 30`, // horizontal line + ].join(' ') +} + export const OffScreenHandle = ({ color, size = 10, @@ -59,3 +75,20 @@ export const OffScreenHandle = ({ strokeLinejoin="round" /> ) + +/** + Points down by default +*/ +export const OffScreenHandleV2 = ({ color, size = 6 }: { color: string; size?: number }) => { + const center = size / 3 + return ( + + ) +} diff --git a/apps/web/src/components/LiquidityChartRangeInput/types.ts b/apps/web/src/components/LiquidityChartRangeInput/types.ts index 15984592ea2..77c5566e06b 100644 --- a/apps/web/src/components/LiquidityChartRangeInput/types.ts +++ b/apps/web/src/components/LiquidityChartRangeInput/types.ts @@ -3,6 +3,7 @@ import { Bound } from 'state/mint/v3/actions' export interface ChartEntry { activeLiquidity: number price0: number + tick?: number } interface Dimensions { diff --git a/apps/web/src/components/Logo/ChainLogo.tsx b/apps/web/src/components/Logo/ChainLogo.tsx index e1b34be1cb1..ebe6dcc55f7 100644 --- a/apps/web/src/components/Logo/ChainLogo.tsx +++ b/apps/web/src/components/Logo/ChainLogo.tsx @@ -16,7 +16,7 @@ import { ZORA_LOGO, } from 'ui/src/assets' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' type ChainUI = { symbol: string; bgColor: string; textColor: string } diff --git a/apps/web/src/components/NavBar/ChainSelector/index.tsx b/apps/web/src/components/NavBar/ChainSelector/index.tsx index c53e950be3b..2aef761734d 100644 --- a/apps/web/src/components/NavBar/ChainSelector/index.tsx +++ b/apps/web/src/components/NavBar/ChainSelector/index.tsx @@ -6,7 +6,8 @@ import { useMultichainContext } from 'state/multichain/useMultichainContext' import { Flex, Popover } from 'ui/src' import { NetworkFilter } from 'uniswap/src/components/network/NetworkFilter' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useEnabledChains, useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' type ChainSelectorProps = { diff --git a/apps/web/src/components/NavBar/MobileBottomBar/TDPActionTabs.tsx b/apps/web/src/components/NavBar/MobileBottomBar/TDPActionTabs.tsx index ef906a8c875..42c740f1044 100644 --- a/apps/web/src/components/NavBar/MobileBottomBar/TDPActionTabs.tsx +++ b/apps/web/src/components/NavBar/MobileBottomBar/TDPActionTabs.tsx @@ -17,14 +17,15 @@ const TDPActionPill = styled.button<{ $color?: string }>` align-items: center; justify-content: center; text-align: center; + height: 48px; gap: 8px; border: none; border-radius: 50px; transition: color 0.2s; background-color: ${({ $color, theme }) => $color || theme.neutral2}; color: ${({ theme }) => theme.neutralContrast}; - padding: 12px 20px 12px 16px; - font-size: 18px; + padding: 12px; + font-size: 16px; font-weight: 535; flex-grow: 1; ${ClickableStyle} diff --git a/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx b/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx index de8647c79b9..454aae0f018 100644 --- a/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx +++ b/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx @@ -20,7 +20,7 @@ import { Flex } from 'ui/src' import { Verified } from 'ui/src/components/icons/Verified' import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' import { Token, TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' @@ -29,7 +29,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { InterfaceSearchResultSelectionProperties } from 'uniswap/src/features/telemetry/types' import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils' import { Trans, useTranslation } from 'uniswap/src/i18n' -import { shortenAddress } from 'uniswap/src/utils/addresses' +import { shortenAddress } from 'utilities/src/addresses' import { NumberType, useFormatter } from 'utils/formatNumbers' const PriceChangeContainer = styled.div` diff --git a/apps/web/src/components/NavBar/Tabs/TabsContent.tsx b/apps/web/src/components/NavBar/Tabs/TabsContent.tsx index 5024cee0a2a..04238b24687 100644 --- a/apps/web/src/components/NavBar/Tabs/TabsContent.tsx +++ b/apps/web/src/components/NavBar/Tabs/TabsContent.tsx @@ -24,7 +24,7 @@ export type TabsItem = MenuItem & { export const useTabsContent = (): TabsSection[] => { const { t } = useTranslation() - const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const isLPRedesignEnabled = useFeatureFlag(FeatureFlags.LPRedesign) const { pathname } = useLocation() const theme = useTheme() @@ -81,19 +81,19 @@ export const useTabsContent = (): TabsSection[] => { }, { title: t('common.pool'), - href: isV4EverywhereEnabled ? '/positions' : '/pool', + href: isLPRedesignEnabled ? '/positions' : '/pool', isActive: pathname.startsWith('/pool'), items: [ { label: t('nav.tabs.viewPositions'), quickKey: 'V', - href: isV4EverywhereEnabled ? '/positions' : '/pool', + href: isLPRedesignEnabled ? '/positions' : '/pool', internal: true, }, { label: t('nav.tabs.createPosition'), quickKey: 'V', - href: isV4EverywhereEnabled ? '/positions/create' : '/add', + href: isLPRedesignEnabled ? '/positions/create' : '/add', internal: true, }, ], diff --git a/apps/web/src/components/NavBar/index.tsx b/apps/web/src/components/NavBar/index.tsx index 14ae0064cd8..4e45dbfc98b 100644 --- a/apps/web/src/components/NavBar/index.tsx +++ b/apps/web/src/components/NavBar/index.tsx @@ -26,7 +26,7 @@ import { useProfilePageState } from 'nft/hooks' import { ProfilePageStateType } from 'nft/types' import { BREAKPOINTS } from 'theme' import { Z_INDEX } from 'theme/zIndex' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { INTERFACE_NAV_HEIGHT } from 'uniswap/src/theme/heights' const Nav = styled.nav` diff --git a/apps/web/src/components/Pools/PoolDetails/ChartSection/hooks.ts b/apps/web/src/components/Pools/PoolDetails/ChartSection/hooks.ts index 8c091dd126d..33c0c3b91e2 100644 --- a/apps/web/src/components/Pools/PoolDetails/ChartSection/hooks.ts +++ b/apps/web/src/components/Pools/PoolDetails/ChartSection/hooks.ts @@ -18,10 +18,9 @@ export function usePDPPriceChartData( poolData: PoolData | undefined, tokenA: Token | undefined, tokenB: Token | undefined, - isReversed: boolean, protocolVersion: ProtocolVersion, ): ChartQueryResult { - return usePoolPriceChartData(variables, tokenA, tokenB, protocolVersion, poolData?.token0?.address ?? '', isReversed) + return usePoolPriceChartData(variables, tokenA, tokenB, protocolVersion, poolData?.token0?.address ?? '') } export function usePDPVolumeChartData( diff --git a/apps/web/src/components/Pools/PoolDetails/ChartSection/index.tsx b/apps/web/src/components/Pools/PoolDetails/ChartSection/index.tsx index fb29c0a9ed3..6343258c33b 100644 --- a/apps/web/src/components/Pools/PoolDetails/ChartSection/index.tsx +++ b/apps/web/src/components/Pools/PoolDetails/ChartSection/index.tsx @@ -29,7 +29,7 @@ import { EllipsisStyle, ThemedText } from 'theme/components' import { textFadeIn } from 'theme/styles' import { SegmentedControl } from 'ui/src' import { Chain, ProtocolVersion } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans, t } from 'uniswap/src/i18n' @@ -116,7 +116,13 @@ function usePDPChartState( isV2, } - const priceQuery = usePDPPriceChartData(variables, poolData, tokenA, tokenB, isReversed, protocolVersion) + const priceQuery = usePDPPriceChartData( + variables, + poolData, + isReversed ? tokenB : tokenA, + isReversed ? tokenA : tokenB, + protocolVersion, + ) const volumeQuery = usePDPVolumeChartData(variables) return useMemo(() => { diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.tsx index 9efb21657c6..67fa1027ef5 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.tsx @@ -32,7 +32,7 @@ import { Flex, TouchableArea } from 'ui/src' import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown' import { BIPS_BASE } from 'uniswap/src/constants/misc' import { ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans, t } from 'uniswap/src/i18n' diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx index a2e0a9f7144..90f20021007 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx @@ -15,7 +15,7 @@ import { useNavigate } from 'react-router-dom' import { BREAKPOINTS } from 'theme' import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans, t } from 'uniswap/src/i18n' diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx index 44b07e10bad..f6d4480867d 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx @@ -18,7 +18,7 @@ import { BREAKPOINTS } from 'theme' import { ClickableStyle, ThemedText } from 'theme/components' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx index 13e1a3826b5..4cc3bafbd7d 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx @@ -4,30 +4,30 @@ import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools import { CurrencySelect } from 'components/CurrencyInputPanel/SwapCurrencyInputPanel' import Column from 'components/deprecated/Column' import Row from 'components/deprecated/Row' +import { MobileBottomBar } from 'components/NavBar/MobileBottomBar' import { SwapWrapperOuter } from 'components/swap/styled' import { LoadingBubble } from 'components/Tokens/loading' import TokenSafetyMessage from 'components/TokenSafety/DeprecatedTokenSafetyMessage' import { getPriorityWarning, StrongWarning, useTokenWarning } from 'constants/deprecatedTokenSafety' import { NATIVE_CHAIN_ID } from 'constants/tokens' -import { useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' import { gqlToCurrency } from 'graphql/data/util' import { useScreenSize } from 'hooks/screenSize/useScreenSize' import { useAccount } from 'hooks/useAccount' +import { ScrollDirection, useScroll } from 'hooks/useScroll' import { useSwitchChain } from 'hooks/useSwitchChain' import styled from 'lib/styled-components' import { Swap } from 'pages/Swap' -import { useCallback, useMemo, useReducer, useState } from 'react' +import { ReactNode, useCallback, useReducer, useState } from 'react' import { Plus, X } from 'react-feather' import { useLocation, useNavigate } from 'react-router-dom' import { BREAKPOINTS } from 'theme' import { ClickableStyle, ThemedText } from 'theme/components' import { opacify } from 'theme/utils' import { Z_INDEX } from 'theme/zIndex' +import { Flex, useIsTouchDevice } from 'ui/src' import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' @@ -36,7 +36,6 @@ import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { Trans } from 'uniswap/src/i18n' import { currencyId } from 'uniswap/src/utils/currencyId' -import { NumberType, useFormatter } from 'utils/formatNumbers' const PoolDetailsStatsButtonsRow = styled(Row)` gap: 12px; @@ -59,18 +58,15 @@ const PoolDetailsStatsButtonsRow = styled(Row)` } z-index: ${Z_INDEX.sticky}; } - @media (max-width: ${BREAKPOINTS.md}px) { - bottom: 56px; - } ` -const PoolButton = styled.button<{ $open?: boolean; $hideOnMobile?: boolean; $fixedWidth?: boolean }>` +const PoolButton = styled.button<{ $open?: boolean; $fixedWidth?: boolean }>` display: flex; flex-direction: row; + flex: 1; padding: 12px 16px 12px 12px; border: unset; border-radius: 900px; - width: ${({ $open }) => ($open ? '100px' : '50%')}; gap: 8px; color: ${({ theme, $open }) => ($open ? theme.neutral1 : theme.accent1)}; background-color: ${({ theme, $open }) => ($open ? theme.surface1 : opacify(12, theme.accent1))}; @@ -82,14 +78,15 @@ const PoolButton = styled.button<{ $open?: boolean; $hideOnMobile?: boolean; $fi width: ${({ $fixedWidth }) => $fixedWidth && '120px'}; } @media (max-width: ${BREAKPOINTS.sm}px) { - display: ${({ $hideOnMobile }) => $hideOnMobile && 'none'}; width: ${({ $fixedWidth }) => !$fixedWidth && '100%'}; + background-color: ${({ theme, $open }) => ($open ? theme.surface1 : theme.accent1)}; + color: ${({ theme, $open }) => ($open ? theme.neutral1 : theme.white)}; } ` const ButtonBubble = styled(LoadingBubble)` height: 44px; - width: 175px; + width: 50%; border-radius: 900px; ` @@ -128,14 +125,6 @@ const SwapModalWrapper = styled(Column)<{ open?: boolean }>` } ` -const MobileBalance = styled(Column)` - gap: 2px; - display: none; - @media (max-width: ${BREAKPOINTS.lg}px) { - display: flex; - } -` - interface PoolDetailsStatsButtonsProps { chainId?: UniverseChainId token0?: Token @@ -162,7 +151,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load const position = userOwnedPositions && findMatchingPosition(userOwnedPositions, token0, token1, feeTier) const tokenId = position?.details.tokenId const switchChain = useSwitchChain() - const { defaultChainId } = useEnabledChains() const navigate = useNavigate() const location = useLocation() const currency0 = token0 && gqlToCurrency(token0) @@ -170,36 +158,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load const currencyInfo0 = useCurrencyInfo(currency0 && currencyId(currency0)) const currencyInfo1 = useCurrencyInfo(currency1 && currencyId(currency1)) - // Mobile Balance Data - const { data: balanceQuery } = useTokenBalancesQuery() - const { balance0, balance1, balance0Fiat, balance1Fiat } = useMemo(() => { - const filteredBalances = balanceQuery?.portfolios?.[0]?.tokenBalances?.filter( - (tokenBalance) => tokenBalance?.token?.chain === toGraphQLChain(chainId ?? defaultChainId), - ) - const tokenBalance0 = filteredBalances?.find((tokenBalance) => tokenBalance?.token?.address === token0?.address) - const tokenBalance1 = filteredBalances?.find((tokenBalance) => tokenBalance?.token?.address === token1?.address) - return { - balance0: tokenBalance0?.quantity ?? 0, - balance1: tokenBalance1?.quantity ?? 0, - balance0Fiat: tokenBalance0?.denominatedValue?.value ?? 0, - balance1Fiat: tokenBalance1?.denominatedValue?.value ?? 0, - } - }, [balanceQuery?.portfolios, chainId, defaultChainId, token0?.address, token1?.address]) - const { formatNumber } = useFormatter() - const formattedBalance0 = formatNumber({ - input: balance0, - type: NumberType.TokenNonTx, - }) - const formattedBalance1 = formatNumber({ - input: balance1, - type: NumberType.TokenNonTx, - }) - const totalFiatValue = balance0Fiat + balance1Fiat - const formattedFiatValue = formatNumber({ - input: totalFiatValue, - type: NumberType.PortfolioBalance, - }) - const handleAddLiquidity = async () => { if (currency0 && currency1) { if (account.chainId !== chainId && chainId) { @@ -213,9 +171,11 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load } } const [swapModalOpen, toggleSwapModalOpen] = useReducer((state) => !state, false) + const isScreenSize = useScreenSize() const screenSizeLargerThanTablet = isScreenSize['lg'] const isMobile = !isScreenSize['sm'] + const token0Warning = useTokenWarning(token0?.address, chainId) const token1Warning = useTokenWarning(token1?.address, chainId) const priorityWarning = getPriorityWarning(token0Warning, token1Warning) @@ -232,9 +192,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load if (loading || !currency0 || !currency1) { return ( - - - @@ -242,59 +199,38 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load } return ( - - - {account.address && ( - - - - - - - {formattedBalance0} {currency0.symbol} - - | - - {formattedBalance1} {currency1.symbol} - - {Boolean(totalFiatValue) && !isMobile && ({formattedFiatValue})} - - - )} - - {swapModalOpen ? ( - <> - {screenSizeLargerThanTablet && } - - - - - ) : ( - <> - {screenSizeLargerThanTablet && } - - - - - )} - - - {screenSizeLargerThanTablet && } - - - - - + + + + + {swapModalOpen ? ( + <> + + + + + + ) : ( + <> + + + + + + )} + + + + + + + + + - + ) } + +interface PoolButtonsWrapperProps { + children: ReactNode + isMobile: boolean +} + +function PoolButtonsWrapper({ children, isMobile }: PoolButtonsWrapperProps) { + const isTouchDevice = useIsTouchDevice() + const { direction: scrollDirection } = useScroll() + + // Determine wrapper component for pool buttons based on viewport size + const Wrapper = isMobile ? MobileBottomBar : PoolDetailsStatsButtonsRow + const wrapperProps = isMobile ? { hide: isTouchDevice && scrollDirection === ScrollDirection.DOWN } : {} + + return {children} +} diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap index b04a77f633a..7bf76dc0791 100644 --- a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap @@ -26,20 +26,6 @@ exports[`PoolDetailsStatsButton loading skeleton shown correctly 1`] = ` } .c3 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c5 { border-radius: 12px; border-radius: 12px; height: 24px; @@ -59,17 +45,12 @@ exports[`PoolDetailsStatsButton loading skeleton shown correctly 1`] = ` z-index: 1; } -.c6 { +.c4 { height: 44px; - width: 175px; + width: 50%; border-radius: 900px; } -.c4 { - gap: 2px; - display: none; -} - @media (max-width:1024px) { .c2 { gap: 8px; @@ -92,21 +73,6 @@ exports[`PoolDetailsStatsButton loading skeleton shown correctly 1`] = ` } } -@media (max-width:768px) { - .c2 { - bottom: 56px; - } -} - -@media (max-width:1024px) { - .c4 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } -} -
-
-
-
@@ -143,13 +102,13 @@ exports[`PoolDetailsStatsButton loading skeleton shown correctly 1`] = ` exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` - .c1 { + .c0 { box-sizing: border-box; margin: 0; min-width: 0; } -.c36 { +.c29 { box-sizing: border-box; margin: 0; min-width: 0; @@ -172,24 +131,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` border-radius: 4px; } -.c2 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c7 { +.c1 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -204,55 +146,30 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` -webkit-justify-content: flex-start; -ms-flex-pack: start; justify-content: flex-start; - gap: 8px; } -.c17 { +.c10 { -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } -.c19 { +.c12 { position: relative; width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; } -.c6 { - color: #7D7D7D; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; - letter-spacing: -0.01em; -} - -.c8 { - color: #222222; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; - letter-spacing: -0.01em; -} - -.c9 { - color: #CECECE; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; - letter-spacing: -0.01em; -} - -.c11 { +.c4 { -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } -.c22 { +.c15 { outline: none; border: none; font-size: inherit; @@ -268,12 +185,12 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` transition-property: opacity,color,background-color; } -.c22:focus { +.c15:focus { -webkit-text-decoration: underline; text-decoration: underline; } -.c38 { +.c31 { padding: 16px; width: 100%; line-height: 24px; @@ -312,25 +229,25 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` transform: perspective(1px) translateZ(0); } -.c38:disabled { +.c31:disabled { opacity: 50%; cursor: auto; pointer-events: none; } -.c38 > * { +.c31 > * { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } -.c38 > a { +.c31 > a { -webkit-text-decoration: none; text-decoration: none; } -.c51 { +.c44 { background-color: #FC72FF; font-size: 20px; font-weight: 535; @@ -338,21 +255,21 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` color: #FFFFFF; } -.c51:focus { +.c44:focus { box-shadow: 0 0 0 1pt #fb58ff; background-color: #fb58ff; } -.c51:hover { +.c44:hover { background-color: #fb58ff; } -.c51:active { +.c44:active { box-shadow: 0 0 0 1pt #fb3fff; background-color: #fb3fff; } -.c51:disabled { +.c44:disabled { background-color: #22222212; color: #7D7D7D; cursor: auto; @@ -361,7 +278,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` outline: none; } -.c39 { +.c32 { background-color: #FFFFFF; color: #7D7D7D; border: 1px solid #22222212; @@ -369,30 +286,15 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` font-weight: 535; } -.c39:hover { +.c32:hover { background-color: #ececec; } -.c39:active { +.c32:active { background-color: #e0e0e0; } -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 24px; -} - -.c4 { +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -406,18 +308,18 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` justify-content: flex-start; } -.c49 { +.c42 { display: grid; grid-auto-rows: auto; grid-row-gap: 4px; } -.c35 { +.c28 { display: inline-block; height: inherit; } -.c46 { +.c39 { -webkit-filter: none; filter: none; opacity: 1; @@ -425,7 +327,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` transition: opacity 250ms ease-in-out; } -.c53 { +.c46 { z-index: 1020; overflow: hidden; top: 0; @@ -438,7 +340,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` pointer-events: none; } -.c33 { +.c26 { color: #222222; pointer-events: auto; width: 0; @@ -459,36 +361,36 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` text-align: right; } -.c33::-webkit-search-decoration { +.c26::-webkit-search-decoration { -webkit-appearance: none; } -.c33 [type='number'] { +.c26 [type='number'] { -moz-appearance: textfield; } -.c33::-webkit-outer-spin-button, -.c33::-webkit-inner-spin-button { +.c26::-webkit-outer-spin-button, +.c26::-webkit-inner-spin-button { -webkit-appearance: none; } -.c33::-webkit-input-placeholder { +.c26::-webkit-input-placeholder { color: #CECECE; } -.c33::-moz-placeholder { +.c26::-moz-placeholder { color: #CECECE; } -.c33:-ms-input-placeholder { +.c26:-ms-input-placeholder { color: #CECECE; } -.c33::placeholder { +.c26::placeholder { color: #CECECE; } -.c34 { +.c27 { -webkit-filter: none; filter: none; opacity: 1; @@ -500,7 +402,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` max-height: 44px; } -.c30 { +.c23 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -517,13 +419,13 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` will-change: height; } -.c31 { +.c24 { min-height: 44px; border-radius: 20px; width: initial; } -.c40 { +.c33 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -556,12 +458,12 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` animation: none; } -.c40:hover, -.c40:active { +.c33:hover, +.c33:active { background-color: #F9F9F9; } -.c40:before { +.c33:before { background-size: 100%; border-radius: inherit; position: absolute; @@ -572,15 +474,15 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` content: ''; } -.c40:hover:before { +.c33:hover:before { background-color: #98A1C014; } -.c40:active:before { +.c33:active:before { background-color: #B8C0DC3d; } -.c32 { +.c25 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -599,7 +501,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` margin-top: 4px; } -.c44 { +.c37 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -616,12 +518,12 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` line-height: 1rem; } -.c44 span:hover { +.c37 span:hover { cursor: pointer; color: #4a4a4a; } -.c45 { +.c38 { -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; @@ -630,7 +532,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` padding: 8px 0px 0px 0px; } -.c41 { +.c34 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -646,24 +548,24 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` width: 100%; } -.c43 { +.c36 { margin: 0 0.25rem 0 0.35rem; height: 35%; margin-left: 8px; } -.c43 path { +.c36 path { stroke: #222222; stroke-width: 2px; } -.c42 { +.c35 { margin: 0 0.25rem 0 0.25rem; font-size: 20px; font-weight: 535; } -.c15 { +.c8 { position: relative; z-index: 1; -webkit-transition: -webkit-transform 250ms ease; @@ -672,12 +574,12 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` border-radius: 24px; } -.c16 { +.c9 { border-radius: 24px; z-index: -1; } -.c47 { +.c40 { border-radius: 12px; height: 40px; width: 40px; @@ -692,12 +594,12 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` z-index: 2; } -.c47:hover { +.c40:hover { cursor: pointer; opacity: 0.8; } -.c29 { +.c22 { background-color: #F9F9F9; border-radius: 16px; color: #7D7D7D; @@ -709,7 +611,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` position: relative; } -.c29:before { +.c22:before { box-sizing: border-box; background-size: 100%; border-radius: inherit; @@ -723,19 +625,19 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` border: 1px solid #F9F9F9; } -.c29:hover:before { +.c22:hover:before { border-color: #98A1C014; } -.c29:focus-within:before { +.c22:focus-within:before { border-color: #B8C0DC3d; } -.c50 { +.c43 { border-bottom: 1px solid #FFFFFF; } -.c48 { +.c41 { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -752,7 +654,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` height: 100%; } -.c23 { +.c16 { color: #222222; background-color: #22222212; padding: 8px 16px; @@ -761,17 +663,17 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` font-weight: 485; } -.c23:focus { +.c16:focus { -webkit-text-decoration: none; text-decoration: none; } -.c23:active { +.c16:active { -webkit-text-decoration: none; text-decoration: none; } -.c24 { +.c17 { color: #7D7D7D; padding: 8px 16px; border-radius: 20px; @@ -779,26 +681,26 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` font-weight: 485; } -.c24:focus { +.c17:focus { -webkit-text-decoration: none; text-decoration: none; } -.c24:active { +.c17:active { -webkit-text-decoration: none; text-decoration: none; } -.c28 { +.c21 { height: 24px; width: 24px; } -.c28 > * { +.c21 > * { fill: #7D7D7D; } -.c26 { +.c19 { border: none; background-color: transparent; margin: 0; @@ -807,75 +709,39 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` outline: none; } -.c26:not([disabled]):hover { +.c19:not([disabled]):hover { opacity: 0.7; } -.c27 { +.c20 { padding: 6px 12px; border-radius: 16px; } -.c25 { +.c18 { position: relative; } -.c18 { +.c11 { margin-bottom: 12px; padding-right: 4px; color: #7D7D7D; } -.c20 { +.c13 { gap: 0px; } -.c20 .c21 { +.c13 .c14 { padding: 8px 12px; } -.c3 { +.c2 { gap: 12px; z-index: 1; } -.c10 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - padding: 12px 16px 12px 12px; - border: unset; - border-radius: 900px; - width: 50%; - gap: 8px; - color: #FC72FF; - background-color: #FC72FF1f; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-transition: width 250ms ease-in-out; - transition: width 250ms ease-in-out; - -webkit-text-decoration: none; - text-decoration: none; - cursor: pointer; - -webkit-transition-duration: 125ms; - transition-duration: 125ms; -} - -.c10:hover { - opacity: 0.6; -} - -.c10:active { - opacity: 0.4; -} - -.c12 { +.c3 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -883,10 +749,12 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; padding: 12px 16px 12px 12px; border: unset; border-radius: 900px; - width: 50%; gap: 8px; color: #FC72FF; background-color: #FC72FF1f; @@ -903,15 +771,15 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` transition-duration: 125ms; } -.c12:hover { +.c3:hover { opacity: 0.6; } -.c12:active { +.c3:active { opacity: 0.4; } -.c13 { +.c6 { z-index: 0; gap: 24px; visibility: hidden; @@ -922,33 +790,28 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` padding-bottom: 0; } -.c13 .c14:before { +.c6 .c7:before { background-color: unset; } -.c13 .c37 { +.c6 .c30 { visibility: hidden; } -.c5 { - gap: 2px; - display: none; -} - @media (hover:hover) and (pointer:fine) { - .c22:hover { + .c15:hover { opacity: 0.6; } } @media (max-width:720px) { - .c52 { + .c45 { display: none; } } @media only screen and (max-width:1024px) { - .c53 { + .c46 { opacity: 0; pointer-events: none; -webkit-transition: opacity 250ms ease-in-out; @@ -957,7 +820,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` } @media (max-width:1024px) { - .c3 { + .c2 { gap: 8px; position: fixed; bottom: 0px; @@ -973,41 +836,25 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` z-index: 1020; } - .c3 > :first-child { + .c2 > :first-child { margin-right: auto; } } -@media (max-width:768px) { - .c3 { - bottom: 56px; - } -} - @media (max-width:1024px) { - .c10 { - width: 120px; - } -} -@media (max-width:640px) { - -} - -@media (max-width:1024px) { - .c12 { - width: 120px; - } } @media (max-width:640px) { - .c12 { - display: none; + .c3 { + width: 100%; + background-color: #FC72FF; + color: #FFFFFF; } } @media (max-width:1024px) { - .c13 { + .c6 { position: fixed; width: calc(100% - 16px); padding: 0px 12px 12px; @@ -1025,15 +872,6 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` } } -@media (max-width:1024px) { - .c5 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } -} -
-
- Your balances -
-
-
- 0 USDC -
+ +
- | + Swap
+ +
+
- -

Swap

@@ -1278,17 +1129,17 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = `

@@ -1317,18 +1168,18 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = `
@@ -1420,7 +1271,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = `

@@ -1429,7 +1280,7 @@ exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = `

diff --git a/apps/web/src/components/Pools/PoolTable/PoolTable.tsx b/apps/web/src/components/Pools/PoolTable/PoolTable.tsx index 10995cbe609..507d96eff23 100644 --- a/apps/web/src/components/Pools/PoolTable/PoolTable.tsx +++ b/apps/web/src/components/Pools/PoolTable/PoolTable.tsx @@ -26,7 +26,7 @@ import { ExternalLink, TamaguiClickableStyle } from 'theme/components' import { Flex, Text, styled } from 'ui/src' import { BIPS_BASE } from 'uniswap/src/constants/misc' import { Chain, ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' diff --git a/apps/web/src/components/Popups/PopupContent.tsx b/apps/web/src/components/Popups/PopupContent.tsx index 11ca59e7b8b..45e630cd85d 100644 --- a/apps/web/src/components/Popups/PopupContent.tsx +++ b/apps/web/src/components/Popups/PopupContent.tsx @@ -20,7 +20,7 @@ import { Flex, useSporeColors } from 'ui/src' import { BridgeIcon } from 'uniswap/src/components/CurrencyLogo/SplitLogo' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' diff --git a/apps/web/src/components/Popups/PopupItem.tsx b/apps/web/src/components/Popups/PopupItem.tsx index 9eebd20c3a3..43f3810a3a7 100644 --- a/apps/web/src/components/Popups/PopupItem.tsx +++ b/apps/web/src/components/Popups/PopupItem.tsx @@ -12,7 +12,7 @@ import { Flex, Text } from 'ui/src' import { Shuffle } from 'ui/src/components/icons/Shuffle' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { t } from 'uniswap/src/i18n' import { SwapTab } from 'uniswap/src/types/screens/interface' diff --git a/apps/web/src/components/PositionCard/index.tsx b/apps/web/src/components/PositionCard/index.tsx index d519157bb95..0db1fe63ae6 100644 --- a/apps/web/src/components/PositionCard/index.tsx +++ b/apps/web/src/components/PositionCard/index.tsx @@ -21,7 +21,7 @@ import { Link } from 'react-router-dom' import { Text } from 'rebass' import { useTokenBalance } from 'state/connection/hooks' import { StyledInternalLink, ThemedText } from 'theme/components' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' import { currencyId } from 'utils/currencyId' diff --git a/apps/web/src/components/ReceiveCryptoModal/ChooseProvider.tsx b/apps/web/src/components/ReceiveCryptoModal/ChooseProvider.tsx index c43efeefa1f..e60034764ad 100644 --- a/apps/web/src/components/ReceiveCryptoModal/ChooseProvider.tsx +++ b/apps/web/src/components/ReceiveCryptoModal/ChooseProvider.tsx @@ -7,16 +7,7 @@ import { useTheme } from 'lib/styled-components' import { useOpenModal, useToggleModal } from 'state/application/hooks' import { ApplicationModal } from 'state/application/reducer' import { CopyToClipboard } from 'theme/components' -import { - Button, - Flex, - GeneratedIcon, - HeightAnimator, - ImpactFeedbackStyle, - Separator, - Text, - TouchableArea, -} from 'ui/src' +import { Button, Flex, GeneratedIcon, HeightAnimator, Separator, Text, TouchableArea } from 'ui/src' import { CopySheets } from 'ui/src/components/icons/CopySheets' import { QrCode } from 'ui/src/components/icons/QrCode' import { iconSizes } from 'ui/src/theme' @@ -81,7 +72,7 @@ function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element { - + diff --git a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx index 035c3796fc6..7e3493b779a 100644 --- a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx +++ b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx @@ -20,6 +20,8 @@ export type RemoveLiquidityTxInfo = { decreaseCalldataLoading: boolean approvalLoading: boolean txContext?: ValidatedDecreasePositionTxAndGasInfo + error?: boolean + refetch?: () => void } const RemoveLiquidityTxContext = createContext(undefined) @@ -29,7 +31,7 @@ export function RemoveLiquidityTxContextProvider({ children }: PropsWithChildren const { positionInfo, percent } = useRemoveLiquidityModalContext() const removeLiquidityTxInfo = useRemoveLiquidityTxAndGasInfo({ account: account?.address }) - const { approvalLoading, decreaseCalldataLoading, decreaseCalldata } = removeLiquidityTxInfo + const { approvalLoading, decreaseCalldataLoading, decreaseCalldata, error, refetch } = removeLiquidityTxInfo const datadogEnabled = useFeatureFlag(FeatureFlags.Datadog) useEffect(() => { @@ -71,7 +73,9 @@ export function RemoveLiquidityTxContextProvider({ children }: PropsWithChildren }, [approvalLoading, positionInfo, decreaseCalldataLoading, decreaseCalldata, removeLiquidityTxInfo, percent]) return ( - + {children} ) diff --git a/apps/web/src/components/RemoveLiquidity/hooks.ts b/apps/web/src/components/RemoveLiquidity/hooks.ts index f31d170ecea..c36d331b00d 100644 --- a/apps/web/src/components/RemoveLiquidity/hooks.ts +++ b/apps/web/src/components/RemoveLiquidity/hooks.ts @@ -15,10 +15,13 @@ import { ProtocolItems, } from 'uniswap/src/data/tradingApi/__generated__' import { useTransactionGasFee, useUSDCurrencyAmountOfGasFee } from 'uniswap/src/features/gas/hooks' +import { getTradeSettingsDeadline } from 'uniswap/src/features/transactions/swap/form/utils' +import { useSwapSettingsContext } from 'uniswap/src/features/transactions/swap/settings/contexts/SwapSettingsContext' import { ONE_SECOND_MS } from 'utilities/src/time/time' export function useRemoveLiquidityTxAndGasInfo({ account }: { account?: string }): RemoveLiquidityTxInfo { const { positionInfo, percent, percentInvalid } = useRemoveLiquidityModalContext() + const { customDeadline, customSlippageTolerance } = useSwapSettingsContext() const pool = positionInfo?.version === ProtocolVersion.V3 || positionInfo?.version === ProtocolVersion.V4 @@ -40,7 +43,12 @@ export function useRemoveLiquidityTxAndGasInfo({ account }: { account?: string } .quotient.toString(), } }, [positionInfo, percent, account, percentInvalid]) - const { data: v2LpTokenApproval, isLoading: v2ApprovalLoading } = useCheckLpApprovalQuery({ + const { + data: v2LpTokenApproval, + isLoading: v2ApprovalLoading, + error: approvalError, + refetch: approvalRefetch, + } = useCheckLpApprovalQuery({ params: v2LpTokenApprovalQueryParams, staleTime: 5 * ONE_SECOND_MS, }) @@ -59,6 +67,9 @@ export function useRemoveLiquidityTxAndGasInfo({ account }: { account?: string } if (!positionInfo || !apiProtocolItems || !account || percentInvalid) { return undefined } + + const deadline = getTradeSettingsDeadline(customDeadline) + return { simulateTransaction: !approvalsNeeded, protocol: apiProtocolItems, @@ -96,10 +107,28 @@ export function useRemoveLiquidityTxAndGasInfo({ account }: { account?: string } hooks: positionInfo.v4hook, }, }, + deadline, + slippageTolerance: customSlippageTolerance, } - }, [account, positionInfo, percentInvalid, percent, pool, approvalsNeeded, feeValue0, feeValue1]) + }, [ + account, + positionInfo, + percentInvalid, + percent, + pool, + approvalsNeeded, + feeValue0, + feeValue1, + customDeadline, + customSlippageTolerance, + ]) - const { data: decreaseCalldata, isLoading: decreaseCalldataLoading } = useDecreaseLpPositionCalldataQuery({ + const { + data: decreaseCalldata, + isLoading: decreaseCalldataLoading, + error: calldataError, + refetch: calldataRefetch, + } = useDecreaseLpPositionCalldataQuery({ params: decreaseCalldataQueryParams, staleTime: 5 * ONE_SECOND_MS, }) @@ -117,5 +146,7 @@ export function useRemoveLiquidityTxAndGasInfo({ account }: { account?: string } decreaseCalldata, v2LpTokenApproval, approvalLoading: v2ApprovalLoading, + error: Boolean(approvalError || calldataError), + refetch: approvalError ? approvalRefetch : calldataError ? calldataRefetch : undefined, } } diff --git a/apps/web/src/components/SearchModal/CurrencySearch.tsx b/apps/web/src/components/SearchModal/CurrencySearch.tsx index 53704bd5ce1..8c6c118ad52 100644 --- a/apps/web/src/components/SearchModal/CurrencySearch.tsx +++ b/apps/web/src/components/SearchModal/CurrencySearch.tsx @@ -9,7 +9,7 @@ import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { Flex } from 'ui/src' import { TokenSelectorContent, TokenSelectorVariation } from 'uniswap/src/components/TokenSelector/TokenSelector' import { TokenSelectorFlow } from 'uniswap/src/components/TokenSelector/types' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import Trace from 'uniswap/src/features/telemetry/Trace' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' diff --git a/apps/web/src/components/Settings/index.test.tsx b/apps/web/src/components/Settings/index.test.tsx index 442e38e0fff..913943576d2 100644 --- a/apps/web/src/components/Settings/index.test.tsx +++ b/apps/web/src/components/Settings/index.test.tsx @@ -4,13 +4,16 @@ import { useAccount } from 'hooks/useAccount' import { useIsUniswapXSupportedChain } from 'hooks/useIsUniswapXSupportedChain' import { mocked } from 'test-utils/mocked' import { fireEvent, render, screen, waitFor } from 'test-utils/render' -import { useEnabledChains, useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' const slippage = new Percent(75, 10_000) jest.mock('hooks/useIsUniswapXSupportedChain') -jest.mock('uniswap/src/features/chains/hooks', () => ({ +jest.mock('uniswap/src/features/chains/hooks/useEnabledChains', () => ({ useEnabledChains: jest.fn(), +})) +jest.mock('uniswap/src/features/chains/hooks/useSupportedChainId', () => ({ useIsSupportedChainId: jest.fn(), })) jest.mock('hooks/useAccount') diff --git a/apps/web/src/components/Settings/index.tsx b/apps/web/src/components/Settings/index.tsx index 7ea5b60ff3c..67e5af87607 100644 --- a/apps/web/src/components/Settings/index.tsx +++ b/apps/web/src/components/Settings/index.tsx @@ -22,7 +22,7 @@ import { InterfaceTrade } from 'state/routing/types' import { isUniswapXTrade } from 'state/routing/utils' import { Divider, ThemedText } from 'theme/components' import { Z_INDEX } from 'theme/zIndex' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { isL2ChainId } from 'uniswap/src/features/chains/utils' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' diff --git a/apps/web/src/components/SwapBottomCard.tsx b/apps/web/src/components/SwapBottomCard.tsx index 25065fd49d6..dfd71156a59 100644 --- a/apps/web/src/components/SwapBottomCard.tsx +++ b/apps/web/src/components/SwapBottomCard.tsx @@ -19,7 +19,7 @@ import { selectHasViewedBridgingBanner } from 'uniswap/src/features/behaviorHist import { setHasViewedBridgingBanner } from 'uniswap/src/features/behaviorHistory/slice' import { useIsBridgingChain, useNumBridgingChains } from 'uniswap/src/features/bridging/hooks/chains' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useTranslation } from 'uniswap/src/i18n' import { ONE_SECOND_MS } from 'utilities/src/time/time' diff --git a/apps/web/src/components/Table/styled.tsx b/apps/web/src/components/Table/styled.tsx index a9de95e74c7..f49f30c813d 100644 --- a/apps/web/src/components/Table/styled.tsx +++ b/apps/web/src/components/Table/styled.tsx @@ -13,7 +13,7 @@ import { ClickableStyle, ClickableTamaguiStyle, EllipsisTamaguiStyle, ThemedText import { Z_INDEX } from 'theme/zIndex' import { Anchor, Flex, Text, View, styled } from 'ui/src' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { useCurrentLocale } from 'uniswap/src/features/language/hooks' import { useTranslation } from 'uniswap/src/i18n' diff --git a/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx b/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx index 22b9f9b0a06..3bbdf6dc90b 100644 --- a/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx @@ -10,7 +10,7 @@ import { useNavigate } from 'react-router-dom' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' import { NumberType, useFormatter } from 'utils/formatNumbers' diff --git a/apps/web/src/components/Tokens/TokenDetails/InvalidTokenDetails.tsx b/apps/web/src/components/Tokens/TokenDetails/InvalidTokenDetails.tsx index 9584a258561..570317f452e 100644 --- a/apps/web/src/components/Tokens/TokenDetails/InvalidTokenDetails.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/InvalidTokenDetails.tsx @@ -5,7 +5,7 @@ import useSelectChain from 'hooks/useSelectChain' import styled from 'lib/styled-components' import { useNavigate } from 'react-router-dom' import { ThemedText } from 'theme/components' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getChainLabel } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' diff --git a/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx b/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx index 0ad811b8704..8ab7a34f9ee 100644 --- a/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx @@ -8,7 +8,7 @@ import { ReactNode } from 'react' import { ExternalLink, ThemedText } from 'theme/components' import { textFadeIn } from 'theme/styles' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' import { NumberType, useFormatter } from 'utils/formatNumbers' diff --git a/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx index b670e144532..fcb21893302 100644 --- a/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx @@ -44,7 +44,8 @@ const TokenInfoButton = styled(Text, { variant: 'buttonLabel3', display: 'flex', flexDirection: 'row', - gap: '$gap8', + alignItems: 'center', + gap: '$gap4', py: '$padding8', px: '$padding12', borderRadius: '$rounded20', @@ -82,7 +83,7 @@ const TRUNCATE_CHARACTER_COUNT = 200 export function TokenDescription() { const { address, currency, tokenQuery } = useTDPContext() - const { neutral2 } = useTheme() + const { neutral1 } = useTheme() const { description, homepageUrl, twitterName } = tokenQuery.data?.token?.project ?? {} const explorerUrl = getExplorerLink( @@ -118,14 +119,14 @@ export function TokenDescription() { {!currency.isNative && ( - + {shortenAddress(currency.address)} )} - + {currency.chainId === UniverseChainId.Mainnet ? ( ) : ( @@ -136,7 +137,7 @@ export function TokenDescription() { {homepageUrl && ( - + @@ -144,7 +145,7 @@ export function TokenDescription() { {twitterName && ( - + diff --git a/apps/web/src/components/Tokens/TokenDetails/__snapshots__/TokenDescription.test.tsx.snap b/apps/web/src/components/Tokens/TokenDetails/__snapshots__/TokenDescription.test.tsx.snap index cfda3a1b0fc..e6c6367a794 100644 --- a/apps/web/src/components/Tokens/TokenDetails/__snapshots__/TokenDescription.test.tsx.snap +++ b/apps/web/src/components/Tokens/TokenDetails/__snapshots__/TokenDescription.test.tsx.snap @@ -60,13 +60,13 @@ exports[`TokenDescription no description or social buttons shown when not availa class="c0" > Etherscan @@ -204,13 +204,13 @@ exports[`TokenDescription renders token information correctly with defaults 1`] class="c0" > Etherscan @@ -270,11 +270,11 @@ exports[`TokenDescription renders token information correctly with defaults 1`] target="_blank" > Website @@ -296,11 +296,11 @@ exports[`TokenDescription renders token information correctly with defaults 1`] target="_blank" > Twitter @@ -413,13 +413,13 @@ exports[`TokenDescription truncates description and shows more 1`] = ` class="c0" > Etherscan @@ -479,11 +479,11 @@ exports[`TokenDescription truncates description and shows more 1`] = ` target="_blank" > Website @@ -505,11 +505,11 @@ exports[`TokenDescription truncates description and shows more 1`] = ` target="_blank" > Twitter diff --git a/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx b/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx index 2b316b49531..1045620b2e5 100644 --- a/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx +++ b/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx @@ -10,10 +10,14 @@ import { Dispatch, SetStateAction, memo, useCallback, useState } from 'react' import { Check } from 'react-feather' import { useNavigate } from 'react-router-dom' import { EllipsisTamaguiStyle } from 'theme/components' -import { Flex, FlexProps, ScrollView, Text, styled } from 'ui/src' +import { ElementAfterText, Flex, FlexProps, ScrollView, styled } from 'ui/src' +import { NewTag } from 'uniswap/src/components/pill/NewTag' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useEnabledChains, useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks' -import { ALL_CHAIN_IDS, GqlChainId, UniverseChainId, UniverseChainInfo } from 'uniswap/src/features/chains/types' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useNewChainIds } from 'uniswap/src/features/chains/hooks/useNewChainIds' +import { useOrderedChainIds } from 'uniswap/src/features/chains/hooks/useOrderedChainIds' +import { useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks/useSupportedChainId' +import { ALL_CHAIN_IDS, UniverseChainId, UniverseChainInfo } from 'uniswap/src/features/chains/types' import { isBackendSupportedChainId, isTestnetChain, toGraphQLChain } from 'uniswap/src/features/chains/utils' import Trace from 'uniswap/src/features/telemetry/Trace' import { ModalName } from 'uniswap/src/features/telemetry/constants' @@ -48,6 +52,7 @@ export default function TableNetworkFilter({ showMultichainOption = true }: { sh const [isMenuOpen, toggleMenu] = useState(false) const isSupportedChainCallback = useIsSupportedChainIdCallback() const { isTestnetModeEnabled } = useEnabledChains() + const orderedChainIds = useOrderedChainIds(ALL_CHAIN_IDS) const exploreParams = useExploreParams() const currentChainId = useChainIdFromUrlParam() @@ -63,7 +68,6 @@ export default function TableNetworkFilter({ showMultichainOption = true }: { sh return ( - {showMultichainOption && } + {showMultichainOption && } {/* non-testnet backend supported chains */} - {ALL_CHAIN_IDS.filter(isBackendSupportedChainId) + {orderedChainIds + .filter(isBackendSupportedChainId) .filter((c) => !isTestnetChain(c)) .map(tableNetworkItemRenderer)} {/* Testnet backend supported chains */} {isTestnetModeEnabled - ? ALL_CHAIN_IDS.filter(isBackendSupportedChainId).filter(isTestnetChain).map(tableNetworkItemRenderer) + ? orderedChainIds.filter(isBackendSupportedChainId).filter(isTestnetChain).map(tableNetworkItemRenderer) : null} {/* Unsupported non-testnet backend supported chains */} - {ALL_CHAIN_IDS.filter((c) => !isBackendSupportedChainId(c) && !isTestnetChain(c)).map( - tableNetworkItemRenderer, - )} + {orderedChainIds + .filter((c) => !isBackendSupportedChainId(c) && !isTestnetChain(c)) + .map(tableNetworkItemRenderer)} } buttonStyle={{ height: 40 }} @@ -119,14 +124,12 @@ export default function TableNetworkFilter({ showMultichainOption = true }: { sh } const TableNetworkItem = memo(function TableNetworkItem({ - display, chainInfo, toggleMenu, tab, unsupported, }: { - display: 'All networks' | GqlChainId - chainInfo?: UniverseChainInfo + chainInfo: UniverseChainInfo | null toggleMenu: Dispatch> tab?: ExploreTab unsupported?: boolean @@ -134,36 +137,43 @@ const TableNetworkItem = memo(function TableNetworkItem({ const navigate = useNavigate() const theme = useTheme() const { t } = useTranslation() - const chainId = chainInfo?.id const exploreParams = useExploreParams() const urlChainId = useChainIdFromUrlParam() const currentChainInfo = urlChainId ? getChainInfo(urlChainId) : undefined + const newChains = useNewChainIds() + + const isAllNetworks = chainInfo === null + const chainId = isAllNetworks ? undefined : chainInfo.id + const isNew = chainId && newChains.includes(chainId) + + const chainName = chainId ? toGraphQLChain(chainId) : 'All networks' + + const isCurrentChain = isAllNetworks ? !currentChainInfo : currentChainInfo?.id === chainId && exploreParams.chainName - const isAllNetworks = display === 'All networks' - const isCurrentChain = isAllNetworks - ? !currentChainInfo - : currentChainInfo?.backendChain.chain === display && exploreParams.chainName return ( { !unsupported && - navigate(`/explore/${tab ?? ExploreTab.Tokens}${!isAllNetworks ? `/${display.toLowerCase()}` : ''}`) + navigate(`/explore/${tab ?? ExploreTab.Tokens}${!isAllNetworks ? `/${chainName.toLowerCase()}` : ''}`) toggleMenu(false) }} > {isAllNetworks ? : }{' '} - - {!isAllNetworks ? chainInfo?.label : t('transaction.network.all')} - + : undefined} + /> + {/* separate from ElementAfterText as this is placed at the far right of the row, not next to the text */} {unsupported ? ( {t('settings.setting.beta.tooltip')} ) : isCurrentChain ? ( diff --git a/apps/web/src/components/Tokens/TokenTable/index.tsx b/apps/web/src/components/Tokens/TokenTable/index.tsx index fe4ca010338..7dbfa5a558c 100644 --- a/apps/web/src/components/Tokens/TokenTable/index.tsx +++ b/apps/web/src/components/Tokens/TokenTable/index.tsx @@ -28,7 +28,7 @@ import { TABLE_PAGE_SIZE, giveExploreStatDefaultValue } from 'state/explore' import { useTopTokens as useRestTopTokens } from 'state/explore/topTokens' import { TokenStat } from 'state/explore/types' import { Flex, Text, styled } from 'ui/src' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' import { getChainIdFromChainUrlParam } from 'utils/chainParams' diff --git a/apps/web/src/components/TransactionConfirmationModal/index.tsx b/apps/web/src/components/TransactionConfirmationModal/index.tsx index aeff239ef2c..daacce5aa7b 100644 --- a/apps/web/src/components/TransactionConfirmationModal/index.tsx +++ b/apps/web/src/components/TransactionConfirmationModal/index.tsx @@ -19,7 +19,7 @@ import { isConfirmedTx } from 'state/transactions/utils' import { CloseIcon, CustomLightSpinner, ExternalLink, ThemedText } from 'theme/components' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isL2ChainId } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' diff --git a/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx b/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx index 93030d6be97..193a6206401 100644 --- a/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx +++ b/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx @@ -3,6 +3,7 @@ import { GooglePlayStoreLogo } from 'components/Icons/GooglePlayStoreLogo' import { DownloadWalletOption } from 'components/WalletModal/DownloadWalletOption' import { DetectedBadge } from 'components/WalletModal/shared' import { useConnectorWithId } from 'components/WalletModal/useOrderedConnections' +import { uniswapWalletConnect } from 'components/Web3Provider/walletConnect' import Column from 'components/deprecated/Column' import Row from 'components/deprecated/Row' import { useConnect } from 'hooks/useConnect' @@ -45,12 +46,6 @@ export const AppIcon = styled.img` export function UniswapWalletOptions() { const uniswapExtensionConnector = useConnectorWithId(CONNECTION_PROVIDER_IDS.UNISWAP_EXTENSION_RDNS) - const uniswapWalletConnectConnector = useConnectorWithId( - CONNECTION_PROVIDER_IDS.UNISWAP_WALLET_CONNECT_CONNECTOR_ID, - { - shouldThrow: true, - }, - ) const { connect } = useConnect() @@ -75,7 +70,17 @@ export function UniswapWalletOptions() { !isMobileWeb ? ( ) : null} - connect({ connector: uniswapWalletConnectConnector })}> + { + connect({ + // Initialize Uniswap Wallet on click instead of in wagmi config + // to avoid multiple wallet connect sockets being opened + // and causing issues with messages getting dropped + connector: uniswapWalletConnect(), + }) + }} + > {isMobileWeb ? ( ) : ( diff --git a/apps/web/src/components/WalletModal/index.tsx b/apps/web/src/components/WalletModal/index.tsx index f8b69e6ffef..6166a15ae3d 100644 --- a/apps/web/src/components/WalletModal/index.tsx +++ b/apps/web/src/components/WalletModal/index.tsx @@ -73,7 +73,7 @@ const StyledCollapsedIcon = styled(CollapsedIcon)` export default function WalletModal() { const { t } = useTranslation() const showMoonpayText = useShowMoonpayText() - const connectors = useOrderedConnections(true /** exclude uniswap connectors since they're shown separately */) + const connectors = useOrderedConnections() const isUniExtensionAvailable = useIsUniExtensionAvailable() const [showOtherWallets, toggleShowOtherWallets] = useReducer((s) => !s, true) diff --git a/apps/web/src/components/WalletModal/useOrderedConnections.test.tsx b/apps/web/src/components/WalletModal/useOrderedConnections.test.tsx index 0013af1d794..3a03cb97e8a 100644 --- a/apps/web/src/components/WalletModal/useOrderedConnections.test.tsx +++ b/apps/web/src/components/WalletModal/useOrderedConnections.test.tsx @@ -31,7 +31,6 @@ jest.mock('components/Web3Provider/constants', () => ({ })) const DEFAULT_CONNECTORS = [ - UNISWAP_MOBILE_CONNECTOR, INJECTED_CONNECTOR, WALLET_CONNECT_CONNECTOR, COINBASE_SDK_CONNECTOR, @@ -50,7 +49,6 @@ describe('useOrderedConnections', () => { const { result } = renderHook(() => useOrderedConnections()) const expectedConnectors = [ - { id: CONNECTION_PROVIDER_IDS.UNISWAP_WALLET_CONNECT_CONNECTOR_ID }, { id: CONNECTION_PROVIDER_IDS.METAMASK_RDNS }, { id: CONNECTION_PROVIDER_IDS.WALLET_CONNECT_CONNECTOR_ID }, { id: CONNECTION_PROVIDER_IDS.COINBASE_SDK_CONNECTOR_ID }, @@ -77,7 +75,6 @@ describe('useOrderedConnections', () => { const expectedConnectors = [ { id: CONNECTION_PROVIDER_IDS.WALLET_CONNECT_CONNECTOR_ID }, - { id: CONNECTION_PROVIDER_IDS.UNISWAP_WALLET_CONNECT_CONNECTOR_ID }, { id: CONNECTION_PROVIDER_IDS.METAMASK_RDNS }, { id: CONNECTION_PROVIDER_IDS.COINBASE_SDK_CONNECTOR_ID }, ] @@ -98,24 +95,18 @@ describe('useOrderedConnections', () => { it('should return only the Coinbase injected connector in the Coinbase Wallet', async () => { UserAgentMock.isMobileWeb = true mocked(useConnect).mockReturnValue({ - connectors: [ - UNISWAP_MOBILE_CONNECTOR, - INJECTED_CONNECTOR, - WALLET_CONNECT_CONNECTOR, - COINBASE_SDK_CONNECTOR, - COINBASE_INJECTED_CONNECTOR, - ], + connectors: [INJECTED_CONNECTOR, WALLET_CONNECT_CONNECTOR, COINBASE_SDK_CONNECTOR, COINBASE_INJECTED_CONNECTOR], } as unknown as ReturnType) const { result } = renderHook(() => useOrderedConnections()) expect(result.current.length).toEqual(1) expect(result.current[0].id).toEqual(CONNECTION_PROVIDER_IDS.COINBASE_SDK_CONNECTOR_ID) }) - it('should not return uniswap connections when excludeUniswapConnections is true', () => { + it('should not return uniswap connections', () => { mocked(useConnect).mockReturnValue({ connectors: [...DEFAULT_CONNECTORS, UNISWAP_EXTENSION_CONNECTOR], } as unknown as ReturnType) - const { result } = renderHook(() => useOrderedConnections(true)) + const { result } = renderHook(() => useOrderedConnections()) const expectedConnectors = [ { id: CONNECTION_PROVIDER_IDS.METAMASK_RDNS }, @@ -136,7 +127,6 @@ describe('useOrderedConnections', () => { } as unknown as ReturnType) const { result } = renderHook(() => useOrderedConnections()) const expectedConnectors = [ - { id: CONNECTION_PROVIDER_IDS.UNISWAP_WALLET_CONNECT_CONNECTOR_ID }, { id: CONNECTION_PROVIDER_IDS.INJECTED_CONNECTOR_ID }, { id: CONNECTION_PROVIDER_IDS.WALLET_CONNECT_CONNECTOR_ID }, { id: CONNECTION_PROVIDER_IDS.COINBASE_SDK_CONNECTOR_ID }, @@ -151,7 +141,7 @@ describe('useOrderedConnections', () => { UserAgentMock.isMobileWeb = true window.ethereum = true as any mocked(useConnect).mockReturnValue({ - connectors: [UNISWAP_MOBILE_CONNECTOR, INJECTED_CONNECTOR, WALLET_CONNECT_CONNECTOR, COINBASE_SDK_CONNECTOR], + connectors: [INJECTED_CONNECTOR, WALLET_CONNECT_CONNECTOR, COINBASE_SDK_CONNECTOR], } as unknown as ReturnType) const { result } = renderHook(() => useOrderedConnections()) const expectedConnectors = [{ id: CONNECTION_PROVIDER_IDS.INJECTED_CONNECTOR_ID }] diff --git a/apps/web/src/components/WalletModal/useOrderedConnections.tsx b/apps/web/src/components/WalletModal/useOrderedConnections.tsx index 05ad7358030..87d30e6ea91 100644 --- a/apps/web/src/components/WalletModal/useOrderedConnections.tsx +++ b/apps/web/src/components/WalletModal/useOrderedConnections.tsx @@ -2,7 +2,7 @@ import { useRecentConnectorId } from 'components/Web3Provider/constants' import { useConnect } from 'hooks/useConnect' import { useCallback, useMemo } from 'react' import { CONNECTION_PROVIDER_IDS } from 'uniswap/src/constants/web3' -import { isMobileWeb, isTouchable, isWebAndroid, isWebIOS } from 'utilities/src/platform' +import { isMobileWeb } from 'utilities/src/platform' import { Connector } from 'wagmi' type ConnectorID = (typeof CONNECTION_PROVIDER_IDS)[keyof typeof CONNECTION_PROVIDER_IDS] @@ -38,7 +38,7 @@ export function useConnectorWithId(id: ConnectorID, options?: { shouldThrow: tru ) } -function getInjectedConnectors(connectors: readonly Connector[], excludeUniswapConnections?: boolean) { +function getInjectedConnectors(connectors: readonly Connector[]) { let isCoinbaseWalletBrowser = false const injectedConnectors = connectors.filter((c) => { // Special-case: Ignore coinbase eip6963-injected connector; coinbase connection is handled via the SDK connector. @@ -50,7 +50,7 @@ function getInjectedConnectors(connectors: readonly Connector[], excludeUniswapC } // Special-case: Ignore the Uniswap Extension injection here if it's being displayed separately. - if (c.id === CONNECTION_PROVIDER_IDS.UNISWAP_EXTENSION_RDNS && excludeUniswapConnections) { + if (c.id === CONNECTION_PROVIDER_IDS.UNISWAP_EXTENSION_RDNS) { return false } @@ -71,8 +71,12 @@ function getInjectedConnectors(connectors: readonly Connector[], excludeUniswapC return { injectedConnectors, isCoinbaseWalletBrowser } } +/** + * These connectors do not include Uniswap Wallets because those are + * handled separately. See + */ type InjectableConnector = Connector & { isInjected?: boolean } -export function useOrderedConnections(excludeUniswapConnections?: boolean): InjectableConnector[] { +export function useOrderedConnections(): InjectableConnector[] { const { connectors } = useConnect() const recentConnectorId = useRecentConnectorId() @@ -90,10 +94,7 @@ export function useOrderedConnections(excludeUniswapConnections?: boolean): Inje ) return useMemo(() => { - const { injectedConnectors: injectedConnectorsBase, isCoinbaseWalletBrowser } = getInjectedConnectors( - connectors, - excludeUniswapConnections, - ) + const { injectedConnectors: injectedConnectorsBase, isCoinbaseWalletBrowser } = getInjectedConnectors(connectors) const injectedConnectors = injectedConnectorsBase.map((c) => ({ ...c, isInjected: true })) const coinbaseSdkConnector = getConnectorWithId( @@ -106,12 +107,7 @@ export function useOrderedConnections(excludeUniswapConnections?: boolean): Inje CONNECTION_PROVIDER_IDS.WALLET_CONNECT_CONNECTOR_ID, SHOULD_THROW, ) - const uniswapWalletConnectConnector = getConnectorWithId( - connectors, - CONNECTION_PROVIDER_IDS.UNISWAP_WALLET_CONNECT_CONNECTOR_ID, - SHOULD_THROW, - ) - if (!coinbaseSdkConnector || !walletConnectConnector || !uniswapWalletConnectConnector) { + if (!coinbaseSdkConnector || !walletConnectConnector) { throw new Error('Expected connector(s) missing from wagmi context.') } @@ -126,12 +122,6 @@ export function useOrderedConnections(excludeUniswapConnections?: boolean): Inje } const orderedConnectors: InjectableConnector[] = [] - const shouldDisplayUniswapWallet = !excludeUniswapConnections && (isWebIOS || isWebAndroid || !isTouchable) - - // Place the Uniswap Wallet at the top of the list by default. - if (shouldDisplayUniswapWallet) { - orderedConnectors.push(uniswapWalletConnectConnector) - } // Injected connectors should appear next in the list, as the user intentionally installed/uses them. orderedConnectors.push(...injectedConnectors) @@ -143,7 +133,7 @@ export function useOrderedConnections(excludeUniswapConnections?: boolean): Inje // Place the most recent connector at the top of the list. orderedConnectors.sort(sortByRecent) return orderedConnectors - }, [connectors, excludeUniswapConnections, sortByRecent]) + }, [connectors, sortByRecent]) } export enum ExtensionRequestMethods { diff --git a/apps/web/src/components/Web3Provider/index.tsx b/apps/web/src/components/Web3Provider/index.tsx index 063ed5988bf..ac09b7166ca 100644 --- a/apps/web/src/components/Web3Provider/index.tsx +++ b/apps/web/src/components/Web3Provider/index.tsx @@ -13,7 +13,7 @@ import { useUpdateAtom } from 'jotai/utils' import { ReactNode, useEffect } from 'react' import { useLocation } from 'react-router-dom' import { useConnectedWallets } from 'state/wallets/hooks' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/web/src/components/Web3Provider/wagmiConfig.ts b/apps/web/src/components/Web3Provider/wagmiConfig.ts index 73c116df417..2a8aadf6d45 100644 --- a/apps/web/src/components/Web3Provider/wagmiConfig.ts +++ b/apps/web/src/components/Web3Provider/wagmiConfig.ts @@ -1,6 +1,6 @@ import { QueryClient } from '@tanstack/react-query' import { injectedWithFallback } from 'components/Web3Provider/injectedWithFallback' -import { WC_PARAMS, uniswapWalletConnect } from 'components/Web3Provider/walletConnect' +import { WC_PARAMS } from 'components/Web3Provider/walletConnect' import { UNISWAP_LOGO } from 'ui/src/assets' import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' @@ -21,7 +21,6 @@ export const wagmiConfig = createConfig({ connectors: [ injectedWithFallback(), walletConnect(WC_PARAMS), - uniswapWalletConnect(), coinbaseWallet({ appName: 'Uniswap', // CB SDK doesn't pass the parent origin context to their passkey site diff --git a/apps/web/src/components/Web3Provider/walletConnect.ts b/apps/web/src/components/Web3Provider/walletConnect.ts index 68bf6f92a02..766ea1086e5 100644 --- a/apps/web/src/components/Web3Provider/walletConnect.ts +++ b/apps/web/src/components/Web3Provider/walletConnect.ts @@ -55,7 +55,9 @@ export function uniswapWalletConnect() { if (type === 'display_uri') { // Emits custom wallet connect code, parseable by the Uniswap Wallet const uniswapWalletUri = `https://uniswap.org/app/wc?uri=${data}` - config.emitter.emit('message', { type: 'display_uniswap_uri', data: uniswapWalletUri }) + + // Emits custom event to display the Uniswap Wallet URI + window.dispatchEvent(new MessageEvent('display_uniswap_uri', { data: uniswapWalletUri })) // Opens deeplink to Uniswap Wallet if on mobile if (isWebIOS || isWebAndroid) { diff --git a/apps/web/src/components/swap/GasBreakdownTooltip.tsx b/apps/web/src/components/swap/GasBreakdownTooltip.tsx index 1a8345780aa..7aab19f3060 100644 --- a/apps/web/src/components/swap/GasBreakdownTooltip.tsx +++ b/apps/web/src/components/swap/GasBreakdownTooltip.tsx @@ -9,7 +9,8 @@ import { isPreviewTrade, isUniswapXTrade } from 'state/routing/utils' import { Divider, ExternalLink, ThemedText } from 'theme/components' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' import { NumberType, useFormatter } from 'utils/formatNumbers' diff --git a/apps/web/src/components/swap/SwapHeader.test.tsx b/apps/web/src/components/swap/SwapHeader.test.tsx index 13cdbadfb53..3ebea8bbbcc 100644 --- a/apps/web/src/components/swap/SwapHeader.test.tsx +++ b/apps/web/src/components/swap/SwapHeader.test.tsx @@ -17,6 +17,7 @@ function Wrapper(props: PropsWithChildren) { return ( void }) => { - if ((loadingMoreV4.current && v4Enabled) || loadingMoreV3.current || loadingMoreV2.current) { + if ((loadingMoreV4.current && isV4DataEnabled) || loadingMoreV3.current || loadingMoreV2.current) { return } loadingMoreV4.current = true @@ -137,11 +137,19 @@ export function usePoolsFromTokenAddress( }, }) }, - [dataV2?.topV2Pairs, dataV3?.topV3Pools, dataV4?.topV4Pools, fetchMoreV2, fetchMoreV3, fetchMoreV4, v4Enabled], + [ + dataV2?.topV2Pairs, + dataV3?.topV3Pools, + dataV4?.topV4Pools, + fetchMoreV2, + fetchMoreV3, + fetchMoreV4, + isV4DataEnabled, + ], ) return useMemo(() => { - const topV4Pools: TablePool[] = v4Enabled + const topV4Pools: TablePool[] = isV4DataEnabled ? dataV4?.topV4Pools?.map((pool) => { return { hash: pool.poolId, @@ -201,6 +209,6 @@ export function usePoolsFromTokenAddress( loadMore, loading, sortState, - v4Enabled, + isV4DataEnabled, ]) } diff --git a/apps/web/src/graphql/data/useTokenTransactions.ts b/apps/web/src/graphql/data/useTokenTransactions.ts index 7ca9ce7fe4b..8982e1aac26 100644 --- a/apps/web/src/graphql/data/useTokenTransactions.ts +++ b/apps/web/src/graphql/data/useTokenTransactions.ts @@ -6,7 +6,7 @@ import { useV2TokenTransactionsQuery, useV3TokenTransactionsQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' diff --git a/apps/web/src/hooks/Tokens.ts b/apps/web/src/hooks/Tokens.ts index 185a35bdcaf..3efe8cad12f 100644 --- a/apps/web/src/hooks/Tokens.ts +++ b/apps/web/src/hooks/Tokens.ts @@ -4,7 +4,7 @@ import { useAccount } from 'hooks/useAccount' import { useMemo } from 'react' import { COMMON_BASES } from 'uniswap/src/constants/routing' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useCurrencyInfo as useUniswapCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' diff --git a/apps/web/src/hooks/useAccount.ts b/apps/web/src/hooks/useAccount.ts index 17a89eeedfe..f7a7c7f1dff 100644 --- a/apps/web/src/hooks/useAccount.ts +++ b/apps/web/src/hooks/useAccount.ts @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/no-undefined-or */ import { useMemo } from 'react' -import { useSupportedChainIdWithConnector } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainIdWithConnector } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { UseAccountReturnType as UseAccountReturnTypeWagmi, useAccount as useAccountWagmi, useChainId } from 'wagmi' diff --git a/apps/web/src/hooks/useFeeTierDistribution.ts b/apps/web/src/hooks/useFeeTierDistribution.ts index 862d4794ba9..c787035d5f5 100644 --- a/apps/web/src/hooks/useFeeTierDistribution.ts +++ b/apps/web/src/hooks/useFeeTierDistribution.ts @@ -7,7 +7,7 @@ import { useFeeTierDistributionQuery, useIsV3SubgraphStaleQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { logger } from 'utilities/src/logger/logger' diff --git a/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts b/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts index 9566af6d4a8..d75e141db81 100644 --- a/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts +++ b/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts @@ -12,7 +12,7 @@ import { TokenDocument, TokenQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { hasURL } from 'utils/urlChecks' diff --git a/apps/web/src/hooks/useGlobalChainSwitch.ts b/apps/web/src/hooks/useGlobalChainSwitch.ts index 2e381f0b666..8c7a8f5a43e 100644 --- a/apps/web/src/hooks/useGlobalChainSwitch.ts +++ b/apps/web/src/hooks/useGlobalChainSwitch.ts @@ -2,7 +2,7 @@ import { useAccount } from 'hooks/useAccount' import { useEffect } from 'react' import { useAppSelector } from 'state/hooks' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' export const useOnGlobalChainSwitch = (callback: (chainId: number, chain?: Chain) => void) => { diff --git a/apps/web/src/hooks/usePoolPriceChartData.tsx b/apps/web/src/hooks/usePoolPriceChartData.tsx index 077954034ac..e5c65992ffa 100644 --- a/apps/web/src/hooks/usePoolPriceChartData.tsx +++ b/apps/web/src/hooks/usePoolPriceChartData.tsx @@ -30,22 +30,17 @@ export function usePoolPriceChartData( currencyB: OptionalCurrency, protocolVersion: ProtocolVersion, sortedCurrencyAAddress: string, - isReversed: boolean, ): ChartQueryResult { - const { data, loading } = usePoolPriceHistoryQuery({ variables }) + const { data, loading } = usePoolPriceHistoryQuery({ variables, skip: !variables?.addressOrId }) return useMemo(() => { const { priceHistory } = data?.v2Pair ?? data?.v3Pool ?? {} - const referenceToken = isReversed ? currencyA : currencyB const entries = priceHistory ?.filter((price): price is TimestampedPoolPrice => price !== null) .map((price) => { - const value = isSameAddress( - sortedCurrencyAAddress, - getCurrencyAddressWithWrap(referenceToken, protocolVersion), - ) + const value = isSameAddress(sortedCurrencyAAddress, getCurrencyAddressWithWrap(currencyA, protocolVersion)) ? price?.token0Price : price?.token1Price @@ -64,5 +59,5 @@ export function usePoolPriceChartData( const dataQuality = loading || !priceHistory || !priceHistory.length ? DataQuality.INVALID : DataQuality.VALID return { chartType: ChartType.PRICE, entries, loading, dataQuality } - }, [data?.v2Pair, data?.v3Pool, isReversed, loading, currencyA, currencyB, sortedCurrencyAAddress, protocolVersion]) + }, [data?.v2Pair, data?.v3Pool, loading, currencyA, sortedCurrencyAAddress, protocolVersion]) } diff --git a/apps/web/src/hooks/usePoolTickData.ts b/apps/web/src/hooks/usePoolTickData.ts index 1080d4ec93b..53f4dbfdf18 100644 --- a/apps/web/src/hooks/usePoolTickData.ts +++ b/apps/web/src/hooks/usePoolTickData.ts @@ -1,4 +1,4 @@ -import { Currency, Price, Token, V3_CORE_FACTORY_ADDRESSES } from '@uniswap/sdk-core' +import { Currency, V3_CORE_FACTORY_ADDRESSES } from '@uniswap/sdk-core' import { FeeAmount, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk' import { TickData, Ticks } from 'graphql/data/AllV3TicksQuery' import { PoolState, usePoolMultichain } from 'hooks/usePools' @@ -7,23 +7,15 @@ import ms from 'ms' import { useEffect, useMemo, useState } from 'react' import { useMultichainContext } from 'state/multichain/useMultichainContext' import { useAllV3TicksQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { logger } from 'utilities/src/logger/logger' -import computeSurroundingTicks from 'utils/computeSurroundingTicks' +import computeSurroundingTicks, { TickProcessed } from 'utils/computeSurroundingTicks' const PRICE_FIXED_DIGITS = 8 -// Tick with fields parsed to JSBIs, and active liquidity computed. -export interface TickProcessed { - tick: number - liquidityActive: JSBI - liquidityNet: JSBI - price0: string - sdkPrice: Price -} - const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | undefined) => tickCurrent && feeAmount ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined diff --git a/apps/web/src/hooks/useSendCallback.ts b/apps/web/src/hooks/useSendCallback.ts index 8158d511b70..64c7adef294 100644 --- a/apps/web/src/hooks/useSendCallback.ts +++ b/apps/web/src/hooks/useSendCallback.ts @@ -9,7 +9,7 @@ import { useCallback, useRef } from 'react' import { useTransactionAdder } from 'state/transactions/hooks' import { SendTransactionInfo, TransactionType } from 'state/transactions/types' import { trace } from 'tracing/trace' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { currencyId } from 'utils/currencyId' import { UserRejectedRequestError, toReadableError } from 'utils/errors' diff --git a/apps/web/src/hooks/useStablecoinPrice.ts b/apps/web/src/hooks/useStablecoinPrice.ts index 831055f13c5..d66b475373e 100644 --- a/apps/web/src/hooks/useStablecoinPrice.ts +++ b/apps/web/src/hooks/useStablecoinPrice.ts @@ -5,7 +5,7 @@ import { useMemo, useRef } from 'react' import { ClassicTrade, INTERNAL_ROUTER_PREFERENCE_PRICE, TradeState } from 'state/routing/types' import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' /** * Returns the price in USDC of the input currency diff --git a/apps/web/src/hooks/useSwapCallback.tsx b/apps/web/src/hooks/useSwapCallback.tsx index 3e5a9db78b9..ab41f461475 100644 --- a/apps/web/src/hooks/useSwapCallback.tsx +++ b/apps/web/src/hooks/useSwapCallback.tsx @@ -19,7 +19,7 @@ import { ExactOutputSwapTransactionInfo, TransactionType, } from 'state/transactions/types' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { currencyId } from 'utils/currencyId' diff --git a/apps/web/src/hooks/useSwitchChain.ts b/apps/web/src/hooks/useSwitchChain.ts index ac343b7ad05..3fe666ef403 100644 --- a/apps/web/src/hooks/useSwitchChain.ts +++ b/apps/web/src/hooks/useSwitchChain.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react' import { useDispatch } from 'react-redux' import { endSwitchingChain, startSwitchingChain } from 'state/wallets/reducer' import { trace } from 'tracing/trace' -import { useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useSwitchChain as useSwitchChainWagmi } from 'wagmi' diff --git a/apps/web/src/hooks/useTokenBalances.ts b/apps/web/src/hooks/useTokenBalances.ts index ee10382e314..7c982c89d26 100644 --- a/apps/web/src/hooks/useTokenBalances.ts +++ b/apps/web/src/hooks/useTokenBalances.ts @@ -7,7 +7,7 @@ import { QuickTokenBalancePartsFragment, useQuickTokenBalancesWebQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { currencyKeyFromGraphQL } from 'utils/currencyKey' /** diff --git a/apps/web/src/hooks/useUSDPrice.ts b/apps/web/src/hooks/useUSDPrice.ts index 8e9c4470e14..e945101cba6 100644 --- a/apps/web/src/hooks/useUSDPrice.ts +++ b/apps/web/src/hooks/useUSDPrice.ts @@ -8,7 +8,8 @@ import { ClassicTrade, INTERNAL_ROUTER_PREFERENCE_PRICE, TradeState } from 'stat import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { Chain, useTokenSpotPriceQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains, useIsSupportedChainId, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useIsSupportedChainId, useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { getNativeTokenDBAddress } from 'utils/nativeTokens' diff --git a/apps/web/src/hooks/useUSDTokenUpdater.ts b/apps/web/src/hooks/useUSDTokenUpdater.ts index 2e21baee27e..1191e355549 100644 --- a/apps/web/src/hooks/useUSDTokenUpdater.ts +++ b/apps/web/src/hooks/useUSDTokenUpdater.ts @@ -4,7 +4,7 @@ import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { useMemo } from 'react' import { TradeState } from 'state/routing/types' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { NumberType, useFormatter } from 'utils/formatNumbers' const NUM_DECIMALS_USD = 2 diff --git a/apps/web/src/nft/components/bag/BagFooter.tsx b/apps/web/src/nft/components/bag/BagFooter.tsx index 278d943522b..08355edfd33 100644 --- a/apps/web/src/nft/components/bag/BagFooter.tsx +++ b/apps/web/src/nft/components/bag/BagFooter.tsx @@ -35,7 +35,7 @@ import { PropsWithChildren, useEffect, useMemo, useState } from 'react' import { AlertTriangle, ChevronDown } from 'react-feather' import { InterfaceTrade, TradeFillType, TradeState } from 'state/routing/types' import { ThemedText } from 'theme/components' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/web/src/pages/AddLiquidityV2/redirects.tsx b/apps/web/src/pages/AddLiquidityV2/redirects.tsx index cbd9bcd18e3..bfc730132ed 100644 --- a/apps/web/src/pages/AddLiquidityV2/redirects.tsx +++ b/apps/web/src/pages/AddLiquidityV2/redirects.tsx @@ -4,9 +4,9 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' export default function AddLiquidityV2WithTokenRedirects() { - const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const isLPRedesignEnabled = useFeatureFlag(FeatureFlags.LPRedesign) const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string }>() - if (isV4EverywhereEnabled) { + if (isLPRedesignEnabled) { // TODO(WEB-5361): update this to enable prefilling form from URL currencyIdA and currencyIdB return } diff --git a/apps/web/src/pages/AddLiquidityV3/index.tsx b/apps/web/src/pages/AddLiquidityV3/index.tsx index 8c9ad9e973d..940e9bac0c8 100644 --- a/apps/web/src/pages/AddLiquidityV3/index.tsx +++ b/apps/web/src/pages/AddLiquidityV3/index.tsx @@ -81,7 +81,7 @@ import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { ThemedText } from 'theme/components' import { Text } from 'ui/src' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId, isUniverseChainId } from 'uniswap/src/features/chains/types' import { getChainLabel } from 'uniswap/src/features/chains/utils' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -500,7 +500,14 @@ function AddLiquidity() { const { [Bound.LOWER]: priceLower, [Bound.UPPER]: priceUpper } = pricesAtTicks const { getDecrementLower, getIncrementLower, getDecrementUpper, getIncrementUpper, getSetFullRange } = - useRangeHopCallbacks(baseCurrency ?? undefined, quoteCurrency ?? undefined, feeAmount, tickLower, tickUpper, pool) + useRangeHopCallbacks({ + baseCurrency, + quoteCurrency, + feeAmount, + tickLower, + tickUpper, + pool, + }) // we need an existence check on parsed amounts for single-asset deposits const showApprovalA = diff --git a/apps/web/src/pages/AddLiquidityV3/redirects.tsx b/apps/web/src/pages/AddLiquidityV3/redirects.tsx index 3d6fa744754..d8be3c84f6b 100644 --- a/apps/web/src/pages/AddLiquidityV3/redirects.tsx +++ b/apps/web/src/pages/AddLiquidityV3/redirects.tsx @@ -7,12 +7,12 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' export default function AddLiquidityV3WithTokenRedirects() { - const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const isLPRedesignEnabled = useFeatureFlag(FeatureFlags.LPRedesign) const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string; feeAmount?: string }>() const { chainId } = useAccount() - if (isV4EverywhereEnabled) { + if (isLPRedesignEnabled) { // TODO(WEB-5361): update this to enable prefilling form from URL currencyIdA and currencyIdB return } diff --git a/apps/web/src/pages/App/Layout.tsx b/apps/web/src/pages/App/Layout.tsx index 6c5314f065b..27652e7a397 100644 --- a/apps/web/src/pages/App/Layout.tsx +++ b/apps/web/src/pages/App/Layout.tsx @@ -6,6 +6,8 @@ import { BREAKPOINTS } from 'theme' const AppContainer = styled.div` min-height: 100vh; + max-width: 100vw; + overflow-x: hidden; // grid container settings display: grid; diff --git a/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx b/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx index 16d1db78414..9ba0304e09b 100644 --- a/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx +++ b/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx @@ -72,11 +72,9 @@ function VolumeChartSection() { const [timePeriod, setTimePeriod] = useState(TimePeriod.DAY) const theme = useTheme() const isSmallScreen = !useScreenSize()['sm'] - const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( - FeatureFlags.V4Everywhere, - ) - const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading - const EXPLORE_PRICE_SOURCES = isV4EverywhereEnabled ? EXPLORE_PRICE_SOURCES_V4 : EXPLORE_PRICE_SOURCES_V3 + const { value: isV4DataEnabledLoaded, isLoading: isV4DataLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Data) + const isV4DataEnabled = isV4DataEnabledLoaded || isV4DataLoading + const EXPLORE_PRICE_SOURCES = isV4DataEnabled ? EXPLORE_PRICE_SOURCES_V4 : EXPLORE_PRICE_SOURCES_V3 const refitChartContent = useAtomValue(refitChartContentAtom) function timeGranularityToHistoryDuration(timePeriod: TimePeriod): HistoryDuration { @@ -98,14 +96,14 @@ function VolumeChartSection() { ) const protocolColors = useMemo( () => - isV4EverywhereEnabled + isV4DataEnabled ? [ getProtocolColor(PriceSource.SubgraphV4, theme), getProtocolColor(PriceSource.SubgraphV3, theme), getProtocolColor(PriceSource.SubgraphV2, theme), ] : [getProtocolColor(PriceSource.SubgraphV3, theme), getProtocolColor(PriceSource.SubgraphV2, theme)], - [isV4EverywhereEnabled, theme], + [isV4DataEnabled, theme], ) const params = useMemo<{ data: StackedHistogramData[] @@ -188,11 +186,9 @@ function VolumeChartSection() { function TVLChartSection() { const theme = useTheme() - const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( - FeatureFlags.V4Everywhere, - ) - const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading - const EXPLORE_PRICE_SOURCES = isV4EverywhereEnabled ? EXPLORE_PRICE_SOURCES_V4 : EXPLORE_PRICE_SOURCES_V3 + const { value: isV4DataEnabledLoaded, isLoading: isV4DataLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Data) + const isV4DataEnabled = isV4DataEnabledLoaded || isV4DataLoading + const EXPLORE_PRICE_SOURCES = isV4DataEnabled ? EXPLORE_PRICE_SOURCES_V4 : EXPLORE_PRICE_SOURCES_V3 const { entries, loading, dataQuality } = useRestDailyProtocolTVL() const lastEntry = entries[entries.length - 1] diff --git a/apps/web/src/pages/Explore/index.tsx b/apps/web/src/pages/Explore/index.tsx index 81ef08c15f1..cab71879fa8 100644 --- a/apps/web/src/pages/Explore/index.tsx +++ b/apps/web/src/pages/Explore/index.tsx @@ -92,7 +92,7 @@ const HeaderTab = tamaguiStyled(Text, { const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { const tabNavRef = useRef(null) const resetManualOutage = useResetAtom(manualChainOutageAtom) - const v4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const isLPRedesignEnabled = useFeatureFlag(FeatureFlags.LPRedesign) const initialKey: number = useMemo(() => { const key = initialTab && Pages.findIndex((page) => page.key === initialTab) @@ -108,7 +108,9 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { const offsetTop = tabNavRef.current.getBoundingClientRect().top + window.scrollY window.scrollTo({ top: offsetTop - 90, behavior: 'smooth' }) } - }, [initialTab]) + // scroll to tab navbar on initial page mount only + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) const [currentTab, setCurrentTab] = useState(initialKey) @@ -179,6 +181,7 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { data-testid="explore-navbar" > {Pages.map(({ title, loggingElementName, key }, index) => { + const url = getTokenExploreURL({ tab: key, chain: chainInfo?.backendChain.chain }) return ( { element={loggingElementName} key={index} > - { - // Update tab and only replace url when navigating from the header nav - setCurrentTab(index) - }} - active={currentTab === index} - key={key} - > + navigate(url)} active={currentTab === index} key={key}> {title} @@ -201,7 +197,7 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { })} - {currentKey === ExploreTab.Pools && v4EverywhereEnabled && ( + {currentKey === ExploreTab.Pools && isLPRedesignEnabled && ( + ) +} + +const Toolbar = ({ + defaultInitialToken, + isV4DataEnabled, +}: { + defaultInitialToken: Currency + isV4DataEnabled: boolean +}) => { + const { positionState, setPositionState, setStep } = useCreatePositionContext() + const { protocolVersion } = positionState + const { setPriceRangeState } = usePriceRangeContext() + const navigate = useNavigate() const handleVersionChange = useCallback( (version: ProtocolVersion) => { @@ -209,7 +270,7 @@ const Toolbar = () => { currencyInputs: prevState.currencyInputs, protocolVersion: version, })) - setPriceRangeState(DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS) + setPriceRangeState(DEFAULT_PRICE_RANGE_STATE) setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) }, [setPositionState, setPriceRangeState, setStep, navigate], @@ -217,7 +278,10 @@ const Toolbar = () => { const versionOptions = useMemo( () => - [ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2] + (isV4DataEnabled + ? [ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2] + : [ProtocolVersion.V3, ProtocolVersion.V2] + ) .filter((version) => version != protocolVersion) .map((version) => ({ key: `version-${version}`, @@ -230,23 +294,21 @@ const Toolbar = () => { ), })), - [handleVersionChange, protocolVersion], + [handleVersionChange, protocolVersion, isV4DataEnabled], ) return ( - + + + - + ) } export function CreatePosition() { - const { value: v4Enabled, isLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Everywhere) + const { value: lpRedesignEnabled, isLoading } = useFeatureFlagWithLoading(FeatureFlags.LPRedesign) + const isV4DataEnabled = useFeatureFlag(FeatureFlags.V4Data) + const { protocolVersion } = useParams<{ protocolVersion: string }>() + const { defaultChainId } = useEnabledChains() + const paramsProtocolVersion = parseProtocolVersion(protocolVersion) + const initialProtocolVersion = useMemo((): ProtocolVersion => { + if (isV4DataEnabled) { + return paramsProtocolVersion ?? ProtocolVersion.V4 + } + + if (!paramsProtocolVersion || paramsProtocolVersion === ProtocolVersion.V4) { + return ProtocolVersion.V3 + } + + return paramsProtocolVersion + }, [isV4DataEnabled, paramsProtocolVersion]) + const media = useMedia() + const defaultInitialToken = nativeOnChain(defaultChainId) - if (!isLoading && !v4Enabled) { + if (!isLoading && !lpRedesignEnabled) { return } @@ -314,53 +376,55 @@ export function CreatePosition() { return ( - - - - - - - - - - - - - - - - - - - - - {!media.xl && } - - - + + + + + + + + + + + + + + + + + + + + + + {!media.xl && } + + + + - - - - - + + + + + ) diff --git a/apps/web/src/pages/Pool/Positions/create/CreatePositionContext.tsx b/apps/web/src/pages/Pool/Positions/create/CreatePositionContext.tsx index 850a13e77d8..291c7f51e30 100644 --- a/apps/web/src/pages/Pool/Positions/create/CreatePositionContext.tsx +++ b/apps/web/src/pages/Pool/Positions/create/CreatePositionContext.tsx @@ -13,6 +13,7 @@ import { PositionField } from 'types/position' import { CreatePositionTxAndGasInfo } from 'uniswap/src/features/transactions/liquidity/types' export const CreatePositionContext = React.createContext({ + reset: () => undefined, step: PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER, setStep: () => undefined, positionState: DEFAULT_POSITION_STATE, @@ -35,7 +36,7 @@ export const useCreatePositionContext = () => { return useContext(CreatePositionContext) } -export const DEFAULT_PRICE_RANGE_STATE_CREATING_POOL: PriceRangeState = { +export const DEFAULT_PRICE_RANGE_STATE: PriceRangeState = { priceInverted: false, fullRange: true, minPrice: '', @@ -44,17 +45,9 @@ export const DEFAULT_PRICE_RANGE_STATE_CREATING_POOL: PriceRangeState = { initialPriceInverted: false, } -export const DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS: PriceRangeState = { - priceInverted: false, - fullRange: false, - minPrice: undefined, - maxPrice: undefined, - initialPrice: '', - initialPriceInverted: false, -} - export const PriceRangeContext = React.createContext({ - priceRangeState: DEFAULT_PRICE_RANGE_STATE_CREATING_POOL, + reset: () => undefined, + priceRangeState: DEFAULT_PRICE_RANGE_STATE, setPriceRangeState: () => undefined, derivedPriceRangeInfo: { protocolVersion: ProtocolVersion.V4, @@ -83,6 +76,7 @@ export const DEFAULT_DEPOSIT_STATE: DepositState = { } export const DepositContext = React.createContext({ + reset: () => undefined, depositState: DEFAULT_DEPOSIT_STATE, setDepositState: () => undefined, derivedDepositInfo: {}, @@ -92,7 +86,11 @@ export const useDepositContext = () => { return useContext(DepositContext) } -export const CreateTxContext = React.createContext(undefined) +export const CreateTxContext = React.createContext<{ + txInfo?: CreatePositionTxAndGasInfo + error?: boolean + refetch?: () => void +}>({}) export const useCreateTxContext = () => { return useContext(CreateTxContext) diff --git a/apps/web/src/pages/Pool/Positions/create/CreatePositionModal.tsx b/apps/web/src/pages/Pool/Positions/create/CreatePositionModal.tsx index e9c5b4ca5c5..4637ca7a9bd 100644 --- a/apps/web/src/pages/Pool/Positions/create/CreatePositionModal.tsx +++ b/apps/web/src/pages/Pool/Positions/create/CreatePositionModal.tsx @@ -1,6 +1,6 @@ // eslint-disable-next-line no-restricted-imports -// eslint-disable-next-line no-restricted-imports import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { LoaderButton } from 'components/Button/LoaderButton' import { ButtonError } from 'components/Button/buttons' import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' import { getLPBaseAnalyticsProperties } from 'components/Liquidity/analytics' @@ -17,6 +17,7 @@ import { usePriceRangeContext, } from 'pages/Pool/Positions/create/CreatePositionContext' import { PoolOutOfSyncError } from 'pages/Pool/Positions/create/PoolOutOfSyncError' +import { TradingAPIError } from 'pages/Pool/Positions/create/TradingAPIError' import { formatPrices } from 'pages/Pool/Positions/create/shared' import { getInvertedTuple, getPoolIdOrAddressFromCreatePositionInfo } from 'pages/Pool/Positions/create/utils' import { useCallback, useMemo, useState } from 'react' @@ -67,7 +68,7 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl const [steps, setSteps] = useState([]) const [currentStep, setCurrentStep] = useState<{ step: TransactionStep; accepted: boolean } | undefined>() const dispatch = useDispatch() - const createTxContext = useCreateTxContext() + const { txInfo, error, refetch } = useCreateTxContext() const account = useAccountMeta() const selectChain = useSelectChain() const startChainId = useAccount().chainId @@ -86,7 +87,7 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl }, [onClose, navigate]) const handleCreate = useCallback(() => { - const isValidTx = isValidLiquidityTxContext(createTxContext) + const isValidTx = isValidLiquidityTxContext(txInfo) if ( !account || account?.type !== AccountType.SignerMnemonic || @@ -102,7 +103,7 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl selectChain, startChainId, account, - liquidityTxContext: createTxContext, + liquidityTxContext: txInfo, setCurrentStep, setSteps, onSuccess, @@ -127,7 +128,7 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl }), ) }, [ - createTxContext, + txInfo, account, currencyAmounts?.TOKEN0, currencyAmounts?.TOKEN1, @@ -145,9 +146,16 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl ]) return ( - - - + + + @@ -156,7 +164,7 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl } closeModal={() => onClose()} /> - + @@ -195,7 +203,7 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl )} - + @@ -247,12 +255,21 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl /> + {error && } {currentStep ? ( - - ) : !isPoolOutOfSync || !createTxContext?.action ? ( - )} @@ -116,7 +120,7 @@ function EmptyPositionsView({ isConnected }: { isConnected: boolean }) { pageSize={10} staticSize /> - + {t('explore.more.pools')} @@ -146,12 +150,15 @@ function LearnMoreTile({ img, text, link }: { img: string; text: string; link?: } const chainFilterAtom = atom(null) -const versionFilterAtom = atom([ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2]) +const versionFilterAtom = atom([ProtocolVersion.V3, ProtocolVersion.V2]) const statusFilterAtom = atom([PositionStatus.IN_RANGE, PositionStatus.OUT_OF_RANGE]) export default function Pool() { const { t } = useTranslation() - const { value: v4Enabled, isLoading: isLoadingFeatureFlag } = useFeatureFlagWithLoading(FeatureFlags.V4Everywhere) + const { value: lpRedesignEnabled, isLoading: isLoadingFeatureFlag } = useFeatureFlagWithLoading( + FeatureFlags.LPRedesign, + ) + const isV4DataEnabled = useFeatureFlag(FeatureFlags.V4Data) const [chainFilter, setChainFilter] = useAtom(chainFilterAtom) const { chains: currentModeChains } = useEnabledChains() @@ -163,7 +170,13 @@ export default function Pool() { const { address, isConnected } = account const [currentPage, setCurrentPage] = useState(0) - const { data, isPlaceholderData, refetch } = useGetPositionsQuery( + useEffect(() => { + if (isV4DataEnabled) { + setVersionFilter([ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2]) + } + }, [isV4DataEnabled, setVersionFilter]) + + const { data, isPlaceholderData, refetch, isLoading } = useGetPositionsQuery( { address, chainIds: chainFilter ? [chainFilter] : currentModeChains, @@ -172,7 +185,7 @@ export default function Pool() { }, !isConnected, ) - const isLoadingPositions = !data && account.address + const isLoadingPositions = isLoading || (!data && account.address) usePendingLPTransactionsChangeListener(refetch) @@ -184,7 +197,7 @@ export default function Pool() { const pageCount = data?.positions ? Math.ceil(data?.positions.length / PAGE_SIZE) : undefined const showingEmptyPositions = !isLoadingPositions && currentPageItems.length === 0 - if (!isLoadingFeatureFlag && !v4Enabled) { + if (!isLoadingFeatureFlag && !lpRedesignEnabled) { return } @@ -204,154 +217,152 @@ export default function Pool() { px="$spacing40" $lg={{ px: '$spacing20' }} > - - - { - setChainFilter(selectedChain ?? null) - }} - onVersionChange={(toggledVersion) => { - if (versionFilter?.includes(toggledVersion)) { - setVersionFilter(versionFilter?.filter((v) => v !== toggledVersion)) - } else { - setVersionFilter([...(versionFilter ?? []), toggledVersion]) - } - }} - onStatusChange={(toggledStatus) => { - if (statusFilter?.includes(toggledStatus)) { - setStatusFilter(statusFilter?.filter((s) => s !== toggledStatus)) - } else { - setStatusFilter([...(statusFilter ?? []), toggledStatus]) - } - }} - /> - {!isLoadingPositions ? ( - currentPageItems.length > 0 ? ( - - {currentPageItems.map((position, index) => { - return ( - position && ( - - - - ) - ) - })} - - ) : ( - - ) - ) : ( - - - - - - - - - - - - - - )} - {!statusFilter.includes(PositionStatus.CLOSED) && !closedCTADismissed && account.address && ( - - - - - - - - - - - - - setClosedCTADismissed(true)} cursor="pointer"> - - - - )} - {!!pageCount && pageCount > 1 && data?.positions && ( - - { - setCurrentPage(Math.max(currentPage - 1, 0)) - }} - /> - {Array.from({ length: pageCount > 5 ? 3 : pageCount }).map((_, index) => { - const isSelected = currentPage === index + + { + setChainFilter(selectedChain ?? null) + }} + onVersionChange={(toggledVersion) => { + if (versionFilter?.includes(toggledVersion)) { + setVersionFilter(versionFilter?.filter((v) => v !== toggledVersion)) + } else { + setVersionFilter([...(versionFilter ?? []), toggledVersion]) + } + }} + onStatusChange={(toggledStatus) => { + if (statusFilter?.includes(toggledStatus)) { + setStatusFilter(statusFilter?.filter((s) => s !== toggledStatus)) + } else { + setStatusFilter([...(statusFilter ?? []), toggledStatus]) + } + }} + /> + {!isLoadingPositions ? ( + currentPageItems.length > 0 ? ( + + {currentPageItems.map((position) => { return ( - setCurrentPage(index)} - > - {index + 1} - + position && ( + + + + ) ) })} - {pageCount > 5 && ( - - ... - - )} - {pageCount > 5 && ( + + ) : ( + + ) + ) : ( + + + + + + + + + + + + + + )} + {!statusFilter.includes(PositionStatus.CLOSED) && !closedCTADismissed && account.address && ( + + + + + + + + + + + + + setClosedCTADismissed(true)} cursor="pointer"> + + + + )} + {!!pageCount && pageCount > 1 && data?.positions && ( + + { + setCurrentPage(Math.max(currentPage - 1, 0)) + }} + /> + {Array.from({ length: pageCount > 5 ? 3 : pageCount }).map((_, index) => { + const isSelected = currentPage === index + return ( setCurrentPage(pageCount - 1)} + onPress={() => setCurrentPage(index)} > - {pageCount} + {index + 1} - )} - 5 && ( + + ... + + )} + {pageCount > 5 && ( + { - setCurrentPage(Math.min(currentPage + 1, pageCount - 1)) - }} - /> - - )} - + onPress={() => setCurrentPage(pageCount - 1)} + > + {pageCount} + + )} + { + setCurrentPage(Math.min(currentPage + 1, pageCount - 1)) + }} + /> + + )} - {!showingEmptyPositions && } + {!showingEmptyPositions && } {t('liquidity.learnMoreLabel')} diff --git a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx index 78ca79e3ab2..7071c6140f1 100644 --- a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx +++ b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx @@ -7,6 +7,7 @@ import { useRemoveLiquidityModalContext, } from 'components/RemoveLiquidity/RemoveLiquidityModalContext' import { useRemoveLiquidityTxContext } from 'components/RemoveLiquidity/RemoveLiquidityTxContext' +import { TradingAPIError } from 'pages/Pool/Positions/create/TradingAPIError' import { ClickablePill } from 'pages/Swap/Buy/PredefinedAmount' import { NumericalInputMimic, NumericalInputSymbolContainer, NumericalInputWrapper } from 'pages/Swap/common/shared' import { Flex, Text, useSporeColors } from 'ui/src' @@ -19,9 +20,7 @@ export function RemoveLiquidityForm() { const colors = useSporeColors() const { percent, positionInfo, setPercent, setStep, percentInvalid } = useRemoveLiquidityModalContext() - const removeLiquidityTxContext = useRemoveLiquidityTxContext() - - const { gasFeeEstimateUSD, txContext } = removeLiquidityTxContext + const { gasFeeEstimateUSD, txContext, error, refetch } = useRemoveLiquidityTxContext() if (!positionInfo) { throw new Error('RemoveLiquidityModal must have an initial state when opening') @@ -84,10 +83,11 @@ export function RemoveLiquidityForm() { currency1Amount={currency1Amount} networkCost={gasFeeEstimateUSD} /> + {error && } setStep(DecreaseLiquidityStep.Review)} - loading={!percentInvalid && !txContext?.txRequest} + loading={!error && !percentInvalid && !txContext?.txRequest} buttonKey="RemoveLiquidity-continue" > diff --git a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx index 20732a3cc45..f7973671cf4 100644 --- a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx +++ b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx @@ -11,7 +11,9 @@ import { RemoveLiquidityForm } from 'pages/RemoveLiquidity/RemoveLiquidityForm' import { useCloseModal } from 'state/application/hooks' import { HeightAnimator } from 'ui/src' import { Modal } from 'uniswap/src/components/modals/Modal' +import { MIN_AUTO_SLIPPAGE_TOLERANCE } from 'uniswap/src/constants/transactions' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { SwapSettingsContextProvider } from 'uniswap/src/features/transactions/swap/settings/contexts/SwapSettingsContext' import { useTranslation } from 'uniswap/src/i18n' function RemoveLiquidityModalInner() { @@ -51,9 +53,11 @@ function RemoveLiquidityModalInner() { export function RemoveLiquidityModal() { return ( - - - + + + + + ) } diff --git a/apps/web/src/pages/RemoveLiquidity/V2.tsx b/apps/web/src/pages/RemoveLiquidity/V2.tsx index 4f24823c9f1..a45f68a63a8 100644 --- a/apps/web/src/pages/RemoveLiquidity/V2.tsx +++ b/apps/web/src/pages/RemoveLiquidity/V2.tsx @@ -47,7 +47,8 @@ import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { StyledInternalLink, ThemedText } from 'theme/components' import { Text } from 'ui/src' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' -import { useEnabledChains, useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' @@ -64,13 +65,13 @@ const DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE = new Percent(50, 10_000) export default function RemoveLiquidityV2() { const { chainId } = useAccount() - const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const isLPRedesignEnabled = useFeatureFlag(FeatureFlags.LPRedesign) const isSupportedChain = useIsSupportedChainId(chainId) const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string }>() const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined] const { defaultChainId } = useEnabledChains() - if (isV4EverywhereEnabled) { + if (isLPRedesignEnabled) { // TODO(WEB-5361): prefill poolId from legacy URL /remove/ETH/0x123 const chainName = toGraphQLChain(chainId ?? defaultChainId).toLowerCase() return diff --git a/apps/web/src/pages/RemoveLiquidity/V3.tsx b/apps/web/src/pages/RemoveLiquidity/V3.tsx index 67443804d87..94a4eff1871 100644 --- a/apps/web/src/pages/RemoveLiquidity/V3.tsx +++ b/apps/web/src/pages/RemoveLiquidity/V3.tsx @@ -38,7 +38,8 @@ import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { ThemedText } from 'theme/components' import { Switch, Text } from 'ui/src' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' -import { useEnabledChains, useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' @@ -69,8 +70,8 @@ export default function RemoveLiquidityV3() { }, [tokenId]) const { position, loading } = useV3PositionFromTokenId(parsedTokenId ?? undefined) - const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) - if (isV4EverywhereEnabled) { + const isLPRedesignEnabled = useFeatureFlag(FeatureFlags.LPRedesign) + if (isLPRedesignEnabled) { const chainName = toGraphQLChain(chainId ?? defaultChainId).toLowerCase() return } diff --git a/apps/web/src/pages/Swap/Limit/LimitForm.tsx b/apps/web/src/pages/Swap/Limit/LimitForm.tsx index 5581e4a35c8..c297042bfbc 100644 --- a/apps/web/src/pages/Swap/Limit/LimitForm.tsx +++ b/apps/web/src/pages/Swap/Limit/LimitForm.tsx @@ -41,7 +41,7 @@ import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled import { nativeOnChain } from 'uniswap/src/constants/tokens' import { uniswapUrls } from 'uniswap/src/constants/urls' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { Locale } from 'uniswap/src/features/language/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, InterfacePageNameLocal } from 'uniswap/src/features/telemetry/constants' @@ -499,15 +499,16 @@ function SubmitOrderButton({ ) } + const errorButtonDisabled = !!limitPriceError || !trade return ( - + diff --git a/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.test.tsx b/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.test.tsx index 3a4c25e8bc7..c94d2998217 100644 --- a/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.test.tsx +++ b/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.test.tsx @@ -12,6 +12,7 @@ import { DAI } from 'uniswap/src/constants/tokens' import { SwapTab } from 'uniswap/src/types/screens/interface' const mockMultichainContextValue = { + reset: jest.fn(), setSelectedChainId: jest.fn(), setIsUserSelectedToken: jest.fn(), isSwapAndLimitContext: true, diff --git a/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx b/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx index 6a125b76bbb..6d999b91f80 100644 --- a/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx +++ b/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx @@ -27,7 +27,8 @@ import { ClickableStyle, ThemedText } from 'theme/components' import { Text } from 'ui/src' import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useAppFiatCurrency, useFiatCurrencyComponents } from 'uniswap/src/features/fiatCurrency/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' diff --git a/apps/web/src/pages/Swap/Send/SendRecipientForm.test.tsx b/apps/web/src/pages/Swap/Send/SendRecipientForm.test.tsx index f0d09443426..0185e9e30e1 100644 --- a/apps/web/src/pages/Swap/Send/SendRecipientForm.test.tsx +++ b/apps/web/src/pages/Swap/Send/SendRecipientForm.test.tsx @@ -8,6 +8,7 @@ import { SwapTab } from 'uniswap/src/types/screens/interface' import { shortenAddress } from 'utilities/src/addresses' const mockMultichainContextValue = { + reset: jest.fn(), setSelectedChainId: jest.fn(), setIsUserSelectedToken: jest.fn(), isSwapAndLimitContext: true, diff --git a/apps/web/src/pages/Swap/Send/SendRecipientForm.tsx b/apps/web/src/pages/Swap/Send/SendRecipientForm.tsx index 6b69452e4c2..942d0d2a1c2 100644 --- a/apps/web/src/pages/Swap/Send/SendRecipientForm.tsx +++ b/apps/web/src/pages/Swap/Send/SendRecipientForm.tsx @@ -125,7 +125,7 @@ const AutocompleteRow = ({ const { unitag } = useUnitagByAddress(address) const { ENSName } = useENSName(address) const cachedEnsName = ENSName || validatedEnsName - const formattedAddress = shortenAddress(address, 8, 8) + const formattedAddress = shortenAddress(address, 8) const shouldShowAddress = !unitag?.username && !cachedEnsName const boundSelectRecipient = useCallback( diff --git a/apps/web/src/pages/Swap/Send/SendReviewModal.test.tsx b/apps/web/src/pages/Swap/Send/SendReviewModal.test.tsx index b65fc0bce2e..bd063310052 100644 --- a/apps/web/src/pages/Swap/Send/SendReviewModal.test.tsx +++ b/apps/web/src/pages/Swap/Send/SendReviewModal.test.tsx @@ -11,6 +11,7 @@ import { SwapTab } from 'uniswap/src/types/screens/interface' import { shortenAddress } from 'utilities/src/addresses' const mockMultichainContextValue = { + reset: jest.fn(), setSelectedChainId: jest.fn(), setIsUserSelectedToken: jest.fn(), isSwapAndLimitContext: true, diff --git a/apps/web/src/pages/Swap/SwapForm.tsx b/apps/web/src/pages/Swap/SwapForm.tsx index 0bcb296507f..4a3d396b9b2 100644 --- a/apps/web/src/pages/Swap/SwapForm.tsx +++ b/apps/web/src/pages/Swap/SwapForm.tsx @@ -49,7 +49,7 @@ import { useSwapAndLimitContext, useSwapContext } from 'state/swap/useSwapContex import { ExternalLink, ThemedText } from 'theme/components' import { Text } from 'ui/src' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/web/src/pages/Swap/index.tsx b/apps/web/src/pages/Swap/index.tsx index c117a003cf3..42589b74433 100644 --- a/apps/web/src/pages/Swap/index.tsx +++ b/apps/web/src/pages/Swap/index.tsx @@ -28,7 +28,7 @@ import { AppTFunction } from 'ui/src/i18n/types' import { zIndices } from 'ui/src/theme' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' @@ -45,6 +45,9 @@ import { } from 'uniswap/src/features/transactions/swap/contexts/SwapFormContext' import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/hooks/useSwapPrefilledState' import { Deadline } from 'uniswap/src/features/transactions/swap/settings/configs/Deadline' +import { ProtocolPreference } from 'uniswap/src/features/transactions/swap/settings/configs/ProtocolPreference' +import { Slippage } from 'uniswap/src/features/transactions/swap/settings/configs/Slippage' +import { SwapSettingsContextProvider } from 'uniswap/src/features/transactions/swap/settings/contexts/SwapSettingsContext' import { currencyToAsset } from 'uniswap/src/features/transactions/swap/utils/asset' import { useTranslation } from 'uniswap/src/i18n' import { CurrencyField } from 'uniswap/src/types/currency' @@ -52,8 +55,6 @@ import { SwapTab } from 'uniswap/src/types/screens/interface' import { currencyId } from 'uniswap/src/utils/currencyId' import noop from 'utilities/src/react/noop' -const WEB_CUSTOM_SWAP_SETTINGS = [Deadline] - export function getIsReviewableQuote( trade: InterfaceTrade | undefined, tradeState: TradeState, @@ -165,28 +166,34 @@ export function Swap({ if (universalSwapFlow || isTestnetModeEnabled) { return ( - - - - - {isSharedSwapDisabled && } - - - - - + + + + + + {isSharedSwapDisabled && } + + + + + + ) } @@ -382,7 +389,7 @@ function UniversalSwapFlow({ {currentTab === SwapTab.Swap && ( { } }) +jest.mock('@datadog/browser-rum', () => ({ + init: jest.fn(), + setUser: jest.fn(), + clearUser: jest.fn(), + addAction: jest.fn(), + addError: jest.fn(), +})) + +jest.mock('@datadog/browser-logs', () => ({ + init: jest.fn(), + setUser: jest.fn(), + setUserProperty: jest.fn(), + clearUser: jest.fn(), + addAction: jest.fn(), + addError: jest.fn(), +})) + jest.mock('uniswap/src/features/language/LocalizationContext', () => mockLocalizationContext({})) jest.mock('@web3-react/core', () => { @@ -156,6 +174,13 @@ failOnConsole({ }) jest.mock('uniswap/src/features/gating/hooks') + +jest.mock('uniswap/src/features/chains/hooks/useOrderedChainIds', () => { + return { + useOrderedChainIds: (chainIds: UniverseChainId[]) => chainIds, + } +}) + const originalConsoleDebug = console.debug // Mocks are configured to reset between tests (by CRA), so they must be set in a beforeEach. beforeEach(() => { diff --git a/apps/web/src/state/activity/polling/orders.ts b/apps/web/src/state/activity/polling/orders.ts index 06fde5c5823..6fff099b8a6 100644 --- a/apps/web/src/state/activity/polling/orders.ts +++ b/apps/web/src/state/activity/polling/orders.ts @@ -127,7 +127,7 @@ export function usePollPendingOrders(onActivityUpdate: OnActivityUpdate) { } catch (e) { logger.debug('usePollPendingOrders', 'getOrderStatuses', 'Failed to fetch order statuses', e) } - setCurrentDelay((currentDelay) => Math.min(currentDelay * 2, ms('30s'))) + setCurrentDelay((currentDelay) => Math.min(currentDelay * 1.5, ms('30s'))) timeout = setTimeout(getOrderStatuses, currentDelay) } diff --git a/apps/web/src/state/application/updater.ts b/apps/web/src/state/application/updater.ts index e0e260c01c7..3e1ad7ee205 100644 --- a/apps/web/src/state/application/updater.ts +++ b/apps/web/src/state/application/updater.ts @@ -5,7 +5,7 @@ import useIsWindowVisible from 'hooks/useIsWindowVisible' import { useEffect, useState } from 'react' import { updateChainId } from 'state/application/reducer' import { useAppDispatch } from 'state/hooks' -import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' export default function Updater(): null { const account = useAccount() diff --git a/apps/web/src/state/explore/protocolStats.ts b/apps/web/src/state/explore/protocolStats.ts index 4080cda7188..c205a73160c 100644 --- a/apps/web/src/state/explore/protocolStats.ts +++ b/apps/web/src/state/explore/protocolStats.ts @@ -45,10 +45,8 @@ export function useHistoricalProtocolVolume(duration: HistoryDuration) { protocolStats: { data, isLoading }, } = useContext(ExploreContext) - const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( - FeatureFlags.V4Everywhere, - ) - const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading + const { value: isV4DataEnabledLoaded, isLoading: isV4DataLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Data) + const isV4DataEnabled = isV4DataEnabledLoaded || isV4DataLoading let v4Data: TimestampedAmount[] | undefined let v3Data: TimestampedAmount[] | undefined let v2Data: TimestampedAmount[] | undefined @@ -71,7 +69,7 @@ export function useHistoricalProtocolVolume(duration: HistoryDuration) { } return useMemo(() => { - const dataByTime = mapDataByTimestamp(v2Data, v3Data, isV4EverywhereEnabled ? v4Data : undefined) + const dataByTime = mapDataByTimestamp(v2Data, v3Data, isV4DataEnabled ? v4Data : undefined) const entries = Object.entries(dataByTime).reduce((acc, [timestamp, values]) => { acc.push({ @@ -79,7 +77,7 @@ export function useHistoricalProtocolVolume(duration: HistoryDuration) { values: { ['SUBGRAPH_V2']: values['v2'], ['SUBGRAPH_V3']: values['v3'], - ['SUBGRAPH_V4']: isV4EverywhereEnabled ? values['v4'] : undefined, + ['SUBGRAPH_V4']: isV4DataEnabled ? values['v4'] : undefined, }, }) return acc @@ -87,7 +85,7 @@ export function useHistoricalProtocolVolume(duration: HistoryDuration) { const dataQuality = checkDataQuality(entries, ChartType.VOLUME, duration) return { chartType: ChartType.VOLUME, entries, loading: isLoading, dataQuality } - }, [duration, isLoading, isV4EverywhereEnabled, v2Data, v3Data, v4Data]) + }, [duration, isLoading, isV4DataEnabled, v2Data, v3Data, v4Data]) } export function useDailyProtocolTVL() { @@ -95,22 +93,20 @@ export function useDailyProtocolTVL() { protocolStats: { data, isLoading }, } = useContext(ExploreContext) - const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( - FeatureFlags.V4Everywhere, - ) - const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading + const { value: isV4DataEnabledLoaded, isLoading: isV4DataLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Data) + const isV4DataEnabled = isV4DataEnabledLoaded || isV4DataLoading const v4Data = data?.dailyProtocolTvl?.v4 const v3Data = data?.dailyProtocolTvl?.v3 const v2Data = data?.dailyProtocolTvl?.v2 return useMemo(() => { - const dataByTime = mapDataByTimestamp(v2Data, v3Data, isV4EverywhereEnabled ? v4Data : undefined) + const dataByTime = mapDataByTimestamp(v2Data, v3Data, isV4DataEnabled ? v4Data : undefined) const entries = Object.entries(dataByTime).map(([timestamp, values]) => ({ time: Number(timestamp), - values: isV4EverywhereEnabled ? [values['v2'], values['v3'], values['v4']] : [values['v2'], values['v3']], + values: isV4DataEnabled ? [values['v2'], values['v3'], values['v4']] : [values['v2'], values['v3']], })) as StackedLineData[] const dataQuality = checkDataQuality(entries, ChartType.TVL, HistoryDuration.Year) return { chartType: ChartType.TVL, entries, loading: isLoading, dataQuality } - }, [isLoading, isV4EverywhereEnabled, v2Data, v3Data, v4Data]) + }, [isLoading, isV4DataEnabled, v2Data, v3Data, v4Data]) } diff --git a/apps/web/src/state/explore/topPools.ts b/apps/web/src/state/explore/topPools.ts index 403b93ace13..e9b9a3d32d2 100644 --- a/apps/web/src/state/explore/topPools.ts +++ b/apps/web/src/state/explore/topPools.ts @@ -18,7 +18,7 @@ import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' function useFilteredPools(pools?: PoolStat[]) { const filterString = useAtomValue(exploreSearchStringAtom) - const isV4Enabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const isV4Enabled = useFeatureFlag(FeatureFlags.V4Data) const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString]) diff --git a/apps/web/src/state/mint/v3/hooks.tsx b/apps/web/src/state/mint/v3/hooks.tsx index 24e91a054c3..8de24b32227 100644 --- a/apps/web/src/state/mint/v3/hooks.tsx +++ b/apps/web/src/state/mint/v3/hooks.tsx @@ -1,15 +1,16 @@ import { Currency, CurrencyAmount, Price, Rounding, Token } from '@uniswap/sdk-core' import { FeeAmount, - Pool, Position, TICK_SPACINGS, TickMath, + Pool as V3Pool, encodeSqrtRatioX96, nearestUsableTick, priceToClosestTick, - tickToPrice, + tickToPrice as tickToPriceV3, } from '@uniswap/v3-sdk' +import { Pool as V4Pool, tickToPrice as tickToPriceV4 } from '@uniswap/v4-sdk' import { ConnectWalletButtonText } from 'components/NavBar/accountCTAsExperimentUtils' import { BIG_INT_ZERO } from 'constants/misc' import { useAccount } from 'hooks/useAccount' @@ -17,6 +18,7 @@ import { PoolState, usePool } from 'hooks/usePools' import { useSwapTaxes } from 'hooks/useSwapTaxes' import JSBI from 'jsbi' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' +import { FeeData } from 'pages/Pool/Positions/create/types' import { ReactNode, useCallback, useMemo } from 'react' import { useSearchParams } from 'react-router-dom' import { useCurrencyBalances } from 'state/connection/hooks' @@ -112,7 +114,7 @@ export function useV3DerivedMintInfo( // override for existing position existingPosition?: Position, ): { - pool?: Pool | null + pool?: V3Pool | null poolState: PoolState ticks: { [bound in Bound]?: number | undefined } price?: Price @@ -226,14 +228,14 @@ export function useV3DerivedMintInfo( if (tokenA && tokenB && feeAmount && price && !invalidPrice) { const currentTick = priceToClosestTick(price) const currentSqrt = TickMath.getSqrtRatioAtTick(currentTick) - return new Pool(tokenA, tokenB, feeAmount, currentSqrt, JSBI.BigInt(0), currentTick, []) + return new V3Pool(tokenA, tokenB, feeAmount, currentSqrt, JSBI.BigInt(0), currentTick, []) } else { return undefined } }, [feeAmount, invalidPrice, price, tokenA, tokenB]) // if pool exists use it, if not use the mock pool - const poolForPosition: Pool | undefined = pool ?? mockPool + const poolForPosition: V3Pool | undefined = pool ?? mockPool // lower and upper limits in the tick space for `feeAmount` const tickSpaceLimits = useMemo( @@ -521,70 +523,105 @@ export function useV3DerivedMintInfo( } } -export function useRangeHopCallbacks( - baseCurrency: Currency | undefined, - quoteCurrency: Currency | undefined, - feeAmount: FeeAmount | undefined, - tickLower: number | undefined, - tickUpper: number | undefined, - pool?: Pool | undefined | null, -) { +type BaseUseRangeHopCallbacksProps = { + baseCurrency?: Currency + quoteCurrency?: Currency + tickLower?: number + tickUpper?: number +} + +type V3UseRangeHopCallbacksProps = BaseUseRangeHopCallbacksProps & { + feeAmount?: FeeAmount + pool?: V3Pool | null +} + +type V4UseRangeHopCallbacksProps = BaseUseRangeHopCallbacksProps & { + fee?: FeeData + pool?: V4Pool +} + +export function useRangeHopCallbacks(props: V3UseRangeHopCallbacksProps | V4UseRangeHopCallbacksProps) { + const { baseCurrency, quoteCurrency, tickLower, tickUpper, pool } = props + let tickSpacing: number | undefined + let feeAmount: FeeAmount | number | undefined + + if ('feeAmount' in props) { + feeAmount = props.feeAmount + tickSpacing = props.feeAmount ? TICK_SPACINGS[props.feeAmount] : undefined + } else if ('fee' in props) { + feeAmount = props.fee?.feeAmount + tickSpacing = props.fee?.tickSpacing + } + const dispatch = useAppDispatch() const baseToken = useMemo(() => baseCurrency?.wrapped, [baseCurrency]) const quoteToken = useMemo(() => quoteCurrency?.wrapped, [quoteCurrency]) - const getDecrementLower = useCallback(() => { - if (baseToken && quoteToken && typeof tickLower === 'number' && feeAmount) { - const newPrice = tickToPrice(baseToken, quoteToken, tickLower - TICK_SPACINGS[feeAmount]) + const tickToPrice = useCallback( + (tick: number) => { + if ('feeAmount' in props) { + if (!baseToken || !quoteToken) { + return '' + } + + const newPrice = tickToPriceV3(baseToken, quoteToken, tick) + return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + } + + if (!baseCurrency || !quoteCurrency) { + return '' + } + + const newPrice = tickToPriceV4(baseCurrency, quoteCurrency, tick) return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + }, + [baseCurrency, baseToken, props, quoteCurrency, quoteToken], + ) + + const getDecrementLower = useCallback(() => { + if (typeof tickLower === 'number' && feeAmount && tickSpacing) { + return tickToPrice(tickLower - tickSpacing) } // use pool current tick as starting tick if we have pool but no tick input - if (!(typeof tickLower === 'number') && baseToken && quoteToken && feeAmount && pool) { - const newPrice = tickToPrice(baseToken, quoteToken, pool.tickCurrent - TICK_SPACINGS[feeAmount]) - return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + if (!(typeof tickLower === 'number') && feeAmount && pool && tickSpacing) { + return tickToPrice(pool.tickCurrent - tickSpacing) } return '' - }, [baseToken, quoteToken, tickLower, feeAmount, pool]) + }, [tickLower, feeAmount, tickSpacing, pool, tickToPrice]) const getIncrementLower = useCallback(() => { - if (baseToken && quoteToken && typeof tickLower === 'number' && feeAmount) { - const newPrice = tickToPrice(baseToken, quoteToken, tickLower + TICK_SPACINGS[feeAmount]) - return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + if (typeof tickLower === 'number' && feeAmount && tickSpacing) { + return tickToPrice(tickLower + tickSpacing) } // use pool current tick as starting tick if we have pool but no tick input - if (!(typeof tickLower === 'number') && baseToken && quoteToken && feeAmount && pool) { - const newPrice = tickToPrice(baseToken, quoteToken, pool.tickCurrent + TICK_SPACINGS[feeAmount]) - return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + if (!(typeof tickLower === 'number') && feeAmount && pool && tickSpacing) { + return tickToPrice(pool.tickCurrent + tickSpacing) } return '' - }, [baseToken, quoteToken, tickLower, feeAmount, pool]) + }, [tickLower, feeAmount, tickSpacing, pool, tickToPrice]) const getDecrementUpper = useCallback(() => { - if (baseToken && quoteToken && typeof tickUpper === 'number' && feeAmount) { - const newPrice = tickToPrice(baseToken, quoteToken, tickUpper - TICK_SPACINGS[feeAmount]) - return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + if (typeof tickUpper === 'number' && feeAmount && tickSpacing) { + return tickToPrice(tickUpper - tickSpacing) } // use pool current tick as starting tick if we have pool but no tick input - if (!(typeof tickUpper === 'number') && baseToken && quoteToken && feeAmount && pool) { - const newPrice = tickToPrice(baseToken, quoteToken, pool.tickCurrent - TICK_SPACINGS[feeAmount]) - return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + if (!(typeof tickUpper === 'number') && feeAmount && pool && tickSpacing) { + return tickToPrice(pool.tickCurrent - tickSpacing) } return '' - }, [baseToken, quoteToken, tickUpper, feeAmount, pool]) + }, [tickUpper, feeAmount, tickSpacing, pool, tickToPrice]) const getIncrementUpper = useCallback(() => { - if (baseToken && quoteToken && typeof tickUpper === 'number' && feeAmount) { - const newPrice = tickToPrice(baseToken, quoteToken, tickUpper + TICK_SPACINGS[feeAmount]) - return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + if (typeof tickUpper === 'number' && feeAmount && tickSpacing) { + return tickToPrice(tickUpper + tickSpacing) } // use pool current tick as starting tick if we have pool but no tick input - if (!(typeof tickUpper === 'number') && baseToken && quoteToken && feeAmount && pool) { - const newPrice = tickToPrice(baseToken, quoteToken, pool.tickCurrent + TICK_SPACINGS[feeAmount]) - return newPrice.toSignificant(5, undefined, Rounding.ROUND_UP) + if (!(typeof tickUpper === 'number') && feeAmount && pool && tickSpacing) { + return tickToPrice(pool.tickCurrent + tickSpacing) } return '' - }, [baseToken, quoteToken, tickUpper, feeAmount, pool]) + }, [tickUpper, feeAmount, tickSpacing, pool, tickToPrice]) const getSetFullRange = useCallback(() => { dispatch(setFullRange()) diff --git a/apps/web/src/state/multichain/MultichainContext.tsx b/apps/web/src/state/multichain/MultichainContext.tsx index 9d8d289435e..cfe7255e276 100644 --- a/apps/web/src/state/multichain/MultichainContext.tsx +++ b/apps/web/src/state/multichain/MultichainContext.tsx @@ -1,6 +1,6 @@ import { useUpdateAtom } from 'jotai/utils' import { multicallUpdaterSwapChainIdAtom } from 'lib/hooks/useBlockNumber' -import { PropsWithChildren, useEffect, useMemo, useState } from 'react' +import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react' import { MultichainContext } from 'state/multichain/types' import { UniverseChainId } from 'uniswap/src/features/chains/types' @@ -19,8 +19,14 @@ export function MultichainContextProvider({ setMulticallUpdaterChainId(chainId) }, [selectedChainId, setMulticallUpdaterChainId]) + const reset = useCallback(() => { + setSelectedChainId(initialChainId) + setIsUserSelectedToken(false) + }, [initialChainId]) + const value = useMemo(() => { return { + reset, setSelectedChainId, initialChainId, chainId: selectedChainId ?? undefined, @@ -28,6 +34,6 @@ export function MultichainContextProvider({ isUserSelectedToken, setIsUserSelectedToken, } - }, [initialChainId, selectedChainId, isUserSelectedToken]) + }, [initialChainId, selectedChainId, isUserSelectedToken, reset]) return {children} } diff --git a/apps/web/src/state/multichain/types.ts b/apps/web/src/state/multichain/types.ts index 3393b38a714..a8d65acf6bd 100644 --- a/apps/web/src/state/multichain/types.ts +++ b/apps/web/src/state/multichain/types.ts @@ -2,6 +2,7 @@ import { Dispatch, SetStateAction, createContext } from 'react' import { UniverseChainId } from 'uniswap/src/features/chains/types' type MultichainContextType = { + reset: () => void setSelectedChainId: Dispatch> initialChainId?: UniverseChainId isUserSelectedToken: boolean @@ -15,6 +16,7 @@ type MultichainContextType = { } export const MultichainContext = createContext({ + reset: () => undefined, setSelectedChainId: () => undefined, isUserSelectedToken: false, setIsUserSelectedToken: () => undefined, diff --git a/apps/web/src/state/send/SendContext.tsx b/apps/web/src/state/send/SendContext.tsx index e6cc1e24d4c..447afba3734 100644 --- a/apps/web/src/state/send/SendContext.tsx +++ b/apps/web/src/state/send/SendContext.tsx @@ -11,7 +11,7 @@ import { } from 'react' import { RecipientData, SendInfo, useDerivedSendInfo } from 'state/send/hooks' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' export type SendState = { readonly exactAmountToken?: string diff --git a/apps/web/src/state/swap/SwapContext.tsx b/apps/web/src/state/swap/SwapContext.tsx index 916f2e7a9e6..ad5be4b9164 100644 --- a/apps/web/src/state/swap/SwapContext.tsx +++ b/apps/web/src/state/swap/SwapContext.tsx @@ -6,7 +6,7 @@ import { PropsWithChildren, useEffect, useMemo, useState } from 'react' import { useMultichainContext } from 'state/multichain/useMultichainContext' import { useDerivedSwapInfo } from 'state/swap/hooks' import { CurrencyState, SwapAndLimitContext, SwapContext, SwapState, initialSwapState } from 'state/swap/types' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' import { areCurrenciesEqual } from 'uniswap/src/utils/currencyId' diff --git a/apps/web/src/state/swap/hooks.tsx b/apps/web/src/state/swap/hooks.tsx index f5c50e7220d..7ec328e2f01 100644 --- a/apps/web/src/state/swap/hooks.tsx +++ b/apps/web/src/state/swap/hooks.tsx @@ -20,7 +20,8 @@ import { useSwapAndLimitContext, useSwapContext } from 'state/swap/useSwapContex import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { useUrlContext } from 'uniswap/src/contexts/UrlContext' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId, isUniverseChainId } from 'uniswap/src/features/chains/types' import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' import { Trans } from 'uniswap/src/i18n' diff --git a/apps/web/src/state/user/hooks.tsx b/apps/web/src/state/user/hooks.tsx index e349d1dc242..be2a59a75a0 100644 --- a/apps/web/src/state/user/hooks.tsx +++ b/apps/web/src/state/user/hooks.tsx @@ -20,7 +20,8 @@ import { TokenSortableField, useTopTokensQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { isL2ChainId, toGraphQLChain } from 'uniswap/src/features/chains/utils' import { deserializeToken, serializeToken } from 'uniswap/src/utils/currency' diff --git a/apps/web/src/test-utils/bundle-size-test.ts b/apps/web/src/test-utils/bundle-size-test.ts index 12766745b73..f6f71c4b799 100644 --- a/apps/web/src/test-utils/bundle-size-test.ts +++ b/apps/web/src/test-utils/bundle-size-test.ts @@ -32,8 +32,8 @@ const entryGzipSize = report.reduce( 0, ) -// somewhat arbitrary, based on current size (11/5/2024) -const limit = 2_360_000 +// somewhat arbitrary, based on current size (11/18/2024) +const limit = 2_370_000 if (entryGzipSize > limit) { console.error(`Bundle size has grown too big! Entry JS size is ${entryGzipSize}, over the limit of ${limit}.`) diff --git a/apps/web/src/utils/computeSurroundingTicks.test.ts b/apps/web/src/utils/computeSurroundingTicks.test.ts index 23b17675fe2..39f761ffb0c 100644 --- a/apps/web/src/utils/computeSurroundingTicks.test.ts +++ b/apps/web/src/utils/computeSurroundingTicks.test.ts @@ -1,9 +1,8 @@ import { Price, Token } from '@uniswap/sdk-core' import { FeeAmount, TICK_SPACINGS } from '@uniswap/v3-sdk' import { TickData } from 'graphql/data/AllV3TicksQuery' -import { TickProcessed } from 'hooks/usePoolTickData' import JSBI from 'jsbi' -import computeSurroundingTicks from 'utils/computeSurroundingTicks' +import computeSurroundingTicks, { TickProcessed } from 'utils/computeSurroundingTicks' const getV3Tick = (tick: number, liquidityNet: number): TickData => ({ tick, diff --git a/apps/web/src/utils/computeSurroundingTicks.ts b/apps/web/src/utils/computeSurroundingTicks.ts index becd695a7ce..54dcd545ea9 100644 --- a/apps/web/src/utils/computeSurroundingTicks.ts +++ b/apps/web/src/utils/computeSurroundingTicks.ts @@ -1,11 +1,19 @@ -import { Token } from '@uniswap/sdk-core' +import { Price, Token } from '@uniswap/sdk-core' import { tickToPrice } from '@uniswap/v3-sdk' import { Ticks } from 'graphql/data/AllV3TicksQuery' -import { TickProcessed } from 'hooks/usePoolTickData' import JSBI from 'jsbi' const PRICE_FIXED_DIGITS = 8 +// Tick with fields parsed to JSBIs, and active liquidity computed. +export interface TickProcessed { + tick: number + liquidityActive: JSBI + liquidityNet: JSBI + price0: string + sdkPrice: Price +} + // Computes the numSurroundingTicks above or below the active tick. export default function computeSurroundingTicks( token0: Token, diff --git a/apps/web/src/utils/urlRoutes.ts b/apps/web/src/utils/urlRoutes.ts index 0c73afcde4f..e41c1695e68 100644 --- a/apps/web/src/utils/urlRoutes.ts +++ b/apps/web/src/utils/urlRoutes.ts @@ -14,6 +14,11 @@ export function getCurrentPageFromLocation(locationPathname: string): InterfaceP return InterfacePageName.EXPLORE_PAGE case locationPathname.startsWith('/vote'): return InterfacePageName.VOTE_PAGE + case locationPathname.startsWith('/positions/v2'): + case locationPathname.startsWith('/positions/v3'): + case locationPathname.startsWith('/positions/v4'): + return InterfacePageName.POOL_DETAILS_PAGE + case locationPathname.startsWith('/positions'): case locationPathname.startsWith('/pools'): case locationPathname.startsWith('/pool'): case locationPathname.startsWith('/add'): diff --git a/config/jest-presets/jest/setup.js b/config/jest-presets/jest/setup.js index 166379a9f5c..6bf086488f8 100644 --- a/config/jest-presets/jest/setup.js +++ b/config/jest-presets/jest/setup.js @@ -137,6 +137,14 @@ jest.mock('uniswap/src/features/gating/sdk/statsig', () => { return StatsigMock }) +jest.mock('uniswap/src/features/gating/hooks', () => { + const real = jest.requireActual('uniswap/src/features/gating/hooks') + return { + ...real, + useDynamicConfigValue: (_config, _key, defaultValue, _customTypeGuard) => defaultValue, + } +}) + // TODO: Remove this mock after mocks in jest-expo are fixed // (see the issue: https://github.com/expo/expo/issues/26893) jest.mock('expo-web-browser', () => ({})) diff --git a/eslint-local-rules.js b/eslint-local-rules.js index 919607ee92f..126d70b1997 100644 --- a/eslint-local-rules.js +++ b/eslint-local-rules.js @@ -2,4 +2,5 @@ module.exports = { 'no-unwrapped-t': require('@uniswap/eslint-config/plugins/no-unwrapped-t'), + 'custom-map-sort': require('@uniswap/eslint-config/plugins/custom-map-sort'), } diff --git a/package.json b/package.json index ec44f9500ae..2b20e049810 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "turbo-ignore": "1.11.3" }, "engines": { - "node": "=18.12.1", + "node": "=18.16.0", "yarn": ">=3.2.3", "npm": "please-use-yarn" }, diff --git a/packages/eslint-config/native.js b/packages/eslint-config/native.js index 3844bf55bfb..cddfe7eb7b8 100644 --- a/packages/eslint-config/native.js +++ b/packages/eslint-config/native.js @@ -18,6 +18,86 @@ const complexityRules = { // To use them, explicitly reference them, e.g. `window.name` or `window.status`. const restrictedGlobals = require('confusing-browser-globals') +const noRestrictedImports = [ + { + name: 'expo-haptics', + message: "Use our internal `HapticFeedback` wrapper instead: `import { HapticFeedback } from 'mobile/src'`", + }, + { + name: '@ethersproject', + message: "Please import from 'ethers' directly to support tree-shaking.", + }, + { + name: 'react', + importNames: ['Suspense'], + message: 'Please use Suspense from src/components/data instead.', + }, + { + name: 'src/features/telemetry', + importNames: ['logException'], + message: 'Please use `logger.error` instead.', + }, + { + name: '@tamagui/core', + message: "Please import from 'tamagui' directly to prevent mismatches.", + }, + { + name: 'react-native-safe-area-context', + importNames: ['useSafeAreaInsets'], + message: 'Use our internal `useAppInsets` hook instead.', + }, + { + name: 'react-native', + importNames: ['Switch'], + message: 'Use our custom Switch component instead.', + }, + { + name: 'react-native', + importNames: ['Keyboard'], + message: + 'Please use dismissNativeKeyboard() instead for dismissals. addListener is okay to ignore this import for!', + }, + { + name: 'wallet/src/data/__generated__/types-and-hooks', + importNames: ['usePortfolioBalancesQuery'], + message: 'Use `usePortfolioBalances` instead.', + }, + { + name: 'uniswap/src/features/settings/selectors', + importNames: ['selectIsTestnetModeEnabled'], + message: 'Use `useEnabledChains` instead.', + }, + { + name: 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks', + importNames: ['useAccountListQuery'], + message: 'Use `useAccountList` instead.', + }, + { + name: '@gorhom/bottom-sheet', + importNames: ['BottomSheetTextInput'], + message: 'Use our internal `BottomSheetTextInput` wrapper from `/uniswap/src/components/modals/Modal`.', + }, + { + name: 'wallet/src/data/apollo/usePersistedApolloClient', + importNames: ['usePersistedApolloClient'], + message: + "This hook should only be used once at the top level where the React app is initialized . You can use `import { useApolloClient } from '@apollo/client'` to get the default apollo client from the provider elsewhere in React. If you need access to apollo outside of React, you can use `import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient''`.", + }, + { + name: 'statsig-react', + message: 'Import from internal module uniswap/src/features/gating instead', + }, + { + name: 'statsig-react-native', + message: 'Import from internal module uniswap/src/features/gating instead', + }, + { + name: '@uniswap/analytics', + message: "Did you mean to import from 'uniswap/src/features/telemetry/send'?", + }, + ...restrictedImports.paths, +] + module.exports = { root: true, parser: '@typescript-eslint/parser', @@ -123,85 +203,7 @@ module.exports = { 'no-restricted-imports': [ 'error', { - paths: [ - { - name: '@ethersproject', - message: "Please import from 'ethers' directly to support tree-shaking.", - }, - { - name: 'react', - importNames: ['Suspense'], - message: 'Please use Suspense from src/components/data instead.', - }, - { - name: 'src/features/telemetry', - importNames: ['logException'], - message: 'Please use `logger.error` instead.', - }, - { - name: '@tamagui/core', - message: "Please import from 'tamagui' directly to prevent mismatches.", - }, - { - name: 'react-native-safe-area-context', - importNames: ['useSafeAreaInsets'], - message: 'Use our internal `useAppInsets` hook instead.', - }, - { - name: 'react-native', - importNames: ['Switch'], - message: 'Use our custom Switch component instead.', - }, - { - name: 'react-native', - importNames: ['Keyboard'], - message: - 'Please use dismissNativeKeyboard() instead for dismissals. addListener is okay to ignore this import for!', - }, - { - name: 'wallet/src/data/__generated__/types-and-hooks', - importNames: ['usePortfolioBalancesQuery'], - message: 'Use `usePortfolioBalances` instead.', - }, - { - name: 'uniswap/src/features/settings/selectors', - importNames: ['selectIsTestnetModeEnabled'], - message: 'Use `useEnabledChains` instead.', - }, - { - name: 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks', - importNames: ['useAccountListQuery'], - message: 'Use `useAccountList` instead.', - }, - { - name: '@gorhom/bottom-sheet', - importNames: ['BottomSheetTextInput'], - message: 'Use our internal `BottomSheetTextInput` wrapper from `/uniswap/src/components/modals/Modal`.', - }, - { - name: 'expo-haptics', - message: "Use our internal `HapticFeedback` wrapper instead: `import { HapticFeedback } from 'ui/src'`", - }, - { - name: 'wallet/src/data/apollo/usePersistedApolloClient', - importNames: ['usePersistedApolloClient'], - message: - "This hook should only be used once at the top level where the React app is initialized . You can use `import { useApolloClient } from '@apollo/client'` to get the default apollo client from the provider elsewhere in React. If you need access to apollo outside of React, you can use `import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient''`.", - }, - { - name: 'statsig-react', - message: 'Import from internal module uniswap/src/features/gating instead', - }, - { - name: 'statsig-react-native', - message: 'Import from internal module uniswap/src/features/gating instead', - }, - { - name: '@uniswap/analytics', - message: "Did you mean to import from 'uniswap/src/features/telemetry/send'?", - }, - ...restrictedImports.paths, - ], + paths: noRestrictedImports, }, ], @@ -278,6 +280,17 @@ module.exports = { 'react/no-unsafe': 'error', }, overrides: [ + { + files: ['**/utils/haptics/**'], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: noRestrictedImports.filter((rule) => rule.name !== 'expo-haptics'), + }, + ], + }, + }, { files: ['*.e2e.js'], env: { @@ -466,6 +479,12 @@ module.exports = { ], }, }, + { + files: ['**/features/gating/flags.ts'], + rules: { + 'local-rules/custom-map-sort': 'error', + }, + }, ], globals: { Address: 'readonly', diff --git a/packages/eslint-config/plugins/custom-map-sort.js b/packages/eslint-config/plugins/custom-map-sort.js new file mode 100644 index 00000000000..c5ca67ff5c8 --- /dev/null +++ b/packages/eslint-config/plugins/custom-map-sort.js @@ -0,0 +1,56 @@ +function getMapKeys(node) { + return node.arguments[0]?.elements + ?.map((element) => { + if (element?.type === 'ArrayExpression' && element?.elements?.length > 0) { + const keyElement = element?.elements?.[0] + if (keyElement.type === 'MemberExpression' && keyElement.property.type === 'Identifier') { + return keyElement.property.name + } else if (keyElement.type === 'Identifier') { + return keyElement.name + } else if (keyElement.type === 'Literal') { + return keyElement.value + } + } + return undefined + }) + .filter((key) => key !== undefined) +} + +function create(context) { + return { + NewExpression(node) { + if (node.callee.name !== 'Map') { + return + } + + const keys = getMapKeys(node) + + // Check if keys are iterable + if (!Array.isArray(keys)) { + return + } + + const sortedKeys = [...keys].sort() + + if (keys.join(',') !== sortedKeys.join(',')) { + context.report({ + node, + message: `Map keys should be sorted alphabetically. Correct order: ${sortedKeys.join(', ')}`, + }) + } + }, + } +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce alphabetical sorting of Map keys', + category: 'Stylistic Issues', + recommended: false, + }, + schema: [], // no options + }, + create, +} diff --git a/packages/eslint-config/plugins/custom-map-sort.test.js b/packages/eslint-config/plugins/custom-map-sort.test.js new file mode 100644 index 00000000000..6ce9eafe0be --- /dev/null +++ b/packages/eslint-config/plugins/custom-map-sort.test.js @@ -0,0 +1,52 @@ +import { RuleTester } from 'eslint' +import sortMapKeys from './custom-map-sort' + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, +}) + +ruleTester.run('sort-map-keys', sortMapKeys, { + valid: [ + { + code: ` + const map = new Map([ + [FeatureFlags.DisableFiatOnRampKorea, 'disable-fiat-onramp-korea'], + [FeatureFlags.ExtensionAutoConnect, 'extension-auto-connect'], + [FeatureFlags.ExtensionClaimUnitag, 'extension-claim-unitag'], + ]); + `, + }, + { + code: ` + const map = new Map([]); + `, + }, + { + code: ` + const map = new Map([ + [FeatureFlags.DisableFiatOnRampKorea, 'disable-fiat-onramp-korea'], + ]); + `, + }, + ], + invalid: [ + { + code: ` + const map = new Map([ + [FeatureFlags.ExtensionAutoConnect, 'extension-auto-connect'], + [FeatureFlags.DisableFiatOnRampKorea, 'disable-fiat-onramp-korea'], + [FeatureFlags.ExtensionClaimUnitag, 'extension-claim-unitag'], + ]); + `, + errors: [ + { + message: + 'Map keys should be sorted alphabetically. Correct order: DisableFiatOnRampKorea, ExtensionAutoConnect, ExtensionClaimUnitag', + }, + ], + }, + ], +}) diff --git a/packages/ui/package.json b/packages/ui/package.json index 089399e1dc2..4b1f3481ab6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,7 +14,6 @@ "@tamagui/theme-base": "1.108.4", "@testing-library/react-hooks": "7.0.2", "ethers": "5.7.2", - "expo-haptics": "12.8.1", "expo-linear-gradient": "12.7.2", "i18next": "23.10.0", "qrcode": "1.5.1", diff --git a/packages/ui/src/animations/components/AnimateInOrder.tsx b/packages/ui/src/animations/components/AnimateInOrder.tsx index e990f37c353..e6048b3da1f 100644 --- a/packages/ui/src/animations/components/AnimateInOrder.tsx +++ b/packages/ui/src/animations/components/AnimateInOrder.tsx @@ -1,6 +1,5 @@ import { PropsWithChildren, useEffect, useState } from 'react' import { Flex, FlexProps } from 'ui/src/components/layout' -import { useHapticFeedback } from 'ui/src/utils/haptics/useHapticFeedback' export const AnimateInOrder = ({ children, @@ -9,18 +8,16 @@ export const AnimateInOrder = ({ enterStyle = { opacity: 0, scale: 0.8 }, exitStyle = { opacity: 0, scale: 0.8 }, delayMs = 150, - hapticOnEnter, ...rest }: PropsWithChildren< { index: number - hapticOnEnter?: boolean delayMs?: number } & Pick & FlexProps >): JSX.Element => { return ( - + {children} @@ -28,23 +25,15 @@ export const AnimateInOrder = ({ ) } -const Delay = ({ - children, - hapticOnEnter, - by, -}: PropsWithChildren<{ by: number; hapticOnEnter?: boolean }>): JSX.Element | null => { +const Delay = ({ children, by }: PropsWithChildren<{ by: number }>): JSX.Element | null => { const [done, setDone] = useState(false) - const { hapticFeedback } = useHapticFeedback() useEffect(() => { const showTimer = setTimeout(async () => { - if (hapticOnEnter) { - await hapticFeedback.light() - } setDone(true) }, by) return () => clearTimeout(showTimer) - }, [by, hapticOnEnter, hapticFeedback]) + }, [by]) return done ? <>{children} : null } diff --git a/packages/ui/src/animations/components/Jiggly.tsx b/packages/ui/src/animations/components/Jiggly.tsx index f8ae192692e..64afbdbfff3 100644 --- a/packages/ui/src/animations/components/Jiggly.tsx +++ b/packages/ui/src/animations/components/Jiggly.tsx @@ -1,18 +1,12 @@ import { PropsWithChildren } from 'react' import { useAnimatedStyle, useSharedValue, withRepeat, withSequence, withTiming } from 'react-native-reanimated' import { AnimatedTouchableArea } from 'ui/src/components/touchable' -import { HapticFeedbackStyle } from 'ui/src/utils/haptics/helpers' -import { useHapticFeedback } from 'ui/src/utils/haptics/useHapticFeedback' export const Jiggly = ({ children, - hapticFeedback: triggerHapticFeedback, - hapticStyle, offset = 8, duration = 100, }: PropsWithChildren<{ - hapticFeedback?: boolean - hapticStyle?: HapticFeedbackStyle offset?: number duration?: number }>): JSX.Element => { @@ -23,13 +17,8 @@ export const Jiggly = ({ }), [rotate], ) - const { hapticFeedback } = useHapticFeedback() const onPress = async (): Promise => { - if (triggerHapticFeedback) { - await hapticFeedback.impact(hapticStyle) - } - rotate.value = withSequence( withTiming(-offset, { duration: duration / 2 }), withRepeat(withTiming(offset, { duration }), 5, true), diff --git a/packages/ui/src/assets/icons/arrow-right.svg b/packages/ui/src/assets/icons/arrow-right.svg new file mode 100644 index 00000000000..e3b1e6191d0 --- /dev/null +++ b/packages/ui/src/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/horizontal-density-chart.svg b/packages/ui/src/assets/icons/horizontal-density-chart.svg new file mode 100644 index 00000000000..f78ba351ab3 --- /dev/null +++ b/packages/ui/src/assets/icons/horizontal-density-chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/loading-price-curve.svg b/packages/ui/src/assets/icons/loading-price-curve.svg new file mode 100644 index 00000000000..6e99baec357 --- /dev/null +++ b/packages/ui/src/assets/icons/loading-price-curve.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/icons/search-minus.svg b/packages/ui/src/assets/icons/search-minus.svg new file mode 100644 index 00000000000..a11be5d4cdc --- /dev/null +++ b/packages/ui/src/assets/icons/search-minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/search-plus.svg b/packages/ui/src/assets/icons/search-plus.svg new file mode 100644 index 00000000000..639d3289916 --- /dev/null +++ b/packages/ui/src/assets/icons/search-plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/components/QRCode/QRCode.tsx b/packages/ui/src/components/QRCode/QRCode.tsx index c6c35674f71..dccc204567d 100644 --- a/packages/ui/src/components/QRCode/QRCode.tsx +++ b/packages/ui/src/components/QRCode/QRCode.tsx @@ -4,9 +4,20 @@ import { create, QRCodeErrorCorrectionLevel, QRCodeSegment } from 'qrcode' import { useMemo } from 'react' import Svg, { Defs, G, LinearGradient, Path, Rect, Stop } from 'react-native-svg' +import { isWeb } from 'tamagui' import { BaseQRProps } from 'ui/src/components/QRCode/QRCodeDisplay' import { useSporeColors } from 'ui/src/hooks/useSporeColors' +// size of the SVG element of the eye for the SVG we use in particular. +const SVG_SIZE = 40 +// Alignment markers always take up 7x7 cells +const EYE_SIZE_UNITS = 7 +/** + * Unsure why this is need but the scaling on the web is 2x. This + * offset multiplier ensures that we the eyes align correctly on web. + */ +const EYE_OFFSET_MULTIPLIER = isWeb ? 2 : 1 + export interface QRCodeProps extends BaseQRProps { /* what the qr code stands for */ value: string @@ -35,7 +46,7 @@ const QREyes = ({ fillColor?: string }): JSX.Element => ( - + ( - + @@ -127,10 +138,20 @@ function genMatrix(value: string | QRCodeSegment[], errorCorrectionLevel: QRCode ) } +/** + * Renders a QR code with custom colors, custom eyes, and more. + * + * @param value - The value to encode in the QR code + * @param size - The size of the QR code + * @param color - The color of the QR code + * @param backgroundColor - The background color of the QR code + * @param overlayColor - Additional color to overlay on top of the QR code + * @param quietZone - The quiet zone of the QR code + * @param ecl - The error correction level of the QR code + */ export function QRCode({ value, size, - eyeSize: inputEyeSize, color, backgroundColor: inputBackgroundColor, overlayColor = '#FFFFFF', @@ -139,13 +160,17 @@ export function QRCode({ }: QRCodeProps): JSX.Element | null { const colors = useSporeColors() - const { path } = useMemo(() => { - return transformMatrixIntoCirclePath(genMatrix(value, ecl), size) + const { matrix, path } = useMemo(() => { + const _matrix = genMatrix(value, ecl) + return { + matrix: _matrix, + path: transformMatrixIntoCirclePath(_matrix, size).path, + } }, [value, size, ecl]) - const eyeSize = inputEyeSize ? inputEyeSize : size / 1.5 - + const eyeSize = size * (EYE_SIZE_UNITS / matrix.length) const backgroundColor = inputBackgroundColor ?? colors.surface1.val + const cornerPosition = (size - eyeSize) / EYE_OFFSET_MULTIPLIER return ( diff --git a/packages/ui/src/components/QRCode/QRCodeDisplay.tsx b/packages/ui/src/components/QRCode/QRCodeDisplay.tsx index 2a770df273a..3e08558945d 100644 --- a/packages/ui/src/components/QRCode/QRCodeDisplay.tsx +++ b/packages/ui/src/components/QRCode/QRCodeDisplay.tsx @@ -8,7 +8,6 @@ import { useSporeColors } from 'ui/src/hooks/useSporeColors' export type BaseQRProps = { ecl?: QRCodeErrorCorrectionLevel size: number - eyeSize?: number color: string } @@ -18,7 +17,7 @@ type AddressQRCodeProps = BaseQRProps & { backgroundColor?: string } -function AddressQRCode({ address, ecl, size, eyeSize, backgroundColor, color }: AddressQRCodeProps): JSX.Element { +function AddressQRCode({ address, ecl, size, backgroundColor, color }: AddressQRCodeProps): JSX.Element { const colors = useSporeColors() return ( @@ -26,7 +25,6 @@ function AddressQRCode({ address, ecl, size, eyeSize, backgroundColor, color }: backgroundColor={backgroundColor} color={color} ecl={ecl} - eyeSize={eyeSize} overlayColor={colors.neutral1.val} size={size} value={address} @@ -43,7 +41,6 @@ const _QRCodeDisplay = ({ encodedValue, ecl = 'H', size, - eyeSize, color, containerBackgroundColor, children, @@ -55,7 +52,6 @@ const _QRCodeDisplay = ({ backgroundColor={containerBackgroundColor} color={color} ecl={ecl} - eyeSize={eyeSize} size={size} /> | null export type ButtonProps = CustomButtonProps & TextParentStyles & { - /** - * use haptic feedback on press (native) - */ - hapticFeedback?: boolean - hapticStyle?: HapticFeedbackStyle - /** * add icon before, passes color and size automatically if Component */ @@ -183,18 +175,10 @@ export type ButtonProps = CustomButtonProps & const ButtonComponent = CustomButtonFrame.styleable((props, ref) => { const { props: buttonProps } = useButton(props) - const { hapticFeedback } = useHapticFeedback() return ( { - // eslint-disable-next-line no-void - void hapticFeedback.impact(buttonProps.hapticStyle) - } - : undefined - } + onPressIn={undefined} // or remove this line entirely if onPressIn is not needed {...buttonProps} /> ) diff --git a/packages/ui/src/components/button/PlusMinusButton.tsx b/packages/ui/src/components/button/PlusMinusButton.tsx index 5a50ec50bab..bac1bda5a16 100644 --- a/packages/ui/src/components/button/PlusMinusButton.tsx +++ b/packages/ui/src/components/button/PlusMinusButton.tsx @@ -19,7 +19,6 @@ export function PlusMinusButton({ }): JSX.Element { return ( { + const onPress = (e: GestureResponderEvent): void => { + // Prevent event from bubbling up to parent + e.preventDefault() + e.stopPropagation() onCheckPressed?.(checked) } diff --git a/packages/ui/src/components/icons/ArrowRight.tsx b/packages/ui/src/components/icons/ArrowRight.tsx new file mode 100644 index 00000000000..fd86901ee35 --- /dev/null +++ b/packages/ui/src/components/icons/ArrowRight.tsx @@ -0,0 +1,17 @@ +import { Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [ArrowRight, AnimatedArrowRight] = createIcon({ + name: 'ArrowRight', + getIcon: (props) => ( + + + + ), + defaultFill: '#333639', +}) diff --git a/packages/ui/src/components/icons/HorizontalDensityChart.tsx b/packages/ui/src/components/icons/HorizontalDensityChart.tsx new file mode 100644 index 00000000000..a9d4fab79a0 --- /dev/null +++ b/packages/ui/src/components/icons/HorizontalDensityChart.tsx @@ -0,0 +1,17 @@ +import { Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [HorizontalDensityChart, AnimatedHorizontalDensityChart] = createIcon({ + name: 'HorizontalDensityChart', + getIcon: (props) => ( + + + + ), +}) diff --git a/packages/ui/src/components/icons/LoadingPriceCurve.tsx b/packages/ui/src/components/icons/LoadingPriceCurve.tsx new file mode 100644 index 00000000000..52325ee0d8d --- /dev/null +++ b/packages/ui/src/components/icons/LoadingPriceCurve.tsx @@ -0,0 +1,45 @@ +import { ClipPath, Defs, G, Path, Rect, Svg, Circle as _Circle } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [LoadingPriceCurve, AnimatedLoadingPriceCurve] = createIcon({ + name: 'LoadingPriceCurve', + getIcon: (props) => ( + + + + + + + + + <_Circle cx="-0.156054" cy="28.1994" r="17.676" fill="currentColor" fillOpacity="0.24" /> + + + + + + + + + + + + + + + ), +}) diff --git a/packages/ui/src/components/icons/SearchMinus.tsx b/packages/ui/src/components/icons/SearchMinus.tsx new file mode 100644 index 00000000000..32084e962ce --- /dev/null +++ b/packages/ui/src/components/icons/SearchMinus.tsx @@ -0,0 +1,18 @@ +import { Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [SearchMinus, AnimatedSearchMinus] = createIcon({ + name: 'SearchMinus', + getIcon: (props) => ( + + + + ), +}) diff --git a/packages/ui/src/components/icons/SearchPlus.tsx b/packages/ui/src/components/icons/SearchPlus.tsx new file mode 100644 index 00000000000..06fc45b28fb --- /dev/null +++ b/packages/ui/src/components/icons/SearchPlus.tsx @@ -0,0 +1,18 @@ +import { Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [SearchPlus, AnimatedSearchPlus] = createIcon({ + name: 'SearchPlus', + getIcon: (props) => ( + + + + ), +}) diff --git a/packages/ui/src/components/icons/exported.ts b/packages/ui/src/components/icons/exported.ts index 816db0df2b5..6266eb69a4f 100644 --- a/packages/ui/src/components/icons/exported.ts +++ b/packages/ui/src/components/icons/exported.ts @@ -16,6 +16,7 @@ export * from './ArrowDown' export * from './ArrowDownCircle' export * from './ArrowDownCircleFilled' export * from './ArrowDownInCircle' +export * from './ArrowRight' export * from './ArrowRightToLine' export * from './ArrowRightwardsDown' export * from './ArrowTurnDownRight' @@ -101,6 +102,7 @@ export * from './Heart' export * from './Help' export * from './HelpCenter' export * from './Home' +export * from './HorizontalDensityChart' export * from './InfoCircle' export * from './InfoCircleFilled' export * from './InformationIcon' @@ -115,6 +117,7 @@ export * from './LineChartDots' export * from './LinkBrokenHorizontal' export * from './LinkHorizontalAlt' export * from './List' +export * from './LoadingPriceCurve' export * from './LoadingSpinnerInner' export * from './LoadingSpinnerOuter' export * from './Lock' @@ -175,6 +178,8 @@ export * from './ScanQrWc' export * from './ScanReceive' export * from './Search' export * from './SearchFocused' +export * from './SearchMinus' +export * from './SearchPlus' export * from './SeedPhraseIcon' export * from './SelectIcon' export * from './SendAction' diff --git a/packages/ui/src/components/text/Text.tsx b/packages/ui/src/components/text/Text.tsx index e79a3fd2013..4071a68dcb0 100644 --- a/packages/ui/src/components/text/Text.tsx +++ b/packages/ui/src/components/text/Text.tsx @@ -125,6 +125,7 @@ export type TextProps = TextFrameProps & { allowFontScaling?: boolean loading?: boolean | 'no-shimmer' loadingPlaceholderText?: string + title?: string } // Use this text component throughout the app instead of diff --git a/packages/ui/src/components/touchable/TouchableArea.tsx b/packages/ui/src/components/touchable/TouchableArea.tsx index 91d14769f2c..5cce24ecb52 100644 --- a/packages/ui/src/components/touchable/TouchableArea.tsx +++ b/packages/ui/src/components/touchable/TouchableArea.tsx @@ -4,7 +4,6 @@ import { TamaguiElement, YStack } from 'tamagui' import { withAnimated } from 'ui/src/components/factories/animated' import { TouchableAreaProps } from 'ui/src/components/touchable/types' import { defaultHitslopInset } from 'ui/src/theme' -import { useHapticFeedback } from 'ui/src/utils/haptics/useHapticFeedback' import { isTestEnv } from 'utilities/src/environment/env' export type TouchableAreaEvent = GestureResponderEvent @@ -17,21 +16,10 @@ export type TouchableAreaEvent = GestureResponderEvent * - custom elements that are clickable (e.g. rows, cards, headers) */ export const TouchableArea = forwardRef(function TouchableArea( - { - hapticFeedback: triggerHapticFeedback = false, - ignoreDragEvents = false, - hapticStyle, - scaleTo, - onPress, - children, - hoverable, - activeOpacity = 0.75, - ...restProps - }, + { ignoreDragEvents = false, scaleTo, onPress, children, hoverable, activeOpacity = 0.75, ...restProps }, ref, ): JSX.Element { const touchActivationPositionRef = useRef | null>(null) - const { hapticFeedback } = useHapticFeedback() const onPressHandler = useCallback( async (event: GestureResponderEvent) => { @@ -53,12 +41,8 @@ export const TouchableArea = forwardRef(func } onPress(event) - - if (triggerHapticFeedback) { - await hapticFeedback.impact(hapticStyle) - } }, - [onPress, ignoreDragEvents, triggerHapticFeedback, hapticFeedback, hapticStyle], + [onPress, ignoreDragEvents], ) const onPressInHandler = useMemo(() => { diff --git a/packages/ui/src/components/touchable/types.tsx b/packages/ui/src/components/touchable/types.tsx index 58a542647ca..2667cd83a95 100644 --- a/packages/ui/src/components/touchable/types.tsx +++ b/packages/ui/src/components/touchable/types.tsx @@ -1,12 +1,9 @@ import type { Insets } from 'react-native' import { StackProps } from 'tamagui' -import { HapticFeedbackStyle } from 'ui/src/utils/haptics/helpers' type ExtraProps = { hitSlop?: Insets | number activeOpacity?: number - hapticFeedback?: boolean - hapticStyle?: HapticFeedbackStyle ignoreDragEvents?: boolean scaleTo?: number disabled?: boolean diff --git a/packages/ui/src/hooks/useSporeColors.ts b/packages/ui/src/hooks/useSporeColors.ts index 2e7b6f06565..0875a112d25 100644 --- a/packages/ui/src/hooks/useSporeColors.ts +++ b/packages/ui/src/hooks/useSporeColors.ts @@ -1,16 +1,25 @@ // until the web app needs all of tamagui, avoid heavy imports there // eslint-disable-next-line no-restricted-imports -import { ColorTokens, ThemeKeys, useTheme } from '@tamagui/core' +import { ColorTokens, ThemeKeys, ThemeProps } from '@tamagui/core' +// eslint-disable-next-line no-restricted-imports +import { useTheme } from '@tamagui/core' +import { useMemo } from 'react' // copied from react-native (avoiding import for web) type OpaqueColorValue = symbol & { __TYPE__: 'Color' } export type DynamicColor = ColorTokens | string | OpaqueColorValue -export const useSporeColors = useTheme as unknown as () => { +type UseSporeColorsReturn = { [key in ThemeKeys]: { val: ColorTokens get: () => DynamicColor variable: string } } + +export const useSporeColors = (name?: ThemeProps['name']): UseSporeColorsReturn => { + const config = useMemo(() => ({ name }), [name]) + + return useTheme(config) as unknown as UseSporeColorsReturn +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 00587c71c16..6bf33996407 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -47,11 +47,13 @@ export type { TamaguiElement, TamaguiProviderProps, ThemeKeys, + ThemeName, Tokens, ViewProps, } from 'tamagui' export { LinearGradient } from 'tamagui/linear-gradient' export * from 'ui/src/animations' +export * from './components/InlineCard/InlineCard' export { QRCodeDisplay } from './components/QRCode/QRCodeDisplay' export * from './components/SegmentedControl/SegmentedControl' export { Unicon } from './components/Unicon' @@ -76,10 +78,6 @@ export { MobileDeviceHeight } from './hooks/constants' export { useIsDarkMode } from './hooks/useIsDarkMode' export { useIsShortMobileDevice } from './hooks/useIsShortMobileDevice' export { useSporeColors, type DynamicColor } from './hooks/useSporeColors' -// eslint-disable-next-line no-restricted-imports -export { ImpactFeedbackStyle } from 'expo-haptics' -export * from './components/InlineCard/InlineCard' -export * from './utils/haptics/useHapticFeedback' // Theme export * from './styles/ScrollbarStyles' diff --git a/packages/ui/src/loading/Shine.tsx b/packages/ui/src/loading/Shine.tsx index ebd0479c6a8..1f0fa6fb958 100644 --- a/packages/ui/src/loading/Shine.tsx +++ b/packages/ui/src/loading/Shine.tsx @@ -1,4 +1,4 @@ -import { Flex } from 'ui/src/components/layout' +import { Flex, FlexProps } from 'ui/src/components/layout' import { ShineProps } from 'ui/src/loading/ShineProps' const shineKeyframe = ` @@ -12,11 +12,12 @@ const shineKeyframe = ` } ` -export function Shine({ children, disabled }: ShineProps): JSX.Element { +export function Shine({ children, disabled, ...rest }: ShineProps & FlexProps): JSX.Element { return ( <> => Promise.resolve(), - light: async (): Promise => Promise.resolve(), - medium: async (): Promise => Promise.resolve(), - heavy: async (): Promise => Promise.resolve(), - success: async (): Promise => Promise.resolve(), - warning: async (): Promise => Promise.resolve(), - error: async (): Promise => Promise.resolve(), - selection: async (): Promise => Promise.resolve(), -} - -export interface HapticFeedbackControl { - hapticFeedback: HapticFeedback - hapticsEnabled: boolean - setHapticsEnabled: (val: boolean) => void -} - -export type HapticFeedbackStyle = ImpactFeedbackStyle | NotificationFeedbackType - -export type HapticFeedback = { - impact: (style?: HapticFeedbackStyle) => Promise - light: () => Promise - medium: () => Promise - heavy: () => Promise - success: () => Promise - warning: () => Promise - error: () => Promise - selection: () => Promise -} diff --git a/packages/ui/src/utils/haptics/useHapticFeedback.native.ts b/packages/ui/src/utils/haptics/useHapticFeedback.native.ts deleted file mode 100644 index 5a0de9d671a..00000000000 --- a/packages/ui/src/utils/haptics/useHapticFeedback.native.ts +++ /dev/null @@ -1,44 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import { - ImpactFeedbackStyle, - NotificationFeedbackType, - impactAsync, - notificationAsync, - selectionAsync, -} from 'expo-haptics' -import { HapticFeedbackControl, HapticFeedbackStyle, NO_HAPTIC_FEEDBACK } from 'ui/src/utils/haptics/helpers' - -// false so that people with haptics disabled don't feel one on any startup case, -// though this should be quickly set on app launch -let hapticsEnabled = false - -const ENABLED_HAPTIC_FEEDBACK = { - impact: async ( - style: ImpactFeedbackStyle | NotificationFeedbackType | undefined = ImpactFeedbackStyle.Light, - ): Promise => { - return isImpactFeedbackStyle(style) ? await impactAsync(style) : await notificationAsync(style) - }, - light: async (): Promise => await impactAsync(ImpactFeedbackStyle.Light), - medium: async (): Promise => await impactAsync(ImpactFeedbackStyle.Medium), - heavy: async (): Promise => await impactAsync(ImpactFeedbackStyle.Heavy), - success: async (): Promise => await notificationAsync(NotificationFeedbackType.Success), - warning: async (): Promise => await notificationAsync(NotificationFeedbackType.Warning), - error: async (): Promise => await notificationAsync(NotificationFeedbackType.Error), - selection: async (): Promise => await selectionAsync(), -} - -const setHapticsEnabled = (enabled: boolean): void => { - hapticsEnabled = enabled -} - -export function useHapticFeedback(): HapticFeedbackControl { - return { - hapticFeedback: hapticsEnabled ? ENABLED_HAPTIC_FEEDBACK : NO_HAPTIC_FEEDBACK, - hapticsEnabled, - setHapticsEnabled, - } -} - -function isImpactFeedbackStyle(style: HapticFeedbackStyle): style is ImpactFeedbackStyle { - return Object.values(ImpactFeedbackStyle).includes(style as ImpactFeedbackStyle) -} diff --git a/packages/ui/src/utils/haptics/useHapticFeedback.ts b/packages/ui/src/utils/haptics/useHapticFeedback.ts deleted file mode 100644 index 0ba603b9855..00000000000 --- a/packages/ui/src/utils/haptics/useHapticFeedback.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { HapticFeedbackControl, NO_HAPTIC_FEEDBACK } from 'ui/src/utils/haptics/helpers' - -export function useHapticFeedback(): HapticFeedbackControl { - return { - hapticFeedback: NO_HAPTIC_FEEDBACK, - hapticsEnabled: false, - setHapticsEnabled: (_enabled: boolean): void => {}, - } -} diff --git a/packages/uniswap/babel.config.js b/packages/uniswap/babel.config.js index bdbf700f6b2..a3422fbeb9c 100644 --- a/packages/uniswap/babel.config.js +++ b/packages/uniswap/babel.config.js @@ -5,7 +5,10 @@ const inProduction = NODE_ENV === 'production' module.exports = function (api) { api.cache.using(() => process.env.NODE_ENV) - var plugins = [ + let plugins = inProduction ? ['transform-remove-console'] : [] + + plugins = [ + ...plugins, [ 'module:react-native-dotenv', { @@ -16,26 +19,24 @@ module.exports = function (api) { allowUndefined: false, }, ], + + 'transform-inline-environment-variables', + // TypeScript compiles this, but in production builds, metro doesn't use tsc + '@babel/plugin-proposal-logical-assignment-operators', + // metro doesn't like these + '@babel/plugin-proposal-numeric-separator', // https://github.com/software-mansion/react-native-reanimated/issues/3364#issuecomment-1268591867 '@babel/plugin-proposal-export-namespace-from', + // 'react-native-reanimated/plugin' must be listed last + // https://arc.net/l/quote/plrvpkad [ 'react-native-reanimated/plugin', { globals: ['__scanCodes', '__scanOCR'], }, ], - 'transform-inline-environment-variables', - // TypeScript compiles this, but in production builds, metro doesn't use tsc - '@babel/plugin-proposal-logical-assignment-operators', - // metro doesn't like these - '@babel/plugin-proposal-numeric-separator', ] - if (inProduction) { - // Remove all console statements in production - plugins = [...plugins, 'transform-remove-console'] - } - return { presets: ['module:@react-native/babel-preset'], plugins, diff --git a/packages/uniswap/jest-setup.js b/packages/uniswap/jest-setup.js index 56c6e726a9f..7c3d7c16dc5 100644 --- a/packages/uniswap/jest-setup.js +++ b/packages/uniswap/jest-setup.js @@ -10,6 +10,15 @@ jest.mock('react-native-localize', () => mockRNLocalize) jest.mock('uniswap/src/features/language/LocalizationContext', () => mockLocalizationContext({})) +jest.mock('uniswap/src/features/transactions/swap/settings/contexts/SwapSettingsContext', () => { + return { + useSwapSettingsContext: () => ({ + customDeadline: 20, + customSlippage: 0.5, + }) + } +}) + // Use native modal jest.mock('uniswap/src/components/modals/Modal', () => { return jest.requireActual('uniswap/src/components/modals/Modal.native.tsx') diff --git a/packages/uniswap/src/components/BaseCard/BaseCard.tsx b/packages/uniswap/src/components/BaseCard/BaseCard.tsx index 40872ea2f36..e8dc13071e3 100644 --- a/packages/uniswap/src/components/BaseCard/BaseCard.tsx +++ b/packages/uniswap/src/components/BaseCard/BaseCard.tsx @@ -104,7 +104,7 @@ function EmptyState({ {buttonLabel && ( - + {buttonLabel} @@ -151,7 +151,7 @@ function ErrorState(props: ErrorStateProps): JSX.Element { {retryButtonLabel ? ( - + {retryButtonLabel} @@ -197,7 +197,7 @@ function InlineErrorState(props: InlineErrorStateProps): JSX.Element { {retry ? ( - + {retryButtonLabel} diff --git a/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx b/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx index 4f8f4aca763..30224c164d7 100644 --- a/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx +++ b/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx @@ -25,7 +25,7 @@ import { MaxAmountButton } from 'uniswap/src/components/CurrencyInputPanel/MaxAm import { SelectTokenButton } from 'uniswap/src/components/CurrencyInputPanel/SelectTokenButton' import { MAX_FIAT_INPUT_DECIMALS } from 'uniswap/src/constants/transactions' import { useAccountMeta } from 'uniswap/src/contexts/UniswapContext' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' import { FeatureFlags } from 'uniswap/src/features/gating/flags' @@ -238,7 +238,6 @@ export const CurrencyInputPanel = memo( return ( ) : ( - + 0 diff --git a/packages/uniswap/src/components/CurrencyInputPanel/MaxAmountButton.tsx b/packages/uniswap/src/components/CurrencyInputPanel/MaxAmountButton.tsx index 9ec0682438c..93935659eb7 100644 --- a/packages/uniswap/src/components/CurrencyInputPanel/MaxAmountButton.tsx +++ b/packages/uniswap/src/components/CurrencyInputPanel/MaxAmountButton.tsx @@ -74,7 +74,6 @@ const MaxButtonContent = memo(function _MaxButtonContent({ element={currencyField === CurrencyField.INPUT ? ElementName.SetMaxInput : ElementName.SetMaxOutput} > } export function InlineWarningCard({ @@ -32,12 +37,14 @@ export function InlineWarningCard({ hideCtaIcon, headingTestId, descriptionTestId, + analyticsProperties, }: InlineWarningCardProps): JSX.Element | null { const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) const [checkedFallback, setCheckedFallback] = useState(false) const { color, textColor, backgroundColor } = getWarningIconColors(severity) const WarningIcon = getWarningIcon(severity, tokenProtectionEnabled) const shouldShowCtaIcon = !hideCtaIcon && severity !== WarningSeverity.Low && severity !== WarningSeverity.None + const trace = useTrace() const onCheckPressed = (isChecked: boolean): void => { if (setChecked) { @@ -45,6 +52,13 @@ export function InlineWarningCard({ } else { setCheckedFallback(!isChecked) } + + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + ...trace, + ...analyticsProperties, + checked: !isChecked, + element: ElementName.InlineWarningCardCheckbox, + } as Record) } if (severity === WarningSeverity.None || !WarningIcon) { diff --git a/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx b/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx index 8e5459d435d..e9de13f60ba 100644 --- a/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx +++ b/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx @@ -21,7 +21,7 @@ import { Modal } from 'uniswap/src/components/modals/Modal' import { NetworkFilter } from 'uniswap/src/components/network/NetworkFilter' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import { TradeableAsset } from 'uniswap/src/entities/assets' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { SearchContext } from 'uniswap/src/features/search/SearchContext' @@ -279,7 +279,7 @@ export function TokenSelectorContent({ backgroundColor="$surface2" endAdornment={ - {hasClipboardString && } + {hasClipboardString && } diff --git a/packages/uniswap/src/components/TokenSelector/items/TokenCard.tsx b/packages/uniswap/src/components/TokenSelector/items/TokenCard.tsx index ffe3f822150..60cd30d7cfb 100644 --- a/packages/uniswap/src/components/TokenSelector/items/TokenCard.tsx +++ b/packages/uniswap/src/components/TokenSelector/items/TokenCard.tsx @@ -1,5 +1,5 @@ import { memo } from 'react' -import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' +import { Flex, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { OnSelectCurrency, TokenOption, TokenSection } from 'uniswap/src/components/TokenSelector/types' @@ -26,10 +26,8 @@ function _TokenCard({ return ( diff --git a/packages/uniswap/src/components/TokenSelector/items/TokenOptionItem.tsx b/packages/uniswap/src/components/TokenSelector/items/TokenOptionItem.tsx index 8859e377616..4c6a75dcc68 100644 --- a/packages/uniswap/src/components/TokenSelector/items/TokenOptionItem.tsx +++ b/packages/uniswap/src/components/TokenSelector/items/TokenOptionItem.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react' -import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' +import { Flex, Text, TouchableArea } from 'ui/src' import { Check } from 'ui/src/components/icons/Check' import { iconSizes } from 'ui/src/theme' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' @@ -13,8 +13,8 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils' -import { shortenAddress } from 'uniswap/src/utils/addresses' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { shortenAddress } from 'utilities/src/addresses' import { dismissNativeKeyboard } from 'utilities/src/device/keyboard' import { isInterface } from 'utilities/src/platform' @@ -107,9 +107,7 @@ function _TokenOptionItem({ return ( <> void beforePress?: () => void afterClipboardReceived?: () => void + textVariant?: Extract }): JSX.Element { const { t } = useTranslation() @@ -33,7 +35,7 @@ export default function PasteButton({ - + {label} diff --git a/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx b/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx index 660fb2edba3..34d2b51c18d 100644 --- a/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx +++ b/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx @@ -2,17 +2,7 @@ import { PropsWithChildren, memo, useEffect, useMemo, useRef, useState } from 'r /* eslint-disable-next-line no-restricted-imports */ import { GestureResponderEvent, type View } from 'react-native' import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' -import { - AnimatePresence, - Flex, - FlexProps, - ImpactFeedbackStyle, - Portal, - TouchableArea, - isWeb, - styled, - useIsDarkMode, -} from 'ui/src' +import { AnimatePresence, Flex, FlexProps, Portal, TouchableArea, isWeb, styled, useIsDarkMode } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { iconSizes, spacing, zIndices } from 'ui/src/theme' @@ -133,12 +123,7 @@ export function ActionSheetDropdown({ return ( <> - + {/* collapsable property prevents removing view on Android. Without this property we were getting undefined in measureInWindow callback. (https://reactnative.dev/docs/view.html#collapsable-android) */} ( { diff --git a/packages/uniswap/src/components/modals/ActionSheetModal.tsx b/packages/uniswap/src/components/modals/ActionSheetModal.tsx index 2e11948064d..d95318ef0ad 100644 --- a/packages/uniswap/src/components/modals/ActionSheetModal.tsx +++ b/packages/uniswap/src/components/modals/ActionSheetModal.tsx @@ -42,7 +42,7 @@ export function ActionSheetModalContent(props: ActionSheetModalContentProps): JS {options.map(({ key, onPress, render }) => { return ( - + {render()} ) @@ -51,7 +51,7 @@ export function ActionSheetModalContent(props: ActionSheetModalContentProps): JS - + {closeButtonLabel} diff --git a/packages/uniswap/src/components/modals/HandleBar.native.tsx b/packages/uniswap/src/components/modals/HandleBar.native.tsx index a4d734bc49e..b338a68437c 100644 --- a/packages/uniswap/src/components/modals/HandleBar.native.tsx +++ b/packages/uniswap/src/components/modals/HandleBar.native.tsx @@ -6,7 +6,12 @@ import { isAndroid } from 'utilities/src/platform' const HANDLEBAR_HEIGHT = spacing.spacing4 const HANDLEBAR_WIDTH = spacing.spacing36 -export const HandleBar = ({ backgroundColor, hidden = false, containerFlexStyles }: HandleBarProps): JSX.Element => { +export const HandleBar = ({ + indicatorColor = '$surface3', + backgroundColor, + hidden = false, + containerFlexStyles, +}: HandleBarProps): JSX.Element => { const colors = useSporeColors() const bg = hidden ? 'transparent' : backgroundColor ?? colors.surface1.get() @@ -24,7 +29,7 @@ export const HandleBar = ({ backgroundColor, hidden = false, containerFlexStyles > ) }, - [backgroundColorValue, hideHandlebar, renderBehindTopInset], + [backgroundColorValue, handlebarColor, hideHandlebar, renderBehindTopInset], ) const animatedBorderRadius = useAnimatedStyle(() => { diff --git a/packages/uniswap/src/components/modals/Modal.web.tsx b/packages/uniswap/src/components/modals/Modal.web.tsx index 887216dd8bc..fcfb91b5511 100644 --- a/packages/uniswap/src/components/modals/Modal.web.tsx +++ b/packages/uniswap/src/components/modals/Modal.web.tsx @@ -25,6 +25,7 @@ export function Modal({ paddingX, paddingY, analyticsProperties, + skipLogImpression, }: ModalProps): JSX.Element { const [fullyClosed, setFullyClosed] = useState(false) @@ -53,7 +54,7 @@ export function Modal({ const ModalComponent = bottomAttachment ? WebModalWithBottomAttachment : AdaptiveWebModal return ( - + ['containerComponent'] footerComponent?: ComponentProps['footerComponent'] fullScreen?: boolean + handlebarColor?: HandleBarProps['indicatorColor'] backgroundColor?: ColorTokens blurredBackground?: boolean dismissOnBackPress?: boolean @@ -29,6 +31,7 @@ export type ModalProps = PropsWithChildren<{ // defaults to `true` isModalOpen?: boolean analyticsProperties?: Record + skipLogImpression?: boolean // TODO MOB-2526 refactor Modal to more platform-agnostic alignment?: 'center' | 'top' diff --git a/packages/uniswap/src/components/modals/WarningModal/WarningModal.tsx b/packages/uniswap/src/components/modals/WarningModal/WarningModal.tsx index 42afec2821b..023b9ce0fbc 100644 --- a/packages/uniswap/src/components/modals/WarningModal/WarningModal.tsx +++ b/packages/uniswap/src/components/modals/WarningModal/WarningModal.tsx @@ -34,6 +34,7 @@ type WarningModalContentProps = { // `undefined` means we use the default color, `false` means no background color backgroundIconColor?: ColorValue | false maxWidth?: number + analyticsProperties?: Record } export type WarningModalProps = { @@ -61,6 +62,7 @@ export function WarningModalContent({ maxWidth, hideHandlebar = false, backgroundIconColor, + analyticsProperties, }: PropsWithChildren): JSX.Element { const colors = useSporeColors() const alertColor = getAlertColor(severity) @@ -107,7 +109,7 @@ export function WarningModalContent({ {children} {rejectText && ( - +