ci(release): publish latest release

parent fcd04a4f
diff --git a/package.json b/package.json
index 251da97dcf5a092c3d2d766e16fe9536b411b9a2..7f74152f2e127a42bac946fcab641b03316e42d9 100644
index e4b37644f3a62171deaff6dbf7731979ce751c78..7b2ca64565c0c6318ba01ff26cb4fad1c7419b0d 100644
--- a/package.json
+++ b/package.json
@@ -15,24 +15,10 @@
@@ -15,22 +15,9 @@
"url": "https://github.com/sponsors/tannerlinsley"
},
"type": "module",
- "types": "build/legacy/index.d.ts",
- "main": "build/legacy/index.cjs",
- "module": "build/legacy/index.js",
+ "types": "build/modern/index.d.ts",
+ "main": "build/modern/index.cjs",
+ "module": "build/modern/index.js",
"react-native": "src/index.ts",
- "exports": {
- ".": {
- "@tanstack/custom-condition": "./src/index.ts",
- "import": {
- "types": "./build/modern/index.d.ts",
- "default": "./build/modern/index.js"
......@@ -27,6 +22,9 @@ index 251da97dcf5a092c3d2d766e16fe9536b411b9a2..7f74152f2e127a42bac946fcab641b03
- },
- "./package.json": "./package.json"
- },
+ "types": "build/modern/index.d.ts",
+ "main": "build/modern/index.cjs",
+ "module": "build/modern/index.js",
"sideEffects": false,
"files": [
"build",
* @uniswap/web-admins
IPFS hash of the deployment:
- CIDv0: `QmdVVhTjJqcLTWyz6A2DAUNM84F6gESEzYGShEWMb7Giid`
- CIDv1: `bafybeihbenlz6ecs7jlsn6yhsykx6wounpbqw7l42nuwu7e6zd7hzfbqvi`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeihbenlz6ecs7jlsn6yhsykx6wounpbqw7l42nuwu7e6zd7hzfbqvi.ipfs.dweb.link/
- [ipfs://QmdVVhTjJqcLTWyz6A2DAUNM84F6gESEzYGShEWMb7Giid/](ipfs://QmdVVhTjJqcLTWyz6A2DAUNM84F6gESEzYGShEWMb7Giid/)
### 5.88.7 (2025-06-13)
### Bug Fixes
* **web:** fix delta formatting error (#20860) 0b9ac1f
We are back with a large update: Smart wallets are here! Enable smart wallets from your home screen to benefit from faster, lower-cost transactions, enabled via an EIP 7702 smart contract.
Other changes:
- When connected to Uniswap Web, you’ll see a new verification check mark so that you know you’re on the right website.
- Various bug fixes and performance improvements
\ No newline at end of file
web/5.88.7
\ No newline at end of file
extension/1.22.2
\ No newline at end of file
......@@ -13,7 +13,6 @@
"@reduxjs/toolkit": "1.9.3",
"@svgr/webpack": "8.0.1",
"@tamagui/core": "1.125.17",
"@tanstack/react-query": "5.77.2",
"@types/uuid": "9.0.1",
"@uniswap/analytics-events": "2.42.0",
"@uniswap/client-embeddedwallet": "0.0.16",
......@@ -36,7 +35,6 @@
"react-native-reanimated": "3.16.7",
"react-native-svg": "15.10.1",
"react-native-web": "0.19.13",
"react-player": "2.16.0",
"react-qr-code": "2.0.12",
"react-redux": "8.0.5",
"react-router-dom": "6.10.0",
......
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { config } from 'uniswap/src/config'
import { SharedQueryClient } from 'uniswap/src/data/apiClients/SharedQueryClient'
import { StatsigProviderWrapper } from 'uniswap/src/features/gating/StatsigProviderWrapper'
import { StatsigCustomAppValue } from 'uniswap/src/features/gating/constants'
import { StatsigClient, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig'
import { statsigBaseConfig } from 'uniswap/src/features/gating/statsigBaseConfig'
import { initializeDatadog } from 'uniswap/src/utils/datadog'
import { getUniqueId } from 'utilities/src/device/uniqueId'
import { uniqueIdQuery } from 'utilities/src/device/uniqueIdQuery'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
function makeStatsigUser(userID: string): StatsigUser {
async function getStatsigUser(): Promise<StatsigUser> {
return {
userID,
userID: await getUniqueId(),
appVersion: process.env.VERSION,
custom: {
app: StatsigCustomAppValue.Extension,
......@@ -28,7 +26,7 @@ export function ExtensionStatsigProvider({
children: React.ReactNode
appName: string
}): JSX.Element {
const { data: uniqueId } = useQuery(uniqueIdQuery(), SharedQueryClient)
const { data: storedUser } = useAsyncData(getStatsigUser)
const [initFinished, setInitFinished] = useState(false)
const [user, setUser] = useState<StatsigUser>({
userID: undefined,
......@@ -39,10 +37,10 @@ export function ExtensionStatsigProvider({
})
useEffect(() => {
if (uniqueId && initFinished) {
setUser(makeStatsigUser(uniqueId))
if (storedUser && initFinished) {
setUser(storedUser)
}
}, [uniqueId, initFinished])
}, [storedUser, initFinished])
const onStatsigInit = (): void => {
setInitFinished(true)
......@@ -57,8 +55,7 @@ export function ExtensionStatsigProvider({
}
export async function initStatSigForBrowserScripts(): Promise<void> {
const uniqueId = await getUniqueId()
const statsigClient = new StatsigClient(config.statsigApiKey, makeStatsigUser(uniqueId), statsigBaseConfig)
const statsigClient = new StatsigClient(config.statsigApiKey, await getStatsigUser(), statsigBaseConfig)
await statsigClient.initializeAsync().catch((error) => {
logger.error(error, {
tags: { file: 'StatsigProvider.tsx', function: 'initStatSigForBrowserScripts' },
......
......@@ -2,7 +2,6 @@ import { useApolloClient } from '@apollo/client'
import { SharedEventName } from '@uniswap/analytics-events'
import { memo, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactPlayer from 'react-player'
import { useDispatch, useSelector } from 'react-redux'
import { ActivityTab } from 'src/app/components/tabs/ActivityTab'
import { NftsTab } from 'src/app/components/tabs/NftsTab'
......@@ -44,19 +43,19 @@ import { setSmartWalletConsent } from 'wallet/src/features/wallet/slice'
const MemoizedVideo = memo(() => (
<Flex borderRadius="$rounded12" overflow="hidden" height="auto" maxWidth="100%" aspectRatio="16 / 9">
<ReactPlayer
url={SMART_WALLET_UPGRADE_VIDEO}
width="100%"
height="100%"
playing={true}
muted={true}
<video
src={SMART_WALLET_UPGRADE_VIDEO}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
fallback={undefined}
autoPlay
muted
/>
</Flex>
))
MemoizedVideo.displayName = 'MemoizedVideo'
export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
......
......@@ -7,6 +7,8 @@ import { navigate } from 'src/app/navigation/state'
import { Flex, Text, getTokenValue, useMedia } from 'ui/src'
import { ArrowDownCircle, Bank, CoinConvert, SendAction } from 'ui/src/components/icons'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal'
......@@ -72,6 +74,7 @@ export const PortfolioActionButtons = memo(function _PortfolioActionButtons(): J
const { t } = useTranslation()
const media = useMedia()
const { isTestnetModeEnabled } = useEnabledChains()
const isFiatOffRampEnabled = useFeatureFlag(FeatureFlags.FiatOffRamp)
const onSendClick = (): void => {
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
......@@ -123,7 +126,11 @@ export const PortfolioActionButtons = memo(function _PortfolioActionButtons(): J
/>
<Flex row shrink gap="$spacing8" width={isGrid ? '100%' : '50%'}>
<ActionButton Icon={<CoinConvert />} label={t('home.label.swap')} onClick={onSwapClick} />
<ActionButton Icon={<Bank />} label={t('home.label.buy')} onClick={onBuyClick} />
<ActionButton
Icon={<Bank />}
label={isFiatOffRampEnabled ? t('home.label.for') : t('home.label.buy')}
onClick={onBuyClick}
/>
</Flex>
<Flex row shrink gap="$spacing8" width={isGrid ? '100%' : '50%'}>
<ActionButton Icon={<SendAction />} label={t('home.label.send')} onClick={onSendClick} />
......
import { useQuery } from '@tanstack/react-query'
import { ComponentProps, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { SelectWalletsSkeleton } from 'src/app/components/loading/SelectWalletSkeleton'
......@@ -14,9 +13,7 @@ import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension'
import { openUri } from 'uniswap/src/utils/linking'
import { useEvent } from 'utilities/src/react/hooks'
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
import { queryWithoutCache } from 'utilities/src/reactQuery/queryOptions'
import { useAsyncData, useEvent } from 'utilities/src/react/hooks'
import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPreviewCard'
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useImportableAccounts } from 'wallet/src/features/onboarding/hooks/useImportableAccounts'
......@@ -31,9 +28,7 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
const { goToNextStep, goToPreviousStep } = useOnboardingSteps()
const { generateAccountsAndImportAddresses, getGeneratedAddresses } = useOnboardingContext()
const { data: generatedAddresses } = useQuery(
queryWithoutCache({ queryFn: getGeneratedAddresses, queryKey: [ReactQueryCacheKey.GeneratedAddresses] }),
)
const { data: generatedAddresses } = useAsyncData(getGeneratedAddresses)
const { importableAccounts, isLoading, showError, refetch } = useImportableAccounts(generatedAddresses)
......
import { useQuery } from '@tanstack/react-query/build/modern/useQuery'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
......@@ -13,9 +12,9 @@ import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Checkbox, Flex, SpinningLoader, Text, TouchableArea } from 'ui/src'
import { AlertTriangleFilled, FileListCheck, FileListLock } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { useEvent } from 'utilities/src/react/hooks'
import { useAsyncData, useEvent } from 'utilities/src/react/hooks'
import { useBooleanState } from 'utilities/src/react/useBooleanState'
import { mnemonicUnlockedQuery } from 'wallet/src/features/wallet/Keyring/queries'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga'
import { BackupType } from 'wallet/src/features/wallet/accounts/types'
import { hasBackup } from 'wallet/src/features/wallet/accounts/utils'
......@@ -154,7 +153,9 @@ function RecoveryPhraseVerificationStep({
const [hasError, setHasError] = useState(false)
const [numberOfWordsVerified, setNumberOfWordsVerified] = useState(0)
const { data: mnemonic, error } = useQuery(mnemonicUnlockedQuery(mnemonicId))
const { data: mnemonic, error } = useAsyncData(
useCallback(async () => Keyring.retrieveMnemonicUnlocked(mnemonicId), [mnemonicId]),
)
if (error) {
// This should never happen. We can't recover from a missing mnemonic.
......
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { LayoutChangeEvent } from 'react-native'
import { CopyButton } from 'src/app/components/buttons/CopyButton'
import { Flex, Separator, Text } from 'ui/src'
......@@ -8,7 +7,8 @@ import { WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { logger } from 'utilities/src/logger/logger'
import { mnemonicUnlockedQuery } from 'wallet/src/features/wallet/Keyring/queries'
import { useAsyncData } from 'utilities/src/react/hooks'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
function SeedPhraseColumnGroup({ recoveryPhraseArray }: { recoveryPhraseArray: string[] }): JSX.Element {
const [largestIndexWidth, setLargestIndexWidth] = useState(0)
......@@ -95,7 +95,9 @@ function SeedPhraseWord({
export function SeedPhraseDisplay({ mnemonicId }: { mnemonicId: string }): JSX.Element {
const placeholderWordArrayLength = 12
const { data: recoveryPhraseString } = useQuery(mnemonicUnlockedQuery(mnemonicId))
const recoveryPhraseString = useAsyncData(
useCallback(async () => Keyring.retrieveMnemonicUnlocked(mnemonicId), [mnemonicId]),
).data
const recoveryPhraseArray = recoveryPhraseString?.split(' ') ?? Array(placeholderWordArrayLength).fill('')
const onCopyPress = async (): Promise<void> => {
......
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { SCREEN_ITEM_HORIZONTAL_PAD } from 'src/app/constants'
import { SettingsItemWithDropdown } from 'src/app/features/settings/SettingsItemWithDropdown'
import ThemeToggle from 'src/app/features/settings/ThemeToggle'
import { SettingsItem } from 'src/app/features/settings/components/SettingsItem'
import { SettingsSection } from 'src/app/features/settings/components/SettingsSection'
import { SettingsToggleRow } from 'src/app/features/settings/components/SettingsToggleRow'
import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { useExtensionNavigation } from 'src/app/navigation/utils'
import { getIsDefaultProviderFromStorage, setIsDefaultProviderToStorage } from 'src/app/utils/provider'
import { Button, Flex, ScrollView, Text } from 'ui/src'
import {
Button,
ColorTokens,
Flex,
GeneratedIcon,
ScrollView,
Switch,
Text,
TouchableArea,
useSporeColors,
} from 'ui/src'
import {
ArrowUpRight,
Chart,
......@@ -23,10 +32,12 @@ import {
LineChartDots,
Lock,
Passkey,
RotatableChevron,
Settings,
Sliders,
Wrench,
} from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { resetUniswapBehaviorHistory } from 'uniswap/src/features/behaviorHistory/slice'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
......@@ -322,3 +333,124 @@ export function SettingsScreen(): JSX.Element {
</Trace>
)
}
function SettingsItem({
Icon,
title,
onPress,
iconProps,
themeProps,
url,
count,
hideChevron = false,
RightIcon,
}: {
Icon: GeneratedIcon
title: string
hideChevron?: boolean
RightIcon?: GeneratedIcon
onPress?: () => void
iconProps?: { strokeWidth?: number }
// TODO: do this with a wrapping Theme, "detrimental" wasn't working
themeProps?: { color?: string; hoverColor?: string }
url?: string
count?: number
}): JSX.Element {
const colors = useSporeColors()
const hoverColor = themeProps?.hoverColor ?? colors.surface2.val
const content = (
<TouchableArea
alignItems="center"
borderRadius="$rounded12"
flexDirection="row"
flexGrow={1}
gap="$spacing12"
hoverStyle={{
backgroundColor: hoverColor as ColorTokens,
}}
justifyContent="space-between"
px="$spacing12"
py="$spacing8"
onPress={onPress}
>
<Flex row justifyContent="space-between" flexGrow={1}>
<Flex row gap="$spacing12">
<Icon
color={themeProps?.color ?? '$neutral2'}
size="$icon.24"
strokeWidth={iconProps?.strokeWidth ?? undefined}
/>
<Text style={{ color: themeProps?.color ?? colors.neutral1.val }} variant="subheading2">
{title}
</Text>
</Flex>
{count !== undefined && (
<Text alignSelf="center" color="$neutral2" variant="subheading2">
{count}
</Text>
)}
</Flex>
{RightIcon ? (
<RightIcon color="$neutral3" size="$icon.24" strokeWidth={iconProps?.strokeWidth ?? undefined} />
) : (
!hideChevron && (
<RotatableChevron color="$neutral3" direction="end" height={iconSizes.icon20} width={iconSizes.icon20} />
)
)}
</TouchableArea>
)
if (url) {
return (
<Link style={{ textDecoration: 'none' }} target="_blank" to={url}>
{content}
</Link>
)
}
return content
}
function SettingsToggleRow({
Icon,
title,
checked,
disabled,
onCheckedChange,
}: {
title: string
Icon: GeneratedIcon
checked: boolean
disabled?: boolean
onCheckedChange: (checked: boolean) => void
}): JSX.Element {
return (
<Flex
alignItems="center"
flexDirection="row"
gap="$spacing16"
justifyContent="space-between"
px={SCREEN_ITEM_HORIZONTAL_PAD}
py="$spacing4"
>
<Flex row gap="$spacing12">
<Icon color="$neutral2" size="$icon.24" />
<Text>{title}</Text>
</Flex>
<Switch checked={checked} variant="branded" disabled={disabled} onCheckedChange={onCheckedChange} />
</Flex>
)
}
function SettingsSection({ title, children }: { title: string; children: JSX.Element | JSX.Element[] }): JSX.Element {
return (
<Flex gap="$spacing4">
<Text color="$neutral2" px={SCREEN_ITEM_HORIZONTAL_PAD} variant="subheading2">
{title}
</Text>
{children}
</Flex>
)
}
import { Link } from 'react-router-dom'
import { ColorTokens, Flex, GeneratedIcon, Text, TouchableArea, useSporeColors } from 'ui/src'
import { RotatableChevron } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
export function SettingsItem({
Icon,
title,
onPress,
iconProps,
themeProps,
url,
count,
hideChevron = false,
RightIcon,
}: {
Icon: GeneratedIcon
title: string
hideChevron?: boolean
RightIcon?: GeneratedIcon
onPress?: () => void
iconProps?: { strokeWidth?: number }
// TODO: do this with a wrapping Theme, "detrimental" wasn't working
themeProps?: { color?: string; hoverColor?: string }
url?: string
count?: number
}): JSX.Element {
const colors = useSporeColors()
const hoverColor = themeProps?.hoverColor ?? colors.surface2.val
const content = (
<TouchableArea
alignItems="center"
borderRadius="$rounded12"
flexDirection="row"
flexGrow={1}
gap="$spacing12"
hoverStyle={{
backgroundColor: hoverColor as ColorTokens,
}}
justifyContent="space-between"
px="$spacing12"
py="$spacing8"
onPress={onPress}
>
<Flex row justifyContent="space-between" flexGrow={1}>
<Flex row gap="$spacing12">
<Icon
color={themeProps?.color ?? '$neutral2'}
size="$icon.24"
strokeWidth={iconProps?.strokeWidth ?? undefined}
/>
<Text style={{ color: themeProps?.color ?? colors.neutral1.val }} variant="subheading2">
{title}
</Text>
</Flex>
{count !== undefined && (
<Text alignSelf="center" color="$neutral2" variant="subheading2">
{count}
</Text>
)}
</Flex>
{RightIcon ? (
<RightIcon color="$neutral3" size="$icon.24" strokeWidth={iconProps?.strokeWidth ?? undefined} />
) : (
!hideChevron && (
<RotatableChevron color="$neutral3" direction="end" height={iconSizes.icon20} width={iconSizes.icon20} />
)
)}
</TouchableArea>
)
if (url) {
return (
<Link style={{ textDecoration: 'none' }} target="_blank" to={url}>
{content}
</Link>
)
}
return content
}
import { SCREEN_ITEM_HORIZONTAL_PAD } from 'src/app/constants'
import { Flex, Text } from 'ui/src'
export function SettingsSection({
title,
children,
}: {
title: string
children: JSX.Element | JSX.Element[]
}): JSX.Element {
return (
<Flex gap="$spacing4">
<Text color="$neutral2" px={SCREEN_ITEM_HORIZONTAL_PAD} variant="subheading2">
{title}
</Text>
{children}
</Flex>
)
}
import { SCREEN_ITEM_HORIZONTAL_PAD } from 'src/app/constants'
import { Flex, GeneratedIcon, Switch, Text } from 'ui/src'
export function SettingsToggleRow({
Icon,
title,
checked,
disabled,
onCheckedChange,
}: {
title: string
Icon: GeneratedIcon
checked: boolean
disabled?: boolean
onCheckedChange: (checked: boolean) => void
}): JSX.Element {
return (
<Flex
alignItems="center"
flexDirection="row"
gap="$spacing16"
justifyContent="space-between"
px={SCREEN_ITEM_HORIZONTAL_PAD}
py="$spacing4"
>
<Flex row gap="$spacing12">
<Icon color="$neutral2" size="$icon.24" />
<Text>{title}</Text>
</Flex>
<Switch checked={checked} variant="branded" disabled={disabled} onCheckedChange={onCheckedChange} />
</Flex>
)
}
import { useCallback, useEffect, useState } from 'react'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { ENCRYPTION_KEY_STORAGE_KEY, PersistedStorage } from 'wallet/src/utils/persistedStorage'
......@@ -52,9 +53,7 @@ export function useIsWalletUnlocked(): boolean | null {
}
}, [checkWalletStatus])
useEffect(() => {
checkWalletStatus()
}, [checkWalletStatus])
useAsyncData(checkWalletStatus)
return isUnlocked
}
import { useMutation } from '@tanstack/react-query'
import { useEffect, useMemo, useRef } from 'react'
import { useCallback, useMemo, useRef } from 'react'
import { useSelector } from 'react-redux'
import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router-dom'
import { SmartWalletNudgeModals } from 'src/app/components/modals/SmartWalletNudgeModals'
......@@ -19,7 +18,7 @@ import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector'
import { AnimatePresence, Flex, SpinningLoader, styled } from 'ui/src'
import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner'
import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused'
import { useEvent, usePrevious } from 'utilities/src/react/hooks'
import { useAsyncData, usePrevious } from 'utilities/src/react/hooks'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater'
import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext'
......@@ -231,27 +230,17 @@ function LoggedOut(): JSX.Element {
const isOnboarded = useSelector(isOnboardedSelector)
const didOpenOnboarding = useRef(false)
const focusOrCreateOnboardingTabMutation = useMutation({
onMutate: () => {
const handleOnboarding = useCallback(async () => {
if (!isOnboarded && !didOpenOnboarding.current) {
// We keep track of this to avoid opening the onboarding page multiple times if this component remounts.
didOpenOnboarding.current = true
},
mutationFn: () => {
return focusOrCreateOnboardingTab()
},
onSuccess: () => {
await focusOrCreateOnboardingTab()
// Automatically close the pop up after focusing on the onboarding tab.
window.close()
},
})
const focusOrCreateOnboardingTabEvent = useEvent(focusOrCreateOnboardingTabMutation.mutate)
useEffect(() => {
if (!focusOrCreateOnboardingTabMutation.isPending && !isOnboarded && !didOpenOnboarding.current) {
focusOrCreateOnboardingTabEvent()
}
}, [focusOrCreateOnboardingTabEvent, isOnboarded, focusOrCreateOnboardingTabMutation.isPending])
}, [isOnboarded])
useAsyncData(handleOnboarding)
// If the user has not onboarded, we render nothing and let the `useEffect` above automatically close the popup.
// We could consider showing a loading spinner while the popup is being closed.
......
......@@ -4,7 +4,7 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or
import { EXTENSION_ORIGIN_APPLICATION } from 'src/app/version'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { getUniqueId } from 'utilities/src/device/uniqueId'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { analytics, getAnalyticsAtomDirect } from 'utilities/src/telemetry/analytics/analytics'
......
......@@ -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.23.0",
"version": "1.22.2",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
......@@ -24,7 +24,6 @@ import {
v20Schema,
v21Schema,
v22Schema,
v23Schema,
v2Schema,
v3Schema,
v4Schema,
......@@ -58,7 +57,6 @@ import {
testAddCreatedOnboardingRedesignAccount,
testAddedHapticSetting,
testDeleteWelcomeWalletCard,
testMoveHapticsToUserSettings,
testMoveTokenAndNFTVisibility,
testMovedCurrencySetting,
testMovedLanguageSetting,
......@@ -322,8 +320,4 @@ describe('Redux state migrations', () => {
it('migrates from v22 to v23', () => {
testMigrateUnknownBackupAccountsToMaybeManualBackup(migrations[23], v22Schema)
})
it('migrates from v23 to v24', () => {
testMoveHapticsToUserSettings(migrations[24], v23Schema)
})
})
......@@ -21,7 +21,6 @@ import {
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting,
moveDismissedTokenWarnings,
moveHapticsToUserSettings,
moveLanguageSetting,
moveTokenAndNFTVisibility,
moveUserSettings,
......@@ -56,7 +55,6 @@ export const migrations = {
21: migratePendingDappRequestsToRecord,
22: addBatchedTransactions,
23: migrateUnknownBackupAccountsToMaybeManualBackup,
24: moveHapticsToUserSettings,
}
export const EXTENSION_STATE_VERSION = 24
export const EXTENSION_STATE_VERSION = 23
......@@ -254,20 +254,5 @@ export const v22Schema = {
batchedTransactions: {},
}
export const v23Schema = v22Schema
const v24SchemaIntermediate = {
...v23Schema,
appearanceSettings: {
...v23Schema.appearanceSettings,
hapticsEnabled: undefined,
},
userSettings: {
...v23Schema.userSettings,
hapticsEnabled: v23Schema.appearanceSettings.hapticsEnabled,
},
}
delete v24SchemaIntermediate.appearanceSettings.hapticsEnabled
const v24Schema = v24SchemaIntermediate
export const getSchema = (): typeof v24Schema => v24Schema
const v23Schema = v22Schema
export const getSchema = (): typeof v23Schema => v23Schema
......@@ -37,3 +37,6 @@ env:
- waitForAnimationToEnd
- runFlow: biometrics-confirm.yaml
- waitForAnimationToEnd
- tapOn:
id: ${output.testIds.SmartWalletUpgradeModalMaybeLater}
- waitForAnimationToEnd
......@@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/monochrome"/>
</adaptive-icon>
......@@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/monochrome"/>
</adaptive-icon>
\ No newline at end of file
......@@ -89,7 +89,7 @@
"@shopify/react-native-performance-navigation": "3.0.0",
"@shopify/react-native-skia": "1.7.2",
"@sparkfabrik/react-native-idfa-aaid": "1.2.0",
"@tanstack/react-query": "5.77.2",
"@tanstack/react-query": "5.51.16",
"@testing-library/react-hooks": "8.0.1",
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.42.0",
......@@ -112,6 +112,7 @@
"expo-blur": "14.0.3",
"expo-camera": "16.0.18",
"expo-clipboard": "7.0.1",
"expo-haptics": "14.0.1",
"expo-linear-gradient": "14.0.2",
"expo-linking": "7.0.5",
"expo-local-authentication": "15.0.2",
......
#!/bin/bash
MAX_SIZE=23.66
MAX_SIZE=24
MAX_BUFFER=0.5
# Check OS type and use appropriate stat command
......
......@@ -89,7 +89,6 @@ import {
v84Schema,
v85Schema,
v86Schema,
v87Schema,
v8Schema,
v9Schema,
} from 'src/app/schema'
......@@ -136,7 +135,6 @@ import {
testMovedLanguageSetting,
testMovedTokenWarnings,
testMovedUserSettings,
testMoveHapticsToUserSettings,
testMoveTokenAndNFTVisibility,
testRemoveCreatedOnboardingRedesignAccount,
testRemoveHoldToSwap,
......@@ -1706,8 +1704,4 @@ describe('Redux state migrations', () => {
},
})
})
it('migrates from v87 to v88', () => {
testMoveHapticsToUserSettings(migrations[88], v87Schema)
})
})
......@@ -34,7 +34,6 @@ import {
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting,
moveDismissedTokenWarnings,
moveHapticsToUserSettings,
moveLanguageSetting,
moveTokenAndNFTVisibility,
moveUserSettings,
......@@ -1054,8 +1053,6 @@ export const migrations = {
transactions: newTransactionState,
}
},
88: moveHapticsToUserSettings,
}
export const MOBILE_STATE_VERSION = 88
export const MOBILE_STATE_VERSION = 87
......@@ -9,8 +9,8 @@ import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks'
import { closeModal } from 'src/features/modals/modalSlice'
import { selectModalState } from 'src/features/modals/selectModalState'
import { useWalletRestore } from 'src/features/wallet/useWalletRestore'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { updateSwapStartTimestamp } from 'uniswap/src/features/timing/slice'
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
......
......@@ -16,13 +16,13 @@ import { useDispatch, useSelector } from 'react-redux'
import { useAppStackNavigation } from 'src/app/navigation/types'
import { pulseAnimation } from 'src/components/buttons/utils'
import { openModal } from 'src/features/modals/modalSlice'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { Flex, FlexProps, LinearGradient, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src'
import { Search } from 'ui/src/components/icons'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { borderRadii, fonts, opacify } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances'
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/contexts/selectors'
......
......@@ -4,10 +4,9 @@ import {
NavigationContainer as NativeNavigationContainer,
NavigationContainerRefWithCurrent,
} from '@react-navigation/native'
import { useMutation } from '@tanstack/react-query'
import { SharedEventName } from '@uniswap/analytics-events'
import React, { FC, PropsWithChildren, useEffect, useRef, useState } from 'react'
import { EmitterSubscription, Linking } from 'react-native'
import React, { FC, PropsWithChildren, useCallback, useState } from 'react'
import { Linking } from 'react-native'
import { useDispatch } from 'react-redux'
import { navigationRef } from 'src/app/navigation/navigationRef'
import { RootParamList } from 'src/app/navigation/types'
......@@ -20,8 +19,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { MobileNavScreen } from 'uniswap/src/types/screens/mobile'
import { datadogEnabledBuild } from 'utilities/src/environment/constants'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } from 'utilities/src/react/hooks'
import { useAsyncData } from 'utilities/src/react/hooks'
import { sleep } from 'utilities/src/time/timing'
interface Props {
......@@ -89,47 +87,20 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
const useManageDeepLinks = (): void => {
const dispatch = useDispatch()
const hasRun = useRef(false)
const urlListener = useRef<EmitterSubscription | undefined>()
const deepLinkMutation = useMutation({
mutationFn: async () => {
if (hasRun.current) {
return
}
const manageDeepLinks = useCallback(async () => {
const url = await Linking.getInitialURL()
if (url) {
dispatch(openDeepLink({ url, coldStart: true }))
}
// we need to set an event listener for deep links, but we don't want to do it immediately on cold start,
// as then there is a change we dispatch `openDeepLink` action twice if app was launched by a deep link
await sleep(2000) // 2000 was chosen empirically
urlListener.current = Linking.addEventListener('url', (event: { url: string }) =>
// as then there is a change we dispatch `openDeepLink` action twice if app was lauched by a deep link
await sleep(2000) // 2000 was chosen imperically
const urlListener = Linking.addEventListener('url', (event: { url: string }) =>
dispatch(openDeepLink({ url: event.url, coldStart: false })),
)
},
onMutate: () => {
hasRun.current = true
},
onError: (error) => {
logger.error(error, {
tags: {
file: 'NavigationContainer',
function: 'useManageDeepLinks',
},
})
},
})
const deepLinkEvent = useEvent(deepLinkMutation.mutate)
return urlListener.remove
}, [dispatch])
useEffect(() => {
deepLinkEvent()
return () => {
if (urlListener.current) {
urlListener.current.remove()
}
}
}, [deepLinkEvent])
useAsyncData(manageDeepLinks)
}
......@@ -679,22 +679,8 @@ export const v86Schema = {
batchedTransactions: {},
}
export const v87Schema = v86Schema
const v88SchemaIntermediate = {
...v87Schema,
appearanceSettings: {
...v87Schema.appearanceSettings,
hapticsEnabled: undefined,
},
userSettings: {
...v87Schema.userSettings,
hapticsEnabled: v87Schema.appearanceSettings.hapticsEnabled,
},
}
delete v88SchemaIntermediate.appearanceSettings.hapticsEnabled
const v88Schema = v88SchemaIntermediate
const v87Schema = v86Schema
// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v88Schema => v88Schema
export const getSchema = (): typeof v87Schema => v87Schema
......@@ -11,6 +11,7 @@ import { useLineChartPrice } from 'src/components/PriceExplorer/usePrice'
import { PriceNumberOfDigits, TokenSpotData, useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory'
import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext'
import { Loader } from 'src/components/loading/loaders'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady'
import { Flex, SegmentedControl, Text } from 'ui/src'
import GraphCurve from 'ui/src/assets/backgrounds/graph-curve.svg'
......@@ -19,7 +20,6 @@ import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__gen
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementNameType } from 'uniswap/src/features/telemetry/constants'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
......
import { useMutation } from '@tanstack/react-query'
import { LinearGradient } from 'expo-linear-gradient'
import { ComponentProps, default as React, useCallback, useEffect, useMemo, useRef } from 'react'
import { ComponentProps, default as React, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
......@@ -9,8 +8,7 @@ import { Flex, Text, useSporeColors } from 'ui/src'
import { opacify, spacing } from 'ui/src/theme'
import { PollingInterval } from 'uniswap/src/constants/misc'
import { AccountType } from 'uniswap/src/features/accounts/types'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } from 'utilities/src/react/hooks'
import { useAsyncData } from 'utilities/src/react/hooks'
import { isNonPollingRequestInFlight } from 'wallet/src/data/utils'
import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData'
import { Account } from 'wallet/src/features/wallet/accounts/types'
......@@ -52,7 +50,6 @@ type AccountListItem =
export function AccountList({ accounts, onPress, isVisible, onClose }: AccountListProps): JSX.Element {
const colors = useSporeColors()
const addresses = useMemo(() => accounts.map((a) => a.address), [accounts])
const hasPollingRun = useRef(false)
const { data, networkStatus, refetch, startPolling, stopPolling } = useAccountListData({
addresses,
......@@ -61,35 +58,15 @@ export function AccountList({ accounts, onPress, isVisible, onClose }: AccountLi
// Only poll account total values when the account list is visible
const controlPolling = useCallback(async () => {
if (hasPollingRun.current) {
return
}
if (isVisible) {
refetch()
await refetch()
startPolling(PollingInterval.Fast)
} else {
stopPolling()
}
}, [isVisible, refetch, startPolling, stopPolling])
const controlPollingMutation = useMutation({
mutationFn: controlPolling,
onMutate: () => {
hasPollingRun.current = true
},
onError: (error) => {
logger.error(error, {
tags: { file: 'AccountList', function: 'controlPolling' },
})
},
})
const controlPollingEvent = useEvent(controlPollingMutation.mutate)
useEffect(() => {
controlPollingEvent()
}, [controlPollingEvent])
useAsyncData(controlPolling)
const isPortfolioValueLoading = isNonPollingRequestInFlight(networkStatus)
......
......@@ -7,11 +7,11 @@ import Sortable from 'react-native-sortables'
import { useDispatch, useSelector } from 'react-redux'
import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow'
import FavoriteTokenCard from 'src/components/explore/FavoriteTokenCard'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { getTokenValue } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors'
import { setFavoriteTokens } from 'uniswap/src/features/favorites/slice'
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
const NUM_COLUMNS = 2
......
import { NativeModules } from 'react-native'
interface RNCloudStorageBackupsManager {
isCloudStorageAvailable: () => Promise<boolean>
deleteCloudStorageMnemonicBackup: (mnemonicId: string) => Promise<boolean>
......@@ -14,6 +12,7 @@ declare module 'react-native' {
RNCloudStorageBackupsManager: RNCloudStorageBackupsManager
}
}
import { NativeModules } from 'react-native'
const { RNCloudStorageBackupsManager } = NativeModules
......
......@@ -5,7 +5,7 @@ import { config } from 'uniswap/src/config'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { getFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/gating/hooks'
import { GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE } from 'uniswap/src/features/portfolio/portfolioUpdates/constants'
import { getUniqueId } from 'utilities/src/device/uniqueId'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { isIOS } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
......
import React, { useEffect, useState } from 'react'
import { SEND_CONTENT_RENDER_DELAY_MS } from 'src/features/send/constants'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { Flex } from 'ui/src/components/layout/Flex'
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
import { TransactionModalInnerContainer } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal'
import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext'
import { SendReviewDetails } from 'wallet/src/features/transactions/send/SendReviewDetails'
......
......@@ -4,7 +4,7 @@ import DeviceInfo from 'react-native-device-info'
import { call, delay, fork, select } from 'typed-redux-saga'
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { MobileUserPropertyName } from 'uniswap/src/features/telemetry/user'
import { getUniqueId } from 'utilities/src/device/uniqueId'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { isAndroid } from 'utilities/src/platform'
import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
......
......@@ -175,9 +175,7 @@ export function* handleGetCapabilities({
const detailsMap = delegationStatusResponse.delegationDetails[accountAddress]
if (detailsMap) {
const hasAtLeastOneDelegation = Object.values(detailsMap).some(
(details) => !!details.currentDelegationAddress && !details.isWalletDelegatedToUniswap,
)
const hasAtLeastOneDelegation = Object.values(detailsMap).some((details) => !!details.currentDelegationAddress)
hasNoExistingDelegations = !hasAtLeastOneDelegation
}
......
import React, { useState } from 'react'
import { I18nManager, ScrollView } from 'react-native'
import { getUniqueIdSync } from 'react-native-device-info'
import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { BackButton } from 'src/components/buttons/BackButton'
......@@ -14,7 +13,9 @@ import { resetDismissedWarnings } from 'uniswap/src/features/tokens/slice/slice'
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { setClipboard } from 'uniswap/src/utils/clipboard'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
import { UniconSampleSheet } from 'wallet/src/components/DevelopmentOnly/UniconSampleSheet'
import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount'
import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga'
......@@ -33,7 +34,7 @@ export function DevScreen(): JSX.Element {
const activeAccount = useActiveAccount()
const [rtlEnabled, setRTLEnabled] = useState(I18nManager.isRTL)
const sortedMnemonicAccounts = useSelector(selectSortedSignerMnemonicAccounts)
const deviceId = getUniqueIdSync()
const { data: deviceId } = useAsyncData(getUniqueId)
const onPressResetTokenWarnings = (): void => {
dispatch(resetDismissedWarnings())
......
......@@ -4,13 +4,13 @@ import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { useOpenReceiveModal } from 'src/features/modals/hooks/useOpenReceiveModal'
import { openModal } from 'src/features/modals/modalSlice'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { ArrowDownCircle, Bank, SendAction } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants'
......
......@@ -35,36 +35,36 @@ const validateForm = ({
validAddress,
name,
walletExists,
isLoading,
loading,
isSmartContractAddress,
isValidSmartContract,
}: {
validAddress: string | null
name: string | null
walletExists: boolean
isLoading: boolean
loading: boolean
isSmartContractAddress: boolean
isValidSmartContract: boolean
}): boolean => {
return (!!validAddress || !!name) && !walletExists && !isLoading && (!isSmartContractAddress || isValidSmartContract)
return (!!validAddress || !!name) && !walletExists && !loading && (!isSmartContractAddress || isValidSmartContract)
}
const getErrorText = ({
walletExists,
isSmartContractAddress,
isLoading,
loading,
t,
}: {
walletExists: boolean
isSmartContractAddress: boolean
isLoading: boolean
loading: boolean
t: TFunction
}): string | undefined => {
if (walletExists) {
return t('account.wallet.watch.error.alreadyImported')
} else if (isSmartContractAddress) {
return t('account.wallet.watch.error.smartContract')
} else if (!isLoading) {
} else if (!loading) {
return t('account.wallet.watch.error.notFound')
}
return undefined
......@@ -91,7 +91,7 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX
autocompleteDomain: !hasSuffixIncluded,
})
const validAddress = getValidAddress(normalizedValue, true, false)
const { isSmartContractAddress, loading: isLoading } = useIsSmartContractAddress(
const { isSmartContractAddress, loading } = useIsSmartContractAddress(
(validAddress || resolvedAddress) ?? undefined,
defaultChainId,
)
......@@ -115,12 +115,12 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX
validAddress,
name,
walletExists,
isLoading,
loading,
isSmartContractAddress,
isValidSmartContract,
})
const errorText = !isValid ? getErrorText({ walletExists, isSmartContractAddress, isLoading, t }) : undefined
const errorText = !isValid ? getErrorText({ walletExists, isSmartContractAddress, loading, t }) : undefined
const onSubmit = useCallback(async () => {
if (isValid && value) {
......
......@@ -28,6 +28,7 @@ import {
} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled'
import { useWalletRestore } from 'src/features/wallet/useWalletRestore'
import { importFromCloudBackupOption, restoreFromCloudBackupOption } from 'src/screens/Import/constants'
import { useHapticFeedback } from 'src/utils/haptics/useHapticFeedback'
import { Flex, IconProps, Text, useSporeColors } from 'ui/src'
import {
Bell,
......@@ -60,7 +61,6 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks'
import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice'
import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback'
import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
......
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { ImpactFeedbackStyle, NotificationFeedbackType, impactAsync, notificationAsync } from 'expo-haptics'
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { setHapticsEnabled } from 'uniswap/src/features/settings/slice'
import {
HapticFeedback,
HapticFeedbackControl,
HapticFeedbackStyle,
NO_HAPTIC_FEEDBACK,
} from 'uniswap/src/features/settings/useHapticFeedback/types'
import { UniswapState } from 'uniswap/src/state/uniswapReducer'
import { selectHapticsEnabled, setHapticsUserSettingEnabled } from 'wallet/src/features/appearance/slice'
type HapticFeedbackStyle = ImpactFeedbackStyle | NotificationFeedbackType
type HapticFeedback = {
impact: (style?: HapticFeedbackStyle) => Promise<void>
light: () => Promise<void>
success: () => Promise<void>
}
const NO_HAPTIC_FEEDBACK: HapticFeedback = {
impact: async () => Promise.resolve(),
light: async () => Promise.resolve(),
success: async () => Promise.resolve(),
}
const ENABLED_HAPTIC_FEEDBACK: HapticFeedback = {
impact: (style?: HapticFeedbackStyle) => {
......@@ -24,13 +30,19 @@ function isImpactFeedbackStyle(style: HapticFeedbackStyle): style is ImpactFeedb
return Object.values(ImpactFeedbackStyle).includes(style as ImpactFeedbackStyle)
}
interface HapticFeedbackControl {
hapticFeedback: HapticFeedback
hapticsEnabled: boolean
setHapticsEnabled: (willBeEnabled: boolean) => void
}
export function useHapticFeedback(): HapticFeedbackControl {
const hapticsEnabled = useSelector((state: UniswapState) => state.userSettings.hapticsEnabled)
const hapticsEnabled = useSelector(selectHapticsEnabled)
const dispatch = useDispatch()
const handleSetEnabled = useCallback(
(enabled: boolean): void => {
dispatch(setHapticsEnabled(enabled))
dispatch(setHapticsUserSettingEnabled(enabled))
},
[dispatch],
)
......
......@@ -129,7 +129,7 @@
"@uniswap/eslint-config": "workspace:^",
"@vercel/og": "0.5.8",
"@vitejs/plugin-react": "4.4.1",
"@wagmi/core": "2.17.2",
"@wagmi/core": "2.10.2",
"babel-jest": "29.7.0",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"browser-cache-mock": "0.1.7",
......@@ -211,8 +211,8 @@
"@tamagui/portal": "1.125.17",
"@tamagui/react-native-svg": "1.125.17",
"@tanstack/query-sync-storage-persister": "5.75.0",
"@tanstack/react-query": "5.77.2",
"@tanstack/react-query-persist-client": "5.77.2",
"@tanstack/react-query": "5.51.16",
"@tanstack/react-query-persist-client": "5.75.2",
"@tanstack/react-table": "8.21.2",
"@types/react-scroll-sync": "0.9.0",
"@uniswap/analytics": "1.7.0",
......@@ -295,8 +295,8 @@
"utilities": "workspace:^",
"uuid": "9.0.0",
"video-extensions": "1.2.0",
"viem": "2.30.5",
"wagmi": "2.15.4",
"viem": "2.22.9",
"wagmi": "2.9.3",
"wcag-contrast": "3.0.0",
"web-vitals": "2.1.4",
"xml2js": "0.6.2",
......
......@@ -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>
This diff is collapsed.
This diff is collapsed.
......@@ -9,7 +9,4 @@
<sitemap>
<loc>https://app.uniswap.org/pools-sitemap.xml</loc>
</sitemap>
<sitemap>
<loc>https://app.uniswap.org/nfts-sitemap.xml</loc>
</sitemap>
</sitemapindex>
This diff is collapsed.
......@@ -35,9 +35,8 @@ function AssetActivityProviderInternal({ children }: PropsWithChildren) {
chains: gqlChains,
// Backend will return off-chain activities even if gqlChains are all testnets.
includeOffChain: !isTestnetModeEnabled,
// Include the externalsessionIDs of all FOR transactions in the local store,
// Include the externalsessionIDs of all fiat on-ramp transactions in the local store,
// so that the backend can find the transactions without signature authentication.
// Note: No FOR transactions are included in activity without explicity passing IDs from local storage
onRampTransactionIDs: transactionIds,
}),
[account.address, gqlChains, isTestnetModeEnabled, transactionIds],
......
......@@ -22,7 +22,6 @@ import { useIsUniExtensionConnected } from 'hooks/useIsUniExtensionConnected'
import { useModalState } from 'hooks/useModalState'
import { useSignOutWithPasskey } from 'hooks/useSignOutWithPasskey'
import { useAtom } from 'jotai'
import { SendFormModal } from 'pages/Swap/Send/SendFormModal'
import { useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
......@@ -31,7 +30,7 @@ import { useUserHasAvailableClaim, useUserUnclaimedAmount } from 'state/claim/ho
import { CopyHelper } from 'theme/components/CopyHelper'
import { Button, Flex, Text } from 'ui/src'
import { ArrowDownCircleFilled } from 'ui/src/components/icons/ArrowDownCircleFilled'
import { SendAction } from 'ui/src/components/icons/SendAction'
import { Bank } from 'ui/src/components/icons/Bank'
import { Shine } from 'ui/src/loading/Shine'
import AnimatedNumber, {
BALANCE_CHANGE_INDICATION_DURATION,
......@@ -57,6 +56,7 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { shortenAddress } from 'utilities/src/addresses'
import { NumberType } from 'utilities/src/format/types'
import { useEvent } from 'utilities/src/react/hooks'
import { isPathBlocked } from 'utils/blockedPaths'
export default function AuthenticatedHeader({ account, openSettings }: { account: string; openSettings: () => void }) {
const { disconnect } = useDisconnect()
......@@ -66,6 +66,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const [modalState, setModalState] = useAtom(miniPortfolioModalStateAtom)
const shouldShowBuyFiatButton = !isPathBlocked('/buy')
const isUniExtensionConnected = useIsUniExtensionConnected()
const { isTestnetModeEnabled } = useEnabledChains()
const connectedAccount = useAccount()
......@@ -98,11 +99,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const openAddressQRModal = useEvent(() => setModalState(ModalState.QR_CODE))
const openCEXTransferModal = useEvent(() => setModalState(ModalState.CEX_TRANSFER))
const openReceiveCryptoModal = useEvent(() => setModalState(ModalState.DEFAULT))
const {
isOpen: isSendFormModalOpen,
openModal: openSendFormModal,
closeModal: closeSendFormModal,
} = useModalState(ModalName.Send)
const { data, networkStatus, loading } = usePortfolioTotalValue({
address: account,
......@@ -218,12 +214,14 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
) : (
<>
<Flex row gap="$gap8">
{shouldShowBuyFiatButton && (
<ActionTile
dataTestId={TestID.Send}
Icon={<SendAction size={24} color="$accent1" />}
name={t('common.send.button')}
onClick={openSendFormModal}
dataTestId={TestID.WalletBuyCrypto}
Icon={<Bank size={24} color="$accent1" />}
name={t('common.buy.label')}
onClick={handleBuyCryptoClick}
/>
)}
<ActionTile
dataTestId={TestID.WalletReceiveCrypto}
Icon={<ArrowDownCircleFilled size={24} color="$accent1" />}
......@@ -250,7 +248,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
</Flex>
</Flex>
{modalState !== undefined && <ReceiveCryptoModal />}
{isSendFormModalOpen && <SendFormModal isModalOpen={isSendFormModalOpen} onClose={closeSendFormModal} />}
{displayDelegationMismatchModal && (
<DelegationMismatchModal onClose={() => setDisplayDelegationMismatchModal(false)} />
)}
......
......@@ -140,7 +140,7 @@ export function useCancelOrdersGasEstimate(orders?: UniswapXOrderDetails[]): Gas
: undefined,
[orders],
)
const cancelTransaction = useCreateCancelTransactionRequest(cancelTransactionParams) ?? undefined
const cancelTransaction = useCreateCancelTransactionRequest(cancelTransactionParams)
const gasEstimate = useTransactionGasFee(cancelTransaction, GasSpeed.Fast)
return gasEstimate
}
......@@ -12,11 +12,6 @@ import {
OrderTextTable,
getActivityTitle,
} from 'components/AccountDrawer/MiniPortfolio/constants'
import { FiatOnRampTransactionStatus } from 'state/fiatOnRampTransactions/types'
import {
forTransactionStatusToTransactionStatus,
statusToTransactionInfoStatus,
} from 'state/fiatOnRampTransactions/utils'
import { isOnChainOrder, useAllSignatures } from 'state/signatures/hooks'
import { SignatureDetails, SignatureType } from 'state/signatures/types'
import { useMultichainTransactions } from 'state/transactions/hooks'
......@@ -44,7 +39,6 @@ import { nativeOnChain } from 'uniswap/src/constants/tokens'
import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FORTransaction } from 'uniswap/src/features/fiatOnRamp/types'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import i18n from 'uniswap/src/i18n'
import { isAddress } from 'utilities/src/addresses'
......@@ -53,7 +47,6 @@ import { logger } from 'utilities/src/logger/logger'
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
type FormatNumberFunctionType = ReturnType<typeof useLocalizationContext>['formatNumberOrString']
type FormatFiatPriceFunctionType = ReturnType<typeof useLocalizationContext>['convertFiatAmountFormatted']
function buildCurrencyDescriptor(
currencyA: Currency | undefined,
......@@ -392,58 +385,6 @@ export function getSignatureToActivityQueryOptions(
})
}
export function getFORTransactionToActivityQueryOptions(
transaction: FORTransaction | undefined,
formatNumber: FormatNumberFunctionType,
formatFiatPrice: FormatFiatPriceFunctionType,
) {
return queryOptions({
queryKey: [ReactQueryCacheKey.TransactionToActivity, transaction],
queryFn: async () => forTransactionToActivity(transaction, formatNumber, formatFiatPrice),
})
}
const forTransactionToActivity = async (
transaction: FORTransaction | undefined,
formatNumber: FormatNumberFunctionType,
formatFiatPrice: FormatFiatPriceFunctionType,
) => {
if (!transaction) {
return undefined
}
const chainId = Number(transaction.cryptoDetails.chainId) as UniverseChainId
const currency = await getCurrency(transaction.sourceCurrencyCode, chainId)
const status = statusToTransactionInfoStatus(transaction.status)
const serviceProvider = transaction.serviceProviderDetails.name
const tokenAmount = formatNumber({ value: transaction.sourceAmount, type: NumberType.TokenNonTx })
const fiatAmount = formatFiatPrice(transaction.destinationAmount, NumberType.FiatTokenPrice)
let title = ''
switch (status) {
case FiatOnRampTransactionStatus.PENDING:
title = i18n.t('transaction.status.sale.pendingOn', { serviceProvider })
break
case FiatOnRampTransactionStatus.COMPLETE:
title = i18n.t('transaction.status.sale.successOn', { serviceProvider })
break
case FiatOnRampTransactionStatus.FAILED:
title = i18n.t('transaction.status.sale.failedOn', { serviceProvider })
break
}
return {
hash: transaction.externalSessionId,
chainId,
title,
descriptor: `${tokenAmount} ${transaction?.sourceCurrencyCode} ${i18n.t('common.for').toLocaleLowerCase()} ${fiatAmount}`,
currencies: [currency],
status: forTransactionStatusToTransactionStatus(status),
timestamp: convertToSecTimestamp(Number(transaction.createdAt)),
from: transaction.cryptoDetails.walletAddress,
}
}
function convertToSecTimestamp(timestamp: number) {
// UNIX timestamp in ms for Jan 1, 2100
const threshold: number = 4102444800000
......
......@@ -34,7 +34,6 @@ import {
NftApprovalPartsFragment,
NftApproveForAllPartsFragment,
NftTransferPartsFragment,
OffRampTransactionDetailsPartsFragment,
OnRampTransactionDetailsPartsFragment,
OnRampTransferPartsFragment,
TokenApprovalPartsFragment,
......@@ -486,7 +485,6 @@ function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: For
type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }
type FiatOnRampActivity = AssetActivityPartsFragment & { details: OnRampTransactionDetailsPartsFragment }
type FiatOffRampActivity = AssetActivityPartsFragment & { details: OffRampTransactionDetailsPartsFragment }
function parseSendReceive(
changes: TransactionChanges,
......@@ -738,42 +736,6 @@ function parseFiatOnRampTransaction(activity: TransactionActivity | FiatOnRampAc
}
}
function parseFiatOffRampTransaction(activity: FiatOffRampActivity): Activity {
const chainId = supportedChainIdFromGQLChain(activity.chain)
if (!chainId) {
const error = new Error('Invalid activity from unsupported chain received from GQL')
logger.error(error, {
tags: {
file: 'parseRemote',
function: 'parseRemote',
},
extra: { activity },
})
throw error
}
const { offRampTransfer } = activity.details
return {
from: activity.details.senderAddress,
hash: activity.id,
chainId,
timestamp: activity.timestamp,
logos: [offRampTransfer.token.project?.logoUrl],
currencies: [gqlToCurrency(offRampTransfer.token)],
title: i18n.t('transaction.status.sale.successOn', {
serviceProvider: offRampTransfer.serviceProvider.name,
}),
descriptor: i18n.t('fiatOffRamp.exchangeRate', {
inputAmount: offRampTransfer.amount,
inputSymbol: offRampTransfer.token.symbol,
outputAmount: offRampTransfer.destinationAmount,
outputSymbol: offRampTransfer.destinationCurrency,
}),
suffixIconSrc: offRampTransfer.serviceProvider.logoDarkUrl,
status: activity.details.status,
}
}
function parseRemoteActivity(
assetActivity: AssetActivityPartsFragment | undefined,
account: string,
......@@ -784,8 +746,13 @@ function parseRemoteActivity(
return undefined
}
if (assetActivity.details.__typename === 'OffRampTransactionDetails') {
return parseFiatOffRampTransaction(assetActivity as FiatOffRampActivity)
// TODO: skip until offramp transactions are supported
if (
assetActivity.details.__typename === 'OffRampTransactionDetails' ||
(assetActivity.details.__typename === 'TransactionDetails' &&
assetActivity.details.type === TransactionType.OffRamp)
) {
return undefined
}
if (assetActivity.details.__typename === 'SwapOrderDetails') {
......
import { TransactionRequest } from '@ethersproject/abstract-provider'
import { Web3Provider } from '@ethersproject/providers'
import { useQuery } from '@tanstack/react-query'
import { permit2Address } from '@uniswap/permit2-sdk'
import {
CosignedPriorityOrder,
......@@ -30,8 +29,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import i18n from 'uniswap/src/i18n'
import { getContract } from 'utilities/src/contracts/getContract'
import { logger } from 'utilities/src/logger/logger'
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
import { queryWithoutCache } from 'utilities/src/reactQuery/queryOptions'
import { useAsyncData } from 'utilities/src/react/hooks'
import { WrongChainError } from 'utils/errors'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
......@@ -230,7 +228,7 @@ export function useCreateCancelTransactionRequest(
chainId: UniverseChainId
}
| undefined,
): Maybe<TransactionRequest> {
): TransactionRequest | undefined {
const permit2 = useContract<Permit2>(permit2Address(params?.chainId), PERMIT2_ABI, true)
const transactionFetcher = useCallback(() => {
if (
......@@ -239,17 +237,12 @@ export function useCreateCancelTransactionRequest(
params.orders.filter(({ encodedOrder }) => Boolean(encodedOrder)).length === 0 ||
!permit2
) {
return null
return undefined
}
return getCancelMultipleUniswapXOrdersTransaction(params.orders, params.chainId, permit2)
}, [params, permit2])
return useQuery(
queryWithoutCache({
queryKey: [ReactQueryCacheKey.CancelTransactionRequest, params],
queryFn: transactionFetcher,
}),
).data
return useAsyncData(transactionFetcher).data
}
export function isLimitCancellable(order: UniswapXOrderDetails) {
......
......@@ -92,7 +92,7 @@ test.describe('Mini Portfolio account drawer', () => {
// Verify wallet state
await expect(page.getByTestId(TestID.MiniPortfolioNavbar)).toContainText('Tokens')
await expect(page.getByTestId(TestID.MiniPortfolioPage)).toContainText('Hidden (4)')
await expect(page.getByTestId(TestID.MiniPortfolioPage)).toContainText('Hidden (5)')
// Check NFTs section
await page.getByTestId(TestID.MiniPortfolioNavbar).getByText('NFTs').click()
......
import { OutageCloseButton } from 'components/Banner/Outage/OutageBanner'
import { useTheme } from 'lib/styled-components'
import { useState } from 'react'
import { Globe } from 'react-feather'
import { useTranslation } from 'react-i18next'
import { Flex, Text } from 'ui/src'
import { zIndexes } from 'ui/src/theme'
export function MonadOutageBanner() {
const { t } = useTranslation()
const [hidden, setHidden] = useState(false)
const theme = useTheme()
if (hidden) {
return null
}
return (
<Flex
width={360}
maxWidth="95%"
$platform-web={{ position: 'fixed' }}
bottom={40}
right={20}
backgroundColor={theme.surface2}
zIndex={zIndexes.sticky}
borderRadius="$rounded20"
borderStyle="solid"
borderWidth={1.3}
borderColor={theme.surface3}
$lg={{
bottom: 62,
}}
$sm={{
bottom: 80,
}}
$xs={{
right: 10,
left: 10,
}}
>
<Flex row p="$spacing8" borderRadius="$rounded20" height="100%">
<Flex
centered
m="$spacing12"
mr="spacing6"
height={45}
width={45}
backgroundColor={theme.warning2}
borderRadius="$rounded12"
>
<Globe size={28} color={theme.warning2} />
</Flex>
<Flex gap="$spacing2" p={10} $xs={{ maxWidth: 270 }} flexShrink={1}>
<Text variant="body2" color={theme.neutral1}>
{t('home.banner.testnetMode.outage.monad.title')}
</Text>
<Text variant="body3" color={theme.neutral2}>
{t('home.banner.testnetMode.outage.monad.description')}
</Text>
</Flex>
<OutageCloseButton
data-testid="monad-outage-banner"
onClick={() => {
setHidden(true)
}}
/>
</Flex>
</Flex>
)
}
......@@ -18,7 +18,7 @@ export function getOutageBannerSessionStorageKey(chainId: UniverseChainId) {
}
// TODO replace with IconButton when it's available from buttons migration
const OutageCloseButton = tamaguiStyled(X, {
export const OutageCloseButton = tamaguiStyled(X, {
...ClickableTamaguiStyle,
size: iconSizes.icon24,
p: '$spacing4',
......
import { InterfacePageName } from '@uniswap/analytics-events'
import { MonadOutageBanner } from 'components/Banner/Outage/MonadOutageBanner'
import { OutageBanner, getOutageBannerSessionStorageKey } from 'components/Banner/Outage/OutageBanner'
import { LPIncentiveAnnouncementBanner } from 'components/Liquidity/LPIncentiveAnnouncementBanner'
import { manualChainOutageAtom } from 'featureFlags/flags/outageBanner'
import { useAtomValue } from 'jotai/utils'
import { useMemo } from 'react'
import { useLocation } from 'react-router-dom'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
......@@ -18,6 +20,9 @@ export function Banners() {
const manualOutage = useAtomValue(manualChainOutageAtom)
const isMonadDownFlag = useFeatureFlag(FeatureFlags.MonadTestnetDown)
const { isTestnetModeEnabled } = useEnabledChains()
// Calculate the chainId for the current page's contextual chain (e.g. /tokens/ethereum or /tokens/arbitrum), if it exists.
const pageChainId = useMemo(() => {
const chainUrlParam = pathname.split('/').find(isChainUrlParam)
......@@ -40,6 +45,11 @@ export function Banners() {
)
}, [currentPage, currentPageHasManualOutage, pageChainId])
// Monad Outage Banner takes precedence if in testnet mode
if (isMonadDownFlag && isTestnetModeEnabled) {
return <MonadOutageBanner />
}
// Outage Banners should take precedence over other promotional banners
if (pageChainId && showOutageBanner) {
return (
......
......@@ -15,6 +15,8 @@ import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext'
import { DEFAULT_MS_BEFORE_WARNING, getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/hooks/usePollingIntervalByChain'
const BodyRow = styled.div`
......@@ -59,6 +61,7 @@ const CloseButton = tamaguiStyled(X, {
export function ChainConnectivityWarning() {
const { defaultChainId } = useEnabledChains()
const [hide, setHide] = useState(false)
const isMonadDownFlag = useFeatureFlag(FeatureFlags.MonadTestnetDown)
const { swapInputChainId: chainId } = useUniswapContext()
const info = getChainInfo(chainId ?? defaultChainId)
const label = info.label
......@@ -73,8 +76,9 @@ export function ChainConnectivityWarning() {
const blockTime = useCurrentBlockTimestamp({ refetchInterval: ms('5min') })
const warning = Boolean(!!blockTime && machineTime - Number(blockTime) * 1000 > waitMsBeforeWarning)
const isMonadDown = chainId === UniverseChainId.MonadTestnet && isMonadDownFlag
if (hide || !warning || isLandingPage) {
if (hide || (!isMonadDown && (!warning || isLandingPage))) {
return null
}
......
import { Currency, Percent } from '@uniswap/sdk-core'
import { LiquidityBarData } from 'components/Charts/LiquidityChart/types'
import { ChartEntry } from 'components/Charts/LiquidityRangeInput/types'
import { DoubleCurrencyLogo } from 'components/Logo/DoubleLogo'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { Flex, FlexProps, Text } from 'ui/src'
import { Flex, Text } from 'ui/src'
import { iconSizes } from 'ui/src/theme'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice'
......@@ -21,7 +20,7 @@ export function TickTooltip({
baseCurrency,
}: {
hoverY: number
hoveredTick: ChartEntry | LiquidityBarData
hoveredTick: ChartEntry
currentPrice: number
currentTick?: number
containerHeight: number
......@@ -30,41 +29,8 @@ export function TickTooltip({
quoteCurrency: Currency
baseCurrency: Currency
}) {
const atTop = hoverY < 20
const atBottom = containerHeight - hoverY < 20
return (
<TickTooltipContent
position="absolute"
top={hoverY - 18}
right={contentWidth + axisLabelPaneWidth + 8}
transform={atBottom ? 'translateY(-12px)' : atTop ? 'translateY(14px)' : undefined}
currentPrice={currentPrice}
hoveredTick={hoveredTick}
currentTick={currentTick}
quoteCurrency={quoteCurrency}
baseCurrency={baseCurrency}
/>
)
}
export function TickTooltipContent({
currentPrice,
hoveredTick,
currentTick,
quoteCurrency,
baseCurrency,
showQuoteCurrencyFirst = true,
...props
}: {
currentPrice: number
hoveredTick: ChartEntry | LiquidityBarData
currentTick?: number
quoteCurrency: Currency
baseCurrency: Currency
showQuoteCurrencyFirst?: boolean
} & FlexProps) {
const { formatPercent, convertFiatAmountFormatted } = useLocalizationContext()
const amountBaseLockedUSD = useUSDCValue(tryParseCurrencyAmount(hoveredTick.amount1Locked?.toFixed(2), baseCurrency))
const amountQuoteLockedUSD = useUSDCValue(
tryParseCurrencyAmount(hoveredTick.amount0Locked?.toFixed(2), quoteCurrency),
......@@ -74,22 +40,25 @@ export function TickTooltipContent({
return null
}
const price0 = typeof hoveredTick.price0 === 'string' ? parseFloat(hoveredTick.price0) : hoveredTick.price0
const showQuoteCurrency = showQuoteCurrencyFirst ? currentPrice >= price0 : currentPrice <= price0
const atTop = hoverY < 20
const atBottom = containerHeight - hoverY < 20
return (
<Flex
position="absolute"
p="$padding8"
gap="$gap4"
top={hoverY - 18}
minWidth={150}
right={contentWidth + axisLabelPaneWidth + 8}
borderRadius="$rounded12"
borderColor="$surface3"
borderWidth="$spacing1"
backgroundColor="$surface2"
pointerEvents="none"
{...props}
transform={atBottom ? 'translateY(-12px)' : atTop ? 'translateY(14px)' : undefined}
>
{(showQuoteCurrency || hoveredTick.tick === currentTick) && (
{(currentPrice >= hoveredTick.price0 || hoveredTick.tick === currentTick) && (
<Flex justifyContent="space-between" row alignItems="center" gap="$gap8">
<Flex row gap="$gap4" alignItems="center">
<DoubleCurrencyLogo currencies={[quoteCurrency]} size={iconSizes.icon16} />
......@@ -114,7 +83,7 @@ export function TickTooltipContent({
</Flex>
</Flex>
)}
{(!showQuoteCurrency || hoveredTick.tick === currentTick) && (
{(currentPrice <= hoveredTick.price0 || hoveredTick.tick === currentTick) && (
<Flex justifyContent="space-between" row alignItems="center" gap="$gap8">
<Flex row gap="$gap4" alignItems="center">
<DoubleCurrencyLogo currencies={[baseCurrency]} size={iconSizes.icon16} />
......
......@@ -43,7 +43,6 @@ interface ChartDataParams<TDataType extends SeriesDataItemType> {
data: TDataType[]
/** Repesents whether `data` is stale. If true, stale UI will appear */
stale?: boolean
hideTooltipBorder?: boolean
}
export type ChartModelParams<TDataType extends SeriesDataItemType> = ChartUtilParams<TDataType> &
......@@ -329,11 +328,10 @@ export function Chart<TParamType extends ChartDataParams<TDataType>, TDataType e
>
{children && children(crosshairData)}
{TooltipBody && crosshairData && (
<ChartTooltip id={chartModelRef.current?.tooltipId} includeBorder={!params.hideTooltipBorder}>
<ChartTooltip id={chartModelRef.current?.tooltipId}>
<TooltipBody data={crosshairData} />
</ChartTooltip>
)}
{params.stale && <StaleBanner />}
</Flex>
)
......@@ -345,20 +343,13 @@ const ChartTooltip = styled(Flex, {
left: 0,
top: 0,
zIndex: '$tooltip',
borderWidth: 0,
borderStyle: 'solid',
variants: {
includeBorder: {
true: {
backgroundColor: '$surface5',
backdropFilter: 'blur(8px)',
borderRadius: '$rounded8',
borderColor: '$surface3',
borderStyle: 'solid',
borderWidth: 1,
p: '$spacing8',
},
},
},
})
const StaleBannerWrapper = styled(ChartTooltip, {
......
......@@ -5,7 +5,11 @@ import { FeeAmount, Pool as PoolV3, TICK_SPACINGS, TickMath as TickMathV3, tickT
import { Pool as PoolV4, tickToPrice as tickToPriceV4 } from '@uniswap/v4-sdk'
import { ChartHoverData, ChartModel, ChartModelParams } from 'components/Charts/ChartModel'
import { LiquidityBarSeries } from 'components/Charts/LiquidityChart/liquidity-bar-series'
import { LiquidityBarData, LiquidityBarProps, LiquidityBarSeriesOptions } from 'components/Charts/LiquidityChart/types'
import {
LiquidityBarData,
LiquidityBarProps,
LiquidityBarSeriesOptions,
} from 'components/Charts/LiquidityChart/renderer'
import { ZERO_ADDRESS } from 'constants/misc'
import { usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import JSBI from 'jsbi'
......
import { LiquidityBarSeriesRenderer } from 'components/Charts/LiquidityChart/renderer'
import { LiquidityBarData, LiquidityBarProps, LiquidityBarSeriesOptions } from 'components/Charts/LiquidityChart/types'
import {
LiquidityBarData,
LiquidityBarProps,
LiquidityBarSeriesOptions,
LiquidityBarSeriesRenderer,
} from 'components/Charts/LiquidityChart/renderer'
import {
CustomSeriesPricePlotValues,
ICustomSeriesPaneView,
......
import { LiquidityBarData, LiquidityBarProps, LiquidityBarSeriesOptions } from 'components/Charts/LiquidityChart/types'
import { ColumnPosition, calculateColumnPositionsInPlace, positionsBox } from 'components/Charts/VolumeChart/utils'
import { roundRect } from 'components/Charts/utils'
import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas'
import { ICustomSeriesPaneRenderer, PaneRendererCustomData, PriceToCoordinateConverter, Time } from 'lightweight-charts'
import {
CustomData,
CustomSeriesOptions,
ICustomSeriesPaneRenderer,
PaneRendererCustomData,
PriceToCoordinateConverter,
Time,
UTCTimestamp,
} from 'lightweight-charts'
export interface LiquidityBarData extends CustomData {
time: UTCTimestamp
tick: number
price0: string
price1: string
liquidity: number
amount0Locked: number
amount1Locked: number
}
interface LiquidityBarItem {
x: number
......@@ -11,6 +28,18 @@ interface LiquidityBarItem {
tick: number
}
export interface LiquidityBarProps {
tokenAColor: string
tokenBColor: string
highlightColor: string
activeTick?: number
activeTickProgress?: number
}
export interface LiquidityBarSeriesOptions extends CustomSeriesOptions, LiquidityBarProps {
hoveredTick?: number
}
export class LiquidityBarSeriesRenderer<TData extends LiquidityBarData> implements ICustomSeriesPaneRenderer {
_data: PaneRendererCustomData<Time, TData> | null = null
_options: LiquidityBarProps & Partial<LiquidityBarSeriesOptions>
......
import { CustomData, CustomSeriesOptions, UTCTimestamp } from 'lightweight-charts'
export interface LiquidityBarData extends CustomData {
time: UTCTimestamp
tick: number
price0: string
price1: string
liquidity: number
amount0Locked: number
amount1Locked: number
}
export interface LiquidityBarProps {
tokenAColor: string
tokenBColor: string
highlightColor: string
activeTick?: number
activeTickProgress?: number
}
export interface LiquidityBarSeriesOptions extends CustomSeriesOptions, LiquidityBarProps {
hoveredTick?: number
}
......@@ -208,8 +208,8 @@ export function PriceChartDelta({ startingPrice, endingPrice, noColor }: PriceCh
return (
<Text variant="body2" display="flex" alignItems="center" gap="$gap4">
{delta && <DeltaArrow delta={delta} formattedDelta={formatPercent(Math.abs(delta))} noColor={noColor} />}
<DeltaText delta={delta}>{delta ? formatPercent(Math.abs(delta)) : '-'}</DeltaText>
<DeltaArrow delta={delta} formattedDelta={formatPercent(Math.abs(delta))} noColor={noColor} />
<DeltaText delta={delta}>{formatPercent(Math.abs(delta))}</DeltaText>
</Text>
)
}
......
......@@ -229,7 +229,9 @@ export default function FeatureFlagModal() {
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="New Chains">
<FeatureFlagOption flag={FeatureFlags.MonadTestnet} label="Enable Monad Testnet" />
<FeatureFlagOption flag={FeatureFlags.Soneium} label="Enable Soneium" />
<FeatureFlagOption flag={FeatureFlags.MonadTestnetDown} label="Enable Monad Testnet Down Banner" />
</FeatureFlagGroup>
<FeatureFlagGroup name="Network Requests">
<DynamicConfigDropdown
......
import { CreditCardIcon } from 'components/Icons/CreditCard'
import { Limit } from 'components/Icons/Limit'
import { Send } from 'components/Icons/Send'
import { SwapV2 } from 'components/Icons/SwapV2'
import { MenuItem } from 'components/NavBar/CompanyMenu/Content'
import { useTheme } from 'lib/styled-components'
......@@ -45,6 +46,16 @@ export const useTabsContent = (): TabsSection[] => {
href: '/limit',
internal: true,
},
...(isFiatOffRampEnabled
? []
: [
{
label: t('common.send.button'),
icon: <Send fill={theme.neutral2} />,
href: '/send',
internal: true,
},
]),
{
label: t('common.buy.label'),
icon: <CreditCardIcon fill={theme.neutral2} />,
......
......@@ -2,7 +2,6 @@ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persist
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { type PropsWithChildren } from 'react'
import { SharedQueryClient } from 'uniswap/src/data/apiClients/SharedQueryClient'
import { sharedDehydrateOptions } from 'uniswap/src/data/apiClients/sharedDehydrateOptions'
import { MAX_REACT_QUERY_CACHE_TIME_MS } from 'utilities/src/time/time'
const persistOptions: React.ComponentProps<typeof PersistQueryClientProvider>['persistOptions'] = {
......@@ -10,7 +9,6 @@ const persistOptions: React.ComponentProps<typeof PersistQueryClientProvider>['p
buster: 'v0',
maxAge: MAX_REACT_QUERY_CACHE_TIME_MS,
persister: createSyncStoragePersister({ storage: localStorage }),
dehydrateOptions: sharedDehydrateOptions,
}
export function QueryClientPersistProvider({ children }: PropsWithChildren): JSX.Element {
......
......@@ -3,11 +3,10 @@ import { Currency, CurrencyAmount, NativeCurrency, Token } from '@uniswap/sdk-co
import { FeeAmount } from '@uniswap/v3-sdk'
import { PoolData } from 'appGraphql/data/pools/usePoolData'
import { TimePeriod, gqlToCurrency, toHistoryDuration } from 'appGraphql/data/util'
import { TickTooltipContent } from 'components/Charts/ActiveLiquidityChart/TickTooltip'
import { ChartHeader } from 'components/Charts/ChartHeader'
import { Chart, refitChartContentAtom } from 'components/Charts/ChartModel'
import { LiquidityBarChartModel, useLiquidityBarData } from 'components/Charts/LiquidityChart'
import { LiquidityBarData } from 'components/Charts/LiquidityChart/types'
import { LiquidityBarData } from 'components/Charts/LiquidityChart/renderer'
import { ChartSkeleton } from 'components/Charts/LoadingState'
import { PriceChartData, PriceChartDelta, PriceChartModel } from 'components/Charts/PriceChart'
import { VolumeChart } from 'components/Charts/VolumeChart'
......@@ -339,6 +338,50 @@ const FadeInSubheader = styled(ThemedText.SubHeader)`
${textFadeIn}
`
function LiquidityTooltipDisplay({
data,
tokenADescriptor,
tokenBDescriptor,
currentTick,
}: {
data: LiquidityBarData
tokenADescriptor: string
tokenBDescriptor: string
currentTick?: number
}) {
const { t } = useTranslation()
const { formatNumberOrString } = useLocalizationContext()
if (!currentTick) {
return null
}
const displayValue0 =
data.tick >= currentTick
? formatNumberOrString({
value: data.amount0Locked,
type: NumberType.TokenQuantityStats,
})
: 0
const displayValue1 =
data.tick <= currentTick
? formatNumberOrString({
value: data.amount1Locked,
type: NumberType.TokenQuantityStats,
})
: 0
return (
<>
<ThemedText.BodySmall>
{t('liquidityPool.chart.tooltip.amount', { token: tokenADescriptor, amount: displayValue0 })}
</ThemedText.BodySmall>
<ThemedText.BodySmall>
{t('liquidityPool.chart.tooltip.amount', { token: tokenBDescriptor, amount: displayValue1 })}
</ThemedText.BodySmall>
</>
)
}
function LiquidityChart({
currencyA,
currencyB,
......@@ -385,7 +428,6 @@ function LiquidityChart({
highlightColor: theme.surface3,
activeTick,
activeTickProgress: tickData?.activeRangePercentage,
hideTooltipBorder: true,
}
}, [activeTick, isReversed, theme, tickData])
......@@ -398,20 +440,16 @@ function LiquidityChart({
height={PDP_CHART_HEIGHT_PX}
Model={LiquidityBarChartModel}
params={params}
TooltipBody={({ data: crosshairData }: { data: LiquidityBarData }) => (
TooltipBody={({ data }: { data: LiquidityBarData }) => (
// TODO(WEB-3628): investigate potential off-by-one or subgraph issues causing calculated TVL issues on 1 bip pools
// Also remove Error Boundary when its determined its not needed
<ErrorBoundary fallback={() => null}>
{tickData?.activeRangeData && (
<TickTooltipContent
baseCurrency={currencyB}
quoteCurrency={currencyA}
hoveredTick={crosshairData}
<LiquidityTooltipDisplay
data={data}
tokenADescriptor={tokenADescriptor}
tokenBDescriptor={tokenBDescriptor}
currentTick={tickData?.activeRangeData?.tick}
currentPrice={parseFloat(tickData?.activeRangeData?.price0)}
showQuoteCurrencyFirst={false}
/>
)}
</ErrorBoundary>
)}
>
......
......@@ -166,9 +166,10 @@ const ContractsDropdownRow = ({
const currency = tokens[0] && gqlToCurrency(tokens[0])
const isPool = tokens.length === 2
const currencies = isPool && tokens[1] ? [currency, gqlToCurrency(tokens[1])] : [currency]
const isNative = address === NATIVE_CHAIN_ID || !address
const isNative = address === NATIVE_CHAIN_ID
const explorerUrl =
chainId &&
address &&
getExplorerLink(
chainId,
address,
......
......@@ -146,7 +146,7 @@ function PoolTableHeader({
<Flex width="100%">
<MouseoverTooltip
disabled={!HEADER_DESCRIPTIONS[category]}
size={TooltipSize.Small}
size={TooltipSize.Max}
text={HEADER_DESCRIPTIONS[category]}
placement="top"
>
......
import { useQuery } from '@tanstack/react-query'
import { useOpenOffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal'
import {
getFORTransactionToActivityQueryOptions,
getSignatureToActivityQueryOptions,
getTransactionToActivityQueryOptions,
} from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
......@@ -15,7 +14,6 @@ import { useTranslation } from 'react-i18next'
import { useOrder } from 'state/signatures/hooks'
import { useTransaction } from 'state/transactions/hooks'
import { isPendingTx } from 'state/transactions/utils'
import { EllipsisTamaguiStyle } from 'theme/components/styles'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import { X } from 'ui/src/components/icons/X'
import { BridgeIcon } from 'uniswap/src/components/CurrencyLogo/SplitLogo'
......@@ -23,11 +21,9 @@ import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__g
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FORTransaction } from 'uniswap/src/features/fiatOnRamp/types'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking'
import noop from 'utilities/src/react/noop'
export function FailedNetworkSwitchPopup({ chainId, onClose }: { chainId: UniverseChainId; onClose: () => void }) {
const isSupportedChain = useIsSupportedChainId(chainId)
......@@ -82,7 +78,7 @@ function ActivityPopupContent({ activity, onClick, onClose }: ActivityPopupConte
width: '100%',
}}
>
<TouchableArea onPress={onClick} flex={1}>
<TouchableArea onPress={onClick}>
<Flex row gap="$gap12" height={68} py="$spacing12" px="$spacing16">
{showPortfolioLogo ? (
<Flex>
......@@ -98,11 +94,11 @@ function ActivityPopupContent({ activity, onClick, onClose }: ActivityPopupConte
<AlertTriangleFilled color="$neutral2" size="32px" />
</Flex>
)}
<Flex justifyContent="center" gap="$gap4" fill>
<Flex justifyContent="center" gap="$gap4">
<Text variant="body2" color="$neutral1">
{activity.title}
</Text>
<Text variant="body3" color="$neutral2" {...EllipsisTamaguiStyle}>
<Text variant="body3" color="$neutral2">
{activity.descriptor}
</Text>
</Flex>
......@@ -170,22 +166,3 @@ export function UniswapXOrderPopupContent({ orderHash, onClose }: { orderHash: s
return <ActivityPopupContent activity={activity} onClose={onClose} onClick={onClick} />
}
export function FORTransactionPopupContent({
transaction,
onClose,
}: {
transaction: FORTransaction
onClose: () => void
}) {
const { formatNumberOrString, convertFiatAmountFormatted } = useLocalizationContext()
const { data: activity } = useQuery(
getFORTransactionToActivityQueryOptions(transaction, formatNumberOrString, convertFiatAmountFormatted),
)
if (!activity || !transaction) {
return null
}
return <ActivityPopupContent activity={activity} onClose={onClose} onClick={noop} />
}
import { MismatchToastItem } from 'components/Popups/MismatchToastItem'
import {
FORTransactionPopupContent,
FailedNetworkSwitchPopup,
TransactionPopupContent,
UniswapXOrderPopupContent,
......@@ -56,9 +55,6 @@ export function PopupItem({ content, onClose }: { content: PopupContent; popKey:
case PopupType.Mismatch: {
return <MismatchToastItem onDismiss={onClose} />
}
case PopupType.FORTransaction: {
return <FORTransactionPopupContent transaction={content.transaction} onClose={onClose} />
}
}
}
......
......@@ -22,8 +22,8 @@ class PopupRegistry {
}
removePopup(key: string): void {
toast.dismiss(this.popupKeyToId.get(key))
this.popupKeyToId.delete(key)
toast.dismiss(this.popupKeyToId.get(key))
}
}
......
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FORTransaction } from 'uniswap/src/features/fiatOnRamp/types'
import { CurrencyId } from 'uniswap/src/types/currency'
import { SwapTab } from 'uniswap/src/types/screens/interface'
export enum PopupType {
......@@ -10,7 +8,6 @@ export enum PopupType {
SwitchNetwork = 'switchNetwork',
Bridge = 'bridge',
Mismatch = 'mismatch',
FORTransaction = 'forTransaction',
}
export type PopupContent =
......@@ -39,8 +36,3 @@ export type PopupContent =
| {
type: PopupType.Mismatch
}
| {
type: PopupType.FORTransaction
transaction: FORTransaction
currencyId: CurrencyId
}
import { InterfaceElementName } from '@uniswap/analytics-events'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import Loader from 'components/Icons/LoadingSpinner'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { MenuItem } from 'components/SearchModal/styled'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { useAccount } from 'hooks/useAccount'
import { useTokenBalances } from 'hooks/useTokenBalances'
import { CSSProperties } from 'react'
import { TokenFromList } from 'state/lists/tokenFromList'
......@@ -33,7 +35,6 @@ const TextOverflowStyle = {
const StyledBalanceText = styled(Text, {
...TextOverflowStyle,
maxWidth: '80px',
textAlign: 'right',
})
const CurrencyName = styled(Text, TextOverflowStyle)
......@@ -52,6 +53,19 @@ const Tag = styled(Text, {
mr: '$spacing4',
})
function Balance({ balance }: { balance: CurrencyAmount<Currency> }) {
const { formatNumberOrString } = useLocalizationContext()
return (
<StyledBalanceText>
{formatNumberOrString({
value: balance.toExact(),
type: NumberType.TokenNonTx,
})}
</StyledBalanceText>
)
}
function TokenTags({ currency }: { currency: Currency }) {
if (!(currency instanceof TokenFromList)) {
return null
......@@ -95,7 +109,6 @@ export function CurrencyRow({
otherSelected,
style,
showCurrencyAmount,
showUsdValue,
eventProperties,
balance,
disabled,
......@@ -108,15 +121,14 @@ export function CurrencyRow({
otherSelected: boolean
style?: CSSProperties
showCurrencyAmount?: boolean
showUsdValue?: boolean
eventProperties: Record<string, unknown>
balance?: CurrencyAmount<Currency>
disabled?: boolean
tooltip?: string
showAddress?: boolean
}) {
const account = useAccount()
const { currency } = currencyInfo
const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext()
const key = currencyListRowKey(currency)
const { tokenWarningDismissed: customAdded } = useDismissedTokenWarnings(currency)
......@@ -125,8 +137,7 @@ export function CurrencyRow({
const blockedTokenOpacity = '0.6'
const { balanceMap } = useTokenBalances({ cacheOnly: true })
const { usdValue, balance: cachedBalance } = balanceMap[currencyKey(currency)] ?? {}
const tokenBalance = balance ? balance.toExact() : cachedBalance
const balanceUSD = balanceMap[currencyKey(currency)]?.usdValue
const Wrapper = tooltip ? MouseoverTooltip : RowWrapper
......@@ -136,7 +147,7 @@ export function CurrencyRow({
logPress
logKeyPress
eventOnTrigger={UniswapEventName.TokenSelected}
properties={{ is_imported_by_user: customAdded, ...eventProperties, token_balance_usd: usdValue }}
properties={{ is_imported_by_user: customAdded, ...eventProperties, token_balance_usd: balanceUSD }}
element={InterfaceElementName.TOKEN_SELECTOR_ROW}
>
<Wrapper
......@@ -176,21 +187,11 @@ export function CurrencyRow({
<TokenTags currency={currency} />
</Flex>
</Flex>
<Flex alignSelf="center" justifyContent="flex-end">
{showUsdValue && usdValue ? (
<StyledBalanceText variant="body4" color="$neutral1">
{convertFiatAmountFormatted(usdValue, NumberType.FiatStandard)}
</StyledBalanceText>
) : null}
{showCurrencyAmount && tokenBalance ? (
<StyledBalanceText variant="body4" color="$neutral2">
{formatNumberOrString({
value: tokenBalance,
type: NumberType.TokenNonTx,
})}
</StyledBalanceText>
) : null}
{showCurrencyAmount && (
<Flex row alignSelf="center" justifyContent="flex-end">
{account.isConnected ? balance ? <Balance balance={balance} /> : <Loader /> : null}
</Flex>
)}
</MenuItem>
</Wrapper>
</Trace>
......
......@@ -12,9 +12,8 @@ const StyledDownArrow = styled(ArrowChangeDown)<{ $noColor?: boolean }>`
$noColor ? theme.neutral2 : theme.darkMode ? colorsDark.statusCritical : colorsLight.statusCritical};
`
export function calculateDelta(start: number, current: number): number | undefined {
const delta = (current / start - 1) * 100
return isValidDelta(delta) ? delta : undefined
export function calculateDelta(start: number, current: number) {
return (current / start - 1) * 100
}
function isValidDelta(delta: number | null | undefined): delta is number {
......
......@@ -125,7 +125,7 @@ function TokenTableHeader({
<Flex width="100%">
<MouseoverTooltip
disabled={!HEADER_DESCRIPTIONS[category]}
size={TooltipSize.Small}
size={TooltipSize.Max}
text={HEADER_DESCRIPTIONS[category]}
placement="top"
>
......
......@@ -144,7 +144,7 @@ export function useOrderedConnections(options?: { showSecondaryConnectors?: bool
// Injected connectors should appear next in the list, as the user intentionally installed/uses them.
if (showSecondaryConnectors) {
if (isMobileWeb && isEmbeddedWalletEnabled) {
if (isMobileWeb) {
orderedConnectors.push(embeddedWalletConnector)
}
const secondaryConnectors = [walletConnectConnector, coinbaseSdkConnector]
......
import { createWeb3Provider } from 'components/Web3Provider/createWeb3Provider'
import { wagmiConfig } from 'components/Web3Provider/wagmiConfig'
/**
* Web3Provider variant for Jest/Playwright.
* - Does NOT attempt to reconnect on mount (avoids wallet pop-ups/mocks).
*
* Tests should import this component instead of the default production provider.
*/
const TestWeb3Provider = createWeb3Provider({
wagmiConfig,
reconnectOnMount: false,
includeCapabilitiesEffects: false,
})
export default TestWeb3Provider
......@@ -24,5 +24,3 @@ export const recentConnectorIdAtom = atomWithStorage<string | undefined>('recent
export function useRecentConnectorId() {
return useAtomValue(recentConnectorIdAtom)
}
export const PLAYWRIGHT_CONNECT_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
import React, { ReactNode } from 'react'
import type { Config } from 'wagmi'
import { WagmiProvider } from 'wagmi'
import { ConnectionProvider } from 'hooks/useConnect'
import { useWalletCapabilitiesStateEffect } from 'state/walletCapabilities/hooks/useWalletCapabilitiesStateEffect'
export function createWeb3Provider(params: {
wagmiConfig: Config
reconnectOnMount?: boolean
includeCapabilitiesEffects?: boolean
}) {
const { wagmiConfig, reconnectOnMount = true, includeCapabilitiesEffects = true } = params
const WalletCapabilitiesEffects: React.FC = () => {
useWalletCapabilitiesStateEffect()
return null
}
const Provider = ({ children }: { children: ReactNode }) => (
<WagmiProvider config={wagmiConfig} reconnectOnMount={reconnectOnMount}>
<ConnectionProvider>
{includeCapabilitiesEffects && <WalletCapabilitiesEffects />}
{children}
</ConnectionProvider>
</WagmiProvider>
)
Provider.displayName = 'Web3Provider'
return Provider
}
import { Web3Provider as EthersWeb3Provider, ExternalProvider } from '@ethersproject/providers'
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { UNISWAP_EXTENSION_CONNECTOR_NAME, recentConnectorIdAtom } from 'components/Web3Provider/constants'
import { createWeb3Provider } from 'components/Web3Provider/createWeb3Provider'
import { wagmiConfig } from 'components/Web3Provider/wagmiConfig'
import { walletTypeToAmplitudeWalletType } from 'components/Web3Provider/walletConnect'
import { RPC_PROVIDERS } from 'constants/providers'
import { useAccount } from 'hooks/useAccount'
import { ConnectionProvider } from 'hooks/useConnect'
import { useEthersWeb3Provider } from 'hooks/useEthersProvider'
import usePrevious from 'hooks/usePrevious'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
import { ReactNode, useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useWalletCapabilitiesStateEffect } from 'state/walletCapabilities/hooks/useWalletCapabilitiesStateEffect'
import { useConnectedWallets } from 'state/wallets/hooks'
import { CONVERSION_EVENTS } from 'uniswap/src/data/rest/conversionTracking/constants'
import { useConversionTracking } from 'uniswap/src/data/rest/conversionTracking/useConversionTracking'
......@@ -20,16 +21,23 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { setUserProperty } from 'uniswap/src/features/telemetry/user'
import { isTestEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger'
import { useTrace } from 'utilities/src/telemetry/trace/TraceContext'
import { getCurrentPageFromLocation } from 'utils/urlRoutes'
import { WalletType, getWalletMeta } from 'utils/walletMeta'
import { useAccount as useAccountWagmi } from 'wagmi'
// Production Web3Provider – always reconnects on mount and runs capability effects.
const Web3Provider = createWeb3Provider({ wagmiConfig })
export default Web3Provider
import { WagmiProvider, useAccount as useAccountWagmi } from 'wagmi'
export default function Web3Provider({ children }: { children: ReactNode }) {
return (
<WagmiProvider config={wagmiConfig}>
<ConnectionProvider>
<WalletCapabilitiesEffects />
{children}
</ConnectionProvider>
</WagmiProvider>
)
}
/** A component to run hooks under the Web3ReactProvider context. */
export function Web3ProviderUpdater() {
......@@ -189,3 +197,17 @@ function trace(event: any) {
const { method, id, params } = event.request
logger.debug('Web3Provider', 'provider', 'trace', { method, id, params })
}
/**
* WalletCapabilitiesEffectsInner -- handles the effects related to wallet capabilities
* @returns null
*/
const WalletCapabilitiesEffectsInner: React.FC = () => {
// get the wallet capabilities for the current account on connect (and reset on disconnect)
useWalletCapabilitiesStateEffect()
return null
}
// we don't want to run the smart account wallet effects in tests
const WalletCapabilitiesEffects: React.FC = isTestEnv() ? () => null : WalletCapabilitiesEffectsInner
import { PLAYWRIGHT_CONNECT_ADDRESS } from 'components/Web3Provider/constants'
import { wagmiConfig } from 'components/Web3Provider/wagmiConfig'
import { isPlaywrightEnv } from 'utilities/src/environment/env'
import { isAddress } from 'viem'
import { connect } from 'wagmi/actions'
import { injected, mock } from 'wagmi/connectors'
// Cypress runner marks window.Cypress – rely on support/commands to set eagerlyConnect flag
if ((window as any).Cypress?.eagerlyConnect) {
connect(wagmiConfig, { connector: injected() })
}
export function setupWagmiAutoConnect() {
// Cypress runner marks window.Cypress – rely on support/commands to set eagerlyConnect flag
if ((window as any).Cypress?.eagerlyConnect) {
connect(wagmiConfig, { connector: injected() })
}
const isEagerlyConnect = !window.location.search.includes('eagerlyConnect=false')
const eagerlyConnectAddress = window.location.search.includes('eagerlyConnectAddress=')
? window.location.search.split('eagerlyConnectAddress=')[1]
: undefined
// Automatically connect if running under Playwright (used by E2E tests)
if (isPlaywrightEnv() && isEagerlyConnect) {
// setTimeout avoids immediate disconnection caused by race condition in wagmi mock connector
setTimeout(() => {
connect(wagmiConfig, {
connector: mock({
features: {},
accounts: [
eagerlyConnectAddress && isAddress(eagerlyConnectAddress)
? eagerlyConnectAddress
: PLAYWRIGHT_CONNECT_ADDRESS,
],
}),
})
}, 1)
}
}
import { PLAYWRIGHT_CONNECT_ADDRESS } from 'components/Web3Provider/constants'
import { injectedWithFallback } from 'components/Web3Provider/injectedWithFallback'
import { WC_PARAMS } from 'components/Web3Provider/walletConnect'
import { embeddedWallet } from 'connection/EmbeddedWalletConnector'
......@@ -9,9 +8,16 @@ import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/type
import { isTestnetChain } from 'uniswap/src/features/chains/utils'
import { isPlaywrightEnv } from 'utilities/src/environment/env'
import { logger } from 'utilities/src/logger/logger'
import { Chain, createClient } from 'viem'
import { Config, createConfig, fallback, http } from 'wagmi'
import { coinbaseWallet, mock, safe, walletConnect } from 'wagmi/connectors'
import { Chain, createClient, isAddress } from 'viem'
import { createConfig, fallback, http } from 'wagmi'
import { connect } from 'wagmi/actions'
import { coinbaseWallet, injected, mock, safe, walletConnect } from 'wagmi/connectors'
declare module 'wagmi' {
interface Register {
config: typeof wagmiConfig
}
}
export const orderedTransportUrls = (chain: ReturnType<typeof getChainInfo>): string[] => {
const orderedRpcUrls = [
......@@ -24,13 +30,7 @@ export const orderedTransportUrls = (chain: ReturnType<typeof getChainInfo>): st
return Array.from(new Set(orderedRpcUrls.filter(Boolean)))
}
function createWagmiConnectors(params: {
/** If `true`, appends the wagmi `mock` connector. Used in Playwright. */
includeMockConnector: boolean
}): any[] {
const { includeMockConnector } = params
const baseConnectors = [
const baseConnectors = [
injectedWithFallback(),
walletConnect(WC_PARAMS),
embeddedWallet(),
......@@ -40,30 +40,23 @@ function createWagmiConnectors(params: {
// Flagged to CB team and can remove UNISWAP_WEB_URL once fixed
appLogoUrl: `${UNISWAP_WEB_URL}${UNISWAP_LOGO}`,
reloadOnDisconnect: false,
enableMobileWalletLink: true,
}),
safe(),
]
]
return includeMockConnector
// Only add mock connector in Playwright environment
const connectors = isPlaywrightEnv()
? [
...baseConnectors,
mock({
features: {},
accounts: [PLAYWRIGHT_CONNECT_ADDRESS],
accounts: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'],
}),
]
: baseConnectors
}
function createWagmiConfig(params: {
/** The connector list to use. */
connectors: any[]
/** Optional custom `onFetchResponse` handler – defaults to `defaultOnFetchResponse`. */
onFetchResponse?: (response: Response, chain: Chain, url: string) => void
}): Config {
const { connectors, onFetchResponse = defaultOnFetchResponse } = params
return createConfig({
export const wagmiConfig = createConfig({
chains: [getChainInfo(UniverseChainId.Mainnet), ...ALL_CHAIN_IDS.map(getChainInfo)],
connectors,
client({ chain }) {
......@@ -78,10 +71,9 @@ function createWagmiConfig(params: {
),
})
},
})
}
})
const defaultOnFetchResponse = (response: Response, chain: Chain, url: string) => {
const onFetchResponse = (response: Response, chain: Chain, url: string) => {
if (response.status !== 200) {
const message = `RPC provider returned non-200 status: ${response.status}`
......@@ -109,15 +101,29 @@ const defaultOnFetchResponse = (response: Response, chain: Chain, url: string) =
}
}
const defaultConnectors = createWagmiConnectors({
includeMockConnector: isPlaywrightEnv(),
})
// Automatically connect if running in Cypress environment
if ((window as any).Cypress?.eagerlyConnect) {
connect(wagmiConfig, { connector: injected() })
}
export const wagmiConfig: Config = createWagmiConfig({ connectors: defaultConnectors })
const isEagerlyConnect = !window.location.search.includes('eagerlyConnect=false')
const eagerlyConnectAddress = window.location.search.includes('eagerlyConnectAddress=')
? window.location.search.split('eagerlyConnectAddress=')[1]
: undefined
declare module 'wagmi' {
interface Register {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
config: typeof wagmiConfig
}
// Automatically connect if running in Playwright environment
if (isPlaywrightEnv() && isEagerlyConnect) {
// setTimeout is needed to avoid disconnection
setTimeout(() => {
connect(wagmiConfig, {
connector: mock({
features: {},
accounts: [
eagerlyConnectAddress && isAddress(eagerlyConnectAddress)
? eagerlyConnectAddress
: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
],
}),
})
}, 1)
}
......@@ -11,7 +11,7 @@ import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance'
import { SwapResult } from 'hooks/useSwapCallback'
import styled from 'lib/styled-components'
import ms from 'ms'
import { PropsWithChildren, ReactNode, useMemo, useState } from 'react'
import { ReactNode, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { InterfaceTrade, LimitOrderTrade, RouterPreference } from 'state/routing/types'
import { isClassicTrade, isLimitTrade } from 'state/routing/utils'
......@@ -69,24 +69,13 @@ interface HelpLink {
url: string
}
// TODO: Extract to Spore ExpandoRow component (WEB-7906)
export function DropdownController({
open,
onClick,
children,
}: PropsWithChildren & { open: boolean; onClick: () => void }) {
function DropdownController({ open, onClick }: { open: boolean; onClick: () => void }) {
return (
<DropdownButton onClick={onClick}>
<Separator />
<DropdownControllerWrapper>
<ThemedText.BodySmall color="neutral2">
{children ? (
children
) : open ? (
<Trans i18nKey="common.showLess.button" />
) : (
<Trans i18nKey="common.showMore.button" />
)}
{open ? <Trans i18nKey="common.showLess.button" /> : <Trans i18nKey="common.showMore.button" />}
</ThemedText.BodySmall>
{open ? <ExpandoIconOpened /> : <ExpandoIconClosed />}
</DropdownControllerWrapper>
......
......@@ -2,14 +2,12 @@ import { ReactNode } from 'react'
import { Flex, styled as TamaguiStyled, Text } from 'ui/src'
import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled'
export const PAGE_WRAPPER_MAX_WIDTH = 480
export const PageWrapper = TamaguiStyled(Flex, {
pt: '$spacing60',
px: '$spacing8',
pb: '$spacing40',
width: '100%',
maxWidth: PAGE_WRAPPER_MAX_WIDTH,
maxWidth: 480,
$lg: {
pt: '$spacing48',
},
......
......@@ -57,7 +57,7 @@ export function ConnectionProvider({ children }: PropsWithChildren) {
* Wraps wagmi.useConnect in a singleton provider to provide the same connect state to all callers.
* @see {@link https://wagmi.sh/react/api/hooks/useConnect}
*/
export function useConnect(): UseConnectReturnType<ResolvedRegister['config']> {
export function useConnect() {
const value = useContext(ConnectionContext)
if (!value) {
throw new Error('useConnect must be used within a ConnectionProvider')
......
import { Web3Provider } from '@ethersproject/providers'
import { useAccount } from 'hooks/useAccount'
import { useMemo } from 'react'
import type { Chain, Client, Transport } from 'viem'
import { UniverseChainInfo } from 'uniswap/src/features/chains/types'
import type { Client, Transport } from 'viem'
import { useClient, useConnectorClient } from 'wagmi'
const providers = new WeakMap<Client, Web3Provider>()
export function clientToProvider(client?: Client<Transport, Chain>, chainId?: number) {
export function clientToProvider(client?: Client<Transport, UniverseChainInfo>, chainId?: number) {
if (!client) {
return undefined
}
......
import { Web3Provider } from '@ethersproject/providers'
import { useMemo } from 'react'
import type { Account, Chain, Client, Transport } from 'viem'
import { UniverseChainInfo } from 'uniswap/src/features/chains/types'
import type { Account, Client, Transport } from 'viem'
import { useConnectorClient } from 'wagmi'
function clientToSigner(client?: Client<Transport, Chain, Account>) {
function clientToSigner(client?: Client<Transport, UniverseChainInfo, Account>) {
if (!client || !client.chain) {
return undefined
}
......
import { TransactionRequest } from '@ethersproject/abstract-provider'
import { UseQueryResult, useQuery } from '@tanstack/react-query'
import { useCallback, useMemo } from 'react'
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
import { queryWithoutCache } from 'utilities/src/reactQuery/queryOptions'
import { useAsyncData } from 'utilities/src/react/hooks'
enum FeeType {
Legacy = 'legacy',
......@@ -69,8 +67,13 @@ export enum GasSpeed {
Urgent = 'urgent',
}
export function useTransactionGasFee(tx?: TransactionRequest, speed: GasSpeed = GasSpeed.Urgent): GasFeeResult {
const { data, isLoading } = useGasFeeQuery(tx)
export function useTransactionGasFee(
tx?: TransactionRequest,
speed: GasSpeed = GasSpeed.Urgent,
skip: boolean = !tx,
): GasFeeResult {
const gasFeeFetcher = useGasFeeQuery(tx, skip)
const { data, isLoading } = useAsyncData(gasFeeFetcher)
return useMemo(() => {
if (!data) {
......@@ -102,10 +105,12 @@ const UNISWAP_API_URL = process.env.REACT_APP_UNISWAP_BASE_API_URL
const isErrorResponse = (res: Response, gasFee: GasFeeResponse): gasFee is GasFeeResponseError =>
res.status < 200 || res.status > 202
function useGasFeeQuery(tx?: TransactionRequest): UseQueryResult<GasFeeResponseEip1559 | GasFeeResponseLegacy | null> {
const skip = !tx
function useGasFeeQuery(tx?: TransactionRequest, skip: boolean = !tx) {
const gasFeeFetcher = useCallback(async () => {
if (skip) {
return undefined
}
const res = await fetch(`${UNISWAP_API_URL}/v1/gas-fee`, {
method: 'POST',
body: JSON.stringify(tx),
......@@ -114,17 +119,11 @@ function useGasFeeQuery(tx?: TransactionRequest): UseQueryResult<GasFeeResponseE
const body = (await res.json()) as GasFeeResponse
if (isErrorResponse(res, body)) {
return null
return undefined
}
return body
}, [tx])
return useQuery(
queryWithoutCache({
queryKey: [ReactQueryCacheKey.WebTransactionGasFee, tx],
queryFn: gasFeeFetcher,
enabled: !skip,
}),
)
}, [skip, tx])
return gasFeeFetcher
}
......@@ -20,7 +20,7 @@ test('should increase liquidity of a position', async ({ page, anvil }) => {
await page.getByTestId(TestID.AmountInputIn).nth(1).click()
await page.getByTestId(TestID.AmountInputIn).nth(1).fill('1')
await page.getByRole('button', { name: 'Review' }).click()
await page.getByRole('button', { name: 'Add' }).click()
await page.getByRole('button', { name: 'Confirm' }).click()
await expect(page.getByText('Approved')).toBeVisible()
})
......@@ -32,6 +32,7 @@ const tabs = [
dropdown: [
{ label: 'Swap', path: '/swap' },
{ label: 'Limit', path: '/limit' },
{ label: 'Send', path: '/send' },
{ label: 'Buy', path: '/buy' },
],
},
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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