ci(release): publish latest release

parent 35df6944
......@@ -51,6 +51,3 @@ packages/uniswap/src/i18n/locales/source/*_old.json
# Vercel
.vercel
# CodeTours Extension
.tours/*
* @uniswap/web-admins
IPFS hash of the deployment:
- CIDv0: `QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo`
- CIDv1: `bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni`
### Lots of new updates!
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.
**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.
### Claim usernames
IPFS gateways:
- https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.dweb.link/
- https://bafybeiawfdwfe6ayrmkbbgbjos2noxf2w7ihz4tk7x5tu67ybggnmuvrni.ipfs.cf-ipfs.com/
- [ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/](ipfs://QmPq9sW2ih541PM9991Trh3Cocrdstrh63m6UWRbZb6Nqo/)
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.
## 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
* **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
Users now have access to all ur regular features for this new chain.
### Other changes:
- Various bug fixes and performance improvements
\ No newline at end of file
web/5.56.0
\ No newline at end of file
extension/1.8.0
\ No newline at end of file
......@@ -15,7 +15,7 @@
"@tamagui/core": "1.108.4",
"@types/uuid": "9.0.1",
"@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/v3-sdk": "3.18.1",
"@uniswap/v4-sdk": "1.10.3",
......
......@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal'
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 { iconSizes } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
......@@ -17,8 +17,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { NumberType } from 'utilities/src/format/types'
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 { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
import { DisplayNameType } from 'wallet/src/features/wallet/types'
......
......@@ -14,7 +14,7 @@ import { PopupName, openPopup } from 'src/app/features/popups/slice'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
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 { spacing } from 'ui/src/theme'
import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal'
......@@ -31,8 +31,6 @@ import { logger } from 'utilities/src/logger/logger'
import { sleep } from 'utilities/src/time/timing'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
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 { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types'
......
......@@ -37,7 +37,7 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps
const [inputText, setInputText] = useState<string>(defaultText)
const [isfocused, setIsFocused] = useState(false)
const { canClaimUnitag } = useCanActiveAddressClaimUnitag()
const { canClaimUnitag } = useCanActiveAddressClaimUnitag(address)
const unitagsClaimEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag)
const onConfirm = useCallback(async () => {
......
......@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useInterfaceBuyNavigator } from 'src/app/features/for/utils'
import { AppRoutes } from 'src/app/navigation/constants'
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 { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal'
......@@ -15,7 +15,6 @@ import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InformationBanner } from 'wallet/src/components/banners/InformationBanner'
import { ContextMenu } from 'wallet/src/components/menu/ContextMenu'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow'
......@@ -212,12 +211,7 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item:
return (
<TokenContextMenu portfolioBalance={portfolioBalance}>
<TokenBalanceItem
isLoading={isWarmLoading}
portfolioBalanceId={portfolioBalance.id}
currencyInfo={portfolioBalance.currencyInfo}
onPressToken={onPressToken}
/>
<TokenBalanceItem isLoading={isWarmLoading} portfolioBalance={portfolioBalance} onPressToken={onPressToken} />
</TokenContextMenu>
)
})
......
......@@ -50,7 +50,7 @@ export function ClaimUnitagScreen(): JSX.Element {
onBack={handleBack}
onSkip={goToNextStep}
>
<Flex gap="$spacing16" py="$spacing24" width="100%">
<Flex gap="$spacing16" pt="$spacing24" width="100%">
<ClaimUnitagContent
animateY={false}
entryPoint={ExtensionOnboardingFlow.New}
......
......@@ -35,7 +35,7 @@ export function RecipientPanel({ chainId }: RecipientPanelProps): JSX.Element {
onSetShowRecipientSelector(!showRecipientSelector)
}, [onSetShowRecipientSelector, showRecipientSelector])
const { sections } = useFilteredRecipientSections(pattern)
const sections = useFilteredRecipientSections(pattern)
const onSelectRecipient = useCallback((newRecipient: string) => {
setSelectedRecipient(newRecipient)
......
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
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 { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants'
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'
const PowerCircle = (): JSX.Element => (
......
......@@ -37,7 +37,7 @@ export function SettingsRecoveryPhrase({
</Flex>
</Flex>
<Flex grow>{children}</Flex>
<Flex mt="$spacing12">
<Flex>
<Button
disabled={!nextButtonEnabled}
flexGrow={1}
......
......@@ -6,14 +6,12 @@ import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state'
import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels'
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 { iconSizes } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks'
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 { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal'
import { EditUnitagProfileContent } from 'wallet/src/features/unitags/EditUnitagProfileContent'
......
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.9.0",
"version": "1.8.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
This diff is collapsed.
......@@ -90,9 +90,9 @@ if (isCI && datadogPropertiesAvailable && !isDetox) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.39"
def betaVersionName = "1.39"
def prodVersionName = "1.39"
def devVersionName = "1.38"
def betaVersionName = "1.38"
def prodVersionName = "1.38"
android {
ndkVersion rootProject.ext.ndkVersion
......
......@@ -9,7 +9,6 @@
<string>applinks:uniswap.org</string>
<string>applinks:app.uniswap.org</string>
<string>applinks:app.corn-staging.com</string>
<string>webcredentials:app.uniswap.org</string>
</array>
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
......
......@@ -90,7 +90,7 @@
"@uniswap/analytics-events": "2.38.0",
"@uniswap/client-explore": "0.0.10",
"@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/react-native-compat": "2.17.1",
"@walletconnect/utils": "2.17.1",
......
......@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated'
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 { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { iconSizes } from 'ui/src/theme'
......@@ -51,7 +51,7 @@ export function _RecipientSelect({
const [showQRScanner, setShowQRScanner] = useState(false)
const [checkSpeedBumps, setCheckSpeedBumps] = useState(false)
const [selectedRecipient, setSelectedRecipient] = useState(recipient)
const { sections, loading } = useFilteredRecipientSections(pattern)
const sections = useFilteredRecipientSections(pattern)
useEffect(() => {
if (focusInput) {
......@@ -104,9 +104,7 @@ export function _RecipientSelect({
onBack={recipient ? onHideRecipientSelector : undefined}
onChangeText={setPattern}
/>
{loading ? (
<Loader.SearchResult />
) : !sections.length ? (
{!sections.length ? (
<Flex centered gap="$spacing12" mt="$spacing24" px="$spacing24">
<Text variant="buttonLabel2">{t('send.recipient.results.empty')}</Text>
<Text color="$neutral3" textAlign="center" variant="body1">
......@@ -114,6 +112,7 @@ export function _RecipientSelect({
</Text>
</Flex>
) : (
// Show either suggested recipients or filtered sections based on query
<RecipientList renderedInModal={renderedInModal} sections={sections} onPress={onSelect} />
)}
</AnimatedFlex>
......
......@@ -109,8 +109,8 @@ export function SettingsRow({
) : screen || modal ? (
<Flex centered row>
{currentSetting ? (
<Flex shrink alignItems="flex-end" flexBasis="35%" justifyContent="flex-end">
<Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={2} variant="body3">
<Flex row shrink alignItems="flex-end" flexBasis="30%" justifyContent="flex-end">
<Text adjustsFontSizeToFit color="$neutral2" mr="$spacing8" numberOfLines={1} variant="body3">
{currentSetting}
</Text>
</Flex>
......
import React, { PropsWithChildren, memo, useMemo } from 'react'
import React, { memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ContextMenu from 'react-native-context-menu-view'
import { borderRadii } from 'ui/src/theme'
......@@ -6,12 +6,13 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generat
import { PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu'
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItemContextMenu({
export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({
portfolioBalance,
children,
}: PropsWithChildren<{
}: {
portfolioBalance: PortfolioBalance
}>) {
children: React.ReactNode
}) {
const { t } = useTranslation()
const { menuActions, onContextMenuPress } = useTokenContextMenu({
......
......@@ -7,8 +7,6 @@ import {
TokenDetailsScreenQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
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'
export interface TokenDetailsHeaderProps {
......@@ -22,12 +20,9 @@ export function TokenDetailsHeader({
loading = false,
onPressWarningIcon,
}: TokenDetailsHeaderProps): JSX.Element {
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const token = data?.token
const tokenProject = token?.project
const shouldShowWarningIcon =
!tokenProtectionEnabled &&
(tokenProject?.safetyLevel === SafetyLevel.StrongWarning || tokenProject?.safetyLevel === SafetyLevel.Blocked)
return (
<Flex gap="$spacing12" mx="$spacing16">
<TokenLogo
......@@ -47,7 +42,9 @@ export function TokenDetailsHeader({
>
{token?.name ?? ''}
</Text>
{shouldShowWarningIcon && (
{/* Suppress warning icon on low warning level */}
{(tokenProject?.safetyLevel === SafetyLevel.StrongWarning ||
tokenProject?.safetyLevel === SafetyLevel.Blocked) && (
<TouchableArea onPress={onPressWarningIcon}>
<WarningIcon safetyLevel={tokenProject?.safetyLevel} size="$icon.20" strokeColorOverride="$neutral3" />
</TouchableArea>
......
......@@ -48,7 +48,7 @@ function TokenOptionItemWrapper({
[currencyInfo, balanceUSD, quantity, isUnsupported],
)
const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency])
const { tokenWarningDismissed } = useDismissedTokenWarnings(currencyInfo?.currency)
const { tokenWarningDismissed, onDismissTokenWarning } = useDismissedTokenWarnings(currencyInfo?.currency)
const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext()
if (!option) {
......@@ -62,6 +62,7 @@ function TokenOptionItemWrapper({
return (
<TokenOptionItem
balance={convertFiatAmountFormatted(option.balanceUSD, NumberType.FiatTokenPrice)}
dismissWarningCallback={onDismissTokenWarning}
isSelected={isSelected}
option={option}
quantity={option.quantity}
......@@ -148,10 +149,6 @@ function _TokenFiatOnRampList({
return <></>
}
if (section.data.length === 0) {
return <></>
}
return (
<Flex mt="$spacing12">
<ListSeparatorToggle
......
import React, { memo, useCallback } from 'react'
import { ViewProps } from 'react-native'
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 { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks'
import RemoveButton from 'src/components/explore/RemoveButton'
......@@ -125,6 +125,7 @@ function FavoriteTokenCard({
borderColor={opacify(0.05, colors.surface3.val)}
borderRadius="$rounded16"
borderWidth={isDarkMode ? '$none' : '$spacing1'}
entering={FadeIn}
hapticFeedback={!isEditing}
hapticStyle={ImpactFeedbackStyle.Light}
m="$spacing4"
......
......@@ -206,7 +206,7 @@ describe(useExploreTokenContextMenu, () => {
payload: {
name: 'swap-modal',
initialState: {
exactAmountToken: '',
exactAmountToken: '0',
exactCurrencyField: 'input',
[CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: {
......
......@@ -59,7 +59,7 @@ export function useExploreTokenContextMenu({
const onPressSwap = useCallback(() => {
const swapFormState: TransactionState = {
exactCurrencyField: CurrencyField.INPUT,
exactAmountToken: '',
exactAmountToken: '0',
[CurrencyField.INPUT]: null,
[CurrencyField.OUTPUT]: {
chainId,
......
......@@ -4,11 +4,9 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem'
import { getSearchResultId } from 'src/components/explore/search/utils'
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 { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings'
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 { UniverseChainId } from 'uniswap/src/types/chains'
import { RankingType } from 'wallet/src/features/wallet/types'
......@@ -34,14 +32,7 @@ function tokenStatsToTokenSearchResult(token: Maybe<TokenRankingsStat>): TokenSe
name,
symbol,
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: SafetyLevel.Verified,
safetyInfo: {
tokenList: TokenList.Default,
attackType: undefined,
protectionResult: ProtectionResult.Benign,
},
feeData: null,
safetyLevel: null,
}
}
......
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 { SearchHeader } from 'src/components/explore/search/types'
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'
function SectionLoader({ searchHeader, repeat = 1 }: { searchHeader: SearchHeader; repeat?: number }): JSX.Element {
return (
<Flex gap="$spacing12">
<SectionHeaderText icon={searchHeader.icon} title={searchHeader.title} />
<Flex mx="$spacing24">
<Loader.SearchResult repeat={repeat} />
</Flex>
</Flex>
)
}
export const SearchResultsLoader = ({ selectedChain }: { selectedChain: UniverseChainId | null }): JSX.Element => {
const { t } = useTranslation()
/**
* 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
const hideNftLoading = selectedChain !== null && selectedChain !== UniverseChainId.Mainnet
return (
<Flex gap="$spacing16">
<SectionLoader searchHeader={TokenHeaderItem} repeat={2} />
<SectionLoader searchHeader={WalletHeaderItem} />
{!hideNftLoading && <SectionLoader searchHeader={NFTHeaderItem} repeat={2} />}
<Flex gap="$spacing12">
<SectionHeaderText
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>
)
}
......@@ -4,13 +4,7 @@ import { FlatList, ListRenderItemInfo } from 'react-native'
import { FadeIn, FadeOut } from 'react-native-reanimated'
import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader'
import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader'
import {
EtherscanHeaderItem,
NFTHeaderItem,
SEARCH_RESULT_HEADER_KEY,
TokenHeaderItem,
WalletHeaderItem,
} from 'src/components/explore/search/constants'
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
import { useWalletSearchResults } from 'src/components/explore/search/hooks'
import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem'
import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem'
......@@ -25,8 +19,10 @@ import {
getSearchResultId,
} from 'src/components/explore/search/utils'
import { Flex, Text } from 'ui/src'
import { Coin, Gallery, Person } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
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 { SearchContext } from 'uniswap/src/features/search/SearchContext'
import {
......@@ -35,10 +31,36 @@ import {
TokenSearchResult,
} from 'uniswap/src/features/search/SearchResult'
import { useEnabledChains } from 'uniswap/src/features/settings/hooks'
import i18n from 'uniswap/src/i18n/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { getValidAddress } from 'uniswap/src/utils/addresses'
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']
export function SearchResultsSection({
......
import { SearchHeader, SearchHeaderKey } from 'src/components/explore/search/types'
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,
}),
})
export const SEARCH_RESULT_HEADER_KEY = 'header'
......@@ -7,7 +7,7 @@ import { disableOnPress } from 'src/utils/disableOnPress'
import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src'
import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo'
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 { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult'
import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
......@@ -28,12 +28,11 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
const dispatch = useDispatch()
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 currencyInfo = useCurrencyInfo(currencyId)
const severity = getTokenWarningSeverity(currencyInfo)
// in mobile search, we only show the warning icon if token is >=Medium severity
const { colorSecondary: warningIconColor } = getWarningIconColors(severity)
const warningIconColor = getWarningIconColorOverride(severity)
const onPress = (): void => {
tokenDetailsNavigation.preload(currencyId)
......@@ -61,7 +60,6 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps):
logoUrl,
safetyLevel,
safetyInfo,
feeData,
},
}),
)
......
import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants'
import { SearchResult } from 'uniswap/src/features/search/SearchResult'
export type SearchHeaderKey = 'header'
export type SearchHeader = { type: SearchHeaderKey; title: string; icon?: JSX.Element }
export type SearchResultOrHeader = SearchResult | SearchHeader
// Header type used to render header text instead of SearchResult item
export type SearchResultOrHeader =
| SearchResult
| { type: typeof SEARCH_RESULT_HEADER_KEY; title: string; icon?: JSX.Element }
......@@ -6,7 +6,6 @@ import {
} from 'src/components/explore/search/utils'
import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
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 { amount, ethToken, nftCollection, nftContract, token, tokenMarket } from 'uniswap/src/test/fixtures'
import { createArray } from 'uniswap/src/test/utils'
......@@ -80,10 +79,6 @@ describe(formatTokenSearchResults, () => {
expect(result?.[0]?.symbol).toEqual(searchToken.symbol)
expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl)
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, () => {
......
......@@ -33,7 +33,7 @@ export function formatTokenSearchResults(
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 shoulderFilterByChain = !!selectedChain
......@@ -55,7 +55,6 @@ export function formatTokenSearchResults(
logoUrl: logoUrl ?? null,
volume1D: market?.volume?.value ?? 0,
safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo),
feeData: feeData ?? null,
}
// 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
const listener = Linking.addEventListener('url', (event) => {
if (event.url.startsWith('uniswap://openai')) {
const capturedPhrase = decodeURI(event.url.split('uniswap://openai?capturedPhrase=')[1] ?? '')
capturedPhrase &&
sendMessage(capturedPhrase).catch((e) =>
logger.error(e, { tags: { file: 'OpenAIContext', function: 'siriListener' } }),
)
capturedPhrase && sendMessage(capturedPhrase).catch(console.error)
}
})
return listener.remove
......
......@@ -2,7 +2,7 @@
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { AlertCircle } from 'ui/src/components/icons'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
......@@ -16,7 +16,6 @@ import { MAX_FIAT_INPUT_DECIMALS } from 'uniswap/src/constants/transactions'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import {
DecimalPadCalculateSpace,
DecimalPadCalculatedSpaceId,
DecimalPadInput,
DecimalPadInputRef,
} from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput'
......@@ -43,6 +42,7 @@ const TRANSFER_DIRECTION_BUTTON_BORDER_WIDTH = spacing.spacing4
export function SendTokenForm(): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const isShortMobileDevice = useIsShortMobileDevice()
const { fullHeight } = useDeviceDimensions()
const { walletNeedsRestore, openWalletRestoreModal } = useTransactionModalContext()
......@@ -220,6 +220,8 @@ export function SendTokenForm(): JSX.Element {
maxDecimals,
})
console.log('truncatedValue in decimal set value', truncatedValue)
if (isFiatInput) {
exactAmountFiatRef.current = truncatedValue
} else {
......@@ -380,11 +382,9 @@ export function SendTokenForm(): JSX.Element {
) : null}
</Flex>
</Flex>
{!nftIn && (
<>
<DecimalPadCalculateSpace id={DecimalPadCalculatedSpaceId.Send} decimalPadRef={decimalPadRef} />
<DecimalPadCalculateSpace decimalPadRef={decimalPadRef} isShortMobileDevice={isShortMobileDevice} />
<Flex
animation="quick"
bottom={0}
......
......@@ -54,7 +54,6 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { FORAmountEnteredProperties } from 'uniswap/src/features/telemetry/types'
import {
DecimalPadCalculateSpace,
DecimalPadCalculatedSpaceId,
DecimalPadInput,
DecimalPadInputRef,
} from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput'
......@@ -330,8 +329,6 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
setValue('')
setAmount(0)
valueRef.current = ''
resetSelection({ start: 0 })
setQuoteCurrency(defaultCurrency)
sendAnalyticsEvent(FiatOffRampEventName.FORBuySellToggled, {
......@@ -402,9 +399,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
setShowTokenSelector(true)
}}
/>
<DecimalPadCalculateSpace id={DecimalPadCalculatedSpaceId.FiatOnRamp} decimalPadRef={decimalPadRef} />
<DecimalPadCalculateSpace decimalPadRef={decimalPadRef} isShortMobileDevice={isShortMobileDevice} />
<AnimatedFlex
bottom={0}
exiting={FadeOutDown}
......
......@@ -65,7 +65,7 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop
<Text adjustsFontSizeToFit variant="subheading1">
{sanitizeAddressText(shortenAddress(mnemonicId))}
</Text>
<Text adjustsFontSizeToFit color="$neutral2" variant="body3">
<Text adjustsFontSizeToFit color="$neutral2" variant="buttonLabel2">
{localizedDayjs.unix(createdAt).format(FORMAT_DATE_TIME_SHORT)}
</Text>
</Flex>
......
......@@ -16,6 +16,7 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import i18n from 'uniswap/src/i18n/i18n'
import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
import { isIOS } from 'utilities/src/platform'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks'
import { openSettings } from 'wallet/src/utils/linking'
......@@ -73,7 +74,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop
title={t('onboarding.notification.title')}
onSkip={navigateToNextScreen}
>
<Flex centered shrink>
<Flex centered shrink py={isIOS ? '$spacing60' : '$spacing16'}>
<NotificationsBackgroundImage />
</Flex>
<Trace logPress element={ElementName.Enable}>
......
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BlurView } from 'expo-blur'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ActivityIndicator, Alert, Image, Platform, StyleSheet } from 'react-native'
......@@ -17,7 +18,10 @@ import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks'
import { Button, Flex, useIsDarkMode, useSporeColors } from 'ui/src'
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 { borderRadii, imageSizes, opacify } from 'ui/src/theme'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ImportType } from 'uniswap/src/types/onboarding'
......@@ -31,6 +35,7 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element {
const { t } = useTranslation()
const colors = useSporeColors()
const dispatch = useDispatch()
const isDarkMode = useIsDarkMode()
const [isLoadingAccount, setIsLoadingAccount] = useState(false)
const [showWarningModal, setShowWarningModal] = useState(false)
......@@ -114,10 +119,27 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element {
title={t('onboarding.security.title')}
onSkip={onSkipPressed}
>
<Flex centered shrink gap="$spacing16">
<Flex>
<Flex centered shrink gap="$spacing16" my="$spacing12" position="relative" py="$spacing24">
<Flex pt="$spacing24">
<SecurityBackgroundImage />
</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>
<Trace logPress element={ElementName.Enable}>
<Button theme="primary" onPress={onPressEnableSecurity}>
......@@ -147,6 +169,10 @@ const SecurityBackgroundImage = (): JSX.Element => {
}
const styles = StyleSheet.create({
blurView: {
...StyleSheet.absoluteFillObject,
borderRadius: borderRadii.rounded16,
},
image: {
height: '100%',
},
......
......@@ -200,7 +200,7 @@ function TokenDetails({
const [showWarningModal, setShowWarningModal] = useState(false)
const [showBuyNativeTokenModal, setShowBuyNativeTokenModal] = useState(false)
const { tokenWarningDismissed } = useDismissedTokenWarnings(
const { tokenWarningDismissed, onDismissTokenWarning } = useDismissedTokenWarnings(
isNativeCurrency ? undefined : { chainId: currencyChainId, address: currencyAddress },
)
......@@ -294,11 +294,12 @@ function TokenDetails({
])
const onAcceptWarning = useCallback(() => {
onDismissTokenWarning()
setShowWarningModal(false)
if (activeTransactionType !== undefined) {
navigateToSwapFlow({ currencyField: activeTransactionType, currencyAddress, currencyChainId })
}
}, [activeTransactionType, currencyAddress, currencyChainId, navigateToSwapFlow])
}, [activeTransactionType, currencyAddress, currencyChainId, onDismissTokenWarning, navigateToSwapFlow])
const openTokenWarningModal = (): void => {
setShowWarningModal(true)
......@@ -344,7 +345,9 @@ function TokenDetails({
</AnimatedFlex>
) : null}
<Flex gap="$spacing16" mb="$spacing8" px="$spacing16">
{tokenProtectionEnabled && <TokenWarningCard currencyInfo={currencyInfo} onPress={openTokenWarningModal} />}
{tokenProtectionEnabled && (
<TokenWarningCard currencyInfo={currencyInfo} onPressCtaButton={openTokenWarningModal} />
)}
{isChainEnabled && (
<TokenBalances
currentChainBalance={currentChainBalance}
......
......@@ -21,15 +21,3 @@ describe('RedirectExplore', () => {
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 @@
"@uniswap/permit2-sdk": "1.3.0",
"@uniswap/redux-multicall": "1.1.8",
"@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/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/v2-core": "1.0.1",
"@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": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap.mobile",
"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",
"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"
]
"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", "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": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap.mobile.beta",
"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",
"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"
]
"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", "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": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap.mobile.dev",
"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",
"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"
]
"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", "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 @@
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</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>
......@@ -42,12 +42,5 @@
]
}
]
},
"webcredentials": {
"apps": [
"JH3UHGZD75.com.uniswap.mobile",
"JH3UHGZD75.com.uniswap.mobile.beta",
"JH3UHGZD75.com.uniswap.mobile.dev"
]
}
}
......@@ -94,6 +94,7 @@
"https://wallet.crypto.com",
"https://web3.1inch.io",
"https://mainnet.era.zksync.io/",
"https://9bxqhlmige.execute-api.us-east-2.amazonaws.com",
"wss://*.uniswap.org",
"wss://relay.walletconnect.com",
"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'
import { ThemedText } from 'theme/components/text'
import { Flex, Text, styled } from 'ui/src'
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'
export type ChartHeaderProtocolInfo = { protocol: PriceSource; value?: number }
......@@ -20,7 +22,7 @@ const ProtocolLegendWrapper = styled(Flex, {
gap: '$gap12',
pointerEvents: 'none',
variants: {
hover: {
isMultichainExploreEnabled: {
true: {
right: 'unset',
p: '$spacing8',
......@@ -39,28 +41,49 @@ const ProtocolLegendWrapper = styled(Flex, {
function ProtocolLegend({ protocolData }: { protocolData?: ChartHeaderProtocolInfo[] }) {
const { formatFiatPrice } = useFormatter()
const theme = useTheme()
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
return (
<ProtocolLegendWrapper id={PROTOCOL_LEGEND_ELEMENT_ID} hover={true}>
<ProtocolLegendWrapper
id={isMultichainExploreEnabled ? PROTOCOL_LEGEND_ELEMENT_ID : undefined}
isMultichainExploreEnabled={isMultichainExploreEnabled}
>
{protocolData
?.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 (
!!display && (
<Flex row gap={8} justifyContent="flex-end" key={protocol + '_blip'} width="max-content">
<Text variant="body4" textAlign="right" color="$neutral2" lineHeight={12}>
{getProtocolName(protocol)}
</Text>
<Flex
row
gap={isMultichainExploreEnabled ? 8 : 6}
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
borderRadius="$rounded4"
width={12}
height={12}
backgroundColor={getProtocolColor(protocol, theme)}
/>
<Text variant="body4" textAlign="right" lineHeight={12} {...EllipsisTamaguiStyle}>
{display}
</Text>
{isMultichainExploreEnabled && (
<Text variant="body4" textAlign="right" lineHeight={12} {...EllipsisTamaguiStyle}>
{display}
</Text>
)}
</Flex>
)
)
......@@ -118,6 +141,7 @@ export function ChartHeader({
additionalFields,
}: ChartHeaderProps) {
const isHovered = !!time
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
return (
<Flex
row
......@@ -136,7 +160,7 @@ export function ChartHeader({
<HeaderTimeDisplay time={time} timePlaceholder={timePlaceholder} />
</Flex>
</Flex>
{isHovered && protocolData && <ProtocolLegend protocolData={protocolData} />}
{((isHovered && protocolData) || !isMultichainExploreEnabled) && <ProtocolLegend protocolData={protocolData} />}
</Flex>
)
}
......@@ -4,7 +4,7 @@ import { LoadingBubble } from 'components/Tokens/loading'
import { getChainFromChainUrlParam } from 'constants/chains'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
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 styled, { useTheme } from 'lib/styled-components'
import { memo } from 'react'
......
......@@ -87,11 +87,14 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements
const colorsCount = options.colors.length
const isHovered = options.hoveredLogicalIndex && options.hoveredLogicalIndex !== -1
const isMultichainExploreEnabled = !!options.gradients
areaPaths.forEach((areaPath, index) => {
// Modification: determine area fill opacity based on number of lines and hover state
if (areaPaths.length === 1) {
ctx.globalAlpha = 0.12 // single-line charts have low opacity fill
} else if (!isMultichainExploreEnabled) {
ctx.globalAlpha = isHovered ? 0.24 : 1
}
const gradient = options.gradients
......@@ -106,7 +109,7 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements
ctx.fill(areaPath)
})
ctx.lineWidth = options.lineWidth
ctx.lineWidth = options.lineWidth * (isMultichainExploreEnabled ? 1 : renderingScope.verticalPixelRatio)
ctx.lineJoin = 'round'
fullLinesMeshed.toReversed().forEach((linePath, index) => {
......@@ -114,7 +117,7 @@ export class StackedAreaSeriesRenderer<TData extends StackedAreaData> implements
const color = options.colors[unreversedIndex % colorsCount]
ctx.strokeStyle = 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
if (index !== fullLinesMeshed.length - 1) {
......
......@@ -9,6 +9,7 @@ export type CustomVolumeChartModelParams = {
colors: string[]
headerHeight: number
useThinCrosshair?: boolean
isMultichainExploreEnabled?: boolean
background?: string
}
......@@ -25,6 +26,7 @@ export class CustomVolumeChartModel<TDataType extends CustomHistogramData> exten
this.series = this.api.addCustomSeries(
new CustomHistogramSeries({
colors: params.colors,
isMultichainExploreEnabled: params.isMultichainExploreEnabled,
background: params.background,
}),
)
......
......@@ -23,11 +23,13 @@ export class CustomHistogramSeries<TData extends CustomHistogramData>
{
_renderer: CustomHistogramSeriesRenderer<TData>
_colors: string[]
_isMultichainExploreEnabled?: boolean
_background?: string
constructor(props: CustomHistogramProps) {
this._renderer = new CustomHistogramSeriesRenderer(props)
this._colors = props.colors
this._isMultichainExploreEnabled = props.isMultichainExploreEnabled
this._background = props.background
}
......
......@@ -57,6 +57,7 @@ function cumulativeBuildUp(data: StackedHistogramData): number[] {
export interface CustomHistogramProps {
colors: string[]
isMultichainExploreEnabled?: boolean
background?: string
}
......@@ -64,10 +65,12 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im
_data: PaneRendererCustomData<Time, TData> | null = null
_options: CustomHistogramSeriesOptions | null = null
_colors: string[]
_isMultichainExploreEnabled?: boolean
_background?: string
constructor(props: CustomHistogramProps) {
this._colors = props.colors
this._isMultichainExploreEnabled = props.isMultichainExploreEnabled
this._background = props.background
}
......@@ -131,11 +134,18 @@ export class CustomHistogramSeriesRenderer<TData extends CustomHistogramData> im
const totalBox = positionsBox(zeroY, stack.ys[stack.ys.length - 1], renderingScope.verticalPixelRatio)
ctx.beginPath()
const isMultichainExploreEnabled = this._isMultichainExploreEnabled
if (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()
// 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
const color = this._colors[this._colors.length - 1 - index] // color v2, then v3
const stackBoxPositions = positionsBox(previousY, y, renderingScope.verticalPixelRatio)
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)
if (isStackedHistogram && !isHovered) {
if (isStackedHistogram && isMultichainExploreEnabled && !isHovered) {
ctx.globalAlpha = 1
ctx.fillStyle = color
ctx.fillRect(column.left + margin, stackBoxPositions.position, width - margin, 2)
......
......@@ -242,13 +242,18 @@ export default function FeatureFlagModal() {
<FeatureFlagOption flag={FeatureFlags.V2Everywhere} label="Enable V2 Everywhere" />
<FeatureFlagOption flag={FeatureFlags.V4Everywhere} label="Enable V4 Everywhere" />
<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.NavigationHotkeys} label="Navigation hotkeys" />
<FeatureFlagOption flag={FeatureFlags.TokenProtection} label="Warning UX for scam/dangerous tokens" />
<FeatureFlagGroup name="New Chains">
<FeatureFlagOption flag={FeatureFlags.Zora} label="Enable Zora" />
<FeatureFlagOption flag={FeatureFlags.WorldChain} label="Enable World Chain" />
</FeatureFlagGroup>
<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">
<FeatureFlagOption flag={FeatureFlags.QuickRouteMainnet} label="Enable quick routes for Mainnet" />
<DynamicConfigDropdown
......
......@@ -10,12 +10,11 @@ import { useEffect, useState } from 'react'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import styled from 'styled-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 { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled'
import { Plus } from 'ui/src/components/icons/Plus'
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 { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useTranslation } from 'uniswap/src/i18n'
......@@ -79,17 +78,11 @@ export function FeeTierSearchModal() {
setCreateFeeValue((prev) => {
let newValue = parseFloat(prev)
if (autoDecrementing) {
if (!prev || prev === '') {
return '0'
}
newValue -= 0.01
if (newValue < 0) {
return '0'
}
} else if (autoIncrementing) {
if (!prev || prev === '') {
return '0.01'
}
newValue += 0.01
if (newValue > 100) {
return '100'
......@@ -255,54 +248,19 @@ export function FeeTierSearchModal() {
</Flex>
) : (
<>
<Flex
row
alignItems="center"
py="$padding12"
px="$padding8"
backgroundColor="$surface2"
borderRadius="$rounded24"
gap="$gap8"
>
<Flex row py="$padding12" px="$padding8" backgroundColor="$surface2" borderRadius="$rounded24">
<Search size={20} color="$neutral2" />
<AmountInput
<Input
width="100%"
autoFocus
alignSelf="stretch"
height="100%"
fontWeight="$book"
backgroundColor="$transparent"
borderRadius={0}
borderWidth={0}
textAlign="left"
value={searchValue}
fontFamily="$subHeading"
fontSize={18}
px="$none"
py="$none"
placeholder={t('fee.tier.search.short')}
placeholderTextColor="$neutral3"
onChangeText={(value) => {
if (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 : '')
placeholderTextColor="$neutral2"
onChange={(event: any) => {
setSearchValue(event.target.value)
}}
value={searchValue}
/>
</Flex>
<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
import { PositionStatus } from '@uniswap/client-pools/dist/pools/v1/types_pb'
import { LiquidityPositionFeeStats } from 'components/Liquidity/LiquidityPositionFeeStats'
import { LiquidityPositionInfo } from 'components/Liquidity/LiquidityPositionInfo'
import { useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks'
......@@ -21,7 +20,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit
fiatValue0 && fiatValue1
? formatCurrencyAmount({
value: fiatValue0.add(fiatValue1),
type: NumberType.FiatStandard,
type: NumberType.FiatTokenPrice,
})
: undefined
const v2FormattedUsdValue =
......@@ -33,8 +32,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit
fiatFeeValue0 && fiatFeeValue1
? formatCurrencyAmount({
value: fiatFeeValue0.add(fiatFeeValue1),
type:
liquidityPosition.status === PositionStatus.CLOSED ? NumberType.FiatStandard : NumberType.FiatTokenPrice,
type: NumberType.FiatTokenPrice,
})
: undefined
......
import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges'
import { BadgeData, LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges'
import { LiquidityPositionStatusIndicator } from 'components/Liquidity/LiquidityPositionStatusIndicator'
import { PositionInfo } from 'components/Liquidity/types'
import { getProtocolVersionLabel } from 'components/Liquidity/utils'
import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo'
import { ZERO_ADDRESS } from 'constants/misc'
import { Flex, Text } from 'ui/src'
import { DocumentList } from 'ui/src/components/icons/DocumentList'
interface LiquidityPositionInfoProps {
positionInfo: PositionInfo
......@@ -25,7 +27,18 @@ export function LiquidityPositionInfo({ positionInfo }: LiquidityPositionInfoPro
{currency0Amount?.currency.symbol} / {currency1Amount?.currency.symbol}
</Text>
<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>
<LiquidityPositionStatusIndicator status={status} />
......
import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges'
import { render } from 'test-utils/render'
const testBadgeData = [{ label: 'test', copyable: true }, { label: 'test2' }]
describe('LiquidityPositionInfoBadges', () => {
it('should render with default size', () => {
const { getByText } = render(<LiquidityPositionInfoBadges versionLabel="2" feeTier="100" size="default" />)
expect(getByText('2')).toBeInTheDocument()
const { getByText } = render(<LiquidityPositionInfoBadges badges={testBadgeData} size="default" />)
expect(getByText('test')).toBeInTheDocument()
})
it('should render with small size', () => {
const { getByText } = render(<LiquidityPositionInfoBadges versionLabel="2" feeTier="100" size="small" />)
expect(getByText('2')).toBeInTheDocument()
const { getByText } = render(<LiquidityPositionInfoBadges badges={testBadgeData} size="small" />)
expect(getByText('test')).toBeInTheDocument()
})
it('should render with multiple badges', () => {
const { getByText } = render(<LiquidityPositionInfoBadges versionLabel="2" feeTier="100" size="default" />)
expect(getByText('2')).toBeInTheDocument()
expect(getByText('0.01%')).toBeInTheDocument()
const { getByText } = render(<LiquidityPositionInfoBadges badges={testBadgeData} size="default" />)
expect(getByText('test')).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 { styled, Text } from 'ui/src'
import { DocumentList } from 'ui/src/components/icons/DocumentList'
import { isAddress, shortenAddress } from 'utilities/src/addresses'
export const PositionInfoBadge = styled(Text, {
......@@ -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'
}
interface BadgeData {
export interface BadgeData {
label: string
copyable?: boolean
icon?: JSX.Element
}
export function LiquidityPositionInfoBadges({
versionLabel,
v4hook,
feeTier,
badges,
size = 'default',
}: {
versionLabel?: string
v4hook?: string
feeTier?: string | FeeAmount
badges: BadgeData[]
size: 'small' | 'default'
}): 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 (
<>
{badges.map(({ label, copyable, icon }, index) => {
......
// 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 { LiquidityPositionStatusIndicator } from 'components/Liquidity/LiquidityPositionStatusIndicator'
import { useGetRangeDisplay } from 'components/Liquidity/hooks'
import { PriceOrdering } from 'components/PositionListItem'
import { useMemo, useState } from 'react'
......@@ -17,6 +19,7 @@ const InnerTile = styled(Flex, {
})
interface LiquidityPositionPriceRangeTileProps {
status?: PositionStatus
priceOrdering: PriceOrdering
token0CurrentPrice: Price<Currency, Currency>
token1CurrentPrice: Price<Currency, Currency>
......@@ -26,6 +29,7 @@ interface LiquidityPositionPriceRangeTileProps {
}
export function LiquidityPositionPriceRangeTile({
status,
priceOrdering,
token0CurrentPrice,
token1CurrentPrice,
......@@ -73,6 +77,7 @@ export function LiquidityPositionPriceRangeTile({
<Text variant="subheading1">
<Trans i18nKey="pool.priceRange" />
</Text>
{status && <LiquidityPositionStatusIndicator status={status} />}
</Flex>
<SegmentedControl
options={controlOptions}
......@@ -82,7 +87,7 @@ export function LiquidityPositionPriceRangeTile({
}}
/>
</Flex>
<Flex row width="100%" gap="$gap12" $lg={{ row: false }}>
<Flex row width="100%" gap="$gap12">
<InnerTile>
<Text variant="subheading2" color="$neutral2">
<Trans i18nKey="pool.minPrice" />
......
......@@ -55,7 +55,6 @@ export type V3PositionInfo = BasePositionInfo & {
version: ProtocolVersion.V3
tokenId: string
pool?: V3Pool
poolId?: string
feeTier?: FeeAmount
position?: V3Position
v4hook: undefined
......@@ -65,7 +64,6 @@ type V4PositionInfo = BasePositionInfo & {
version: ProtocolVersion.V4
tokenId: string
pool?: V4Pool
poolId?: string
position?: V4Position
feeTier?: 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
feeTier: parseV3FeeTier(v3Position.feeTier),
version: ProtocolVersion.V3,
pool,
poolId: position.position.value.poolId,
position: sdkPosition,
tickLower: v3Position.tickLower,
tickUpper: v3Position.tickUpper,
......@@ -286,21 +285,19 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef
tickUpper: Number(v4Position.tickUpper),
})
: undefined
const poolId = V4Pool.getPoolId(token0, token1, Number(v4Position.feeTier), Number(v4Position.tickSpacing), hook)
return {
status: position.status,
feeTier: v4Position.feeTier,
feeTier: v4Position?.feeTier,
version: ProtocolVersion.V4,
position: sdkPosition,
pool,
poolId,
v4hook: hook,
tokenId: v4Position.tokenId,
tickLower: v4Position.tickLower,
tickUpper: v4Position.tickUpper,
tickSpacing: Number(v4Position.tickSpacing),
currency0Amount: CurrencyAmount.fromRawAmount(token0, v4Position.amount0 ?? 0),
currency1Amount: CurrencyAmount.fromRawAmount(token1, v4Position.amount1 ?? 0),
tickLower: v4Position?.tickLower,
tickUpper: v4Position?.tickUpper,
tickSpacing: Number(v4Position?.tickSpacing),
currency0Amount: CurrencyAmount.fromRawAmount(token0, v4Position?.amount0 ?? 0),
currency1Amount: CurrencyAmount.fromRawAmount(token1, v4Position?.amount1 ?? 0),
token0UncollectedFees: v4Position.token0UncollectedFees,
token1UncollectedFees: v4Position.token1UncollectedFees,
liquidity: v4Position.liquidity,
......@@ -355,86 +352,3 @@ export function calculateInvertedPrice({ price, invert }: { price?: Price<Curren
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'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { GqlSearchToken } from 'graphql/data/SearchTokens'
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 useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
......
......@@ -498,13 +498,6 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = `
}
.c1 {
grid-column: 2;
grid-row: 1;
font-size: 16px;
font-weight: 485;
}
.c2 {
grid-column: 3;
grid-row: 1;
display: -webkit-box;
......@@ -531,23 +524,10 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = `
>
<button
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
class="c1"
>
World Chain
</div>
<div
class="c2"
/>
</button>
</span>
......
......@@ -7,7 +7,6 @@ import Column from 'components/deprecated/Column'
import { useTokenWarning } from 'constants/deprecatedTokenSafety'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { GqlSearchToken } from 'graphql/data/SearchTokens'
import { gqlTokenToCurrencyInfo } from 'graphql/data/types'
import { getTokenDetailsURL, supportedChainIdFromGQLChain } from 'graphql/data/util'
import styled, { css } from 'lib/styled-components'
import { searchGenieCollectionToTokenSearchResult, searchTokenToTokenSearchResult } from 'lib/utils/searchBar'
......@@ -18,14 +17,10 @@ import { Link, useNavigate } from 'react-router-dom'
import { EllipsisStyle, ThemedText } from 'theme/components'
import { Flex } from 'ui/src'
import { Verified } from 'ui/src/components/icons/Verified'
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { Token, TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { InterfaceSearchResultSelectionProperties } from 'uniswap/src/features/telemetry/types'
import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils'
import { Trans, useTranslation } from 'uniswap/src/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { shortenAddress } from 'uniswap/src/utils/addresses'
......@@ -110,15 +105,10 @@ export function SuggestionRow({
const navigate = useNavigate()
const { formatFiatPrice, formatDelta, formatNumberOrString } = useFormatter()
const [brokenCollectionImage, setBrokenCollectionImage] = useState(false)
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const warning = useTokenWarning(
isToken ? suggestion.address : undefined,
isToken ? supportedChainIdFromGQLChain(suggestion.chain) : UniverseChainId.Mainnet,
)
const tokenWarningSeverity = isToken
? getTokenWarningSeverity(gqlTokenToCurrencyInfo(suggestion as Token)) // casting GqlSearchToken to Token
: undefined
const handleClick = useCallback(() => {
const address =
......@@ -190,23 +180,9 @@ export function SuggestionRow({
/>
)}
<Flex alignItems="flex-start" justifyContent="flex-start" shrink grow>
<Flex
row
gap="$spacing4"
shrink
width="95%"
{...(isToken && tokenProtectionEnabled && { alignItems: 'center' })}
>
<Flex row gap="$spacing4" shrink width="95%">
<PrimaryText lineHeight="24px">{suggestion.name}</PrimaryText>
{isToken ? (
tokenProtectionEnabled ? (
<WarningIcon severity={tokenWarningSeverity} size="$icon.16" />
) : (
<TokenSafetyIcon warning={warning} />
)
) : (
suggestion.isVerified && <Verified size={14} />
)}
{isToken ? <TokenSafetyIcon warning={warning} /> : suggestion.isVerified && <Verified size={14} />}
</Flex>
<Flex row gap="$spacing4">
<ThemedText.SubHeaderSmall lineHeight="20px">
......
......@@ -25,6 +25,7 @@ export type TabsItem = MenuItem & {
export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSection[] => {
const { t } = useTranslation()
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere)
const { pathname } = useLocation()
const theme = useTheme()
......@@ -76,7 +77,7 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti
{
label: t('common.transactions'),
quickKey: 'X',
href: '/explore/transactions/ethereum',
href: `/explore/transactions${isMultichainExploreEnabled ? '/ethereum' : ''}`,
internal: true,
},
{ label: t('common.nfts'), quickKey: 'N', href: '/nfts', internal: true },
......@@ -84,7 +85,7 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti
},
{
title: t('common.pool'),
href: isV4EverywhereEnabled ? '/positions' : '/pool',
href: '/pool',
isActive: pathname.startsWith('/pool'),
items: [
{
......
......@@ -23,6 +23,8 @@ import { useProfilePageState } from 'nft/hooks'
import { ProfilePageStateType } from 'nft/types'
import { BREAKPOINTS } from 'theme'
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 { INTERFACE_NAV_HEIGHT } from 'uniswap/src/theme/heights'
......@@ -76,12 +78,20 @@ function useShouldHideChainSelector() {
const isSwapPage = useIsSwapPage()
const isLimitPage = useIsLimitPage()
const isExplorePage = useIsExplorePage()
const { value: multichainExploreFlagEnabled, isLoading: isMultichainExploreFlagLoading } = useFeatureFlagWithLoading(
FeatureFlags.MultichainExplore,
)
const baseHiddenPages = isNftPage
const multichainHiddenPages =
isLandingPage || isSendPage || isSwapPage || isLimitPage || baseHiddenPages || isExplorePage
const multichainHiddenPages = isLandingPage || isSendPage || isSwapPage || isLimitPage || baseHiddenPages
const multichainExploreHiddenPages = multichainHiddenPages || isExplorePage
const hideChainSelector =
multichainExploreFlagEnabled || isMultichainExploreFlagLoading
? multichainExploreHiddenPages
: multichainHiddenPages
return multichainHiddenPages
return hideChainSelector
}
export default function Navbar() {
......
......@@ -6,7 +6,6 @@ import { ExplorerIcon } from 'components/Icons/ExplorerIcon'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo'
import { DetailBubble } from 'components/Pools/PoolDetails/shared'
import { PoolDetailsBadge } from 'components/Pools/PoolTable/PoolTable'
import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
import { ActionButtonStyle, ActionMenuFlyoutStyle } from 'components/Tokens/TokenDetails/shared'
import { LoadingBubble } from 'components/Tokens/loading'
......@@ -22,7 +21,7 @@ import { ChevronRight, ExternalLink as ExternalLinkIcon } from 'react-feather'
import { Link } from 'react-router-dom'
import { ClickableStyle, ClickableTamaguiStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components'
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 { BIPS_BASE } from 'uniswap/src/constants/misc'
import { ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
......@@ -48,6 +47,12 @@ const HeaderContainer = styled.div`
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)`
width: 32px;
height: 32px;
......@@ -141,17 +146,8 @@ const PoolDetailsTitle = ({
</StyledLink>
</PoolName>
</div>
<Flex row gap="$gap4" alignItems="center">
<PoolDetailsBadge variant="body3" $position="left">
{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>
{protocolVersion === ProtocolVersion.V2 && <Badge>v2</Badge>}
{!!feePercent && <Badge>{feePercent}</Badge>}
<TouchableArea hoverStyle={{ opacity: 0.8 }} onPress={toggleReversed}>
<ArrowUpDown
{...ClickableTamaguiStyle}
......
......@@ -6,7 +6,7 @@ import Column from 'components/deprecated/Column'
import Row from 'components/deprecated/Row'
import { SwapWrapperOuter } from 'components/swap/styled'
import { LoadingBubble } from 'components/Tokens/loading'
import TokenSafetyMessage from 'components/TokenSafety/DeprecatedTokenSafetyMessage'
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import { chainIdToBackendChain } from 'constants/chains'
import { getPriorityWarning, StrongWarning, useTokenWarning } from 'constants/deprecatedTokenSafety'
import { useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider'
......@@ -16,7 +16,7 @@ import { useAccount } from 'hooks/useAccount'
import { useSwitchChain } from 'hooks/useSwitchChain'
import styled from 'lib/styled-components'
import { Swap } from 'pages/Swap'
import { useCallback, useMemo, useReducer, useState } from 'react'
import { useMemo, useReducer } from 'react'
import { Plus, X } from 'react-feather'
import { useLocation, useNavigate } from 'react-router-dom'
import { BREAKPOINTS } from 'theme'
......@@ -25,15 +25,9 @@ import { opacify } from 'theme/utils'
import { Z_INDEX } from 'theme/zIndex'
import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown'
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 { UniverseChainId } from 'uniswap/src/types/chains'
import { currencyId } from 'uniswap/src/utils/currencyId'
import { currencyId } from 'utils/currencyId'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const PoolDetailsStatsButtonsRow = styled(Row)`
......@@ -164,8 +158,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
const location = useLocation()
const currency0 = token0 && gqlToCurrency(token0)
const currency1 = token1 && gqlToCurrency(token1)
const currencyInfo0 = useCurrencyInfo(currency0 && currencyId(currency0))
const currencyInfo1 = useCurrencyInfo(currency1 && currencyId(currency1))
// Mobile Balance Data
const { data: balanceQuery } = useTokenBalancesQuery()
......@@ -202,9 +194,7 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
if (account.chainId !== chainId && chainId) {
await switchChain(chainId)
}
const currency0Address = currency0.isNative ? 'ETH' : currency0.address
const currency1Address = currency1.isNative ? 'ETH' : currency1.address
navigate(`/add/${currency0Address}/${currency1Address}/${feeTier}${tokenId ? `/${tokenId}` : ''}`, {
navigate(`/add/${currencyId(currency0)}/${currencyId(currency1)}/${feeTier}${tokenId ? `/${tokenId}` : ''}`, {
state: { from: location.pathname },
})
}
......@@ -217,15 +207,6 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
const token1Warning = useTokenWarning(token1?.address, chainId)
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) {
return (
<PoolDetailsStatsButtonsRow data-testid="pdp-buttons-loading-skeleton">
......@@ -301,30 +282,13 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
compact
disableTokenInputs={chainId !== account.chainId}
/>
{tokenProtectionEnabled ? (
<>
<TokenWarningCard currencyInfo={currencyInfo0} onPress={() => onWarningCardCtaPressed(currencyInfo0)} />
<TokenWarningCard currencyInfo={currencyInfo1} onPress={() => onWarningCardCtaPressed(currencyInfo1)} />
{warningModalCurrencyInfo && (
// Intentionally duplicative with the TokenWarningModal in the swap component; this one only displays when user clicks "i" Info button on the TokenWarningCard
<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}
/>
)
{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>
<Scrim
......
......@@ -284,7 +284,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
min-width: 0;
}
.c10 {
.c12 {
box-sizing: border-box;
margin: 0;
min-width: 0;
......@@ -311,7 +311,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
gap: 12px;
}
.c11 {
.c13 {
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
......@@ -331,7 +331,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
gap: 8px;
}
.c12 {
.c14 {
display: inline-block;
height: inherit;
}
......@@ -344,6 +344,14 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
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 {
display: -webkit-box;
display: -webkit-flex;
......@@ -403,6 +411,12 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
animation-duration: 250ms;
}
.c11 {
background: #F9F9F9;
padding: 2px 6px;
border-radius: 4px;
}
.c6 {
display: -webkit-box;
display: -webkit-flex;
......@@ -509,18 +523,9 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
</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
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>
0.05%
</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"
......@@ -543,7 +548,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
</div>
</div>
<div
class="c10 c11"
class="c12 c13"
width="max-content"
>
<div
......@@ -555,7 +560,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
id="Dropdown"
>
<div
class="c12"
class="c14"
style="width: 100%;"
>
<div>
......@@ -600,7 +605,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
id="Dropdown"
>
<div
class="c12"
class="c14"
style="width: 100%;"
>
<div>
......
import 'test-utils/tokens/mocks'
import { ApolloError } from '@apollo/client'
import { Percent } from '@uniswap/sdk-core'
import { TopPoolTable } from 'components/Pools/PoolTable/PoolTable'
import { useTopPools } from 'graphql/data/pools/useTopPools'
import Router from 'react-router-dom'
import { useTopPools } from 'state/explore/topPools'
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 { 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.requireActual('react-router-dom'),
useParams: jest.fn(),
......@@ -22,8 +23,9 @@ describe('PoolTable', () => {
it('renders loading state', () => {
mocked(useTopPools).mockReturnValue({
isLoading: true,
isError: false,
loading: true,
errorV3: undefined,
errorV2: undefined,
topPools: [],
})
......@@ -34,8 +36,9 @@ describe('PoolTable', () => {
it('renders error state', () => {
mocked(useTopPools).mockReturnValue({
isLoading: false,
isError: true,
loading: false,
errorV3: new ApolloError({ errorMessage: 'error fetching data' }),
errorV2: new ApolloError({ errorMessage: 'error fetching data' }),
topPools: [],
})
......@@ -47,10 +50,8 @@ describe('PoolTable', () => {
it('renders data filled state', () => {
const mockData = [
{
id: '1',
chain: 'mainnet',
token0: validRestPoolToken0,
token1: validRestPoolToken1,
token0: validBEPoolToken0,
token1: validBEPoolToken1,
feeTier: 10000,
hash: '0x123',
txCount: 200,
......@@ -64,8 +65,9 @@ describe('PoolTable', () => {
]
mocked(useTopPools).mockReturnValue({
topPools: mockData,
isLoading: false,
isError: false,
loading: false,
errorV3: undefined,
errorV2: undefined,
})
const { asFragment } = render(<TopPoolTable />)
......
......@@ -12,9 +12,16 @@ import { EllipsisText } from 'components/Tokens/TokenTable'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
import { exploreSearchStringAtom } from 'components/Tokens/state'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { chainIdToBackendChain } from 'constants/chains'
import { PoolSortFields, TablePool } from 'graphql/data/pools/useTopPools'
import { OrderDirection, gqlToCurrency, supportedChainIdFromGQLChain, unwrapToken } from 'graphql/data/util'
import { chainIdToBackendChain, useChainFromUrlParam } from 'constants/chains'
import { useUpdateManualOutage } from 'featureFlags/flags/outageBanner'
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 useSimplePagination from 'hooks/useSimplePagination'
import { useAtom } from 'jotai'
......@@ -26,6 +33,8 @@ import { PoolStat } from 'state/explore/types'
import { Flex, Text, styled } from 'ui/src'
import { BIPS_BASE } from 'uniswap/src/constants/misc'
import { Chain, ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { Trans } from 'uniswap/src/i18n'
import { UniverseChainId } from 'uniswap/src/types/chains'
import { NumberType, useFormatter } from 'utils/formatNumbers'
......@@ -43,23 +52,13 @@ const TableWrapper = styled(Flex, {
maxWidth: MAX_WIDTH_MEDIA_BREAKPOINT,
})
export const PoolDetailsBadge = styled(Text, {
const Badge = styled(Text, {
py: 2,
px: 6,
backgroundColor: '$surface2',
borderRadius: '$rounded6',
variant: 'body4',
color: '$neutral2',
variants: {
$position: {
right: {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
left: {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
},
},
})
interface PoolTableValues {
......@@ -94,31 +93,24 @@ function PoolDescription({
chainId: UniverseChainId
protocolVersion?: ProtocolVersion | string
}) {
const isRestPool = token0 && !('id' in token0)
const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore)
const currencies = [token0 ? gqlToCurrency(token0) : undefined, token1 ? gqlToCurrency(token1) : undefined]
// skip is isRestExploreEnabled
const currencyLogos = [
useCurrencyInfo(currencies?.[0], chainId, isRestPool)?.logoUrl,
useCurrencyInfo(currencies?.[1], chainId, isRestPool)?.logoUrl,
useCurrencyInfo(currencies?.[0], chainId, isRestExploreEnabled)?.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 (
<Flex row gap="$gap8" alignItems="center">
<PortfolioLogo currencies={currencies} chainId={chainId} images={images} size={28} />
<EllipsisText>
{token0?.symbol}/{token1?.symbol}
</EllipsisText>
<Flex row gap="$gap4" alignItems="center">
<PoolDetailsBadge variant="body4" $position="left">
{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>
{protocolVersion === ProtocolVersion.V2 && <Badge>{protocolVersion.toLowerCase()}</Badge>}
{feeTier && <Badge>{feeTier / BIPS_BASE}%</Badge>}
</Flex>
)
}
......@@ -176,6 +168,7 @@ function PoolTableHeader({
}
export const TopPoolTable = memo(function TopPoolTable() {
const chain = getSupportedGraphQlChain(useChainFromUrlParam(), { fallbackToEthereum: true })
const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
......@@ -186,22 +179,38 @@ export const TopPoolTable = memo(function TopPoolTable() {
resetSortAscending()
}, [resetSortAscending, resetSortMethod])
const { topPools, isLoading, isError } = useRestTopPools({
sortBy: sortMethod,
sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc,
})
const {
topPools: gqlTopPools,
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 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 (
<TableWrapper data-testid="top-pools-explore-table">
<PoolsTable
pools={topPools?.slice(0, page * TABLE_PAGE_SIZE)}
loading={isLoading}
error={isError}
loadMore={loadMore}
maxWidth={1200}
/>
<PoolsTable pools={topPools} loading={loading} error={error} loadMore={loadMore} maxWidth={1200} />
</TableWrapper>
)
})
......
......@@ -332,22 +332,12 @@ exports[`PoolTable renders data filled state 1`] = `
>
USDC/ETH
</span>
<div
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _gap-1316331199 _alignItems-center"
<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 _borderTopLeftRadius-1307609936 _borderTopRightRadius-1307609936 _borderBottomRightRadius-1307609936 _borderBottomLeftRadius-1307609936 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202"
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-229441127 _lineHeight-222976480 _fontWeight-233016202 _borderTopLeftRadius-4px _borderBottomLeftRadius-4px"
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>
1%
</span>
</div>
</div>
</div>
......
......@@ -55,7 +55,6 @@ export function RemoveLiquidityTxContextProvider({ children }: PropsWithChildren
action: {
currency0Amount: currency0AmountToRemove,
currency1Amount: currency1AmountToRemove,
liquidityToken: positionInfo.liquidityToken,
},
approvePositionTokenRequest,
txRequest,
......
......@@ -37,7 +37,7 @@ export function SwapBottomCard() {
const hasViewedBridgingBanner = useSelector(selectHasViewedBridgingBanner)
const bridgingEnabled = useFeatureFlag(FeatureFlags.Bridging)
const isBridgingSupportedChain = useIsBridgingChain(chainId ?? UniverseChainId.Mainnet)
const isBridgingSupported = useIsBridgingChain(chainId ?? UniverseChainId.Mainnet)
const numBridgingChains = useNumBridgingChains()
const handleBridgingDismiss = useCallback(
(shouldNavigate: boolean) => {
......@@ -61,8 +61,7 @@ export function SwapBottomCard() {
return null
}
const isBridgingBannerChain = chainId === null || chainId === UniverseChainId.Mainnet || isBridgingSupportedChain
const shouldShowBridgingBanner = bridgingEnabled && !hasViewedBridgingBanner && isBridgingBannerChain
const shouldShowBridgingBanner = bridgingEnabled && !hasViewedBridgingBanner && isBridgingSupported
const shouldShowLegacyTreatment = !bridgingEnabled
......@@ -85,7 +84,7 @@ export function SwapBottomCard() {
/>
</TouchableArea>
)
} else if (shouldShowLegacyTreatment || !isBridgingSupportedChain) {
} else if (shouldShowLegacyTreatment || !isBridgingSupported) {
return <NetworkAlert chainId={chainId} />
} else {
return null
......
......@@ -8,7 +8,6 @@ const WarningContainer = styled(Flex, {
justifyContent: 'center',
})
/** @deprecated use WarningIcon from packages/uniswap instead */
export default function TokenSafetyIcon({ warning }: { warning?: Warning }) {
const colors = useSporeColors()
switch (warning?.level) {
......
......@@ -47,7 +47,6 @@ type TokenSafetyMessageProps = {
tokenSymbol?: string
}
/** @deprecated Use TokenWarningCard from packages/uniswap instead */
export default function TokenSafetyMessage({
warning,
tokenAddress,
......
......@@ -2,7 +2,7 @@ import { InterfacePageName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core'
import { BreadcrumbNavContainer, BreadcrumbNavLink, CurrentPageBreadcrumb } from 'components/BreadcrumbNav'
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 BalanceSummary, { PageChainBalanceSummary } from 'components/Tokens/TokenDetails/BalanceSummary'
import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
......@@ -21,19 +21,13 @@ import { ScrollDirection, useScroll } from 'hooks/useScroll'
import deprecatedStyled from 'lib/styled-components'
import { Swap } from 'pages/Swap'
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 { useNavigate } from 'react-router-dom'
import { CurrencyState } from 'state/swap/types'
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 { 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 { currencyId } from 'uniswap/src/utils/currencyId'
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
import { getInitialLogoUrl } from 'utils/getInitialLogoURL'
......@@ -86,11 +80,8 @@ function useSwapInitialInputCurrency() {
function TDPSwapComponent() {
const { address, currency, currencyChainId, warning } = useTDPContext()
const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection)
const navigate = useNavigate()
const currencyInfo = useCurrencyInfo(currencyId(currency))
const handleCurrencyChange = useCallback(
(tokens: CurrencyState) => {
const inputCurrencyURLAddress = getCurrencyURLAddress(tokens.inputCurrency)
......@@ -133,9 +124,6 @@ function TDPSwapComponent() {
// Other token to prefill the swap form with
const initialInputCurrency = useSwapInitialInputCurrency()
const [showWarningModal, setShowWarningModal] = useState(false)
const closeWarningModal = useCallback(() => setShowWarningModal(false), [])
return (
<>
<Swap
......@@ -146,23 +134,7 @@ function TDPSwapComponent() {
onCurrencyChange={handleCurrencyChange}
compact
/>
{tokenProtectionEnabled ? (
<>
<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} />
)}
{warning && <TokenSafetyMessage tokenAddress={address} warning={warning} />}
</>
)
}
......
......@@ -304,22 +304,12 @@ exports[`TDPPoolTable renders data filled state 1`] = `
>
USDC/ETH
</span>
<div
class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _gap-1316331199 _alignItems-center"
<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 _borderTopLeftRadius-1307609936 _borderTopRightRadius-1307609936 _borderBottomRightRadius-1307609936 _borderBottomLeftRadius-1307609936 _fontSize-229441127 _lineHeight-222976480 _fontWeight-233016202"
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-229441127 _lineHeight-222976480 _fontWeight-233016202 _borderTopLeftRadius-4px _borderBottomLeftRadius-4px"
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>
1%
</span>
</div>
</div>
</div>
......
......@@ -6,7 +6,6 @@ import { AllNetworksIcon } from 'components/Tokens/TokenTable/icons'
import {
BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS,
BACKEND_SUPPORTED_CHAINS,
BACKEND_SUPPORTED_TESTNET_CHAINS,
InterfaceGqlChain,
useChainFromUrlParam,
useIsSupportedChainIdCallback,
......@@ -21,7 +20,8 @@ import { useNavigate } from 'react-router-dom'
import { EllipsisTamaguiStyle } from 'theme/components'
import { Flex, FlexProps, ScrollView, Text, styled } from 'ui/src'
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 { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useTranslation } from 'uniswap/src/i18n'
......@@ -54,10 +54,12 @@ const StyledDropdown = {
export default function TableNetworkFilter() {
const [isMenuOpen, toggleMenu] = useState(false)
const isSupportedChainCallback = useIsSupportedChainIdCallback()
const { isTestnetModeEnabled } = useEnabledChains()
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
const exploreParams = useExploreParams()
const currentChain = getSupportedGraphQlChain(useChainFromUrlParam())
const currentChain = getSupportedGraphQlChain(useChainFromUrlParam(), {
fallbackToEthereum: !isMultichainExploreEnabled,
})
const tab = exploreParams.tab
return (
......@@ -77,7 +79,9 @@ export default function TableNetworkFilter() {
}
internalMenuItems={
<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) => {
const chainId = supportedChainIdFromGQLChain(network)
const isSupportedChain = isSupportedChainCallback(chainId)
......@@ -92,22 +96,6 @@ export default function TableNetworkFilter() {
/>
) : 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) => {
const isSupportedChain = isSupportedChainCallback(network)
const chainInfo = isSupportedChain ? UNIVERSE_CHAIN_INFO[network] : undefined
......@@ -148,10 +136,14 @@ const TableNetworkItem = memo(function TableNetworkItem({
const navigate = useNavigate()
const theme = useTheme()
const { t } = useTranslation()
const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore)
const chainId = chainInfo?.id
const exploreParams = useExploreParams()
const currentChain = getSupportedGraphQlChain(useChainFromUrlParam())
const isAllNetworks = display === 'All networks'
const currentChain = getSupportedGraphQlChain(
useChainFromUrlParam(),
isMultichainExploreEnabled ? undefined : { fallbackToEthereum: true },
)
const isAllNetworks = display === 'All networks' && isMultichainExploreEnabled
const isCurrentChain = isAllNetworks
? !currentChain
: currentChain?.backendChain.chain === display && exploreParams.chainName
......
......@@ -18,10 +18,10 @@ import {
useSetSortMethod,
} from 'components/Tokens/state'
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 { SparklineMap, TopToken } from 'graphql/data/types'
import { OrderDirection, getTokenDetailsURL, unwrapToken } from 'graphql/data/util'
import { SparklineMap, TopToken, useTopTokens } from 'graphql/data/TopTokens'
import { OrderDirection, getSupportedGraphQlChain, getTokenDetailsURL, unwrapToken } from 'graphql/data/util'
import useSimplePagination from 'hooks/useSimplePagination'
import { useAtomValue } from 'jotai/utils'
import { ReactElement, ReactNode, memo, useMemo } from 'react'
......@@ -29,6 +29,8 @@ import { TABLE_PAGE_SIZE, giveExploreStatDefaultValue } from 'state/explore'
import { useTopTokens as useRestTopTokens } from 'state/explore/topTokens'
import { TokenStat } from 'state/explore/types'
import { Flex, Text, styled } from 'ui/src'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { Trans } from 'uniswap/src/i18n'
import { NumberType, useFormatter } from 'utils/formatNumbers'
......@@ -87,19 +89,65 @@ function TokenDescription({ token }: { token: TopToken | TokenStat }) {
}
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 { 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 (
<TableWrapper data-testid="top-tokens-explore-table">
<TokenTable
tokens={topTokens?.slice(0, page * TABLE_PAGE_SIZE)}
tokens={tokens}
tokenSortRank={tokenSortRank}
sparklines={sparklines}
loading={isLoading}
loading={loading}
loadMore={loadMore}
error={isError}
error={error}
/>
</TableWrapper>
)
......
......@@ -81,19 +81,12 @@ export function LaunchModal({
</Flex>
<Flex gap="$gap8" row>
<Trace logPress element={InterfaceElementName.CLOSE_BUTTON}>
<Button
size="small"
theme="secondary"
fontSize="$micro"
fill
flexBasis={0}
onPress={() => setShowModal(false)}
>
<Button size="small" theme="secondary" fontSize="$micro" fill onPress={() => setShowModal(false)}>
{t('common.button.dismiss')}
</Button>
</Trace>
<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')}
</Button>
</Trace>
......
import {
BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS,
BACKEND_SUPPORTED_CHAINS,
BACKEND_SUPPORTED_TESTNET_CHAINS,
CHAIN_IDS_TO_NAMES,
CHAIN_ID_TO_BACKEND_NAME,
CHAIN_NAME_TO_CHAIN_ID,
......@@ -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
test('BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS array is empty', () => {
......
......@@ -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)
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(
(chain) => !BACKEND_SUPPORTED_CHAINS.includes(chain),
).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 { useAccount } from 'hooks/useAccount'
import { mocked } from 'test-utils/mocked'
......@@ -7,8 +7,6 @@ import { useOnAssetActivitySubscription } from 'uniswap/src/data/graphql/uniswap
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
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 mockBalanceQueryResponse = [
mockLazyFetch,
......@@ -47,17 +45,17 @@ describe('TokenBalancesProvider', () => {
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 />)
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(0), { timeout: 3500 })
expect(mockLazyFetch).toHaveBeenCalledTimes(0)
})
describe('useTokenBalancesQuery', () => {
it('should only refetch balances when stale', async () => {
it('should only refetch balances when stale', () => {
const { rerender, unmount } = renderHook(() => useTokenBalancesQuery())
// Rendering useTokenBalancesQuery should trigger a fetch
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 })
expect(mockLazyFetch).toHaveBeenCalledTimes(1)
// Rerender to clear staleness
rerender()
......@@ -65,31 +63,31 @@ describe('TokenBalancesProvider', () => {
// Receiving a new value from subscription should trigger a fetch while useTokenBalancesQuery hooks are mounted
triggerSubscriptionUpdate()
rerender()
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 })
expect(mockLazyFetch).toHaveBeenCalledTimes(2)
// Unmounting the hooks should not trigger any fetches
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
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(() => ({
hook1: useTokenBalancesQuery(),
hook2: useTokenBalancesQuery(),
}))
// 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())
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 })
expect(mockLazyFetch).toHaveBeenCalledTimes(1)
// Rerender to clear staleness
rerender()
......@@ -98,12 +96,12 @@ describe('TokenBalancesProvider', () => {
mocked(useAccount).mockReturnValue({ address: '0xaddress2', chainId: 1 } as any)
rerender()
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 })
expect(mockLazyFetch).toHaveBeenCalledTimes(2)
})
})
describe('PrefetchBalancesWrapper', () => {
it('should fetch balances when a PrefetchBalancesWrapper is hovered', async () => {
it('should fetch balances when a PrefetchBalancesWrapper is hovered', () => {
const { rerender } = render(
<PrefetchBalancesWrapper>
<div>hi</div>
......@@ -119,17 +117,17 @@ describe('TokenBalancesProvider', () => {
)
// Should not fetch balances before hover
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(0), { timeout: 3500 })
expect(mockLazyFetch).toHaveBeenCalledTimes(0)
// Hovering component should trigger a fetch
fireEvent.mouseEnter(wrappedComponent)
fireEvent.mouseLeave(wrappedComponent)
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 })
expect(mockLazyFetch).toHaveBeenCalledTimes(1)
// Subsequent hover should not trigger a fetch
fireEvent.mouseEnter(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
triggerSubscriptionUpdate()
......@@ -138,10 +136,10 @@ describe('TokenBalancesProvider', () => {
<div>hi</div>
</PrefetchBalancesWrapper>,
)
await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 })
expect(mockLazyFetch).toHaveBeenCalledTimes(1)
fireEvent.mouseEnter(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) {
if (!account.address) {
return
}
// adds a 3 second delay to account for dependency latency after an account update
// TODO(WEB-5370): Remove this delay once we've integrated wallet's refetch logic
setTimeout(
() => {
account.address &&
lazyFetch({
variables: {
ownerAddress: account.address,
chains: gqlChains,
valueModifiers: [
{
ownerAddress: account.address,
includeSpamTokens: valueModifiers.includeSpamTokens,
includeSmallBalances: valueModifiers.includeSmallBalances,
tokenExcludeOverrides: [],
tokenIncludeOverrides: [],
},
],
},
})
lazyFetch({
variables: {
ownerAddress: account.address,
chains: gqlChains,
valueModifiers: [
{
ownerAddress: account.address,
includeSpamTokens: valueModifiers.includeSpamTokens,
includeSmallBalances: valueModifiers.includeSmallBalances,
tokenExcludeOverrides: [],
tokenIncludeOverrides: [],
},
],
},
hasAccountUpdate ? 3000 : 0,
)
}, [
account.address,
hasAccountUpdate,
lazyFetch,
gqlChains,
valueModifiers.includeSpamTokens,
valueModifiers.includeSmallBalances,
])
})
}, [account.address, lazyFetch, valueModifiers, gqlChains])
return (
<AdaptiveTokenBalancesProvider
......
import { ApolloClient, FieldFunctionOptions, HttpLink, InMemoryCache } from '@apollo/client'
import { Reference, StoreObject, relayStylePagination } from '@apollo/client/utilities'
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { Reference, relayStylePagination } from '@apollo/client/utilities'
import { createSubscriptionLink } from 'utilities/src/apollo/SubscriptionLink'
import { splitSubscription } from 'utilities/src/apollo/splitSubscription'
......@@ -47,18 +47,6 @@ export const apolloClient = new ApolloClient({
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: {
......@@ -97,14 +85,3 @@ export const apolloClient = new ApolloClient({
// 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)
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 { PricePoint, fiatOnRampToCurrency, gqlToCurrency } from 'graphql/data/util'
import { fiatOnRampToCurrency, gqlToCurrency } from 'graphql/data/util'
import { COMMON_BASES, buildPartialCurrencyInfo } from 'uniswap/src/constants/routing'
import { USDC_OPTIMISM } from 'uniswap/src/constants/tokens'
import {
Token as GqlToken,
ProtectionResult,
SafetyLevel,
TopTokens100Query,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { CurrencyInfo, TokenList } from 'uniswap/src/features/dataApi/types'
import { buildCurrencyInfo, getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils'
......@@ -83,6 +82,3 @@ export function meldSupportedCurrencyToCurrencyInfo(forCurrency: FORSupportedTok
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