ci(release): publish latest release

parent 35df6944
...@@ -51,6 +51,3 @@ packages/uniswap/src/i18n/locales/source/*_old.json ...@@ -51,6 +51,3 @@ packages/uniswap/src/i18n/locales/source/*_old.json
# Vercel # Vercel
.vercel .vercel
# CodeTours Extension
.tours/*
* @uniswap/web-admins
IPFS hash of the deployment: ### Lots of new updates!
- CIDv0: `QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo`
- CIDv1: `bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). ### Bridging
You can now swap your ETH, USDC, and more across 8+ networks! Try it by pressing the banner on your homepage.
You can also access the Uniswap Interface from an IPFS gateway. ### Claim usernames
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways: You can now claim a free uni.eth for your wallet address, a readable username that makess it easy to identify your wallet and receive crypto.
- https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.dweb.link/
- https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.cf-ipfs.com/
- [ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/](ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/)
## 5.56.0 (2024-10-29) ### Multichain Explore
Users can now see all 12 chains we support on the Explore page, and can also filter by a specific chain.
### Features ### Worldchain
* **web:** add empty states for not connected wallets and wallets with no positions (#12973) 31320f1 Users now have access to all ur regular features for this new chain.
* **web:** add entry points for new lp flow (#13053) 1a7a2d9
* **web:** add hook parsing util (#13364) ed1eeb6
* **web:** add mainnet to bridge banner (#13296) 9f2fe44
* **web:** add new TokenWarningCard to tdp and pdp (#12667) 36688f1
* **web:** add the hook modal (#13371) d18aae7
* **web:** add warning icon to search bar (#12768) 830022a
* **web:** adding liquidity create step (#13014) 5ec4b90
* **web:** adding v4 to the liquidity flow (#12793) 966a85d
* **web:** closed Positions CTA at bottom of positions list (#13308) 2220383
* **web:** handle insufficient swap approvals (#13201) 60f699a
* **web:** improve fingerprinting for swap errors (#13045) cc066b9
* **web:** improve remove liquidity modal (#12936) 1ab10ca
* **web:** include poolId on positionInfo object (#13269) 14c26a1
* **web:** LP creation default one input to native currency (#13167) 9de127c
* **web:** migrate v3 liquidity review modal, saga logic (#13008) 3017b06
* **web:** mweb layouts for new lp pages (#13317) d0836e0
* **web:** redesigned pool table tabs (#13291) 918ee0f
* **web:** remove manual wrapping step (#13022) 956377a
* **web:** Remove Vanilla Extract from non-nft code (#12504) 07440e5
* **web:** support v4 position NFT images (#13349) 88062c8
* **web:** truncate bridge activity for smaller screens (#13074) 1a79bda
* **web:** UI updates for the pdp page for v4 (#12878) 491748a
* **web:** updates types in Create flow to support native (#13024) 5657b0e
* **web:** use live fee tier data for position creation flow (#12880) 9939608
* **web:** use tickspacings when fees are selected (#12945) c5908fd
* **web:** v2 create flow setup (#12767) 4a17ba4
* **web:** v3-v4 migrate calldata query (#12902) c4e9646
* **web:** v4 create flow creating a pool (#12747) 64dcd7d
* **web:** v4 url redirects (#13237) d91a04c
### Bug Fixes
* **web:** [v4] fix "New" button styling on positions page (#13143) 24e8bb9
* **web:** [v4] fix reset button (#13160) 8b10c23
* **web:** [v4] normalize language to collect fees (#13150) 97b4fee
* **web:** [v4] polish (#13204) 9735bed
* **web:** Add 3s delay to portfolio balance refetch (#13367) 4605961
* **web:** add help center links (#13147) a9239c4
* **web:** add missing breadcrumb to LP create page (#13306) f0743d2
* **web:** Align Continue button text (#13023) 9bfcaf5
* **web:** allow pool creation on testnets (#13009) 05dfe00
* **web:** bugs in create flow when initializing pool (#13282) c333def
* **web:** check wrapped input approvals for all uniswapx types (#13377) 59a8378
* **web:** create fee tier alignment and nan (#13157) 701c7a9
* **web:** default price range fix (#13169) adc95d6
* **web:** display bridging options in unconnected state (#13048) 0961f12
* **web:** dont hide position filters (#13194) 2a6b72a
* **web:** fallback to local activity if remote is empty (#13135) b5129d8
* **web:** fix blocked tokens on TDP (#12742) d364216
* **web:** fix broken worldchain images (#13028) b9d436d
* **web:** fix crashe in create flow when changing tokens (#13264) 63b9734
* **web:** Fix explore table only scrolling once (#13110) 73083d7
* **web:** fix fee modal crash (#13083) 91983fc
* **web:** fix formatting for closed positions (#13172) bfdedd9
* **web:** fix link to PosDP from migratev3 page (#13311) a8e2050
* **web:** fix network filter on explore (#12876) e6aaa50
* **web:** handle account chain id switch (#12994) 45fcd58
* **web:** handle selecting coin on diff chain (#13149) 49c903d
* **web:** keep old data in positions list while loading new filter results (#13299) 6cc7159
* **web:** mock pair and mock pool price numerator and denominators are switched (#13279) efe13f3
* **web:** navbar links for v4 positions pages (#13271) 2b3821e
* **web:** numeric input validation in fee tiers search (#13304) b51bf90
* **web:** Only poll for bridging status updates if pending txs (#13066) f31d0fc
* **web:** only show bridging card on swap tab (#13333) c1f3eba
* **web:** persist positions filters and remove "closed" from default filter (#13168) f10fc62
* **web:** prevent swap flow from continuing when approval has not bee… (#13374) 7421208
* **web:** Redirect to security measures article while clicking button in ResetComplete step (#12606) e917818
* **web:** remove outputPositionLiquidity from migration request (#13156) 2b4d71c
* **web:** remove second status on pdp (#13210) db5f3e1
* **web:** remove v2 liquidity (add approve step) (#13314) ce48a33
* **web:** show liqudity info badge in step and confirmation (#13312) 0c23eff
* **web:** show Not Found on PosDP if it doesn't exist (#13250) 0177752
* **web:** temp endpoints (#13093) a50b98d
* **web:** unichain modal button widths (#13092) 0eb5abe
* **web:** Unstick continue button from SettingsRecoveryPhrase screen (#12609) 10b17a2
* **web:** update approved token (#13357) de06e23
* **web:** update v2 remove on L2 functionality (#13037) 6971b0b
* **web:** use position chain id (#13001) 3872b9a
* **web:** use prod url for positions API (#13351) e25b4ca
* **web:** v4 create flow - reset tokens on chain changed (#13249) ae6ef1a
* **web:** v4 fixes (#13223) 6ad8335
* **web:** v4 poolsQueryEnabledCheck (#13078) b9f210d
* **web:** various trading api calls fixes (#13102) ecb1c30
### Continuous Integration
* **web:** update sitemaps afffa8d
### Other changes:
- Various bug fixes and performance improvements
\ No newline at end of file
web/5.56.0 extension/1.8.0
\ No newline at end of file \ No newline at end of file
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
"@tamagui/core": "1.108.4", "@tamagui/core": "1.108.4",
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.38.0", "@uniswap/analytics-events": "2.38.0",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18", "@uniswap/uniswapx-sdk": "^2.1.0-beta.14",
"@uniswap/universal-router-sdk": "4.5.2", "@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/v3-sdk": "3.18.1", "@uniswap/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.10.3", "@uniswap/v4-sdk": "1.10.3",
......
...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' ...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal' import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal'
import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions' import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions'
import { Flex, Text, TouchableArea } from 'ui/src' import { ContextMenu, Flex, MenuContentItem, Text, TouchableArea } from 'ui/src'
import { CopySheets, Edit, Ellipsis, TrashFilled } from 'ui/src/components/icons' import { CopySheets, Edit, Ellipsis, TrashFilled } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
...@@ -17,8 +17,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -17,8 +17,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setClipboard } from 'uniswap/src/utils/clipboard' import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types' import { NumberType } from 'utilities/src/format/types'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { MenuContentItem } from 'wallet/src/components/menu/types'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types' import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
...@@ -14,7 +14,7 @@ import { PopupName, openPopup } from 'src/app/features/popups/slice' ...@@ -14,7 +14,7 @@ import { PopupName, openPopup } from 'src/app/features/popups/slice'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants' import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { focusOrCreateUnitagTab } from 'src/app/navigation/utils' import { focusOrCreateUnitagTab } from 'src/app/navigation/utils'
import { Button, Flex, Popover, ScrollView, Text, useSporeColors } from 'ui/src' import { Button, Flex, MenuContent, MenuContentItem, Popover, ScrollView, Text, useSporeColors } from 'ui/src'
import { WalletFilled, X } from 'ui/src/components/icons' import { WalletFilled, X } from 'ui/src/components/icons'
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
...@@ -31,8 +31,6 @@ import { logger } from 'utilities/src/logger/logger' ...@@ -31,8 +31,6 @@ import { logger } from 'utilities/src/logger/logger'
import { sleep } from 'utilities/src/time/timing' import { sleep } from 'utilities/src/time/timing'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle'
import { MenuContent } from 'wallet/src/components/menu/MenuContent'
import { MenuContentItem } from 'wallet/src/components/menu/types'
import { useAccountList } from 'wallet/src/features/accounts/hooks' import { useAccountList } from 'wallet/src/features/accounts/hooks'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
......
...@@ -37,7 +37,7 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps ...@@ -37,7 +37,7 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps
const [inputText, setInputText] = useState<string>(defaultText) const [inputText, setInputText] = useState<string>(defaultText)
const [isfocused, setIsFocused] = useState(false) const [isfocused, setIsFocused] = useState(false)
const { canClaimUnitag } = useCanActiveAddressClaimUnitag() const { canClaimUnitag } = useCanActiveAddressClaimUnitag(address)
const unitagsClaimEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag) const unitagsClaimEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag)
const onConfirm = useCallback(async () => { const onConfirm = useCallback(async () => {
......
...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' ...@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useInterfaceBuyNavigator } from 'src/app/features/for/utils' import { useInterfaceBuyNavigator } from 'src/app/features/for/utils'
import { AppRoutes } from 'src/app/navigation/constants' import { AppRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { AnimatePresence, Flex, Loader } from 'ui/src' import { AnimatePresence, ContextMenu, Flex, Loader } from 'ui/src'
import { ShieldCheck } from 'ui/src/components/icons' import { ShieldCheck } from 'ui/src/components/icons'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal' import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal'
...@@ -15,7 +15,6 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks' ...@@ -15,7 +15,6 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner' import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow' import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
...@@ -212,12 +211,7 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item: ...@@ -212,12 +211,7 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item:
return ( return (
<TokenContextMenu portfolioBalance={portfolioBalance}> <TokenContextMenu portfolioBalance={portfolioBalance}>
<TokenBalanceItem <TokenBalanceItem isLoading={isWarmLoading} portfolioBalance={portfolioBalance} onPressToken={onPressToken} />
isLoading={isWarmLoading}
portfolioBalanceId={portfolioBalance.id}
currencyInfo={portfolioBalance.currencyInfo}
onPressToken={onPressToken}
/>
</TokenContextMenu> </TokenContextMenu>
) )
}) })
......
...@@ -50,7 +50,7 @@ export function ClaimUnitagScreen(): JSX.Element { ...@@ -50,7 +50,7 @@ export function ClaimUnitagScreen(): JSX.Element {
onBack={handleBack} onBack={handleBack}
onSkip={goToNextStep} onSkip={goToNextStep}
> >
<Flex gap="$spacing16" py="$spacing24" width="100%"> <Flex gap="$spacing16" pt="$spacing24" width="100%">
<ClaimUnitagContent <ClaimUnitagContent
animateY={false} animateY={false}
entryPoint={ExtensionOnboardingFlow.New} entryPoint={ExtensionOnboardingFlow.New}
......
...@@ -35,7 +35,7 @@ export function RecipientPanel({ chainId }: RecipientPanelProps): JSX.Element { ...@@ -35,7 +35,7 @@ export function RecipientPanel({ chainId }: RecipientPanelProps): JSX.Element {
onSetShowRecipientSelector(!showRecipientSelector) onSetShowRecipientSelector(!showRecipientSelector)
}, [onSetShowRecipientSelector, showRecipientSelector]) }, [onSetShowRecipientSelector, showRecipientSelector])
const { sections } = useFilteredRecipientSections(pattern) const sections = useFilteredRecipientSections(pattern)
const onSelectRecipient = useCallback((newRecipient: string) => { const onSelectRecipient = useCallback((newRecipient: string) => {
setSelectedRecipient(newRecipient) setSelectedRecipient(newRecipient)
......
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions' import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions'
import { Flex, TouchableArea } from 'ui/src' import { ContextMenu, Flex, TouchableArea } from 'ui/src'
import { Ellipsis, Power } from 'ui/src/components/icons' import { Ellipsis, Power } from 'ui/src/components/icons'
import { pushNotification } from 'uniswap/src/features/notifications/slice' import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types' import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { useActiveAccountAddress, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useActiveAccountAddress, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
const PowerCircle = (): JSX.Element => ( const PowerCircle = (): JSX.Element => (
......
...@@ -37,7 +37,7 @@ export function SettingsRecoveryPhrase({ ...@@ -37,7 +37,7 @@ export function SettingsRecoveryPhrase({
</Flex> </Flex>
</Flex> </Flex>
<Flex grow>{children}</Flex> <Flex grow>{children}</Flex>
<Flex mt="$spacing12"> <Flex>
<Button <Button
disabled={!nextButtonEnabled} disabled={!nextButtonEnabled}
flexGrow={1} flexGrow={1}
......
...@@ -6,14 +6,12 @@ import { UnitagClaimRoutes } from 'src/app/navigation/constants' ...@@ -6,14 +6,12 @@ import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels' import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests'
import { AnimatePresence, Flex } from 'ui/src' import { AnimatePresence, ContextMenu, Flex, MenuContentItem } from 'ui/src'
import { Edit, Ellipsis, Trash } from 'ui/src/components/icons' import { Edit, Ellipsis, Trash } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
import { UnitagScreens } from 'uniswap/src/types/screens/mobile' import { UnitagScreens } from 'uniswap/src/types/screens/mobile'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { MenuContentItem } from 'wallet/src/components/menu/types'
import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal' import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal'
import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal' import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
import { EditUnitagProfileContent } from 'wallet/src/features/unitags/EditUnitagProfileContent' import { EditUnitagProfileContent } from 'wallet/src/features/unitags/EditUnitagProfileContent'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.9.0", "version": "1.8.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
This diff is collapsed.
...@@ -90,9 +90,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) { ...@@ -90,9 +90,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.39" def devVersionName = "1.38"
def betaVersionName = "1.39" def betaVersionName = "1.38"
def prodVersionName = "1.39" def prodVersionName = "1.38"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
<string>applinks:uniswap.org</string> <string>applinks:uniswap.org</string>
<string>applinks:app.uniswap.org</string> <string>applinks:app.uniswap.org</string>
<string>applinks:app.corn-staging.com</string> <string>applinks:app.corn-staging.com</string>
<string>webcredentials:app.uniswap.org</string>
</array> </array>
<key>com.apple.developer.devicecheck.appattest-environment</key> <key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string> <string>production</string>
......
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
"@uniswap/analytics-events": "2.38.0", "@uniswap/analytics-events": "2.38.0",
"@uniswap/client-explore": "0.0.10", "@uniswap/client-explore": "0.0.10",
"@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/ethers-rs-mobile": "0.0.5",
"@uniswap/sdk-core": "5.9.0", "@uniswap/sdk-core": "5.8.4",
"@walletconnect/core": "2.17.1", "@walletconnect/core": "2.17.1",
"@walletconnect/react-native-compat": "2.17.1", "@walletconnect/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1", "@walletconnect/utils": "2.17.1",
......
...@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' ...@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native' import { TextInput } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal' import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal'
import { Flex, Loader, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import ScanQRIcon from 'ui/src/assets/icons/scan.svg'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
...@@ -51,7 +51,7 @@ export function _RecipientSelect({ ...@@ -51,7 +51,7 @@ export function _RecipientSelect({
const [showQRScanner, setShowQRScanner] = useState(false) const [showQRScanner, setShowQRScanner] = useState(false)
const [checkSpeedBumps, setCheckSpeedBumps] = useState(false) const [checkSpeedBumps, setCheckSpeedBumps] = useState(false)
const [selectedRecipient, setSelectedRecipient] = useState(recipient) const [selectedRecipient, setSelectedRecipient] = useState(recipient)
const { sections, loading } = useFilteredRecipientSections(pattern) const sections = useFilteredRecipientSections(pattern)
useEffect(() => { useEffect(() => {
if (focusInput) { if (focusInput) {
...@@ -104,9 +104,7 @@ export function _RecipientSelect({ ...@@ -104,9 +104,7 @@ export function _RecipientSelect({
onBack={recipient ? onHideRecipientSelector : undefined} onBack={recipient ? onHideRecipientSelector : undefined}
onChangeText={setPattern} onChangeText={setPattern}
/> />
{loading ? ( {!sections.length ? (
<Loader.SearchResult />
) : !sections.length ? (
<Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24"> <Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24">
<Text variant="buttonLabel2">{t('send.recipient.results.empty')}</Text> <Text variant="buttonLabel2">{t('send.recipient.results.empty')}</Text>
<Text color="$neutral3" textAlign="center" variant="body1"> <Text color="$neutral3" textAlign="center" variant="body1">
...@@ -114,6 +112,7 @@ export function _RecipientSelect({ ...@@ -114,6 +112,7 @@ export function _RecipientSelect({
</Text> </Text>
</Flex> </Flex>
) : ( ) : (
// Show either suggested recipients or filtered sections based on query
<RecipientList renderedInModal={renderedInModal} sections={sections} onPress={onSelect} /> <RecipientList renderedInModal={renderedInModal} sections={sections} onPress={onSelect} />
)} )}
</AnimatedFlex> </AnimatedFlex>
......
...@@ -109,8 +109,8 @@ export function SettingsRow({ ...@@ -109,8 +109,8 @@ export function SettingsRow({
) : screen || modal ? ( ) : screen || modal ? (
<Flex centered row> <Flex centered row>
{currentSetting ? ( {currentSetting ? (
<Flex shrink alignItems="flex-end" flexBasis="35%" justifyContent="flex-end"> <Flex row shrink alignItems="flex-end" flexBasis="30%" justifyContent="flex-end">
<Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={2} variant="body3"> <Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={1} variant="body3">
{currentSetting} {currentSetting}
</Text> </Text>
</Flex> </Flex>
......
import React, { PropsWithChildren, memo, useMemo } from 'react' import React, { memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { borderRadii } from 'ui/src/theme' import { borderRadii } from 'ui/src/theme'
...@@ -6,12 +6,13 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generat ...@@ -6,12 +6,13 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generat
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu' import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu'
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContextMenu({ export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({
portfolioBalance, portfolioBalance,
children, children,
}: PropsWithChildren<{ }: {
portfolioBalance: PortfolioBalance portfolioBalance: PortfolioBalance
}>) { children: React.ReactNode
}) {
const { t } = useTranslation() const { t } = useTranslation()
const { menuActions, onContextMenuPress } = useTokenContextMenu({ const { menuActions, onContextMenuPress } = useTokenContextMenu({
......
...@@ -7,8 +7,6 @@ import { ...@@ -7,8 +7,6 @@ import {
TokenDetailsScreenQuery, TokenDetailsScreenQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
export interface TokenDetailsHeaderProps { export interface TokenDetailsHeaderProps {
...@@ -22,12 +20,9 @@ export function TokenDetailsHeader({ ...@@ -22,12 +20,9 @@ export function TokenDetailsHeader({
loading = false, loading = false,
onPressWarningIcon, onPressWarningIcon,
}: TokenDetailsHeaderProps): JSX.Element { }: TokenDetailsHeaderProps): JSX.Element {
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const token = data?.token const token = data?.token
const tokenProject = token?.project const tokenProject = token?.project
const shouldShowWarningIcon =
!tokenProtectionEnabled &&
(tokenProject?.safetyLevel === SafetyLevel.StrongWarning || tokenProject?.safetyLevel === SafetyLevel.Blocked)
return ( return (
<Flex gap="$spacing12" mx="$spacing16"> <Flex gap="$spacing12" mx="$spacing16">
<TokenLogo <TokenLogo
...@@ -47,7 +42,9 @@ export function TokenDetailsHeader({ ...@@ -47,7 +42,9 @@ export function TokenDetailsHeader({
> >
{token?.name ?? ''} {token?.name ?? ''}
</Text> </Text>
{shouldShowWarningIcon && ( {/* Suppress warning icon on low warning level */}
{(tokenProject?.safetyLevel === SafetyLevel.StrongWarning ||
tokenProject?.safetyLevel === SafetyLevel.Blocked) && (
<TouchableArea onPress={onPressWarningIcon}> <TouchableArea onPress={onPressWarningIcon}>
<WarningIcon safetyLevel={tokenProject?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" /> <WarningIcon safetyLevel={tokenProject?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" />
</TouchableArea> </TouchableArea>
......
...@@ -48,7 +48,7 @@ function TokenOptionItemWrapper({ ...@@ -48,7 +48,7 @@ function TokenOptionItemWrapper({
[currencyInfo, balanceUSD, quantity, isUnsupported], [currencyInfo, balanceUSD, quantity, isUnsupported],
) )
const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency]) const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency])
const { tokenWarningDismissed } = useDismissedTokenWarnings(currencyInfo?.currency) const { tokenWarningDismissed, onDismissTokenWarning } = useDismissedTokenWarnings(currencyInfo?.currency)
const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext()
if (!option) { if (!option) {
...@@ -62,6 +62,7 @@ function TokenOptionItemWrapper({ ...@@ -62,6 +62,7 @@ function TokenOptionItemWrapper({
return ( return (
<TokenOptionItem <TokenOptionItem
balance={convertFiatAmountFormatted(option.balanceUSD, NumberType.FiatTokenPrice)} balance={convertFiatAmountFormatted(option.balanceUSD, NumberType.FiatTokenPrice)}
dismissWarningCallback={onDismissTokenWarning}
isSelected={isSelected} isSelected={isSelected}
option={option} option={option}
quantity={option.quantity} quantity={option.quantity}
...@@ -148,10 +149,6 @@ function _TokenFiatOnRampList({ ...@@ -148,10 +149,6 @@ function _TokenFiatOnRampList({
return <></> return <></>
} }
if (section.data.length === 0) {
return <></>
}
return ( return (
<Flex mt="$spacing12"> <Flex mt="$spacing12">
<ListSeparatorToggle <ListSeparatorToggle
......
import React, { memo, useCallback } from 'react' import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native' import { ViewProps } from 'react-native'
import ContextMenu from 'react-native-context-menu-view' import ContextMenu from 'react-native-context-menu-view'
import { SharedValue } from 'react-native-reanimated' import { FadeIn, SharedValue } from 'react-native-reanimated'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton' import RemoveButton from 'src/components/explore/RemoveButton'
...@@ -125,6 +125,7 @@ function FavoriteTokenCard({ ...@@ -125,6 +125,7 @@ function FavoriteTokenCard({
borderColor={opacify(0.05, colors.surface3.val)} borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16" borderRadius="$rounded16"
borderWidth={isDarkMode ? '$none' : '$spacing1'} borderWidth={isDarkMode ? '$none' : '$spacing1'}
entering={FadeIn}
hapticFeedback={!isEditing} hapticFeedback={!isEditing}
hapticStyle={ImpactFeedbackStyle.Light} hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4" m="$spacing4"
......
...@@ -206,7 +206,7 @@ describe(useExploreTokenContextMenu, () => { ...@@ -206,7 +206,7 @@ describe(useExploreTokenContextMenu, () => {
payload: { payload: {
name: 'swap-modal', name: 'swap-modal',
initialState: { initialState: {
exactAmountToken: '', exactAmountToken: '0',
exactCurrencyField: 'input', exactCurrencyField: 'input',
[CurrencyField.INPUT]: null, [CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: { [CurrencyField.OUTPUT]: {
......
...@@ -59,7 +59,7 @@ export function useExploreTokenContextMenu({ ...@@ -59,7 +59,7 @@ export function useExploreTokenContextMenu({
const onPressSwap = useCallback(() => { const onPressSwap = useCallback(() => {
const swapFormState: TransactionState = { const swapFormState: TransactionState = {
exactCurrencyField: CurrencyField.INPUT, exactCurrencyField: CurrencyField.INPUT,
exactAmountToken: '', exactAmountToken: '0',
[CurrencyField.INPUT]: null, [CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: { [CurrencyField.OUTPUT]: {
chainId, chainId,
......
...@@ -4,11 +4,9 @@ import { FlatList, ListRenderItemInfo } from 'react-native' ...@@ -4,11 +4,9 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils' import { getSearchResultId } from 'src/components/explore/search/utils'
import { Flex, Loader } from 'ui/src' import { Flex, Loader } from 'ui/src'
import { ProtectionResult, SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base' import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings' import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { TokenList } from 'uniswap/src/features/dataApi/types'
import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { RankingType } from 'wallet/src/features/wallet/types' import { RankingType } from 'wallet/src/features/wallet/types'
...@@ -34,14 +32,7 @@ function tokenStatsToTokenSearchResult(token: Maybe<TokenRankingsStat>): TokenSe ...@@ -34,14 +32,7 @@ function tokenStatsToTokenSearchResult(token: Maybe<TokenRankingsStat>): TokenSe
name, name,
symbol, symbol,
logoUrl: logo ?? null, logoUrl: logo ?? null,
// BE has confirmed that all of these TokenRankingsStat tokens are Verified SafetyLevel, and design confirmed that we can hide the warning icon here safetyLevel: null,
safetyLevel: SafetyLevel.Verified,
safetyInfo: {
tokenList: TokenList.Default,
attackType: undefined,
protectionResult: ProtectionResult.Benign,
},
feeData: null,
} }
} }
......
import React from 'react' import React from 'react'
import { NFTHeaderItem, TokenHeaderItem, WalletHeaderItem } from 'src/components/explore/search/constants' import { useTranslation } from 'react-i18next'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { SearchHeader } from 'src/components/explore/search/types'
import { Flex, Loader } from 'ui/src' import { Flex, Loader } from 'ui/src'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
function SectionLoader({ searchHeader, repeat = 1 }: { searchHeader: SearchHeader; repeat?: number }): JSX.Element { export const SearchResultsLoader = ({ selectedChain }: { selectedChain: UniverseChainId | null }): JSX.Element => {
return ( const { t } = useTranslation()
<Flex gap="$spacing12">
<SectionHeaderText icon={searchHeader.icon} title={searchHeader.title} />
<Flex mx="$spacing24">
<Loader.SearchResult repeat={repeat} />
</Flex>
</Flex>
)
}
/**
* Placeholder component used while a search is loading.
*/
export function SearchResultsLoader({ selectedChain }: { selectedChain: UniverseChainId | null }): JSX.Element {
// Only mainnet or "all" networks support nfts, hide loader otherwise // Only mainnet or "all" networks support nfts, hide loader otherwise
const hideNftLoading = selectedChain !== null && selectedChain !== UniverseChainId.Mainnet const hideNftLoading = selectedChain !== null && selectedChain !== UniverseChainId.Mainnet
return ( return (
<Flex gap="$spacing16"> <Flex gap="$spacing16">
<SectionLoader searchHeader={TokenHeaderItem} repeat={2} /> <Flex gap="$spacing12">
<SectionLoader searchHeader={WalletHeaderItem} /> <SectionHeaderText
{!hideNftLoading && <SectionLoader searchHeader={NFTHeaderItem} repeat={2} />} icon={<Coin color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.tokens')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex>
<Flex gap="$spacing12">
<SectionHeaderText
icon={<Person color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.wallets')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token />
</AnimatedFlex>
</Flex>
{!hideNftLoading && (
<Flex gap="$spacing12">
<SectionHeaderText
icon={<Gallery color="$neutral2" size="$icon.24" />}
title={t('explore.search.section.nft')}
/>
<AnimatedFlex entering={FadeIn} exiting={FadeOut} mx="$spacing24">
<Loader.Token repeat={2} />
</AnimatedFlex>
</Flex>
)}
</Flex> </Flex>
) )
} }
...@@ -4,13 +4,7 @@ import { FlatList, ListRenderItemInfo } from 'react-native' ...@@ -4,13 +4,7 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader' import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import { import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
EtherscanHeaderItem,
NFTHeaderItem,
SEARCH_RESULT_HEADER_KEY,
TokenHeaderItem,
WalletHeaderItem,
} from 'src/components/explore/search/constants'
import { useWalletSearchResults } from 'src/components/explore/search/hooks' import { useWalletSearchResults } from 'src/components/explore/search/hooks'
import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem' import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem'
import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem' import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem'
...@@ -25,8 +19,10 @@ import { ...@@ -25,8 +19,10 @@ import {
getSearchResultId, getSearchResultId,
} from 'src/components/explore/search/utils' } from 'src/components/explore/search/utils'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { import {
...@@ -35,10 +31,36 @@ import { ...@@ -35,10 +31,36 @@ import {
TokenSearchResult, TokenSearchResult,
} from 'uniswap/src/features/search/SearchResult' } from 'uniswap/src/features/search/SearchResult'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import i18n from 'uniswap/src/i18n/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { getValidAddress } from 'uniswap/src/utils/addresses' import { getValidAddress } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger' import { logger } from 'utilities/src/logger/logger'
const ICON_SIZE = '$icon.24'
const ICON_COLOR = '$neutral2'
const WalletHeaderItem: SearchResultOrHeader = {
icon: <Person color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.wallets'),
}
const TokenHeaderItem: SearchResultOrHeader = {
icon: <Coin color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.tokens'),
}
const NFTHeaderItem: SearchResultOrHeader = {
icon: <Gallery color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.nft'),
}
const EtherscanHeaderItem: (chainId: UniverseChainId) => SearchResultOrHeader = (chainId: UniverseChainId) => ({
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.action.viewEtherscan', {
blockExplorerName: UNIVERSE_CHAIN_INFO[chainId].explorer.name,
}),
})
const IGNORED_ERRORS = ['Subgraph provider undefined not supported'] const IGNORED_ERRORS = ['Subgraph provider undefined not supported']
export function SearchResultsSection({ export function SearchResultsSection({
......
import { SearchHeader, SearchHeaderKey } from 'src/components/explore/search/types' export const SEARCH_RESULT_HEADER_KEY = 'header'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import i18n from 'uniswap/src/i18n/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains'
export const SEARCH_RESULT_HEADER_KEY: SearchHeaderKey = 'header'
const ICON_SIZE = '$icon.24'
const ICON_COLOR = '$neutral2'
export const WalletHeaderItem: SearchHeader = {
icon: <Person color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.wallets'),
}
export const TokenHeaderItem: SearchHeader = {
icon: <Coin color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.tokens'),
}
export const NFTHeaderItem: SearchHeader = {
icon: <Gallery color={ICON_COLOR} size={ICON_SIZE} />,
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.section.nft'),
}
export const EtherscanHeaderItem: (chainId: UniverseChainId) => SearchHeader = (chainId: UniverseChainId) => ({
type: SEARCH_RESULT_HEADER_KEY,
title: i18n.t('explore.search.action.viewEtherscan', {
blockExplorerName: UNIVERSE_CHAIN_INFO[chainId].explorer.name,
}),
})
...@@ -7,7 +7,7 @@ import { disableOnPress } from 'src/utils/disableOnPress' ...@@ -7,7 +7,7 @@ import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' import WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { getWarningIconColors } from 'uniswap/src/components/warnings/utils' import { getWarningIconColorOverride } from 'uniswap/src/components/warnings/utils'
import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
...@@ -28,12 +28,11 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): ...@@ -28,12 +28,11 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
const dispatch = useDispatch() const dispatch = useDispatch()
const tokenDetailsNavigation = useTokenDetailsNavigation() const tokenDetailsNavigation = useTokenDetailsNavigation()
const { chainId, address, name, symbol, logoUrl, safetyLevel, safetyInfo, feeData } = token const { chainId, address, name, symbol, logoUrl, safetyLevel, safetyInfo } = token
const currencyId = address ? buildCurrencyId(chainId, address) : buildNativeCurrencyId(chainId as UniverseChainId) const currencyId = address ? buildCurrencyId(chainId, address) : buildNativeCurrencyId(chainId as UniverseChainId)
const currencyInfo = useCurrencyInfo(currencyId) const currencyInfo = useCurrencyInfo(currencyId)
const severity = getTokenWarningSeverity(currencyInfo) const severity = getTokenWarningSeverity(currencyInfo)
// in mobile search, we only show the warning icon if token is >=Medium severity const warningIconColor = getWarningIconColorOverride(severity)
const { colorSecondary: warningIconColor } = getWarningIconColors(severity)
const onPress = (): void => { const onPress = (): void => {
tokenDetailsNavigation.preload(currencyId) tokenDetailsNavigation.preload(currencyId)
...@@ -61,7 +60,6 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): ...@@ -61,7 +60,6 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
logoUrl, logoUrl,
safetyLevel, safetyLevel,
safetyInfo, safetyInfo,
feeData,
}, },
}), }),
) )
......
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
import { SearchResult } from 'uniswap/src/features/search/SearchResult' import { SearchResult } from 'uniswap/src/features/search/SearchResult'
export type SearchHeaderKey = 'header' // Header type used to render header text instead of SearchResult item
export type SearchHeader = { type: SearchHeaderKey; title: string; icon?: JSX.Element }
export type SearchResultOrHeader = SearchResult | SearchHeader export type SearchResultOrHeader =
| SearchResult
| { type: typeof SEARCH_RESULT_HEADER_KEY; title: string; icon?: JSX.Element }
...@@ -6,7 +6,6 @@ import { ...@@ -6,7 +6,6 @@ import {
} from 'src/components/explore/search/utils' } from 'src/components/explore/search/utils'
import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils'
import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { SearchResultType } from 'uniswap/src/features/search/SearchResult'
import { amount, ethToken, nftCollection, nftContract, token, tokenMarket } from 'uniswap/src/test/fixtures' import { amount, ethToken, nftCollection, nftContract, token, tokenMarket } from 'uniswap/src/test/fixtures'
import { createArray } from 'uniswap/src/test/utils' import { createArray } from 'uniswap/src/test/utils'
...@@ -80,10 +79,6 @@ describe(formatTokenSearchResults, () => { ...@@ -80,10 +79,6 @@ describe(formatTokenSearchResults, () => {
expect(result?.[0]?.symbol).toEqual(searchToken.symbol) expect(result?.[0]?.symbol).toEqual(searchToken.symbol)
expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl) expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl)
expect(result?.[0]?.safetyLevel).toEqual(searchToken.project?.safetyLevel) expect(result?.[0]?.safetyLevel).toEqual(searchToken.project?.safetyLevel)
expect(result?.[0]?.feeData).toEqual(searchToken.feeData)
expect(result?.[0]?.safetyInfo).toEqual(
getCurrencySafetyInfo(searchToken.project?.safetyLevel, searchToken.protectionInfo),
)
}) })
describe(gqlNFTToNFTCollectionSearchResult, () => { describe(gqlNFTToNFTCollectionSearchResult, () => {
......
...@@ -33,7 +33,7 @@ export function formatTokenSearchResults( ...@@ -33,7 +33,7 @@ export function formatTokenSearchResults(
return tokensMap return tokensMap
} }
const { name, chain, address, symbol, project, market, protectionInfo, feeData } = token const { name, chain, address, symbol, project, market, protectionInfo } = token
const chainId = fromGraphQLChain(chain) const chainId = fromGraphQLChain(chain)
const shoulderFilterByChain = !!selectedChain const shoulderFilterByChain = !!selectedChain
...@@ -55,7 +55,6 @@ export function formatTokenSearchResults( ...@@ -55,7 +55,6 @@ export function formatTokenSearchResults(
logoUrl: logoUrl ?? null, logoUrl: logoUrl ?? null,
volume1D: market?.volume?.value ?? 0, volume1D: market?.volume?.value ?? 0,
safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo), safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo),
feeData: feeData ?? null,
} }
// For token results that share the same TokenProject id, use the token with highest volume // For token results that share the same TokenProject id, use the token with highest volume
......
...@@ -397,10 +397,7 @@ function _OpenAIContextProvider({ children }: { children: React.ReactNode }): JS ...@@ -397,10 +397,7 @@ function _OpenAIContextProvider({ children }: { children: React.ReactNode }): JS
const listener = Linking.addEventListener('url', (event) => { const listener = Linking.addEventListener('url', (event) => {
if (event.url.startsWith('uniswap://openai')) { if (event.url.startsWith('uniswap://openai')) {
const capturedPhrase = decodeURI(event.url.split('uniswap://openai?capturedPhrase=')[1] ?? '') const capturedPhrase = decodeURI(event.url.split('uniswap://openai?capturedPhrase=')[1] ?? '')
capturedPhrase && capturedPhrase && sendMessage(capturedPhrase).catch(console.error)
sendMessage(capturedPhrase).catch((e) =>
logger.error(e, { tags: { file: 'OpenAIContext', function: 'siriListener' } }),
)
} }
}) })
return listener.remove return listener.remove
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { Flex, Text, TouchableArea, useIsShortMobileDevice, useSporeColors } from 'ui/src'
import InfoCircleFilled from 'ui/src/assets/icons/info-circle-filled.svg' import InfoCircleFilled from 'ui/src/assets/icons/info-circle-filled.svg'
import { AlertCircle } from 'ui/src/components/icons' import { AlertCircle } from 'ui/src/components/icons'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
...@@ -16,7 +16,6 @@ import { MAX_FIAT_INPUT_DECIMALS } from 'uniswap/src/constants/transactions' ...@@ -16,7 +16,6 @@ import { MAX_FIAT_INPUT_DECIMALS } from 'uniswap/src/constants/transactions'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { import {
DecimalPadCalculateSpace, DecimalPadCalculateSpace,
DecimalPadCalculatedSpaceId,
DecimalPadInput, DecimalPadInput,
DecimalPadInputRef, DecimalPadInputRef,
} from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput' } from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput'
...@@ -43,6 +42,7 @@ const TRANSFER_DIRECTION_BUTTON_BORDER_WIDTH = spacing.spacing4 ...@@ -43,6 +42,7 @@ const TRANSFER_DIRECTION_BUTTON_BORDER_WIDTH = spacing.spacing4
export function SendTokenForm(): JSX.Element { export function SendTokenForm(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
const isShortMobileDevice = useIsShortMobileDevice()
const { fullHeight } = useDeviceDimensions() const { fullHeight } = useDeviceDimensions()
const { walletNeedsRestore, openWalletRestoreModal } = useTransactionModalContext() const { walletNeedsRestore, openWalletRestoreModal } = useTransactionModalContext()
...@@ -220,6 +220,8 @@ export function SendTokenForm(): JSX.Element { ...@@ -220,6 +220,8 @@ export function SendTokenForm(): JSX.Element {
maxDecimals, maxDecimals,
}) })
console.log('truncatedValue in decimal set value', truncatedValue)
if (isFiatInput) { if (isFiatInput) {
exactAmountFiatRef.current = truncatedValue exactAmountFiatRef.current = truncatedValue
} else { } else {
...@@ -380,11 +382,9 @@ export function SendTokenForm(): JSX.Element { ...@@ -380,11 +382,9 @@ export function SendTokenForm(): JSX.Element {
) : null} ) : null}
</Flex> </Flex>
</Flex> </Flex>
{!nftIn && ( {!nftIn && (
<> <>
<DecimalPadCalculateSpace id={DecimalPadCalculatedSpaceId.Send} decimalPadRef={decimalPadRef} /> <DecimalPadCalculateSpace decimalPadRef={decimalPadRef} isShortMobileDevice={isShortMobileDevice} />
<Flex <Flex
animation="quick" animation="quick"
bottom={0} bottom={0}
......
...@@ -54,7 +54,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' ...@@ -54,7 +54,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { FORAmountEnteredProperties } from 'uniswap/src/features/telemetry/types' import { FORAmountEnteredProperties } from 'uniswap/src/features/telemetry/types'
import { import {
DecimalPadCalculateSpace, DecimalPadCalculateSpace,
DecimalPadCalculatedSpaceId,
DecimalPadInput, DecimalPadInput,
DecimalPadInputRef, DecimalPadInputRef,
} from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput' } from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput'
...@@ -330,8 +329,6 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -330,8 +329,6 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
setValue('') setValue('')
setAmount(0) setAmount(0)
valueRef.current = ''
resetSelection({ start: 0 })
setQuoteCurrency(defaultCurrency) setQuoteCurrency(defaultCurrency)
sendAnalyticsEvent(FiatOffRampEventName.FORBuySellToggled, { sendAnalyticsEvent(FiatOffRampEventName.FORBuySellToggled, {
...@@ -402,9 +399,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { ...@@ -402,9 +399,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
setShowTokenSelector(true) setShowTokenSelector(true)
}} }}
/> />
<DecimalPadCalculateSpace decimalPadRef={decimalPadRef} isShortMobileDevice={isShortMobileDevice} />
<DecimalPadCalculateSpace id={DecimalPadCalculatedSpaceId.FiatOnRamp} decimalPadRef={decimalPadRef} />
<AnimatedFlex <AnimatedFlex
bottom={0} bottom={0}
exiting={FadeOutDown} exiting={FadeOutDown}
......
...@@ -65,7 +65,7 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop ...@@ -65,7 +65,7 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop
<Text adjustsFontSizeToFit variant="subheading1"> <Text adjustsFontSizeToFit variant="subheading1">
{sanitizeAddressText(shortenAddress(mnemonicId))} {sanitizeAddressText(shortenAddress(mnemonicId))}
</Text> </Text>
<Text adjustsFontSizeToFit color="$neutral2" variant="body3"> <Text adjustsFontSizeToFit color="$neutral2" variant="buttonLabel2">
{localizedDayjs.unix(createdAt).format(FORMAT_DATE_TIME_SHORT)} {localizedDayjs.unix(createdAt).format(FORMAT_DATE_TIME_SHORT)}
</Text> </Text>
</Flex> </Flex>
......
...@@ -16,6 +16,7 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants' ...@@ -16,6 +16,7 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import i18n from 'uniswap/src/i18n/i18n' import i18n from 'uniswap/src/i18n/i18n'
import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'utilities/src/platform'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
import { openSettings } from 'wallet/src/utils/linking' import { openSettings } from 'wallet/src/utils/linking'
...@@ -73,7 +74,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop ...@@ -73,7 +74,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop
title={t('onboarding.notification.title')} title={t('onboarding.notification.title')}
onSkip={navigateToNextScreen} onSkip={navigateToNextScreen}
> >
<Flex centered shrink> <Flex centered shrink py={isIOS ? '$spacing60' : '$spacing16'}>
<NotificationsBackgroundImage /> <NotificationsBackgroundImage />
</Flex> </Flex>
<Trace logPress element={ElementName.Enable}> <Trace logPress element={ElementName.Enable}>
......
import { NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BlurView } from 'expo-blur'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ActivityIndicator, Alert, Image, Platform, StyleSheet } from 'react-native' import { ActivityIndicator, Alert, Image, Platform, StyleSheet } from 'react-native'
...@@ -17,7 +18,10 @@ import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' ...@@ -17,7 +18,10 @@ import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks' import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks'
import { Button, Flex, useIsDarkMode, useSporeColors } from 'ui/src' import { Button, Flex, useIsDarkMode, useSporeColors } from 'ui/src'
import { SECURITY_SCREEN_BACKGROUND_DARK, SECURITY_SCREEN_BACKGROUND_LIGHT } from 'ui/src/assets' import { SECURITY_SCREEN_BACKGROUND_DARK, SECURITY_SCREEN_BACKGROUND_LIGHT } from 'ui/src/assets'
import FaceIcon from 'ui/src/assets/icons/faceid-thin.svg'
import FingerprintIcon from 'ui/src/assets/icons/fingerprint.svg'
import { Lock } from 'ui/src/components/icons' import { Lock } from 'ui/src/components/icons'
import { borderRadii, imageSizes, opacify } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants' import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ImportType } from 'uniswap/src/types/onboarding' import { ImportType } from 'uniswap/src/types/onboarding'
...@@ -31,6 +35,7 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { ...@@ -31,6 +35,7 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const colors = useSporeColors() const colors = useSporeColors()
const dispatch = useDispatch() const dispatch = useDispatch()
const isDarkMode = useIsDarkMode()
const [isLoadingAccount, setIsLoadingAccount] = useState(false) const [isLoadingAccount, setIsLoadingAccount] = useState(false)
const [showWarningModal, setShowWarningModal] = useState(false) const [showWarningModal, setShowWarningModal] = useState(false)
...@@ -114,10 +119,27 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { ...@@ -114,10 +119,27 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element {
title={t('onboarding.security.title')} title={t('onboarding.security.title')}
onSkip={onSkipPressed} onSkip={onSkipPressed}
> >
<Flex centered shrink gap="$spacing16"> <Flex centered shrink gap="$spacing16" my="$spacing12" position="relative" py="$spacing24">
<Flex> <Flex pt="$spacing24">
<SecurityBackgroundImage /> <SecurityBackgroundImage />
</Flex> </Flex>
<Flex
backgroundColor={opacify(35, colors.surface1.val)}
borderColor={opacify(15, colors.white.val)}
borderRadius="$rounded16"
borderWidth={1}
overflow="hidden"
p="$spacing36"
position="absolute"
top={0}
>
<BlurView intensity={isDarkMode ? (isIOS ? 20 : 80) : 40} style={styles.blurView} tint="dark" />
{isTouchIdDevice ? (
<FingerprintIcon color={colors.white.val} height={imageSizes.image48} width={imageSizes.image48} />
) : (
<FaceIcon color={colors.white.val} height={imageSizes.image48} width={imageSizes.image48} />
)}
</Flex>
</Flex> </Flex>
<Trace logPress element={ElementName.Enable}> <Trace logPress element={ElementName.Enable}>
<Button theme="primary" onPress={onPressEnableSecurity}> <Button theme="primary" onPress={onPressEnableSecurity}>
...@@ -147,6 +169,10 @@ const SecurityBackgroundImage = (): JSX.Element => { ...@@ -147,6 +169,10 @@ const SecurityBackgroundImage = (): JSX.Element => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
blurView: {
...StyleSheet.absoluteFillObject,
borderRadius: borderRadii.rounded16,
},
image: { image: {
height: '100%', height: '100%',
}, },
......
...@@ -200,7 +200,7 @@ function TokenDetails({ ...@@ -200,7 +200,7 @@ function TokenDetails({
const [showWarningModal, setShowWarningModal] = useState(false) const [showWarningModal, setShowWarningModal] = useState(false)
const [showBuyNativeTokenModal, setShowBuyNativeTokenModal] = useState(false) const [showBuyNativeTokenModal, setShowBuyNativeTokenModal] = useState(false)
const { tokenWarningDismissed } = useDismissedTokenWarnings( const { tokenWarningDismissed, onDismissTokenWarning } = useDismissedTokenWarnings(
isNativeCurrency ? undefined : { chainId: currencyChainId, address: currencyAddress }, isNativeCurrency ? undefined : { chainId: currencyChainId, address: currencyAddress },
) )
...@@ -294,11 +294,12 @@ function TokenDetails({ ...@@ -294,11 +294,12 @@ function TokenDetails({
]) ])
const onAcceptWarning = useCallback(() => { const onAcceptWarning = useCallback(() => {
onDismissTokenWarning()
setShowWarningModal(false) setShowWarningModal(false)
if (activeTransactionType !== undefined) { if (activeTransactionType !== undefined) {
navigateToSwapFlow({ currencyField: activeTransactionType, currencyAddress, currencyChainId }) navigateToSwapFlow({ currencyField: activeTransactionType, currencyAddress, currencyChainId })
} }
}, [activeTransactionType, currencyAddress, currencyChainId, navigateToSwapFlow]) }, [activeTransactionType, currencyAddress, currencyChainId, onDismissTokenWarning, navigateToSwapFlow])
const openTokenWarningModal = (): void => { const openTokenWarningModal = (): void => {
setShowWarningModal(true) setShowWarningModal(true)
...@@ -344,7 +345,9 @@ function TokenDetails({ ...@@ -344,7 +345,9 @@ function TokenDetails({
</AnimatedFlex> </AnimatedFlex>
) : null} ) : null}
<Flex gap="$spacing16" mb="$spacing8" px="$spacing16"> <Flex gap="$spacing16" mb="$spacing8" px="$spacing16">
{tokenProtectionEnabled && <TokenWarningCard currencyInfo={currencyInfo} onPress={openTokenWarningModal} />} {tokenProtectionEnabled && (
<TokenWarningCard currencyInfo={currencyInfo} onPressCtaButton={openTokenWarningModal} />
)}
{isChainEnabled && ( {isChainEnabled && (
<TokenBalances <TokenBalances
currentChainBalance={currentChainBalance} currentChainBalance={currentChainBalance}
......
...@@ -21,15 +21,3 @@ describe('RedirectExplore', () => { ...@@ -21,15 +21,3 @@ describe('RedirectExplore', () => {
cy.url().should('match', /\/explore\/tokens\/optimism\/NATIVE/) cy.url().should('match', /\/explore\/tokens\/optimism\/NATIVE/)
}) })
}) })
describe('Legacy Pool Redirects', () => {
it('should redirect /pool to /positions', () => {
cy.visit('/pool')
cy.url().should('match', /\/positions/)
})
it('should redirect /pool/:tokenId with chain param to /positions/v3/:chainName/:tokenId', () => {
cy.visit('/pool/123?chain=mainnet')
cy.url().should('match', /\/positions\/v3\/ethereum\/123/)
})
})
...@@ -189,10 +189,10 @@ ...@@ -189,10 +189,10 @@
"@uniswap/permit2-sdk": "1.3.0", "@uniswap/permit2-sdk": "1.3.0",
"@uniswap/redux-multicall": "1.1.8", "@uniswap/redux-multicall": "1.1.8",
"@uniswap/router-sdk": "1.14.3", "@uniswap/router-sdk": "1.14.3",
"@uniswap/sdk-core": "5.9.0", "@uniswap/sdk-core": "5.8.4",
"@uniswap/smart-order-router": "3.17.3", "@uniswap/smart-order-router": "3.17.3",
"@uniswap/token-lists": "1.0.0-beta.33", "@uniswap/token-lists": "1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "2.1.0-beta.18", "@uniswap/uniswapx-sdk": "^2.1.0-beta.14",
"@uniswap/universal-router-sdk": "4.5.2", "@uniswap/universal-router-sdk": "4.5.2",
"@uniswap/v2-core": "1.0.1", "@uniswap/v2-core": "1.0.1",
"@uniswap/v2-periphery": "1.1.0-beta.0", "@uniswap/v2-periphery": "1.1.0-beta.0",
......
{
"applinks": {
"details": [
{
"appIDs": [
"JH3UHGZD75.com.uniswap.mobile",
"JH3UHGZD75.com.uniswap.mobile.dev"
],
"components": [
{
"#": "/nfts/asset/*",
"comment": "NFT Item"
},
{
"#": "/nfts/collection/*",
"comment": "NFT Collection"
},
{
"#": "/tokens/*",
"comment": "Token address"
},
{
"#": "/address/*",
"comment": "Wallet address"
},
{
"/": "/nfts/asset/*",
"comment": "NFT Item"
},
{
"/": "/nfts/collection/*",
"comment": "NFT Collection"
},
{
"/": "/tokens/*",
"comment": "Token address"
},
{
"/": "/address/*",
"comment": "Wallet address"
}
]
}
]
},
"webcredentials": {
"apps": [
"JH3UHGZD75.com.uniswap.mobile",
"JH3UHGZD75.com.uniswap.mobile.beta",
"JH3UHGZD75.com.uniswap.mobile.dev"
]
}
}
[ [
{ {
"relation": [ "relation": ["delegate_permission/common.handle_all_urls"],
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": { "target": {
"namespace": "android_app", "namespace": "android_app",
"package_name": "com.uniswap.mobile", "package_name": "com.uniswap.mobile",
"sha256_cert_fingerprints": [ "sha256_cert_fingerprints":
"49:D9:3D:5D:FB:AA:64:A4:64:80:85:0F:39:A8:C1:D9:25:D3:D4:BC:8E:6B:1F:45:0C:EA:AF:B1:0C:27:DF:B8", ["49:D9:3D:5D:FB:AA:64:A4:64:80:85:0F:39:A8:C1:D9:25:D3:D4:BC:8E:6B:1F:45:0C:EA:AF:B1:0C:27:DF:B8", "F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"]
"F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"
]
} }
}, },
{ {
"relation": [ "relation": ["delegate_permission/common.handle_all_urls"],
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": { "target": {
"namespace": "android_app", "namespace": "android_app",
"package_name": "com.uniswap.mobile.beta", "package_name": "com.uniswap.mobile.beta",
"sha256_cert_fingerprints": [ "sha256_cert_fingerprints":
"75:41:9C:2D:01:4A:88:4E:8D:C6:EF:E5:51:54:28:6B:99:05:31:43:AD:84:B4:EB:39:28:B8:C3:C4:CE:48:E3", ["75:41:9C:2D:01:4A:88:4E:8D:C6:EF:E5:51:54:28:6B:99:05:31:43:AD:84:B4:EB:39:28:B8:C3:C4:CE:48:E3", "54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"]
"54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"
]
} }
}, },
{ {
"relation": [ "relation": ["delegate_permission/common.handle_all_urls"],
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": { "target": {
"namespace": "android_app", "namespace": "android_app",
"package_name": "com.uniswap.mobile.dev", "package_name": "com.uniswap.mobile.dev",
"sha256_cert_fingerprints": [ "sha256_cert_fingerprints":
"45:F8:15:02:C5:4F:AD:82:E7:51:F0:9C:D1:CA:77:C8:C9:BF:06:A6:D9:5A:55:4F:9E:B8:5F:81:33:2B:D0:DB", ["45:F8:15:02:C5:4F:AD:82:E7:51:F0:9C:D1:CA:77:C8:C9:BF:06:A6:D9:5A:55:4F:9E:B8:5F:81:33:2B:D0:DB", "02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
"02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3",
"FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"
]
} }
} }
] ]
\ No newline at end of file
...@@ -126,4 +126,16 @@ ...@@ -126,4 +126,16 @@
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.5</priority> <priority>0.5</priority>
</url> </url>
<url>
<loc>https://app.uniswap.org/positions/create</loc>
<lastmod>2024-09-17T19:57:27.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://app.uniswap.org/positions</loc>
<lastmod>2024-09-17T19:57:27.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset> </urlset>
...@@ -42,12 +42,5 @@ ...@@ -42,12 +42,5 @@
] ]
} }
] ]
},
"webcredentials": {
"apps": [
"JH3UHGZD75.com.uniswap.mobile",
"JH3UHGZD75.com.uniswap.mobile.beta",
"JH3UHGZD75.com.uniswap.mobile.dev"
]
} }
} }
...@@ -94,6 +94,7 @@ ...@@ -94,6 +94,7 @@
"https://wallet.crypto.com", "https://wallet.crypto.com",
"https://web3.1inch.io", "https://web3.1inch.io",
"https://mainnet.era.zksync.io/", "https://mainnet.era.zksync.io/",
"https://9bxqhlmige.execute-api.us-east-2.amazonaws.com",
"wss://*.uniswap.org", "wss://*.uniswap.org",
"wss://relay.walletconnect.com", "wss://relay.walletconnect.com",
"wss://relay.walletconnect.org", "wss://relay.walletconnect.org",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -8,6 +8,8 @@ import { EllipsisTamaguiStyle } from 'theme/components' ...@@ -8,6 +8,8 @@ import { EllipsisTamaguiStyle } from 'theme/components'
import { ThemedText } from 'theme/components/text' import { ThemedText } from 'theme/components/text'
import { Flex, Text, styled } from 'ui/src' import { Flex, Text, styled } from 'ui/src'
import { PriceSource } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { PriceSource } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
export type ChartHeaderProtocolInfo = { protocol: PriceSource; value?: number } export type ChartHeaderProtocolInfo = { protocol: PriceSource; value?: number }
...@@ -20,7 +22,7 @@ const ProtocolLegendWrapper = styled(Flex, { ...@@ -20,7 +22,7 @@ const ProtocolLegendWrapper = styled(Flex, {
gap: '$gap12', gap: '$gap12',
pointerEvents: 'none', pointerEvents: 'none',
variants: { variants: {
hover: { isMultichainExploreEnabled: {
true: { true: {
right: 'unset', right: 'unset',
p: '$spacing8', p: '$spacing8',
...@@ -39,28 +41,49 @@ const ProtocolLegendWrapper = styled(Flex, { ...@@ -39,28 +41,49 @@ const ProtocolLegendWrapper = styled(Flex, {
function ProtocolLegend({ protocolData }: { protocolData?: ChartHeaderProtocolInfo[] }) { function ProtocolLegend({ protocolData }: { protocolData?: ChartHeaderProtocolInfo[] }) {
const { formatFiatPrice } = useFormatter() const { formatFiatPrice } = useFormatter()
const theme = useTheme() const theme = useTheme()
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
return ( return (
<ProtocolLegendWrapper id={PROTOCOL_LEGEND_ELEMENT_ID} hover={true}> <ProtocolLegendWrapper
id={isMultichainExploreEnabled ? PROTOCOL_LEGEND_ELEMENT_ID : undefined}
isMultichainExploreEnabled={isMultichainExploreEnabled}
>
{protocolData {protocolData
?.map(({ value, protocol }) => { ?.map(({ value, protocol }) => {
const display = value ? formatFiatPrice({ price: value, type: NumberType.ChartFiatValue }) : null const display = value
? formatFiatPrice({ price: value, type: NumberType.ChartFiatValue })
: isMultichainExploreEnabled
? null
: getProtocolName(protocol)
return ( return (
!!display && ( !!display && (
<Flex row gap={8} justifyContent="flex-end" key={protocol + '_blip'} width="max-content"> <Flex
<Text variant="body4" textAlign="right" color="$neutral2" lineHeight={12}> row
{getProtocolName(protocol)} gap={isMultichainExploreEnabled ? 8 : 6}
</Text> justifyContent="flex-end"
key={protocol + '_blip'}
width={isMultichainExploreEnabled ? 'max-content' : undefined}
>
{isMultichainExploreEnabled ? (
<Text variant="body4" textAlign="right" color="$neutral2" lineHeight={12}>
{getProtocolName(protocol)}
</Text>
) : (
<Text variant="body4" width={80} textAlign="right" lineHeight={12} {...EllipsisTamaguiStyle}>
{display}
</Text>
)}
<Flex <Flex
borderRadius="$rounded4" borderRadius="$rounded4"
width={12} width={12}
height={12} height={12}
backgroundColor={getProtocolColor(protocol, theme)} backgroundColor={getProtocolColor(protocol, theme)}
/> />
<Text variant="body4" textAlign="right" lineHeight={12} {...EllipsisTamaguiStyle}> {isMultichainExploreEnabled && (
{display} <Text variant="body4" textAlign="right" lineHeight={12} {...EllipsisTamaguiStyle}>
</Text> {display}
</Text>
)}
</Flex> </Flex>
) )
) )
...@@ -118,6 +141,7 @@ export function ChartHeader({ ...@@ -118,6 +141,7 @@ export function ChartHeader({
additionalFields, additionalFields,
}: ChartHeaderProps) { }: ChartHeaderProps) {
const isHovered = !!time const isHovered = !!time
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
return ( return (
<Flex <Flex
row row
...@@ -136,7 +160,7 @@ export function ChartHeader({ ...@@ -136,7 +160,7 @@ export function ChartHeader({
<HeaderTimeDisplay time={time} timePlaceholder={timePlaceholder} /> <HeaderTimeDisplay time={time} timePlaceholder={timePlaceholder} />
</Flex> </Flex>
</Flex> </Flex>
{isHovered && protocolData && <ProtocolLegend protocolData={protocolData} />} {((isHovered && protocolData) || !isMultichainExploreEnabled) && <ProtocolLegend protocolData={protocolData} />}
</Flex> </Flex>
) )
} }
...@@ -4,7 +4,7 @@ import { LoadingBubble } from 'components/Tokens/loading' ...@@ -4,7 +4,7 @@ import { LoadingBubble } from 'components/Tokens/loading'
import { getChainFromChainUrlParam } from 'constants/chains' import { getChainFromChainUrlParam } from 'constants/chains'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { curveCardinal, scaleLinear } from 'd3' import { curveCardinal, scaleLinear } from 'd3'
import { SparklineMap, TopToken } from 'graphql/data/types' import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { PricePoint } from 'graphql/data/util' import { PricePoint } from 'graphql/data/util'
import styled, { useTheme } from 'lib/styled-components' import styled, { useTheme } from 'lib/styled-components'
import { memo } from 'react' import { memo } from 'react'
......
...@@ -87,11 +87,14 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements ...@@ -87,11 +87,14 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements
const colorsCount = options.colors.length const colorsCount = options.colors.length
const isHovered = options.hoveredLogicalIndex && options.hoveredLogicalIndex !== -1 const isHovered = options.hoveredLogicalIndex && options.hoveredLogicalIndex !== -1
const isMultichainExploreEnabled = !!options.gradients
areaPaths.forEach((areaPath, index) => { areaPaths.forEach((areaPath, index) => {
// Modification: determine area fill opacity based on number of lines and hover state // Modification: determine area fill opacity based on number of lines and hover state
if (areaPaths.length === 1) { if (areaPaths.length === 1) {
ctx.globalAlpha = 0.12 // single-line charts have low opacity fill ctx.globalAlpha = 0.12 // single-line charts have low opacity fill
} else if (!isMultichainExploreEnabled) {
ctx.globalAlpha = isHovered ? 0.24 : 1
} }
const gradient = options.gradients const gradient = options.gradients
...@@ -106,7 +109,7 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements ...@@ -106,7 +109,7 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements
ctx.fill(areaPath) ctx.fill(areaPath)
}) })
ctx.lineWidth = options.lineWidth ctx.lineWidth = options.lineWidth * (isMultichainExploreEnabled ? 1 : renderingScope.verticalPixelRatio)
ctx.lineJoin = 'round' ctx.lineJoin = 'round'
fullLinesMeshed.toReversed().forEach((linePath, index) => { fullLinesMeshed.toReversed().forEach((linePath, index) => {
...@@ -114,7 +117,7 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements ...@@ -114,7 +117,7 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements
const color = options.colors[unreversedIndex % colorsCount] const color = options.colors[unreversedIndex % colorsCount]
ctx.strokeStyle = color ctx.strokeStyle = color
ctx.fillStyle = color ctx.fillStyle = color
ctx.globalAlpha = isHovered ? 0.24 : 1 ctx.globalAlpha = isHovered && isMultichainExploreEnabled ? 0.24 : 1
// Bottom line is just the x-axis, which should not be drawn // Bottom line is just the x-axis, which should not be drawn
if (index !== fullLinesMeshed.length - 1) { if (index !== fullLinesMeshed.length - 1) {
......
...@@ -9,6 +9,7 @@ export type CustomVolumeChartModelParams = { ...@@ -9,6 +9,7 @@ export type CustomVolumeChartModelParams = {
colors: string[] colors: string[]
headerHeight: number headerHeight: number
useThinCrosshair?: boolean useThinCrosshair?: boolean
isMultichainExploreEnabled?: boolean
background?: string background?: string
} }
...@@ -25,6 +26,7 @@ export class CustomVolumeChartModel<TDataType extends CustomHistogramData> exten ...@@ -25,6 +26,7 @@ export class CustomVolumeChartModel<TDataType extends CustomHistogramData> exten
this.series = this.api.addCustomSeries( this.series = this.api.addCustomSeries(
new CustomHistogramSeries({ new CustomHistogramSeries({
colors: params.colors, colors: params.colors,
isMultichainExploreEnabled: params.isMultichainExploreEnabled,
background: params.background, background: params.background,
}), }),
) )
......
...@@ -23,11 +23,13 @@ export class CustomHistogramSeries<TData extends CustomHistogramData> ...@@ -23,11 +23,13 @@ export class CustomHistogramSeries<TData extends CustomHistogramData>
{ {
_renderer: CustomHistogramSeriesRenderer<TData> _renderer: CustomHistogramSeriesRenderer<TData>
_colors: string[] _colors: string[]
_isMultichainExploreEnabled?: boolean
_background?: string _background?: string
constructor(props: CustomHistogramProps) { constructor(props: CustomHistogramProps) {
this._renderer = new CustomHistogramSeriesRenderer(props) this._renderer = new CustomHistogramSeriesRenderer(props)
this._colors = props.colors this._colors = props.colors
this._isMultichainExploreEnabled = props.isMultichainExploreEnabled
this._background = props.background this._background = props.background
} }
......
...@@ -57,6 +57,7 @@ function cumulativeBuildUp(data: StackedHistogramData): number[] { ...@@ -57,6 +57,7 @@ function cumulativeBuildUp(data: StackedHistogramData): number[] {
export interface CustomHistogramProps { export interface CustomHistogramProps {
colors: string[] colors: string[]
isMultichainExploreEnabled?: boolean
background?: string background?: string
} }
...@@ -64,10 +65,12 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im ...@@ -64,10 +65,12 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im
_data: PaneRendererCustomData<Time, TData> | null = null _data: PaneRendererCustomData<Time, TData> | null = null
_options: CustomHistogramSeriesOptions | null = null _options: CustomHistogramSeriesOptions | null = null
_colors: string[] _colors: string[]
_isMultichainExploreEnabled?: boolean
_background?: string _background?: string
constructor(props: CustomHistogramProps) { constructor(props: CustomHistogramProps) {
this._colors = props.colors this._colors = props.colors
this._isMultichainExploreEnabled = props.isMultichainExploreEnabled
this._background = props.background this._background = props.background
} }
...@@ -131,11 +134,18 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im ...@@ -131,11 +134,18 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im
const totalBox = positionsBox(zeroY, stack.ys[stack.ys.length - 1], renderingScope.verticalPixelRatio) const totalBox = positionsBox(zeroY, stack.ys[stack.ys.length - 1], renderingScope.verticalPixelRatio)
ctx.beginPath() ctx.beginPath()
const isMultichainExploreEnabled = this._isMultichainExploreEnabled
if (this._background) { if (this._background) {
ctx.fillStyle = this._background ctx.fillStyle = this._background
} }
ctx.roundRect(column.left + margin, totalBox.position, width - margin, totalBox.length, 4) ctx.roundRect(
column.left + margin,
totalBox.position,
width - margin,
totalBox.length,
isMultichainExploreEnabled ? 4 : 8,
)
ctx.fill() ctx.fill()
// Modification: draw the stack's boxes atop the total volume bar, resulting in the top and bottom boxes being rounded // Modification: draw the stack's boxes atop the total volume bar, resulting in the top and bottom boxes being rounded
...@@ -147,9 +157,9 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im ...@@ -147,9 +157,9 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im
const color = this._colors[this._colors.length - 1 - index] // color v2, then v3 const color = this._colors[this._colors.length - 1 - index] // color v2, then v3
const stackBoxPositions = positionsBox(previousY, y, renderingScope.verticalPixelRatio) const stackBoxPositions = positionsBox(previousY, y, renderingScope.verticalPixelRatio)
ctx.fillStyle = color ctx.fillStyle = color
ctx.globalAlpha = isStackedHistogram && !isHovered ? 0.24 : 1 ctx.globalAlpha = isStackedHistogram && !isHovered && isMultichainExploreEnabled ? 0.24 : 1
ctx.fillRect(column.left + margin, stackBoxPositions.position, width - margin, stackBoxPositions.length) ctx.fillRect(column.left + margin, stackBoxPositions.position, width - margin, stackBoxPositions.length)
if (isStackedHistogram && !isHovered) { if (isStackedHistogram && isMultichainExploreEnabled && !isHovered) {
ctx.globalAlpha = 1 ctx.globalAlpha = 1
ctx.fillStyle = color ctx.fillStyle = color
ctx.fillRect(column.left + margin, stackBoxPositions.position, width - margin, 2) ctx.fillRect(column.left + margin, stackBoxPositions.position, width - margin, 2)
......
...@@ -242,13 +242,18 @@ export default function FeatureFlagModal() { ...@@ -242,13 +242,18 @@ export default function FeatureFlagModal() {
<FeatureFlagOption flag={FeatureFlags.V2Everywhere} label="Enable V2 Everywhere" /> <FeatureFlagOption flag={FeatureFlags.V2Everywhere} label="Enable V2 Everywhere" />
<FeatureFlagOption flag={FeatureFlags.V4Everywhere} label="Enable V4 Everywhere" /> <FeatureFlagOption flag={FeatureFlags.V4Everywhere} label="Enable V4 Everywhere" />
<FeatureFlagOption flag={FeatureFlags.Realtime} label="Realtime activity updates" /> <FeatureFlagOption flag={FeatureFlags.Realtime} label="Realtime activity updates" />
<FeatureFlagOption flag={FeatureFlags.RestExplore} label="Explore Data from new REST backend" />
<FeatureFlagOption flag={FeatureFlags.MultipleRoutingOptions} label="Enable Multiple Routing Options" /> <FeatureFlagOption flag={FeatureFlags.MultipleRoutingOptions} label="Enable Multiple Routing Options" />
<FeatureFlagOption flag={FeatureFlags.NavigationHotkeys} label="Navigation hotkeys" /> <FeatureFlagOption flag={FeatureFlags.NavigationHotkeys} label="Navigation hotkeys" />
<FeatureFlagOption flag={FeatureFlags.TokenProtection} label="Warning UX for scam/dangerous tokens" /> <FeatureFlagOption flag={FeatureFlags.TokenProtection} label="Warning UX for scam/dangerous tokens" />
<FeatureFlagGroup name="New Chains"> <FeatureFlagGroup name="New Chains">
<FeatureFlagOption flag={FeatureFlags.Zora} label="Enable Zora" /> <FeatureFlagOption flag={FeatureFlags.Zora} label="Enable Zora" />
<FeatureFlagOption flag={FeatureFlags.WorldChain} label="Enable World Chain" />
</FeatureFlagGroup> </FeatureFlagGroup>
<FeatureFlagOption flag={FeatureFlags.L2NFTs} label="L2 NFTs" /> <FeatureFlagOption flag={FeatureFlags.L2NFTs} label="L2 NFTs" />
<FeatureFlagGroup name="Multichain UX">
<FeatureFlagOption flag={FeatureFlags.MultichainExplore} label="Enable Multichain Explore Page" />
</FeatureFlagGroup>
<FeatureFlagGroup name="Quick routes"> <FeatureFlagGroup name="Quick routes">
<FeatureFlagOption flag={FeatureFlags.QuickRouteMainnet} label="Enable quick routes for Mainnet" /> <FeatureFlagOption flag={FeatureFlags.QuickRouteMainnet} label="Enable quick routes for Mainnet" />
<DynamicConfigDropdown <DynamicConfigDropdown
......
...@@ -10,12 +10,11 @@ import { useEffect, useState } from 'react' ...@@ -10,12 +10,11 @@ import { useEffect, useState } from 'react'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import styled from 'styled-components' import styled from 'styled-components'
import { ClickableTamaguiStyle, CloseIcon } from 'theme/components' import { ClickableTamaguiStyle, CloseIcon } from 'theme/components'
import { Button, Flex, Text } from 'ui/src' import { Button, Flex, Input, Text } from 'ui/src'
import { BackArrow } from 'ui/src/components/icons/BackArrow' import { BackArrow } from 'ui/src/components/icons/BackArrow'
import { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled' import { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled'
import { Plus } from 'ui/src/components/icons/Plus' import { Plus } from 'ui/src/components/icons/Plus'
import { Search } from 'ui/src/components/icons/Search' import { Search } from 'ui/src/components/icons/Search'
import { AmountInput, numericInputRegex } from 'uniswap/src/components/CurrencyInputPanel/AmountInput'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useTranslation } from 'uniswap/src/i18n' import { useTranslation } from 'uniswap/src/i18n'
...@@ -79,17 +78,11 @@ export function FeeTierSearchModal() { ...@@ -79,17 +78,11 @@ export function FeeTierSearchModal() {
setCreateFeeValue((prev) => { setCreateFeeValue((prev) => {
let newValue = parseFloat(prev) let newValue = parseFloat(prev)
if (autoDecrementing) { if (autoDecrementing) {
if (!prev || prev === '') {
return '0'
}
newValue -= 0.01 newValue -= 0.01
if (newValue < 0) { if (newValue < 0) {
return '0' return '0'
} }
} else if (autoIncrementing) { } else if (autoIncrementing) {
if (!prev || prev === '') {
return '0.01'
}
newValue += 0.01 newValue += 0.01
if (newValue > 100) { if (newValue > 100) {
return '100' return '100'
...@@ -255,54 +248,19 @@ export function FeeTierSearchModal() { ...@@ -255,54 +248,19 @@ export function FeeTierSearchModal() {
</Flex> </Flex>
) : ( ) : (
<> <>
<Flex <Flex row py="$padding12" px="$padding8" backgroundColor="$surface2" borderRadius="$rounded24">
row
alignItems="center"
py="$padding12"
px="$padding8"
backgroundColor="$surface2"
borderRadius="$rounded24"
gap="$gap8"
>
<Search size={20} color="$neutral2" /> <Search size={20} color="$neutral2" />
<AmountInput <Input
width="100%" width="100%"
autoFocus height="100%"
alignSelf="stretch" fontWeight="$book"
backgroundColor="$transparent" backgroundColor="$transparent"
borderRadius={0}
borderWidth={0}
textAlign="left"
value={searchValue}
fontFamily="$subHeading"
fontSize={18}
px="$none"
py="$none"
placeholder={t('fee.tier.search.short')} placeholder={t('fee.tier.search.short')}
placeholderTextColor="$neutral3" placeholderTextColor="$neutral2"
onChangeText={(value) => { onChange={(event: any) => {
if (value === '.') { setSearchValue(event.target.value)
setSearchValue('0.')
return
}
// Prevent two decimals
if (value.indexOf('.') !== -1 && value.indexOf('.', value.indexOf('.') + 1) !== -1) {
return
}
// Prevent addition of non-numeric characters to the end of the string
if (!numericInputRegex.test(value)) {
setSearchValue(value.slice(0, -1))
return
}
const newValue = parseFloat(value)
if (newValue > 100) {
setSearchValue('100')
return
}
setSearchValue(newValue >= 0 ? value : '')
}} }}
value={searchValue}
/> />
</Flex> </Flex>
<Flex width="100%" gap="$gap4" maxHeight={350} overflow="scroll"> <Flex width="100%" gap="$gap4" maxHeight={350} overflow="scroll">
......
import { FlagWarning, getFlagsFromContractAddress, getFlagWarning } from 'components/Liquidity/utils'
import { GetHelpHeader } from 'components/Modal/GetHelpHeader'
import { useCreatePositionContext } from 'pages/Pool/Positions/create/CreatePositionContext'
import { useMemo, useState } from 'react'
import { CopyHelper } from 'theme/components'
import { Button, Checkbox, Flex, HeightAnimator, Separator, Text, TouchableArea } from 'ui/src'
import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useTranslation } from 'uniswap/src/i18n'
import { shortenAddress } from 'uniswap/src/utils/addresses'
function HookWarnings({ flags }: { flags: FlagWarning[] }) {
const { t } = useTranslation()
const [expandedProperties, setExpandedProperties] = useState(false)
const toggleExpandedProperties = () => {
setExpandedProperties((state) => !state)
}
if (!flags.length) {
return null
}
return (
<>
<Separator my="$gap8" />
<TouchableArea onPress={toggleExpandedProperties}>
<Flex row alignItems="center">
<Flex flex={1}>
<Text variant="buttonLabel3" color="$neutral2">
{t('position.addingHook.viewProperties')}
</Text>
</Flex>
<RotatableChevron direction={expandedProperties ? 'up' : 'down'} color="$neutral2" width={16} height={16} />
</Flex>
</TouchableArea>
{expandedProperties && (
<Flex gap="$gap8" mt="$padding16">
{flags.map(({ name, info, dangerous }) => (
<Flex key={name} row>
<Flex flex={1}>
<Text variant="body3" color={dangerous ? '$statusCritical' : '$neutral2'}>
{name}
</Text>
</Flex>
<Flex flexWrap="wrap" width="55%">
<Text variant="body4" color={dangerous ? '$statusCritical' : '$neutral2'}>
{info}
</Text>
</Flex>
</Flex>
))}
</Flex>
)}
</>
)
}
export function HookModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const { t } = useTranslation()
const [disclaimerChecked, setDisclaimerChecked] = useState(false)
const {
setPositionState,
positionState: { hook },
} = useCreatePositionContext()
const clearHook = () => {
setPositionState((state) => ({
...state,
hook: undefined,
}))
onClose()
}
const onContinue = () => {
if (disclaimerChecked) {
onClose()
}
}
const onDisclaimerChecked = () => {
setDisclaimerChecked((state) => !state)
}
const { flags, hasDangerous } = useMemo(() => {
if (!hook) {
return {
flags: [],
hasDangerous: false,
}
}
let hasDangerous = false
const flagInfos: Record<string, FlagWarning> = {}
getFlagsFromContractAddress(hook).forEach((flag) => {
const warning = getFlagWarning(flag, t)
if (warning?.dangerous) {
hasDangerous = true
}
if (warning?.name) {
flagInfos[warning.name] = warning
}
})
return {
flags: Object.values(flagInfos),
hasDangerous,
}
}, [hook, t])
if (!hook) {
return null
}
// TODO(WEB-5289): match entrance/exit animations with the currency selector
return (
<Modal name={ModalName.Hook} onClose={onClose} isModalOpen={isOpen}>
<Flex gap="$spacing24">
<GetHelpHeader closeModal={onClose} />
<Flex gap="$gap8" alignContent="center" px="$padding20">
<Text variant="subheading1" textAlign="center">
{hasDangerous ? t('position.hook.warningHeader') : t('position.addingHook')}
</Text>
<Text variant="body2" color="$neutral2" textAlign="center">
{hasDangerous ? t('position.hook.warningInfo') : t('position.addingHook.disclaimer')}
</Text>
<LearnMoreLink centered url={uniswapUrls.helpArticleUrls.v4HooksInfo} textVariant="buttonLabel3" />
</Flex>
<HeightAnimator animation="fast">
<Flex borderRadius="$rounded16" backgroundColor="$surface2" py="$gap12" px="$gap16">
<Flex row>
<Flex flex={1}>
<Text variant="body3" color="$neutral2">
{t('common.text.contract')}
</Text>
</Flex>
<CopyHelper toCopy={hook} iconSize={16} iconPosition="right" color="$neutral2">
<Text variant="body3" color="$neutral2">
{shortenAddress(hook)}
</Text>
</CopyHelper>
</Flex>
<HookWarnings flags={flags} />
</Flex>
{hasDangerous && (
<Flex
row
alignItems="center"
gap="$gap12"
borderRadius="$rounded16"
backgroundColor="$surface2"
p="$gap12"
mt="$spacing24"
>
<Checkbox size="$icon.16" checked={disclaimerChecked} onPress={onDisclaimerChecked} />
<Text variant="buttonLabel4" color="$neutral2">
{t('position.hook.disclaimer')}
</Text>
</Flex>
)}
<Flex row gap="$gap8" mt="$spacing24">
<Button size="small" theme="secondary" width="49%" onPress={clearHook}>
{t('position.removeHook')}
</Button>
<Button disabled={!disclaimerChecked} size="small" theme="primary" width="49%" onPress={onContinue}>
{t('common.button.continue')}
</Button>
</Flex>
</HeightAnimator>
</Flex>
</Modal>
)
}
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PositionStatus } from '@uniswap/client-pools/dist/pools/v1/types_pb'
import { LiquidityPositionFeeStats } from 'components/Liquidity/LiquidityPositionFeeStats' import { LiquidityPositionFeeStats } from 'components/Liquidity/LiquidityPositionFeeStats'
import { LiquidityPositionInfo } from 'components/Liquidity/LiquidityPositionInfo' import { LiquidityPositionInfo } from 'components/Liquidity/LiquidityPositionInfo'
import { useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks' import { useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks'
...@@ -21,7 +20,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit ...@@ -21,7 +20,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit
fiatValue0 && fiatValue1 fiatValue0 && fiatValue1
? formatCurrencyAmount({ ? formatCurrencyAmount({
value: fiatValue0.add(fiatValue1), value: fiatValue0.add(fiatValue1),
type: NumberType.FiatStandard, type: NumberType.FiatTokenPrice,
}) })
: undefined : undefined
const v2FormattedUsdValue = const v2FormattedUsdValue =
...@@ -33,8 +32,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit ...@@ -33,8 +32,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit
fiatFeeValue0 && fiatFeeValue1 fiatFeeValue0 && fiatFeeValue1
? formatCurrencyAmount({ ? formatCurrencyAmount({
value: fiatFeeValue0.add(fiatFeeValue1), value: fiatFeeValue0.add(fiatFeeValue1),
type: type: NumberType.FiatTokenPrice,
liquidityPosition.status === PositionStatus.CLOSED ? NumberType.FiatStandard : NumberType.FiatTokenPrice,
}) })
: undefined : undefined
......
import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' import { BadgeData, LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges'
import { LiquidityPositionStatusIndicator } from 'components/Liquidity/LiquidityPositionStatusIndicator' import { LiquidityPositionStatusIndicator } from 'components/Liquidity/LiquidityPositionStatusIndicator'
import { PositionInfo } from 'components/Liquidity/types' import { PositionInfo } from 'components/Liquidity/types'
import { getProtocolVersionLabel } from 'components/Liquidity/utils' import { getProtocolVersionLabel } from 'components/Liquidity/utils'
import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo' import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo'
import { ZERO_ADDRESS } from 'constants/misc'
import { Flex, Text } from 'ui/src' import { Flex, Text } from 'ui/src'
import { DocumentList } from 'ui/src/components/icons/DocumentList'
interface LiquidityPositionInfoProps { interface LiquidityPositionInfoProps {
positionInfo: PositionInfo positionInfo: PositionInfo
...@@ -25,7 +27,18 @@ export function LiquidityPositionInfo({ positionInfo }: LiquidityPositionInfoPro ...@@ -25,7 +27,18 @@ export function LiquidityPositionInfo({ positionInfo }: LiquidityPositionInfoPro
{currency0Amount?.currency.symbol} / {currency1Amount?.currency.symbol} {currency0Amount?.currency.symbol} / {currency1Amount?.currency.symbol}
</Text> </Text>
<Flex row gap={2} alignItems="center"> <Flex row gap={2} alignItems="center">
<LiquidityPositionInfoBadges size="small" versionLabel={versionLabel} v4hook={v4hook} feeTier={feeTier} /> <LiquidityPositionInfoBadges
size="small"
badges={
[
versionLabel ? { label: versionLabel } : undefined,
v4hook && v4hook !== ZERO_ADDRESS
? { label: v4hook, copyable: true, icon: <DocumentList color="$neutral2" size={16} /> }
: undefined,
feeTier ? { label: `${Number(feeTier) / 10000}%` } : undefined,
].filter(Boolean) as BadgeData[]
}
/>
</Flex> </Flex>
</Flex> </Flex>
<LiquidityPositionStatusIndicator status={status} /> <LiquidityPositionStatusIndicator status={status} />
......
import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges'
import { render } from 'test-utils/render' import { render } from 'test-utils/render'
const testBadgeData = [{ label: 'test', copyable: true }, { label: 'test2' }]
describe('LiquidityPositionInfoBadges', () => { describe('LiquidityPositionInfoBadges', () => {
it('should render with default size', () => { it('should render with default size', () => {
const { getByText } = render(<LiquidityPositionInfoBadges versionLabel="2" feeTier="100" size="default" />) const { getByText } = render(<LiquidityPositionInfoBadges badges={testBadgeData} size="default" />)
expect(getByText('2')).toBeInTheDocument() expect(getByText('test')).toBeInTheDocument()
}) })
it('should render with small size', () => { it('should render with small size', () => {
const { getByText } = render(<LiquidityPositionInfoBadges versionLabel="2" feeTier="100" size="small" />) const { getByText } = render(<LiquidityPositionInfoBadges badges={testBadgeData} size="small" />)
expect(getByText('2')).toBeInTheDocument() expect(getByText('test')).toBeInTheDocument()
}) })
it('should render with multiple badges', () => { it('should render with multiple badges', () => {
const { getByText } = render(<LiquidityPositionInfoBadges versionLabel="2" feeTier="100" size="default" />) const { getByText } = render(<LiquidityPositionInfoBadges badges={testBadgeData} size="default" />)
expect(getByText('2')).toBeInTheDocument() expect(getByText('test')).toBeInTheDocument()
expect(getByText('0.01%')).toBeInTheDocument() expect(getByText('test2')).toBeInTheDocument()
}) })
}) })
import { FeeAmount } from '@uniswap/v3-sdk'
import { ZERO_ADDRESS } from 'constants/misc'
import { useMemo } from 'react'
import { CopyHelper } from 'theme/components' import { CopyHelper } from 'theme/components'
import { styled, Text } from 'ui/src' import { styled, Text } from 'ui/src'
import { DocumentList } from 'ui/src/components/icons/DocumentList'
import { isAddress, shortenAddress } from 'utilities/src/addresses' import { isAddress, shortenAddress } from 'utilities/src/addresses'
export const PositionInfoBadge = styled(Text, { export const PositionInfoBadge = styled(Text, {
...@@ -46,33 +42,19 @@ function getPlacement(index: number, length: number): 'start' | 'middle' | 'end' ...@@ -46,33 +42,19 @@ function getPlacement(index: number, length: number): 'start' | 'middle' | 'end'
return length === 1 ? 'only' : index === 0 ? 'start' : index === length - 1 ? 'end' : 'middle' return length === 1 ? 'only' : index === 0 ? 'start' : index === length - 1 ? 'end' : 'middle'
} }
interface BadgeData { export interface BadgeData {
label: string label: string
copyable?: boolean copyable?: boolean
icon?: JSX.Element icon?: JSX.Element
} }
export function LiquidityPositionInfoBadges({ export function LiquidityPositionInfoBadges({
versionLabel, badges,
v4hook,
feeTier,
size = 'default', size = 'default',
}: { }: {
versionLabel?: string badges: BadgeData[]
v4hook?: string
feeTier?: string | FeeAmount
size: 'small' | 'default' size: 'small' | 'default'
}): JSX.Element { }): JSX.Element {
const badges = useMemo(() => {
return [
versionLabel ? { label: versionLabel } : undefined,
v4hook && v4hook !== ZERO_ADDRESS
? { label: v4hook, copyable: true, icon: <DocumentList color="$neutral2" size={16} /> }
: undefined,
feeTier ? { label: `${Number(feeTier) / 10000}%` } : undefined,
].filter(Boolean) as BadgeData[]
}, [versionLabel, v4hook, feeTier])
return ( return (
<> <>
{badges.map(({ label, copyable, icon }, index) => { {badges.map(({ label, copyable, icon }, index) => {
......
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PositionStatus } from '@uniswap/client-pools/dist/pools/v1/types_pb'
import { Currency, Price } from '@uniswap/sdk-core' import { Currency, Price } from '@uniswap/sdk-core'
import { LiquidityPositionStatusIndicator } from 'components/Liquidity/LiquidityPositionStatusIndicator'
import { useGetRangeDisplay } from 'components/Liquidity/hooks' import { useGetRangeDisplay } from 'components/Liquidity/hooks'
import { PriceOrdering } from 'components/PositionListItem' import { PriceOrdering } from 'components/PositionListItem'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
...@@ -17,6 +19,7 @@ const InnerTile = styled(Flex, { ...@@ -17,6 +19,7 @@ const InnerTile = styled(Flex, {
}) })
interface LiquidityPositionPriceRangeTileProps { interface LiquidityPositionPriceRangeTileProps {
status?: PositionStatus
priceOrdering: PriceOrdering priceOrdering: PriceOrdering
token0CurrentPrice: Price<Currency, Currency> token0CurrentPrice: Price<Currency, Currency>
token1CurrentPrice: Price<Currency, Currency> token1CurrentPrice: Price<Currency, Currency>
...@@ -26,6 +29,7 @@ interface LiquidityPositionPriceRangeTileProps { ...@@ -26,6 +29,7 @@ interface LiquidityPositionPriceRangeTileProps {
} }
export function LiquidityPositionPriceRangeTile({ export function LiquidityPositionPriceRangeTile({
status,
priceOrdering, priceOrdering,
token0CurrentPrice, token0CurrentPrice,
token1CurrentPrice, token1CurrentPrice,
...@@ -73,6 +77,7 @@ export function LiquidityPositionPriceRangeTile({ ...@@ -73,6 +77,7 @@ export function LiquidityPositionPriceRangeTile({
<Text variant="subheading1"> <Text variant="subheading1">
<Trans i18nKey="pool.priceRange" /> <Trans i18nKey="pool.priceRange" />
</Text> </Text>
{status && <LiquidityPositionStatusIndicator status={status} />}
</Flex> </Flex>
<SegmentedControl <SegmentedControl
options={controlOptions} options={controlOptions}
...@@ -82,7 +87,7 @@ export function LiquidityPositionPriceRangeTile({ ...@@ -82,7 +87,7 @@ export function LiquidityPositionPriceRangeTile({
}} }}
/> />
</Flex> </Flex>
<Flex row width="100%" gap="$gap12" $lg={{ row: false }}> <Flex row width="100%" gap="$gap12">
<InnerTile> <InnerTile>
<Text variant="subheading2" color="$neutral2"> <Text variant="subheading2" color="$neutral2">
<Trans i18nKey="pool.minPrice" /> <Trans i18nKey="pool.minPrice" />
......
...@@ -55,7 +55,6 @@ export type V3PositionInfo = BasePositionInfo & { ...@@ -55,7 +55,6 @@ export type V3PositionInfo = BasePositionInfo & {
version: ProtocolVersion.V3 version: ProtocolVersion.V3
tokenId: string tokenId: string
pool?: V3Pool pool?: V3Pool
poolId?: string
feeTier?: FeeAmount feeTier?: FeeAmount
position?: V3Position position?: V3Position
v4hook: undefined v4hook: undefined
...@@ -65,7 +64,6 @@ type V4PositionInfo = BasePositionInfo & { ...@@ -65,7 +64,6 @@ type V4PositionInfo = BasePositionInfo & {
version: ProtocolVersion.V4 version: ProtocolVersion.V4
tokenId: string tokenId: string
pool?: V4Pool pool?: V4Pool
poolId?: string
position?: V4Position position?: V4Position
feeTier?: string feeTier?: string
v4hook?: string v4hook?: string
......
import { HookFlag, getFlagsFromContractAddress } from 'components/Liquidity/utils'
describe('getFlagsFromContractAddress', () => {
it('should return an empty array for an address with no flags', () => {
const address = '0x1234567890123456789012345678901234560000'
expect(getFlagsFromContractAddress(address)).toEqual([])
})
it('should correctly identify a single flag', () => {
const address = '0x1234567890123456789012345678901234560200'
expect(getFlagsFromContractAddress(address)).toEqual([HookFlag.BeforeRemoveLiquidity])
})
it('should correctly identify multiple flags', () => {
const address = '0x1234567890123456789012345678901234567FFF'
expect(getFlagsFromContractAddress(address)).toEqual([
HookFlag.BeforeRemoveLiquidity,
HookFlag.AfterRemoveLiquidity,
HookFlag.BeforeAddLiquidity,
HookFlag.AfterAddLiquidity,
HookFlag.BeforeSwap,
HookFlag.AfterSwap,
HookFlag.BeforeDonate,
HookFlag.AfterDonate,
HookFlag.BeforeSwapReturnsDelta,
HookFlag.AfterSwapReturnsDelta,
HookFlag.AfterAddLiquidityReturnsDelta,
HookFlag.AfterRemoveLiquidityReturnsDelta,
])
})
it('should correctly identify a mix of flags (case 1)', () => {
const address = '0x123456789012345678901234567890123456789A'
expect(getFlagsFromContractAddress(address)).toEqual([
HookFlag.BeforeAddLiquidity,
HookFlag.BeforeSwap,
HookFlag.AfterDonate,
HookFlag.BeforeSwapReturnsDelta,
HookFlag.AfterAddLiquidityReturnsDelta,
])
})
it('should correctly identify a mix of flags (case 2)', () => {
const address = '0x12345678901234567890123456789012345678C0'
expect(getFlagsFromContractAddress(address)).toEqual([
HookFlag.BeforeAddLiquidity,
HookFlag.BeforeSwap,
HookFlag.AfterSwap,
])
})
it('should correctly identify a mix of flags (case 3)', () => {
const address = '0x123456789012345678901234567890123456780B'
expect(getFlagsFromContractAddress(address)).toEqual([
HookFlag.BeforeAddLiquidity,
HookFlag.BeforeSwapReturnsDelta,
HookFlag.AfterAddLiquidityReturnsDelta,
HookFlag.AfterRemoveLiquidityReturnsDelta,
])
})
it('should correctly identify a mix of flags (case 4)', () => {
const address = '0x0000000000000000000000000000000000002400'
expect(getFlagsFromContractAddress(address)).toEqual([HookFlag.AfterAddLiquidity])
})
})
...@@ -254,7 +254,6 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef ...@@ -254,7 +254,6 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef
feeTier: parseV3FeeTier(v3Position.feeTier), feeTier: parseV3FeeTier(v3Position.feeTier),
version: ProtocolVersion.V3, version: ProtocolVersion.V3,
pool, pool,
poolId: position.position.value.poolId,
position: sdkPosition, position: sdkPosition,
tickLower: v3Position.tickLower, tickLower: v3Position.tickLower,
tickUpper: v3Position.tickUpper, tickUpper: v3Position.tickUpper,
...@@ -286,21 +285,19 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef ...@@ -286,21 +285,19 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef
tickUpper: Number(v4Position.tickUpper), tickUpper: Number(v4Position.tickUpper),
}) })
: undefined : undefined
const poolId = V4Pool.getPoolId(token0, token1, Number(v4Position.feeTier), Number(v4Position.tickSpacing), hook)
return { return {
status: position.status, status: position.status,
feeTier: v4Position.feeTier, feeTier: v4Position?.feeTier,
version: ProtocolVersion.V4, version: ProtocolVersion.V4,
position: sdkPosition, position: sdkPosition,
pool, pool,
poolId,
v4hook: hook, v4hook: hook,
tokenId: v4Position.tokenId, tokenId: v4Position.tokenId,
tickLower: v4Position.tickLower, tickLower: v4Position?.tickLower,
tickUpper: v4Position.tickUpper, tickUpper: v4Position?.tickUpper,
tickSpacing: Number(v4Position.tickSpacing), tickSpacing: Number(v4Position?.tickSpacing),
currency0Amount: CurrencyAmount.fromRawAmount(token0, v4Position.amount0 ?? 0), currency0Amount: CurrencyAmount.fromRawAmount(token0, v4Position?.amount0 ?? 0),
currency1Amount: CurrencyAmount.fromRawAmount(token1, v4Position.amount1 ?? 0), currency1Amount: CurrencyAmount.fromRawAmount(token1, v4Position?.amount1 ?? 0),
token0UncollectedFees: v4Position.token0UncollectedFees, token0UncollectedFees: v4Position.token0UncollectedFees,
token1UncollectedFees: v4Position.token1UncollectedFees, token1UncollectedFees: v4Position.token1UncollectedFees,
liquidity: v4Position.liquidity, liquidity: v4Position.liquidity,
...@@ -355,86 +352,3 @@ export function calculateInvertedPrice({ price, invert }: { price?: Price<Curren ...@@ -355,86 +352,3 @@ export function calculateInvertedPrice({ price, invert }: { price?: Price<Curren
base: currentPrice?.baseCurrency, base: currentPrice?.baseCurrency,
} }
} }
export enum HookFlag {
BeforeAddLiquidity = 'before-add-liquidity',
AfterAddLiquidity = 'after-add-liquidity',
BeforeRemoveLiquidity = 'before-remove-liquidity',
AfterRemoveLiquidity = 'after-remove-liquidity',
BeforeSwap = 'before-swap',
AfterSwap = 'after-swap',
BeforeDonate = 'before-donate',
AfterDonate = 'after-donate',
BeforeSwapReturnsDelta = 'before-swap-returns-delta',
AfterSwapReturnsDelta = 'after-swap-returns-delta',
AfterAddLiquidityReturnsDelta = 'after-add-liquidity-returns-delta',
AfterRemoveLiquidityReturnsDelta = 'after-remove-liquidity-returns-delta',
}
// The flags are ordered with the dangerous ones on top so they are rendered first
const FLAGS: { [key in HookFlag]: number } = {
[HookFlag.BeforeRemoveLiquidity]: 1 << 9,
[HookFlag.AfterRemoveLiquidity]: 1 << 8,
[HookFlag.BeforeAddLiquidity]: 1 << 11,
[HookFlag.AfterAddLiquidity]: 1 << 10,
[HookFlag.BeforeSwap]: 1 << 7,
[HookFlag.AfterSwap]: 1 << 6,
[HookFlag.BeforeDonate]: 1 << 5,
[HookFlag.AfterDonate]: 1 << 4,
[HookFlag.BeforeSwapReturnsDelta]: 1 << 3,
[HookFlag.AfterSwapReturnsDelta]: 1 << 2,
[HookFlag.AfterAddLiquidityReturnsDelta]: 1 << 1,
[HookFlag.AfterRemoveLiquidityReturnsDelta]: 1 << 0,
}
export function getFlagsFromContractAddress(contractAddress: Address): HookFlag[] {
// Extract the last 4 hexadecimal digits from the address
const last4Hex = contractAddress.slice(-4)
// Convert the hex string to a binary string
const binaryStr = parseInt(last4Hex, 16).toString(2)
// Parse the last 12 bits of the binary string
const relevantBits = binaryStr.slice(-12)
// Determine which flags are active
const activeFlags = Object.entries(FLAGS)
.filter(([, bitPosition]) => (parseInt(relevantBits, 2) & bitPosition) !== 0)
.map(([flag]) => flag as HookFlag)
return activeFlags
}
export interface FlagWarning {
name: string
info: string
dangerous: boolean
}
export function getFlagWarning(flag: HookFlag, t: AppTFunction): FlagWarning | undefined {
switch (flag) {
case HookFlag.BeforeSwap:
case HookFlag.BeforeSwapReturnsDelta:
return {
name: t('common.swap'),
info: t('position.hook.swapWarning'),
dangerous: false,
}
case HookFlag.BeforeAddLiquidity:
case HookFlag.AfterAddLiquidity:
return {
name: t('common.addLiquidity'),
info: t('position.hook.liquidityWarning'),
dangerous: false,
}
case HookFlag.BeforeRemoveLiquidity:
case HookFlag.AfterRemoveLiquidity:
return {
name: t('pool.removeLiquidity'),
info: t('position.hook.removeWarning'),
dangerous: true,
}
}
return undefined
}
...@@ -4,7 +4,7 @@ import { getChainFromChainUrlParam } from 'constants/chains' ...@@ -4,7 +4,7 @@ import { getChainFromChainUrlParam } from 'constants/chains'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { GqlSearchToken } from 'graphql/data/SearchTokens' import { GqlSearchToken } from 'graphql/data/SearchTokens'
import { TokenQueryData } from 'graphql/data/Token' import { TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/types' import { TopToken } from 'graphql/data/TopTokens'
import { gqlToCurrency } from 'graphql/data/util' import { gqlToCurrency } from 'graphql/data/util'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react' import { useMemo } from 'react'
......
...@@ -498,13 +498,6 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = ` ...@@ -498,13 +498,6 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = `
} }
.c1 { .c1 {
grid-column: 2;
grid-row: 1;
font-size: 16px;
font-weight: 485;
}
.c2 {
grid-column: 3; grid-column: 3;
grid-row: 1; grid-row: 1;
display: -webkit-box; display: -webkit-box;
...@@ -531,23 +524,10 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = ` ...@@ -531,23 +524,10 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = `
> >
<button <button
class="c0" class="c0"
data-testid="World Chain-selector" data-testid="undefined-selector"
> >
<img
alt="World Chain logo"
aria-labelledby="titleID"
height="20px"
src="world-chain-logo.png"
style="margin-right: 12px; border-radius: 6px;"
width="20px"
/>
<div <div
class="c1" class="c1"
>
World Chain
</div>
<div
class="c2"
/> />
</button> </button>
</span> </span>
......
...@@ -7,7 +7,6 @@ import Column from 'components/deprecated/Column' ...@@ -7,7 +7,6 @@ import Column from 'components/deprecated/Column'
import { useTokenWarning } from 'constants/deprecatedTokenSafety' import { useTokenWarning } from 'constants/deprecatedTokenSafety'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { GqlSearchToken } from 'graphql/data/SearchTokens' import { GqlSearchToken } from 'graphql/data/SearchTokens'
import { gqlTokenToCurrencyInfo } from 'graphql/data/types'
import { getTokenDetailsURL, supportedChainIdFromGQLChain } from 'graphql/data/util' import { getTokenDetailsURL, supportedChainIdFromGQLChain } from 'graphql/data/util'
import styled, { css } from 'lib/styled-components' import styled, { css } from 'lib/styled-components'
import { searchGenieCollectionToTokenSearchResult, searchTokenToTokenSearchResult } from 'lib/utils/searchBar' import { searchGenieCollectionToTokenSearchResult, searchTokenToTokenSearchResult } from 'lib/utils/searchBar'
...@@ -18,14 +17,10 @@ import { Link, useNavigate } from 'react-router-dom' ...@@ -18,14 +17,10 @@ import { Link, useNavigate } from 'react-router-dom'
import { EllipsisStyle, ThemedText } from 'theme/components' import { EllipsisStyle, ThemedText } from 'theme/components'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
import { Verified } from 'ui/src/components/icons/Verified' import { Verified } from 'ui/src/components/icons/Verified'
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' import { TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { Token, TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InterfaceSearchResultSelectionProperties } from 'uniswap/src/features/telemetry/types' import { InterfaceSearchResultSelectionProperties } from 'uniswap/src/features/telemetry/types'
import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils'
import { Trans, useTranslation } from 'uniswap/src/i18n' import { Trans, useTranslation } from 'uniswap/src/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { shortenAddress } from 'uniswap/src/utils/addresses' import { shortenAddress } from 'uniswap/src/utils/addresses'
...@@ -110,15 +105,10 @@ export function SuggestionRow({ ...@@ -110,15 +105,10 @@ export function SuggestionRow({
const navigate = useNavigate() const navigate = useNavigate()
const { formatFiatPrice, formatDelta, formatNumberOrString } = useFormatter() const { formatFiatPrice, formatDelta, formatNumberOrString } = useFormatter()
const [brokenCollectionImage, setBrokenCollectionImage] = useState(false) const [brokenCollectionImage, setBrokenCollectionImage] = useState(false)
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const warning = useTokenWarning( const warning = useTokenWarning(
isToken ? suggestion.address : undefined, isToken ? suggestion.address : undefined,
isToken ? supportedChainIdFromGQLChain(suggestion.chain) : UniverseChainId.Mainnet, isToken ? supportedChainIdFromGQLChain(suggestion.chain) : UniverseChainId.Mainnet,
) )
const tokenWarningSeverity = isToken
? getTokenWarningSeverity(gqlTokenToCurrencyInfo(suggestion as Token)) // casting GqlSearchToken to Token
: undefined
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
const address = const address =
...@@ -190,23 +180,9 @@ export function SuggestionRow({ ...@@ -190,23 +180,9 @@ export function SuggestionRow({
/> />
)} )}
<Flex alignItems="flex-start" justifyContent="flex-start" shrink grow> <Flex alignItems="flex-start" justifyContent="flex-start" shrink grow>
<Flex <Flex row gap="$spacing4" shrink width="95%">
row
gap="$spacing4"
shrink
width="95%"
{...(isToken && tokenProtectionEnabled && { alignItems: 'center' })}
>
<PrimaryText lineHeight="24px">{suggestion.name}</PrimaryText> <PrimaryText lineHeight="24px">{suggestion.name}</PrimaryText>
{isToken ? ( {isToken ? <TokenSafetyIcon warning={warning} /> : suggestion.isVerified && <Verified size={14} />}
tokenProtectionEnabled ? (
<WarningIcon severity={tokenWarningSeverity} size="$icon.16" />
) : (
<TokenSafetyIcon warning={warning} />
)
) : (
suggestion.isVerified && <Verified size={14} />
)}
</Flex> </Flex>
<Flex row gap="$spacing4"> <Flex row gap="$spacing4">
<ThemedText.SubHeaderSmall lineHeight="20px"> <ThemedText.SubHeaderSmall lineHeight="20px">
......
...@@ -25,6 +25,7 @@ export type TabsItem = MenuItem & { ...@@ -25,6 +25,7 @@ export type TabsItem = MenuItem & {
export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSection[] => { export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSection[] => {
const { t } = useTranslation() const { t } = useTranslation()
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere)
const { pathname } = useLocation() const { pathname } = useLocation()
const theme = useTheme() const theme = useTheme()
...@@ -76,7 +77,7 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti ...@@ -76,7 +77,7 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti
{ {
label: t('common.transactions'), label: t('common.transactions'),
quickKey: 'X', quickKey: 'X',
href: '/explore/transactions/ethereum', href: `/explore/transactions${isMultichainExploreEnabled ? '/ethereum' : ''}`,
internal: true, internal: true,
}, },
{ label: t('common.nfts'), quickKey: 'N', href: '/nfts', internal: true }, { label: t('common.nfts'), quickKey: 'N', href: '/nfts', internal: true },
...@@ -84,7 +85,7 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti ...@@ -84,7 +85,7 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti
}, },
{ {
title: t('common.pool'), title: t('common.pool'),
href: isV4EverywhereEnabled ? '/positions' : '/pool', href: '/pool',
isActive: pathname.startsWith('/pool'), isActive: pathname.startsWith('/pool'),
items: [ items: [
{ {
......
...@@ -23,6 +23,8 @@ import { useProfilePageState } from 'nft/hooks' ...@@ -23,6 +23,8 @@ import { useProfilePageState } from 'nft/hooks'
import { ProfilePageStateType } from 'nft/types' import { ProfilePageStateType } from 'nft/types'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { INTERFACE_NAV_HEIGHT } from 'uniswap/src/theme/heights' import { INTERFACE_NAV_HEIGHT } from 'uniswap/src/theme/heights'
...@@ -76,12 +78,20 @@ function useShouldHideChainSelector() { ...@@ -76,12 +78,20 @@ function useShouldHideChainSelector() {
const isSwapPage = useIsSwapPage() const isSwapPage = useIsSwapPage()
const isLimitPage = useIsLimitPage() const isLimitPage = useIsLimitPage()
const isExplorePage = useIsExplorePage() const isExplorePage = useIsExplorePage()
const { value: multichainExploreFlagEnabled, isLoading: isMultichainExploreFlagLoading } = useFeatureFlagWithLoading(
FeatureFlags.MultichainExplore,
)
const baseHiddenPages = isNftPage const baseHiddenPages = isNftPage
const multichainHiddenPages = const multichainHiddenPages = isLandingPage || isSendPage || isSwapPage || isLimitPage || baseHiddenPages
isLandingPage || isSendPage || isSwapPage || isLimitPage || baseHiddenPages || isExplorePage const multichainExploreHiddenPages = multichainHiddenPages || isExplorePage
const hideChainSelector =
multichainExploreFlagEnabled || isMultichainExploreFlagLoading
? multichainExploreHiddenPages
: multichainHiddenPages
return multichainHiddenPages return hideChainSelector
} }
export default function Navbar() { export default function Navbar() {
......
...@@ -6,7 +6,6 @@ import { ExplorerIcon } from 'components/Icons/ExplorerIcon' ...@@ -6,7 +6,6 @@ import { ExplorerIcon } from 'components/Icons/ExplorerIcon'
import CurrencyLogo from 'components/Logo/CurrencyLogo' import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo' import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo'
import { DetailBubble } from 'components/Pools/PoolDetails/shared' import { DetailBubble } from 'components/Pools/PoolDetails/shared'
import { PoolDetailsBadge } from 'components/Pools/PoolTable/PoolTable'
import ShareButton from 'components/Tokens/TokenDetails/ShareButton' import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
import { ActionButtonStyle, ActionMenuFlyoutStyle } from 'components/Tokens/TokenDetails/shared' import { ActionButtonStyle, ActionMenuFlyoutStyle } from 'components/Tokens/TokenDetails/shared'
import { LoadingBubble } from 'components/Tokens/loading' import { LoadingBubble } from 'components/Tokens/loading'
...@@ -22,7 +21,7 @@ import { ChevronRight, ExternalLink as ExternalLinkIcon } from 'react-feather' ...@@ -22,7 +21,7 @@ import { ChevronRight, ExternalLink as ExternalLinkIcon } from 'react-feather'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ClickableStyle, ClickableTamaguiStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components' import { ClickableStyle, ClickableTamaguiStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components'
import { textFadeIn } from 'theme/styles' import { textFadeIn } from 'theme/styles'
import { Flex, TouchableArea } from 'ui/src' import { TouchableArea } from 'ui/src'
import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown' import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown'
import { BIPS_BASE } from 'uniswap/src/constants/misc' import { BIPS_BASE } from 'uniswap/src/constants/misc'
import { ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
...@@ -48,6 +47,12 @@ const HeaderContainer = styled.div` ...@@ -48,6 +47,12 @@ const HeaderContainer = styled.div`
animation-duration: ${({ theme }) => theme.transition.duration.medium}; animation-duration: ${({ theme }) => theme.transition.duration.medium};
` `
const Badge = styled(ThemedText.LabelMicro)`
background: ${({ theme }) => theme.surface2};
padding: 2px 6px;
border-radius: 4px;
`
const IconBubble = styled(LoadingBubble)` const IconBubble = styled(LoadingBubble)`
width: 32px; width: 32px;
height: 32px; height: 32px;
...@@ -141,17 +146,8 @@ const PoolDetailsTitle = ({ ...@@ -141,17 +146,8 @@ const PoolDetailsTitle = ({
</StyledLink> </StyledLink>
</PoolName> </PoolName>
</div> </div>
<Flex row gap="$gap4" alignItems="center"> {protocolVersion === ProtocolVersion.V2 && <Badge>v2</Badge>}
<PoolDetailsBadge variant="body3" $position="left"> {!!feePercent && <Badge>{feePercent}</Badge>}
{protocolVersion?.toLowerCase()}
</PoolDetailsBadge>
{/* TODO(WEB-5364): add hook badge when data available, it should have a hover state and link out to the explorer */}
{!!feePercent && (
<PoolDetailsBadge variant="body3" $position="right">
{feePercent}
</PoolDetailsBadge>
)}
</Flex>
<TouchableArea hoverStyle={{ opacity: 0.8 }} onPress={toggleReversed}> <TouchableArea hoverStyle={{ opacity: 0.8 }} onPress={toggleReversed}>
<ArrowUpDown <ArrowUpDown
{...ClickableTamaguiStyle} {...ClickableTamaguiStyle}
......
...@@ -6,7 +6,7 @@ import Column from 'components/deprecated/Column' ...@@ -6,7 +6,7 @@ import Column from 'components/deprecated/Column'
import Row from 'components/deprecated/Row' import Row from 'components/deprecated/Row'
import { SwapWrapperOuter } from 'components/swap/styled' import { SwapWrapperOuter } from 'components/swap/styled'
import { LoadingBubble } from 'components/Tokens/loading' import { LoadingBubble } from 'components/Tokens/loading'
import TokenSafetyMessage from 'components/TokenSafety/DeprecatedTokenSafetyMessage' import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import { chainIdToBackendChain } from 'constants/chains' import { chainIdToBackendChain } from 'constants/chains'
import { getPriorityWarning, StrongWarning, useTokenWarning } from 'constants/deprecatedTokenSafety' import { getPriorityWarning, StrongWarning, useTokenWarning } from 'constants/deprecatedTokenSafety'
import { useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' import { useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider'
...@@ -16,7 +16,7 @@ import { useAccount } from 'hooks/useAccount' ...@@ -16,7 +16,7 @@ import { useAccount } from 'hooks/useAccount'
import { useSwitchChain } from 'hooks/useSwitchChain' import { useSwitchChain } from 'hooks/useSwitchChain'
import styled from 'lib/styled-components' import styled from 'lib/styled-components'
import { Swap } from 'pages/Swap' import { Swap } from 'pages/Swap'
import { useCallback, useMemo, useReducer, useState } from 'react' import { useMemo, useReducer } from 'react'
import { Plus, X } from 'react-feather' import { Plus, X } from 'react-feather'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
...@@ -25,15 +25,9 @@ import { opacify } from 'theme/utils' ...@@ -25,15 +25,9 @@ import { opacify } from 'theme/utils'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown' import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown'
import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { TokenWarningCard } from 'uniswap/src/features/tokens/TokenWarningCard'
import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal'
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
import { Trans } from 'uniswap/src/i18n' import { Trans } from 'uniswap/src/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { currencyId } from 'uniswap/src/utils/currencyId' import { currencyId } from 'utils/currencyId'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
const PoolDetailsStatsButtonsRow = styled(Row)` const PoolDetailsStatsButtonsRow = styled(Row)`
...@@ -164,8 +158,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load ...@@ -164,8 +158,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
const location = useLocation() const location = useLocation()
const currency0 = token0 && gqlToCurrency(token0) const currency0 = token0 && gqlToCurrency(token0)
const currency1 = token1 && gqlToCurrency(token1) const currency1 = token1 && gqlToCurrency(token1)
const currencyInfo0 = useCurrencyInfo(currency0 && currencyId(currency0))
const currencyInfo1 = useCurrencyInfo(currency1 && currencyId(currency1))
// Mobile Balance Data // Mobile Balance Data
const { data: balanceQuery } = useTokenBalancesQuery() const { data: balanceQuery } = useTokenBalancesQuery()
...@@ -202,9 +194,7 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load ...@@ -202,9 +194,7 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
if (account.chainId !== chainId && chainId) { if (account.chainId !== chainId && chainId) {
await switchChain(chainId) await switchChain(chainId)
} }
const currency0Address = currency0.isNative ? 'ETH' : currency0.address navigate(`/add/${currencyId(currency0)}/${currencyId(currency1)}/${feeTier}${tokenId ? `/${tokenId}` : ''}`, {
const currency1Address = currency1.isNative ? 'ETH' : currency1.address
navigate(`/add/${currency0Address}/${currency1Address}/${feeTier}${tokenId ? `/${tokenId}` : ''}`, {
state: { from: location.pathname }, state: { from: location.pathname },
}) })
} }
...@@ -217,15 +207,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load ...@@ -217,15 +207,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
const token1Warning = useTokenWarning(token1?.address, chainId) const token1Warning = useTokenWarning(token1?.address, chainId)
const priorityWarning = getPriorityWarning(token0Warning, token1Warning) const priorityWarning = getPriorityWarning(token0Warning, token1Warning)
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const [showWarningModal, setShowWarningModal] = useState(false)
const closeWarningModal = useCallback(() => setShowWarningModal(false), [])
const [warningModalCurrencyInfo, setWarningModalCurrencyInfo] = useState<Maybe<CurrencyInfo>>()
const onWarningCardCtaPressed = useCallback((currencyInfo: Maybe<CurrencyInfo>) => {
setWarningModalCurrencyInfo(currencyInfo)
setShowWarningModal(true)
}, [])
if (loading || !currency0 || !currency1) { if (loading || !currency0 || !currency1) {
return ( return (
<PoolDetailsStatsButtonsRow data-testid="pdp-buttons-loading-skeleton"> <PoolDetailsStatsButtonsRow data-testid="pdp-buttons-loading-skeleton">
...@@ -301,30 +282,13 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load ...@@ -301,30 +282,13 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
compact compact
disableTokenInputs={chainId !== account.chainId} disableTokenInputs={chainId !== account.chainId}
/> />
{tokenProtectionEnabled ? ( {Boolean(priorityWarning) && (
<> <TokenSafetyMessage
<TokenWarningCard currencyInfo={currencyInfo0} onPress={() => onWarningCardCtaPressed(currencyInfo0)} /> tokenAddress={(priorityWarning === token0Warning ? token0?.address : token1?.address) ?? ''}
<TokenWarningCard currencyInfo={currencyInfo1} onPress={() => onWarningCardCtaPressed(currencyInfo1)} /> warning={priorityWarning ?? StrongWarning}
{warningModalCurrencyInfo && ( plural={Boolean(token0Warning && token1Warning)}
// Intentionally duplicative with the TokenWarningModal in the swap component; this one only displays when user clicks "i" Info button on the TokenWarningCard tokenSymbol={priorityWarning === token0Warning ? token0?.symbol : token1?.symbol}
<TokenWarningModal />
currencyInfo0={warningModalCurrencyInfo}
isInfoOnlyWarning
isVisible={showWarningModal}
closeModalOnly={closeWarningModal}
onAcknowledge={closeWarningModal}
/>
)}
</>
) : (
Boolean(priorityWarning) && (
<TokenSafetyMessage
tokenAddress={(priorityWarning === token0Warning ? token0?.address : token1?.address) ?? ''}
warning={priorityWarning ?? StrongWarning}
plural={Boolean(token0Warning && token1Warning)}
tokenSymbol={priorityWarning === token0Warning ? token0?.symbol : token1?.symbol}
/>
)
)} )}
</SwapModalWrapper> </SwapModalWrapper>
<Scrim <Scrim
......
...@@ -284,7 +284,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -284,7 +284,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
min-width: 0; min-width: 0;
} }
.c10 { .c12 {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
min-width: 0; min-width: 0;
...@@ -311,7 +311,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -311,7 +311,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
gap: 12px; gap: 12px;
} }
.c11 { .c13 {
width: -webkit-max-content; width: -webkit-max-content;
width: -moz-max-content; width: -moz-max-content;
width: max-content; width: max-content;
...@@ -331,7 +331,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -331,7 +331,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
gap: 8px; gap: 8px;
} }
.c12 { .c14 {
display: inline-block; display: inline-block;
height: inherit; height: inherit;
} }
...@@ -344,6 +344,14 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -344,6 +344,14 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.c10 {
color: #7D7D7D;
-webkit-letter-spacing: -0.01em;
-moz-letter-spacing: -0.01em;
-ms-letter-spacing: -0.01em;
letter-spacing: -0.01em;
}
.c4 { .c4 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
...@@ -403,6 +411,12 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -403,6 +411,12 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
animation-duration: 250ms; animation-duration: 250ms;
} }
.c11 {
background: #F9F9F9;
padding: 2px 6px;
border-radius: 4px;
}
.c6 { .c6 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
...@@ -509,18 +523,9 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -509,18 +523,9 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
</div> </div>
</div> </div>
<div <div
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _gap-1316331199 _alignItems-center" class="c10 c11 css-1m65e73"
> >
<span 0.05%
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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202 _borderTopLeftRadius-4px _borderBottomLeftRadius-4px"
data-disable-theme="true"
/>
<span
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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _fontSize-229441158 _lineHeight-222976511 _fontWeight-233016202 _borderTopRightRadius-4px _borderBottomRightRadius-4px"
data-disable-theme="true"
>
0.05%
</span>
</div> </div>
<div <div
class="_opacity-0hover-0d0t846 _opacity-0active-0d0t7546 _transform-0active-scale11281 _display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _cursor-pointer" class="_opacity-0hover-0d0t846 _opacity-0active-0d0t7546 _transform-0active-scale11281 _display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _cursor-pointer"
...@@ -543,7 +548,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -543,7 +548,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
</div> </div>
</div> </div>
<div <div
class="c10 c11" class="c12 c13"
width="max-content" width="max-content"
> >
<div <div
...@@ -555,7 +560,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -555,7 +560,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
id="Dropdown" id="Dropdown"
> >
<div <div
class="c12" class="c14"
style="width: 100%;" style="width: 100%;"
> >
<div> <div>
...@@ -600,7 +605,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -600,7 +605,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
id="Dropdown" id="Dropdown"
> >
<div <div
class="c12" class="c14"
style="width: 100%;" style="width: 100%;"
> >
<div> <div>
......
import 'test-utils/tokens/mocks' import 'test-utils/tokens/mocks'
import { ApolloError } from '@apollo/client'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { TopPoolTable } from 'components/Pools/PoolTable/PoolTable' import { TopPoolTable } from 'components/Pools/PoolTable/PoolTable'
import { useTopPools } from 'graphql/data/pools/useTopPools'
import Router from 'react-router-dom' import Router from 'react-router-dom'
import { useTopPools } from 'state/explore/topPools'
import { mocked } from 'test-utils/mocked' import { mocked } from 'test-utils/mocked'
import { validParams, validRestPoolToken0, validRestPoolToken1 } from 'test-utils/pools/fixtures' import { validBEPoolToken0, validBEPoolToken1, validParams } from 'test-utils/pools/fixtures'
import { render, screen } from 'test-utils/render' import { render, screen } from 'test-utils/render'
import { ProtocolVersion } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { ProtocolVersion } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
jest.mock('state/explore/topPools') jest.mock('graphql/data/pools/useTopPools')
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useParams: jest.fn(), useParams: jest.fn(),
...@@ -22,8 +23,9 @@ describe('PoolTable', () => { ...@@ -22,8 +23,9 @@ describe('PoolTable', () => {
it('renders loading state', () => { it('renders loading state', () => {
mocked(useTopPools).mockReturnValue({ mocked(useTopPools).mockReturnValue({
isLoading: true, loading: true,
isError: false, errorV3: undefined,
errorV2: undefined,
topPools: [], topPools: [],
}) })
...@@ -34,8 +36,9 @@ describe('PoolTable', () => { ...@@ -34,8 +36,9 @@ describe('PoolTable', () => {
it('renders error state', () => { it('renders error state', () => {
mocked(useTopPools).mockReturnValue({ mocked(useTopPools).mockReturnValue({
isLoading: false, loading: false,
isError: true, errorV3: new ApolloError({ errorMessage: 'error fetching data' }),
errorV2: new ApolloError({ errorMessage: 'error fetching data' }),
topPools: [], topPools: [],
}) })
...@@ -47,10 +50,8 @@ describe('PoolTable', () => { ...@@ -47,10 +50,8 @@ describe('PoolTable', () => {
it('renders data filled state', () => { it('renders data filled state', () => {
const mockData = [ const mockData = [
{ {
id: '1', token0: validBEPoolToken0,
chain: 'mainnet', token1: validBEPoolToken1,
token0: validRestPoolToken0,
token1: validRestPoolToken1,
feeTier: 10000, feeTier: 10000,
hash: '0x123', hash: '0x123',
txCount: 200, txCount: 200,
...@@ -64,8 +65,9 @@ describe('PoolTable', () => { ...@@ -64,8 +65,9 @@ describe('PoolTable', () => {
] ]
mocked(useTopPools).mockReturnValue({ mocked(useTopPools).mockReturnValue({
topPools: mockData, topPools: mockData,
isLoading: false, loading: false,
isError: false, errorV3: undefined,
errorV2: undefined,
}) })
const { asFragment } = render(<TopPoolTable />) const { asFragment } = render(<TopPoolTable />)
......
...@@ -12,9 +12,16 @@ import { EllipsisText } from 'components/Tokens/TokenTable' ...@@ -12,9 +12,16 @@ import { EllipsisText } from 'components/Tokens/TokenTable'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants' import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
import { exploreSearchStringAtom } from 'components/Tokens/state' import { exploreSearchStringAtom } from 'components/Tokens/state'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { chainIdToBackendChain } from 'constants/chains' import { chainIdToBackendChain, useChainFromUrlParam } from 'constants/chains'
import { PoolSortFields, TablePool } from 'graphql/data/pools/useTopPools' import { useUpdateManualOutage } from 'featureFlags/flags/outageBanner'
import { OrderDirection, gqlToCurrency, supportedChainIdFromGQLChain, unwrapToken } from 'graphql/data/util' import { PoolSortFields, TablePool, useTopPools } from 'graphql/data/pools/useTopPools'
import {
OrderDirection,
getSupportedGraphQlChain,
gqlToCurrency,
supportedChainIdFromGQLChain,
unwrapToken,
} from 'graphql/data/util'
import { useCurrencyInfo } from 'hooks/Tokens' import { useCurrencyInfo } from 'hooks/Tokens'
import useSimplePagination from 'hooks/useSimplePagination' import useSimplePagination from 'hooks/useSimplePagination'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
...@@ -26,6 +33,8 @@ import { PoolStat } from 'state/explore/types' ...@@ -26,6 +33,8 @@ import { PoolStat } from 'state/explore/types'
import { Flex, Text, styled } from 'ui/src' import { Flex, Text, styled } from 'ui/src'
import { BIPS_BASE } from 'uniswap/src/constants/misc' 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 { Chain, ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { Trans } from 'uniswap/src/i18n' import { Trans } from 'uniswap/src/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains' import { UniverseChainId } from 'uniswap/src/types/chains'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
...@@ -43,23 +52,13 @@ const TableWrapper = styled(Flex, { ...@@ -43,23 +52,13 @@ const TableWrapper = styled(Flex, {
maxWidth: MAX_WIDTH_MEDIA_BREAKPOINT, maxWidth: MAX_WIDTH_MEDIA_BREAKPOINT,
}) })
export const PoolDetailsBadge = styled(Text, { const Badge = styled(Text, {
py: 2, py: 2,
px: 6, px: 6,
backgroundColor: '$surface2', backgroundColor: '$surface2',
borderRadius: '$rounded6',
variant: 'body4',
color: '$neutral2', color: '$neutral2',
variants: {
$position: {
right: {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
left: {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
},
},
}) })
interface PoolTableValues { interface PoolTableValues {
...@@ -94,31 +93,24 @@ function PoolDescription({ ...@@ -94,31 +93,24 @@ function PoolDescription({
chainId: UniverseChainId chainId: UniverseChainId
protocolVersion?: ProtocolVersion | string protocolVersion?: ProtocolVersion | string
}) { }) {
const isRestPool = token0 && !('id' in token0) const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore)
const currencies = [token0 ? gqlToCurrency(token0) : undefined, token1 ? gqlToCurrency(token1) : undefined] const currencies = [token0 ? gqlToCurrency(token0) : undefined, token1 ? gqlToCurrency(token1) : undefined]
// skip is isRestExploreEnabled
const currencyLogos = [ const currencyLogos = [
useCurrencyInfo(currencies?.[0], chainId, isRestPool)?.logoUrl, useCurrencyInfo(currencies?.[0], chainId, isRestExploreEnabled)?.logoUrl,
useCurrencyInfo(currencies?.[1], chainId, isRestPool)?.logoUrl, useCurrencyInfo(currencies?.[1], chainId, isRestExploreEnabled)?.logoUrl,
] ]
const images = [getRestTokenLogo(token0, currencyLogos[0]), getRestTokenLogo(token1, currencyLogos[1])] const images = isRestExploreEnabled
? [getRestTokenLogo(token0, currencyLogos[0]), getRestTokenLogo(token1, currencyLogos[1])]
: undefined
return ( return (
<Flex row gap="$gap8" alignItems="center"> <Flex row gap="$gap8" alignItems="center">
<PortfolioLogo currencies={currencies} chainId={chainId} images={images} size={28} /> <PortfolioLogo currencies={currencies} chainId={chainId} images={images} size={28} />
<EllipsisText> <EllipsisText>
{token0?.symbol}/{token1?.symbol} {token0?.symbol}/{token1?.symbol}
</EllipsisText> </EllipsisText>
<Flex row gap="$gap4" alignItems="center"> {protocolVersion === ProtocolVersion.V2 && <Badge>{protocolVersion.toLowerCase()}</Badge>}
<PoolDetailsBadge variant="body4" $position="left"> {feeTier && <Badge>{feeTier / BIPS_BASE}%</Badge>}
{protocolVersion.toLowerCase()}
</PoolDetailsBadge>
{/* TODO(WEB-5364): add hook badge when data available, it should have a hover state and link out to the explorer */}
{feeTier && (
<PoolDetailsBadge variant="body4" $position="right">
{feeTier / BIPS_BASE}%
</PoolDetailsBadge>
)}
</Flex>
</Flex> </Flex>
) )
} }
...@@ -176,6 +168,7 @@ function PoolTableHeader({ ...@@ -176,6 +168,7 @@ function PoolTableHeader({
} }
export const TopPoolTable = memo(function TopPoolTable() { export const TopPoolTable = memo(function TopPoolTable() {
const chain = getSupportedGraphQlChain(useChainFromUrlParam(), { fallbackToEthereum: true })
const sortMethod = useAtomValue(sortMethodAtom) const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom) const sortAscending = useAtomValue(sortAscendingAtom)
...@@ -186,22 +179,38 @@ export const TopPoolTable = memo(function TopPoolTable() { ...@@ -186,22 +179,38 @@ export const TopPoolTable = memo(function TopPoolTable() {
resetSortAscending() resetSortAscending()
}, [resetSortAscending, resetSortMethod]) }, [resetSortAscending, resetSortMethod])
const { topPools, isLoading, isError } = useRestTopPools({ const {
sortBy: sortMethod, topPools: gqlTopPools,
sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc, loading: gqlLoading,
}) errorV3,
errorV2,
} = useTopPools(
{ sortBy: sortMethod, sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc },
chain.id,
)
const combinedError =
errorV2 && errorV3
? new ApolloError({ errorMessage: `Could not retrieve V2 and V3 Top Pools on chain: ${chain.id}` })
: undefined
const allDataStillLoading = gqlLoading && !gqlTopPools.length
useUpdateManualOutage({ chainId: chain.id, errorV3, errorV2 })
const {
topPools: restTopPools,
isLoading: restIsLoading,
isError: restIsError,
} = useRestTopPools({ sortBy: sortMethod, sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc })
const { page, loadMore } = useSimplePagination() const { page, loadMore } = useSimplePagination()
const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore)
const { topPools, loading, error } = isRestExploreEnabled
? { topPools: restTopPools?.slice(0, page * TABLE_PAGE_SIZE), loading: restIsLoading, error: restIsError }
: { topPools: gqlTopPools, loading: allDataStillLoading, error: combinedError }
return ( return (
<TableWrapper data-testid="top-pools-explore-table"> <TableWrapper data-testid="top-pools-explore-table">
<PoolsTable <PoolsTable pools={topPools} loading={loading} error={error} loadMore={loadMore} maxWidth={1200} />
pools={topPools?.slice(0, page * TABLE_PAGE_SIZE)}
loading={isLoading}
error={isError}
loadMore={loadMore}
maxWidth={1200}
/>
</TableWrapper> </TableWrapper>
) )
}) })
......
...@@ -332,22 +332,12 @@ exports[`PoolTable renders data filled state 1`] = ` ...@@ -332,22 +332,12 @@ exports[`PoolTable renders data filled state 1`] = `
> >
USDC/ETH USDC/ETH
</span> </span>
<div <span
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _gap-1316331199 _alignItems-center" 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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _borderTopLeftRadius-1307609936 _borderTopRightRadius-1307609936 _borderBottomRightRadius-1307609936 _borderBottomLeftRadius-1307609936 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202"
data-disable-theme="true"
> >
<span 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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202 _borderTopLeftRadius-4px _borderBottomLeftRadius-4px" </span>
data-disable-theme="true"
>
v3
</span>
<span
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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202 _borderTopRightRadius-4px _borderBottomRightRadius-4px"
data-disable-theme="true"
>
1%
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -55,7 +55,6 @@ export function RemoveLiquidityTxContextProvider({ children }: PropsWithChildren ...@@ -55,7 +55,6 @@ export function RemoveLiquidityTxContextProvider({ children }: PropsWithChildren
action: { action: {
currency0Amount: currency0AmountToRemove, currency0Amount: currency0AmountToRemove,
currency1Amount: currency1AmountToRemove, currency1Amount: currency1AmountToRemove,
liquidityToken: positionInfo.liquidityToken,
}, },
approvePositionTokenRequest, approvePositionTokenRequest,
txRequest, txRequest,
......
...@@ -37,7 +37,7 @@ export function SwapBottomCard() { ...@@ -37,7 +37,7 @@ export function SwapBottomCard() {
const hasViewedBridgingBanner = useSelector(selectHasViewedBridgingBanner) const hasViewedBridgingBanner = useSelector(selectHasViewedBridgingBanner)
const bridgingEnabled = useFeatureFlag(FeatureFlags.Bridging) const bridgingEnabled = useFeatureFlag(FeatureFlags.Bridging)
const isBridgingSupportedChain = useIsBridgingChain(chainId ?? UniverseChainId.Mainnet) const isBridgingSupported = useIsBridgingChain(chainId ?? UniverseChainId.Mainnet)
const numBridgingChains = useNumBridgingChains() const numBridgingChains = useNumBridgingChains()
const handleBridgingDismiss = useCallback( const handleBridgingDismiss = useCallback(
(shouldNavigate: boolean) => { (shouldNavigate: boolean) => {
...@@ -61,8 +61,7 @@ export function SwapBottomCard() { ...@@ -61,8 +61,7 @@ export function SwapBottomCard() {
return null return null
} }
const isBridgingBannerChain = chainId === null || chainId === UniverseChainId.Mainnet || isBridgingSupportedChain const shouldShowBridgingBanner = bridgingEnabled && !hasViewedBridgingBanner && isBridgingSupported
const shouldShowBridgingBanner = bridgingEnabled && !hasViewedBridgingBanner && isBridgingBannerChain
const shouldShowLegacyTreatment = !bridgingEnabled const shouldShowLegacyTreatment = !bridgingEnabled
...@@ -85,7 +84,7 @@ export function SwapBottomCard() { ...@@ -85,7 +84,7 @@ export function SwapBottomCard() {
/> />
</TouchableArea> </TouchableArea>
) )
} else if (shouldShowLegacyTreatment || !isBridgingSupportedChain) { } else if (shouldShowLegacyTreatment || !isBridgingSupported) {
return <NetworkAlert chainId={chainId} /> return <NetworkAlert chainId={chainId} />
} else { } else {
return null return null
......
...@@ -8,7 +8,6 @@ const WarningContainer = styled(Flex, { ...@@ -8,7 +8,6 @@ const WarningContainer = styled(Flex, {
justifyContent: 'center', justifyContent: 'center',
}) })
/** @deprecated use WarningIcon from packages/uniswap instead */
export default function TokenSafetyIcon({ warning }: { warning?: Warning }) { export default function TokenSafetyIcon({ warning }: { warning?: Warning }) {
const colors = useSporeColors() const colors = useSporeColors()
switch (warning?.level) { switch (warning?.level) {
......
...@@ -47,7 +47,6 @@ type TokenSafetyMessageProps = { ...@@ -47,7 +47,6 @@ type TokenSafetyMessageProps = {
tokenSymbol?: string tokenSymbol?: string
} }
/** @deprecated Use TokenWarningCard from packages/uniswap instead */
export default function TokenSafetyMessage({ export default function TokenSafetyMessage({
warning, warning,
tokenAddress, tokenAddress,
......
...@@ -2,7 +2,7 @@ import { InterfacePageName } from '@uniswap/analytics-events' ...@@ -2,7 +2,7 @@ import { InterfacePageName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { BreadcrumbNavContainer, BreadcrumbNavLink, CurrentPageBreadcrumb } from 'components/BreadcrumbNav' import { BreadcrumbNavContainer, BreadcrumbNavLink, CurrentPageBreadcrumb } from 'components/BreadcrumbNav'
import { MobileBottomBar, TDPActionTabs } from 'components/NavBar/MobileBottomBar' import { MobileBottomBar, TDPActionTabs } from 'components/NavBar/MobileBottomBar'
import TokenSafetyMessage from 'components/TokenSafety/DeprecatedTokenSafetyMessage' import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import { ActivitySection } from 'components/Tokens/TokenDetails/ActivitySection' import { ActivitySection } from 'components/Tokens/TokenDetails/ActivitySection'
import BalanceSummary, { PageChainBalanceSummary } from 'components/Tokens/TokenDetails/BalanceSummary' import BalanceSummary, { PageChainBalanceSummary } from 'components/Tokens/TokenDetails/BalanceSummary'
import ChartSection from 'components/Tokens/TokenDetails/ChartSection' import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
...@@ -21,19 +21,13 @@ import { ScrollDirection, useScroll } from 'hooks/useScroll' ...@@ -21,19 +21,13 @@ import { ScrollDirection, useScroll } from 'hooks/useScroll'
import deprecatedStyled from 'lib/styled-components' import deprecatedStyled from 'lib/styled-components'
import { Swap } from 'pages/Swap' import { Swap } from 'pages/Swap'
import { useTDPContext } from 'pages/TokenDetails/TDPContext' import { useTDPContext } from 'pages/TokenDetails/TDPContext'
import { PropsWithChildren, useCallback, useMemo, useState } from 'react' import { PropsWithChildren, useCallback, useMemo } from 'react'
import { ChevronRight } from 'react-feather' import { ChevronRight } from 'react-feather'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { CurrencyState } from 'state/swap/types' import { CurrencyState } from 'state/swap/types'
import { Flex, useIsTouchDevice } from 'ui/src' import { Flex, useIsTouchDevice } from 'ui/src'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { TokenWarningCard } from 'uniswap/src/features/tokens/TokenWarningCard'
import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal'
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
import { Trans } from 'uniswap/src/i18n' import { Trans } from 'uniswap/src/i18n'
import { currencyId } from 'uniswap/src/utils/currencyId'
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent' import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
import { getInitialLogoUrl } from 'utils/getInitialLogoURL' import { getInitialLogoUrl } from 'utils/getInitialLogoURL'
...@@ -86,11 +80,8 @@ function useSwapInitialInputCurrency() { ...@@ -86,11 +80,8 @@ function useSwapInitialInputCurrency() {
function TDPSwapComponent() { function TDPSwapComponent() {
const { address, currency, currencyChainId, warning } = useTDPContext() const { address, currency, currencyChainId, warning } = useTDPContext()
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const navigate = useNavigate() const navigate = useNavigate()
const currencyInfo = useCurrencyInfo(currencyId(currency))
const handleCurrencyChange = useCallback( const handleCurrencyChange = useCallback(
(tokens: CurrencyState) => { (tokens: CurrencyState) => {
const inputCurrencyURLAddress = getCurrencyURLAddress(tokens.inputCurrency) const inputCurrencyURLAddress = getCurrencyURLAddress(tokens.inputCurrency)
...@@ -133,9 +124,6 @@ function TDPSwapComponent() { ...@@ -133,9 +124,6 @@ function TDPSwapComponent() {
// Other token to prefill the swap form with // Other token to prefill the swap form with
const initialInputCurrency = useSwapInitialInputCurrency() const initialInputCurrency = useSwapInitialInputCurrency()
const [showWarningModal, setShowWarningModal] = useState(false)
const closeWarningModal = useCallback(() => setShowWarningModal(false), [])
return ( return (
<> <>
<Swap <Swap
...@@ -146,23 +134,7 @@ function TDPSwapComponent() { ...@@ -146,23 +134,7 @@ function TDPSwapComponent() {
onCurrencyChange={handleCurrencyChange} onCurrencyChange={handleCurrencyChange}
compact compact
/> />
{tokenProtectionEnabled ? ( {warning && <TokenSafetyMessage tokenAddress={address} warning={warning} />}
<>
<TokenWarningCard currencyInfo={currencyInfo} onPress={() => setShowWarningModal(true)} />
{currencyInfo && (
// Intentionally duplicative with the TokenWarningModal in the swap component; this one only displays when user clicks "i" Info button on the TokenWarningCard
<TokenWarningModal
currencyInfo0={currencyInfo}
isInfoOnlyWarning
isVisible={showWarningModal}
closeModalOnly={closeWarningModal}
onAcknowledge={closeWarningModal}
/>
)}
</>
) : (
warning && <TokenSafetyMessage tokenAddress={address} warning={warning} />
)}
</> </>
) )
} }
......
...@@ -304,22 +304,12 @@ exports[`TDPPoolTable renders data filled state 1`] = ` ...@@ -304,22 +304,12 @@ exports[`TDPPoolTable renders data filled state 1`] = `
> >
USDC/ETH USDC/ETH
</span> </span>
<div <span
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _gap-1316331199 _alignItems-center" 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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _borderTopLeftRadius-1307609936 _borderTopRightRadius-1307609936 _borderBottomRightRadius-1307609936 _borderBottomLeftRadius-1307609936 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202"
data-disable-theme="true"
> >
<span 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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202 _borderTopLeftRadius-4px _borderBottomLeftRadius-4px" </span>
data-disable-theme="true"
>
v3
</span>
<span
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 _pt-2px _pb-2px _pr-6px _pl-6px _backgroundColor-568007293 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202 _borderTopRightRadius-4px _borderBottomRightRadius-4px"
data-disable-theme="true"
>
1%
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -6,7 +6,6 @@ import { AllNetworksIcon } from 'components/Tokens/TokenTable/icons' ...@@ -6,7 +6,6 @@ import { AllNetworksIcon } from 'components/Tokens/TokenTable/icons'
import { import {
BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS, BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS,
BACKEND_SUPPORTED_CHAINS, BACKEND_SUPPORTED_CHAINS,
BACKEND_SUPPORTED_TESTNET_CHAINS,
InterfaceGqlChain, InterfaceGqlChain,
useChainFromUrlParam, useChainFromUrlParam,
useIsSupportedChainIdCallback, useIsSupportedChainIdCallback,
...@@ -21,7 +20,8 @@ import { useNavigate } from 'react-router-dom' ...@@ -21,7 +20,8 @@ import { useNavigate } from 'react-router-dom'
import { EllipsisTamaguiStyle } from 'theme/components' import { EllipsisTamaguiStyle } from 'theme/components'
import { Flex, FlexProps, ScrollView, Text, styled } from 'ui/src' import { Flex, FlexProps, ScrollView, Text, styled } from 'ui/src'
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useTranslation } from 'uniswap/src/i18n' import { useTranslation } from 'uniswap/src/i18n'
...@@ -54,10 +54,12 @@ const StyledDropdown = { ...@@ -54,10 +54,12 @@ const StyledDropdown = {
export default function TableNetworkFilter() { export default function TableNetworkFilter() {
const [isMenuOpen, toggleMenu] = useState(false) const [isMenuOpen, toggleMenu] = useState(false)
const isSupportedChainCallback = useIsSupportedChainIdCallback() const isSupportedChainCallback = useIsSupportedChainIdCallback()
const { isTestnetModeEnabled } = useEnabledChains() const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
const exploreParams = useExploreParams() const exploreParams = useExploreParams()
const currentChain = getSupportedGraphQlChain(useChainFromUrlParam()) const currentChain = getSupportedGraphQlChain(useChainFromUrlParam(), {
fallbackToEthereum: !isMultichainExploreEnabled,
})
const tab = exploreParams.tab const tab = exploreParams.tab
return ( return (
...@@ -77,7 +79,9 @@ export default function TableNetworkFilter() { ...@@ -77,7 +79,9 @@ export default function TableNetworkFilter() {
} }
internalMenuItems={ internalMenuItems={
<ScrollView px="$spacing8"> <ScrollView px="$spacing8">
<TableNetworkItem display="All networks" toggleMenu={toggleMenu} tab={tab} /> {isMultichainExploreEnabled && (
<TableNetworkItem display="All networks" toggleMenu={toggleMenu} tab={tab} />
)}
{BACKEND_SUPPORTED_CHAINS.map((network) => { {BACKEND_SUPPORTED_CHAINS.map((network) => {
const chainId = supportedChainIdFromGQLChain(network) const chainId = supportedChainIdFromGQLChain(network)
const isSupportedChain = isSupportedChainCallback(chainId) const isSupportedChain = isSupportedChainCallback(chainId)
...@@ -92,22 +96,6 @@ export default function TableNetworkFilter() { ...@@ -92,22 +96,6 @@ export default function TableNetworkFilter() {
/> />
) : null ) : null
})} })}
{isTestnetModeEnabled
? BACKEND_SUPPORTED_TESTNET_CHAINS.map((network) => {
const chainId = supportedChainIdFromGQLChain(network)
const isSupportedChain = isSupportedChainCallback(chainId)
const chainInfo = isSupportedChain ? UNIVERSE_CHAIN_INFO[chainId] : undefined
return chainInfo ? (
<TableNetworkItem
key={network}
display={network}
chainInfo={chainInfo}
toggleMenu={toggleMenu}
tab={tab}
/>
) : null
})
: null}
{BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.map((network) => { {BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.map((network) => {
const isSupportedChain = isSupportedChainCallback(network) const isSupportedChain = isSupportedChainCallback(network)
const chainInfo = isSupportedChain ? UNIVERSE_CHAIN_INFO[network] : undefined const chainInfo = isSupportedChain ? UNIVERSE_CHAIN_INFO[network] : undefined
...@@ -148,10 +136,14 @@ const TableNetworkItem = memo(function TableNetworkItem({ ...@@ -148,10 +136,14 @@ const TableNetworkItem = memo(function TableNetworkItem({
const navigate = useNavigate() const navigate = useNavigate()
const theme = useTheme() const theme = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
const chainId = chainInfo?.id const chainId = chainInfo?.id
const exploreParams = useExploreParams() const exploreParams = useExploreParams()
const currentChain = getSupportedGraphQlChain(useChainFromUrlParam()) const currentChain = getSupportedGraphQlChain(
const isAllNetworks = display === 'All networks' useChainFromUrlParam(),
isMultichainExploreEnabled ? undefined : { fallbackToEthereum: true },
)
const isAllNetworks = display === 'All networks' && isMultichainExploreEnabled
const isCurrentChain = isAllNetworks const isCurrentChain = isAllNetworks
? !currentChain ? !currentChain
: currentChain?.backendChain.chain === display && exploreParams.chainName : currentChain?.backendChain.chain === display && exploreParams.chainName
......
...@@ -18,10 +18,10 @@ import { ...@@ -18,10 +18,10 @@ import {
useSetSortMethod, useSetSortMethod,
} from 'components/Tokens/state' } from 'components/Tokens/state'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import { chainIdToBackendChain, getChainFromChainUrlParam } from 'constants/chains' import { chainIdToBackendChain, getChainFromChainUrlParam, useChainFromUrlParam } from 'constants/chains'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { SparklineMap, TopToken } from 'graphql/data/types' import { SparklineMap, TopToken, useTopTokens } from 'graphql/data/TopTokens'
import { OrderDirection, getTokenDetailsURL, unwrapToken } from 'graphql/data/util' import { OrderDirection, getSupportedGraphQlChain, getTokenDetailsURL, unwrapToken } from 'graphql/data/util'
import useSimplePagination from 'hooks/useSimplePagination' import useSimplePagination from 'hooks/useSimplePagination'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactElement, ReactNode, memo, useMemo } from 'react' import { ReactElement, ReactNode, memo, useMemo } from 'react'
...@@ -29,6 +29,8 @@ import { TABLE_PAGE_SIZE, giveExploreStatDefaultValue } from 'state/explore' ...@@ -29,6 +29,8 @@ import { TABLE_PAGE_SIZE, giveExploreStatDefaultValue } from 'state/explore'
import { useTopTokens as useRestTopTokens } from 'state/explore/topTokens' import { useTopTokens as useRestTopTokens } from 'state/explore/topTokens'
import { TokenStat } from 'state/explore/types' import { TokenStat } from 'state/explore/types'
import { Flex, Text, styled } from 'ui/src' import { Flex, Text, styled } from 'ui/src'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { Trans } from 'uniswap/src/i18n' import { Trans } from 'uniswap/src/i18n'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
...@@ -87,19 +89,65 @@ function TokenDescription({ token }: { token: TopToken | TokenStat }) { ...@@ -87,19 +89,65 @@ function TokenDescription({ token }: { token: TopToken | TokenStat }) {
} }
export const TopTokensTable = memo(function TopTokensTable() { export const TopTokensTable = memo(function TopTokensTable() {
const { topTokens, tokenSortRank, isLoading, sparklines, isError } = useRestTopTokens() const chain = getSupportedGraphQlChain(useChainFromUrlParam(), { fallbackToEthereum: true })
const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore)
const {
tokens: gqlTokens,
tokenSortRank: gqlTokenSortRank,
loadingTokens: gqlLoadingTokens,
sparklines: gqlSparklines,
error: gqlError,
} = useTopTokens(chain.backendChain.chain, isRestExploreEnabled /* skip */)
const {
topTokens: restTopTokens,
tokenSortRank: restTokenSortRank,
isLoading: restIsLoading,
sparklines: restSparklines,
isError: restError,
} = useRestTopTokens()
const { page, loadMore } = useSimplePagination() const { page, loadMore } = useSimplePagination()
const { tokens, tokenSortRank, sparklines, loading, error } = useMemo(() => {
return isRestExploreEnabled
? {
tokens: restTopTokens?.slice(0, page * TABLE_PAGE_SIZE),
tokenSortRank: restTokenSortRank,
loading: restIsLoading,
sparklines: restSparklines,
error: restError,
}
: {
tokens: gqlTokens,
tokenSortRank: gqlTokenSortRank,
loading: gqlLoadingTokens,
sparklines: gqlSparklines,
error: gqlError,
}
}, [
isRestExploreEnabled,
restTopTokens,
page,
restTokenSortRank,
restIsLoading,
restSparklines,
restError,
gqlTokens,
gqlTokenSortRank,
gqlLoadingTokens,
gqlSparklines,
gqlError,
])
return ( return (
<TableWrapper data-testid="top-tokens-explore-table"> <TableWrapper data-testid="top-tokens-explore-table">
<TokenTable <TokenTable
tokens={topTokens?.slice(0, page * TABLE_PAGE_SIZE)} tokens={tokens}
tokenSortRank={tokenSortRank} tokenSortRank={tokenSortRank}
sparklines={sparklines} sparklines={sparklines}
loading={isLoading} loading={loading}
loadMore={loadMore} loadMore={loadMore}
error={isError} error={error}
/> />
</TableWrapper> </TableWrapper>
) )
......
...@@ -81,19 +81,12 @@ export function LaunchModal({ ...@@ -81,19 +81,12 @@ export function LaunchModal({
</Flex> </Flex>
<Flex gap="$gap8" row> <Flex gap="$gap8" row>
<Trace logPress element={InterfaceElementName.CLOSE_BUTTON}> <Trace logPress element={InterfaceElementName.CLOSE_BUTTON}>
<Button <Button size="small" theme="secondary" fontSize="$micro" fill onPress={() => setShowModal(false)}>
size="small"
theme="secondary"
fontSize="$micro"
fill
flexBasis={0}
onPress={() => setShowModal(false)}
>
{t('common.button.dismiss')} {t('common.button.dismiss')}
</Button> </Button>
</Trace> </Trace>
<Trace logPress element={InterfaceElementName.LEARN_MORE_LINK}> <Trace logPress element={InterfaceElementName.LEARN_MORE_LINK}>
<Button size="small" fontSize="$micro" fill flexBasis={0} onPress={() => openUri(learnMoreUrl)}> <Button size="small" fontSize="$micro" fill onPress={() => openUri(learnMoreUrl)}>
{t('common.button.learn')} {t('common.button.learn')}
</Button> </Button>
</Trace> </Trace>
......
import { import {
BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS, BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS,
BACKEND_SUPPORTED_CHAINS, BACKEND_SUPPORTED_CHAINS,
BACKEND_SUPPORTED_TESTNET_CHAINS,
CHAIN_IDS_TO_NAMES, CHAIN_IDS_TO_NAMES,
CHAIN_ID_TO_BACKEND_NAME, CHAIN_ID_TO_BACKEND_NAME,
CHAIN_NAME_TO_CHAIN_ID, CHAIN_NAME_TO_CHAIN_ID,
...@@ -176,16 +175,6 @@ test.each(backendSupportedChains)( ...@@ -176,16 +175,6 @@ test.each(backendSupportedChains)(
}, },
) )
const backendSupportedTestnetChains = [Chain.EthereumSepolia, Chain.AstrochainSepolia] as const
test.each(backendSupportedTestnetChains)(
'BACKEND_SUPPORTED_TESTNET_CHAINS generates the correct chains',
(chain: InterfaceGqlChain) => {
expect(BACKEND_SUPPORTED_TESTNET_CHAINS.includes(chain)).toBe(true)
expect(BACKEND_SUPPORTED_TESTNET_CHAINS.length).toEqual(backendSupportedTestnetChains.length)
},
)
const backendNotyetSupportedChainIds = [] as const const backendNotyetSupportedChainIds = [] as const
test('BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS array is empty', () => { test('BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS array is empty', () => {
......
...@@ -134,17 +134,6 @@ export const BACKEND_SUPPORTED_CHAINS = Object.keys(UNIVERSE_CHAIN_INFO) ...@@ -134,17 +134,6 @@ export const BACKEND_SUPPORTED_CHAINS = Object.keys(UNIVERSE_CHAIN_INFO)
}) })
.map((key) => UNIVERSE_CHAIN_INFO[parseInt(key) as UniverseChainId].backendChain.chain as InterfaceGqlChain) .map((key) => UNIVERSE_CHAIN_INFO[parseInt(key) as UniverseChainId].backendChain.chain as InterfaceGqlChain)
export const BACKEND_SUPPORTED_TESTNET_CHAINS = Object.keys(UNIVERSE_CHAIN_INFO)
.filter((key) => {
const chainId = parseInt(key) as UniverseChainId
return (
UNIVERSE_CHAIN_INFO[chainId].backendChain.backendSupported &&
!UNIVERSE_CHAIN_INFO[chainId].backendChain.isSecondaryChain &&
UNIVERSE_CHAIN_INFO[chainId].testnet
)
})
.map((key) => UNIVERSE_CHAIN_INFO[parseInt(key) as UniverseChainId].backendChain.chain as InterfaceGqlChain)
export const BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS = GQL_MAINNET_CHAINS.filter( export const BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS = GQL_MAINNET_CHAINS.filter(
(chain) => !BACKEND_SUPPORTED_CHAINS.includes(chain), (chain) => !BACKEND_SUPPORTED_CHAINS.includes(chain),
).map((chain) => CHAIN_NAME_TO_CHAIN_ID[chain]) as [UniverseChainId] ).map((chain) => CHAIN_NAME_TO_CHAIN_ID[chain]) as [UniverseChainId]
......
import { ApolloError } from '@apollo/client'
import {
exploreSearchStringAtom,
filterTimeAtom,
sortAscendingAtom,
sortMethodAtom,
TokenSortMethod,
} from 'components/Tokens/state'
import {
isPricePoint,
PollingInterval,
PricePoint,
supportedChainIdFromGQLChain,
toHistoryDuration,
unwrapToken,
usePollQueryWhileMounted,
} from 'graphql/data/util'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { useAtomValue } from 'jotai/utils'
import { useMemo } from 'react'
import {
Chain,
TopTokens100Query,
useTopTokens100Query,
useTopTokensSparklineQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
const TokenSortMethods = {
[TokenSortMethod.PRICE]: (a: TopToken, b: TopToken) =>
(b?.market?.price?.value ?? 0) - (a?.market?.price?.value ?? 0),
[TokenSortMethod.DAY_CHANGE]: (a: TopToken, b: TopToken) =>
(b?.market?.pricePercentChange1Day?.value ?? 0) - (a?.market?.pricePercentChange1Day?.value ?? 0),
[TokenSortMethod.HOUR_CHANGE]: (a: TopToken, b: TopToken) =>
(b?.market?.pricePercentChange1Hour?.value ?? 0) - (a?.market?.pricePercentChange1Hour?.value ?? 0),
[TokenSortMethod.VOLUME]: (a: TopToken, b: TopToken) =>
(b?.market?.volume?.value ?? 0) - (a?.market?.volume?.value ?? 0),
[TokenSortMethod.FULLY_DILUTED_VALUATION]: (a: TopToken, b: TopToken) =>
(b?.project?.markets?.[0]?.fullyDilutedValuation?.value ?? 0) -
(a?.project?.markets?.[0]?.fullyDilutedValuation?.value ?? 0),
}
function useSortedTokens(tokens: TopTokens100Query['topTokens']) {
const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
return useMemo(() => {
if (!tokens) {
return undefined
}
const tokenArray = Array.from(tokens).sort(TokenSortMethods[sortMethod])
return sortAscending ? tokenArray.reverse() : tokenArray
}, [tokens, sortMethod, sortAscending])
}
function useFilteredTokens(tokens: TopTokens100Query['topTokens']) {
const filterString = useAtomValue(exploreSearchStringAtom)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
return useMemo(() => {
if (!tokens) {
return undefined
}
let returnTokens = tokens
if (lowercaseFilterString) {
returnTokens = returnTokens?.filter((token) => {
const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString)
const projectNameIncludesFilterString = token?.project?.name?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString)
return (
projectNameIncludesFilterString ||
nameIncludesFilterString ||
symbolIncludesFilterString ||
addressIncludesFilterString
)
})
}
return returnTokens
}, [tokens, lowercaseFilterString])
}
export type SparklineMap = { [key: string]: PricePoint[] | undefined }
export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[number]
interface UseTopTokensReturnValue {
tokens?: readonly TopToken[]
tokenSortRank: Record<string, number>
loadingTokens: boolean
sparklines: SparklineMap
error?: ApolloError
}
export function useTopTokens(chain: Chain, skip?: boolean): UseTopTokensReturnValue {
const chainId = supportedChainIdFromGQLChain(chain)
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const isWindowVisible = useIsWindowVisible()
const { data: sparklineQuery } = usePollQueryWhileMounted(
useTopTokensSparklineQuery({
variables: { duration, chain },
skip: !isWindowVisible || skip,
}),
PollingInterval.Slow,
)
const sparklines = useMemo(() => {
const unwrappedTokens = chainId && sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
const map: SparklineMap = {}
unwrappedTokens?.forEach((current) => {
if (current?.address !== undefined) {
map[current.address] = current?.market?.priceHistory?.filter(isPricePoint) as PricePoint[]
}
})
return map
}, [chainId, sparklineQuery?.topTokens])
const {
data,
loading: loadingTokens,
error,
} = usePollQueryWhileMounted(
useTopTokens100Query({
variables: { duration, chain },
skip: !isWindowVisible || skip,
}),
PollingInterval.Fast,
)
const unwrappedTokens = useMemo(
() => chainId && data?.topTokens?.map((token) => unwrapToken(chainId, token)),
[chainId, data],
)
const sortedTokens = useSortedTokens(unwrappedTokens)
const tokenSortRank = useMemo(
() =>
sortedTokens?.reduce((acc, cur, i) => {
if (!cur?.address) {
return acc
}
return {
...acc,
[cur.address]: i + 1,
}
}, {}) ?? {},
[sortedTokens],
)
const filteredTokens = useFilteredTokens(sortedTokens)
return useMemo(
() => ({ tokens: filteredTokens, tokenSortRank, loadingTokens, sparklines, error }),
[filteredTokens, tokenSortRank, loadingTokens, sparklines, error],
)
}
import { fireEvent, screen, waitFor } from '@testing-library/react' import { fireEvent, screen } from '@testing-library/react'
import { PrefetchBalancesWrapper, useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' import { PrefetchBalancesWrapper, useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider'
import { useAccount } from 'hooks/useAccount' import { useAccount } from 'hooks/useAccount'
import { mocked } from 'test-utils/mocked' import { mocked } from 'test-utils/mocked'
...@@ -7,8 +7,6 @@ import { useOnAssetActivitySubscription } from 'uniswap/src/data/graphql/uniswap ...@@ -7,8 +7,6 @@ import { useOnAssetActivitySubscription } from 'uniswap/src/data/graphql/uniswap
import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
// TODO(WEB-5370): Remove this delay + waitFor once we've integrated wallet's refetch logic
jest.setTimeout(10000)
const mockLazyFetch = jest.fn() const mockLazyFetch = jest.fn()
const mockBalanceQueryResponse = [ const mockBalanceQueryResponse = [
mockLazyFetch, mockLazyFetch,
...@@ -47,17 +45,17 @@ describe('TokenBalancesProvider', () => { ...@@ -47,17 +45,17 @@ describe('TokenBalancesProvider', () => {
mocked(useAccount).mockReturnValue({ address: '0xaddress1', chainId: 1 } as any) mocked(useAccount).mockReturnValue({ address: '0xaddress1', chainId: 1 } as any)
}) })
it('TokenBalancesProvider should not fetch balances without calls to useOnAssetActivitySubscription', async () => { it('TokenBalancesProvider should not fetch balances without calls to useOnAssetActivitySubscription', () => {
render(<div />) render(<div />)
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(0), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(0)
}) })
describe('useTokenBalancesQuery', () => { describe('useTokenBalancesQuery', () => {
it('should only refetch balances when stale', async () => { it('should only refetch balances when stale', () => {
const { rerender, unmount } = renderHook(() => useTokenBalancesQuery()) const { rerender, unmount } = renderHook(() => useTokenBalancesQuery())
// Rendering useTokenBalancesQuery should trigger a fetch // Rendering useTokenBalancesQuery should trigger a fetch
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(1)
// Rerender to clear staleness // Rerender to clear staleness
rerender() rerender()
...@@ -65,31 +63,31 @@ describe('TokenBalancesProvider', () => { ...@@ -65,31 +63,31 @@ describe('TokenBalancesProvider', () => {
// Receiving a new value from subscription should trigger a fetch while useTokenBalancesQuery hooks are mounted // Receiving a new value from subscription should trigger a fetch while useTokenBalancesQuery hooks are mounted
triggerSubscriptionUpdate() triggerSubscriptionUpdate()
rerender() rerender()
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(2)
// Unmounting the hooks should not trigger any fetches // Unmounting the hooks should not trigger any fetches
unmount() unmount()
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(2)
// Receiving a new value from subscription should NOT trigger a fetch if no useTokenBalancesQuery hooks are mounted // Receiving a new value from subscription should NOT trigger a fetch if no useTokenBalancesQuery hooks are mounted
triggerSubscriptionUpdate() triggerSubscriptionUpdate()
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(2)
}) })
it('should use cached balances across multiple hook calls', async () => { it('should use cached balances across multiple hook calls', () => {
renderHook(() => ({ renderHook(() => ({
hook1: useTokenBalancesQuery(), hook1: useTokenBalancesQuery(),
hook2: useTokenBalancesQuery(), hook2: useTokenBalancesQuery(),
})) }))
// Rendering useTokenBalancesQuery twice should only trigger one fetch // Rendering useTokenBalancesQuery twice should only trigger one fetch
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(1)
}) })
it('should refetch when account changes', async () => { it('should refetch when account changes', () => {
const { rerender } = renderHook(() => useTokenBalancesQuery()) const { rerender } = renderHook(() => useTokenBalancesQuery())
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(1)
// Rerender to clear staleness // Rerender to clear staleness
rerender() rerender()
...@@ -98,12 +96,12 @@ describe('TokenBalancesProvider', () => { ...@@ -98,12 +96,12 @@ describe('TokenBalancesProvider', () => {
mocked(useAccount).mockReturnValue({ address: '0xaddress2', chainId: 1 } as any) mocked(useAccount).mockReturnValue({ address: '0xaddress2', chainId: 1 } as any)
rerender() rerender()
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(2)
}) })
}) })
describe('PrefetchBalancesWrapper', () => { describe('PrefetchBalancesWrapper', () => {
it('should fetch balances when a PrefetchBalancesWrapper is hovered', async () => { it('should fetch balances when a PrefetchBalancesWrapper is hovered', () => {
const { rerender } = render( const { rerender } = render(
<PrefetchBalancesWrapper> <PrefetchBalancesWrapper>
<div>hi</div> <div>hi</div>
...@@ -119,17 +117,17 @@ describe('TokenBalancesProvider', () => { ...@@ -119,17 +117,17 @@ describe('TokenBalancesProvider', () => {
) )
// Should not fetch balances before hover // Should not fetch balances before hover
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(0), { timeout: 3500 }) expect(mockLazyFetch).toHaveBeenCalledTimes(0)
// Hovering component should trigger a fetch // Hovering component should trigger a fetch
fireEvent.mouseEnter(wrappedComponent) fireEvent.mouseEnter(wrappedComponent)
fireEvent.mouseLeave(wrappedComponent) fireEvent.mouseLeave(wrappedComponent)
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 }) expect(mockLazyFetch).toHaveBeenCalledTimes(1)
// Subsequent hover should not trigger a fetch // Subsequent hover should not trigger a fetch
fireEvent.mouseEnter(wrappedComponent) fireEvent.mouseEnter(wrappedComponent)
fireEvent.mouseLeave(wrappedComponent) fireEvent.mouseLeave(wrappedComponent)
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 }) expect(mockLazyFetch).toHaveBeenCalledTimes(1)
// Subsequent hover should trigger a fetch if the subscription has updated // Subsequent hover should trigger a fetch if the subscription has updated
triggerSubscriptionUpdate() triggerSubscriptionUpdate()
...@@ -138,10 +136,10 @@ describe('TokenBalancesProvider', () => { ...@@ -138,10 +136,10 @@ describe('TokenBalancesProvider', () => {
<div>hi</div> <div>hi</div>
</PrefetchBalancesWrapper>, </PrefetchBalancesWrapper>,
) )
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 }) expect(mockLazyFetch).toHaveBeenCalledTimes(1)
fireEvent.mouseEnter(wrappedComponent) fireEvent.mouseEnter(wrappedComponent)
fireEvent.mouseLeave(wrappedComponent) fireEvent.mouseLeave(wrappedComponent)
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 4000 }) expect(mockLazyFetch).toHaveBeenCalledTimes(2)
}) })
}) })
}) })
...@@ -102,37 +102,22 @@ export function TokenBalancesProvider({ children }: PropsWithChildren) { ...@@ -102,37 +102,22 @@ export function TokenBalancesProvider({ children }: PropsWithChildren) {
if (!account.address) { if (!account.address) {
return return
} }
// adds a 3 second delay to account for dependency latency after an account update lazyFetch({
// TODO(WEB-5370): Remove this delay once we've integrated wallet's refetch logic variables: {
setTimeout( ownerAddress: account.address,
() => { chains: gqlChains,
account.address && valueModifiers: [
lazyFetch({ {
variables: { ownerAddress: account.address,
ownerAddress: account.address, includeSpamTokens: valueModifiers.includeSpamTokens,
chains: gqlChains, includeSmallBalances: valueModifiers.includeSmallBalances,
valueModifiers: [ tokenExcludeOverrides: [],
{ tokenIncludeOverrides: [],
ownerAddress: account.address, },
includeSpamTokens: valueModifiers.includeSpamTokens, ],
includeSmallBalances: valueModifiers.includeSmallBalances,
tokenExcludeOverrides: [],
tokenIncludeOverrides: [],
},
],
},
})
}, },
hasAccountUpdate ? 3000 : 0, })
) }, [account.address, lazyFetch, valueModifiers, gqlChains])
}, [
account.address,
hasAccountUpdate,
lazyFetch,
gqlChains,
valueModifiers.includeSpamTokens,
valueModifiers.includeSmallBalances,
])
return ( return (
<AdaptiveTokenBalancesProvider <AdaptiveTokenBalancesProvider
......
import { ApolloClient, FieldFunctionOptions, HttpLink, InMemoryCache } from '@apollo/client' import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { Reference, StoreObject, relayStylePagination } from '@apollo/client/utilities' import { Reference, relayStylePagination } from '@apollo/client/utilities'
import { createSubscriptionLink } from 'utilities/src/apollo/SubscriptionLink' import { createSubscriptionLink } from 'utilities/src/apollo/SubscriptionLink'
import { splitSubscription } from 'utilities/src/apollo/splitSubscription' import { splitSubscription } from 'utilities/src/apollo/splitSubscription'
...@@ -47,18 +47,6 @@ export const apolloClient = new ApolloClient({ ...@@ -47,18 +47,6 @@ export const apolloClient = new ApolloClient({
return address?.toLowerCase() ?? null return address?.toLowerCase() ?? null
}, },
}, },
feeData: {
// TODO(API-482): remove this once the backend bug is fixed.
// There's a bug in our graphql backend where `feeData` can incorrectly be `null` for certain queries (`topTokens`).
// This field policy ensures that the cache doesn't get overwritten with `null` values triggering unnecessary re-renders.
merge: ignoreIncomingNullValue,
},
protectionInfo: {
// TODO(API-482): remove this once the backend bug is fixed.
// There's a bug in our graphql backend where `protectionInfo` can incorrectly be `null` for certain queries (`topTokens`).
// This field policy ensures that the cache doesn't get overwritten with `null` values triggering unnecessary re-renders.
merge: ignoreIncomingNullValue,
},
}, },
}, },
TokenProject: { TokenProject: {
...@@ -97,14 +85,3 @@ export const apolloClient = new ApolloClient({ ...@@ -97,14 +85,3 @@ export const apolloClient = new ApolloClient({
// This is done after creating the client so that client may be passed to `createSubscriptionLink`. // This is done after creating the client so that client may be passed to `createSubscriptionLink`.
const subscriptionLink = createSubscriptionLink({ uri: REALTIME_URL, token: REALTIME_TOKEN }, apolloClient) const subscriptionLink = createSubscriptionLink({ uri: REALTIME_URL, token: REALTIME_TOKEN }, apolloClient)
apolloClient.setLink(splitSubscription(subscriptionLink, httpLink)) apolloClient.setLink(splitSubscription(subscriptionLink, httpLink))
function ignoreIncomingNullValue(
existing: Reference | StoreObject,
incoming: Reference | StoreObject,
{ mergeObjects }: FieldFunctionOptions<Record<string, unknown>, Record<string, unknown>>,
): Reference | StoreObject {
if (existing && !incoming) {
return existing
}
return mergeObjects(existing, incoming)
}
This diff is collapsed.
import { isSupportedChainId } from 'constants/chains' import { isSupportedChainId } from 'constants/chains'
import { PricePoint, fiatOnRampToCurrency, gqlToCurrency } from 'graphql/data/util' import { fiatOnRampToCurrency, gqlToCurrency } from 'graphql/data/util'
import { COMMON_BASES, buildPartialCurrencyInfo } from 'uniswap/src/constants/routing' import { COMMON_BASES, buildPartialCurrencyInfo } from 'uniswap/src/constants/routing'
import { USDC_OPTIMISM } from 'uniswap/src/constants/tokens' import { USDC_OPTIMISM } from 'uniswap/src/constants/tokens'
import { import {
Token as GqlToken, Token as GqlToken,
ProtectionResult, ProtectionResult,
SafetyLevel, SafetyLevel,
TopTokens100Query,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { CurrencyInfo, TokenList } from 'uniswap/src/features/dataApi/types' import { CurrencyInfo, TokenList } from 'uniswap/src/features/dataApi/types'
import { buildCurrencyInfo, getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils' import { buildCurrencyInfo, getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils'
...@@ -83,6 +82,3 @@ export function meldSupportedCurrencyToCurrencyInfo(forCurrency: FORSupportedTok ...@@ -83,6 +82,3 @@ export function meldSupportedCurrencyToCurrencyInfo(forCurrency: FORSupportedTok
isSpam: false, isSpam: false,
}) })
} }
export type SparklineMap = { [key: string]: PricePoint[] | undefined }
export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[number]
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment