ci(release): publish latest release

parent f494f119
diff --git a/package.json b/package.json
index e4b37644f3a62171deaff6dbf7731979ce751c78..7b2ca64565c0c6318ba01ff26cb4fad1c7419b0d 100644
index 251da97dcf5a092c3d2d766e16fe9536b411b9a2..7f74152f2e127a42bac946fcab641b03316e42d9 100644
--- a/package.json
+++ b/package.json
@@ -15,22 +15,9 @@
@@ -15,24 +15,10 @@
"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"
......@@ -22,9 +27,6 @@ index e4b37644f3a62171deaff6dbf7731979ce751c78..7b2ca64565c0c6318ba01ff26cb4fad1
- },
- "./package.json": "./package.json"
- },
+ "types": "build/modern/index.d.ts",
+ "main": "build/modern/index.cjs",
+ "module": "build/modern/index.js",
"sideEffects": false,
"files": [
"build",
IPFS hash of the deployment:
- CIDv0: `QmYLUSmyP2FahHkscj7gQbA43gJY3PQ7gekdk8Tt3yfrVq`
- CIDv1: `bafybeieurfbwxnagisdo3rash52dwiew7ivubnbpig7sy6ub4d3tjfr73q`
- CIDv0: `QmUKsfqcn6zpJmfBtZD3AV1o9rTrkaJHwNiJnrWZL5j7K8`
- CIDv1: `bafybeicy6mzbrmf3vzpmgzv7xu3fjweebcnnchtzlu4jeir6qyizmfceem`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
......@@ -10,14 +10,9 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeieurfbwxnagisdo3rash52dwiew7ivubnbpig7sy6ub4d3tjfr73q.ipfs.dweb.link/
- [ipfs://QmYLUSmyP2FahHkscj7gQbA43gJY3PQ7gekdk8Tt3yfrVq/](ipfs://QmYLUSmyP2FahHkscj7gQbA43gJY3PQ7gekdk8Tt3yfrVq/)
- https://bafybeicy6mzbrmf3vzpmgzv7xu3fjweebcnnchtzlu4jeir6qyizmfceem.ipfs.dweb.link/
- [ipfs://QmUKsfqcn6zpJmfBtZD3AV1o9rTrkaJHwNiJnrWZL5j7K8/](ipfs://QmUKsfqcn6zpJmfBtZD3AV1o9rTrkaJHwNiJnrWZL5j7K8/)
### 5.87.1 (2025-05-29)
### Bug Fixes
* **web:** fix token warning modal plural crash (#20340) 0adb22e
### 5.88.6 (2025-06-10)
web/5.87.1
\ No newline at end of file
web/5.88.6
\ No newline at end of file
......@@ -13,6 +13,7 @@
"@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",
......
import { useDispatch } from 'react-redux'
import { useSmartWalletNudges } from 'src/app/context/SmartWalletNudgesContext'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { PostSwapSmartWalletNudge } from 'wallet/src/components/smartWallet/modals/PostSwapSmartWalletNudge'
import { SmartWalletCreatedModal } from 'wallet/src/components/smartWallet/modals/SmartWalletCreatedModal'
import { SmartWalletEnabledModal } from 'wallet/src/components/smartWallet/modals/SmartWalletEnabledModal'
import { useActiveAccount } from 'wallet/src/features/wallet/hooks'
import { setSmartWalletConsent } from 'wallet/src/features/wallet/slice'
export function SmartWalletNudgeModals(): JSX.Element | null {
const dispatch = useDispatch()
const address = useActiveAccount()?.address
const { activeModal, closeModal, openModal, dappInfo } = useSmartWalletNudges()
if (!activeModal) {
......@@ -20,7 +25,14 @@ export function SmartWalletNudgeModals(): JSX.Element | null {
isOpen
onClose={closeModal}
dappInfo={dappInfo}
onEnableSmartWallet={() => openModal(ModalName.SmartWalletEnabledModal)}
onEnableSmartWallet={() => {
if (!address) {
return
}
dispatch(setSmartWalletConsent({ address, smartWalletConsent: true }))
openModal(ModalName.SmartWalletEnabledModal)
}}
/>
)
case ModalName.SmartWalletEnabledModal:
......
......@@ -2,9 +2,16 @@ import { memo } from 'react'
import { ScrollView } from 'ui/src'
import { useActivityData } from 'wallet/src/features/activity/hooks/useActivityData'
export const ActivityTab = memo(function _ActivityTab({ address }: { address: Address }): JSX.Element {
export const ActivityTab = memo(function _ActivityTab({
address,
skip,
}: {
address: Address
skip?: boolean
}): JSX.Element {
const { maybeEmptyComponent, renderActivityItem, sectionData } = useActivityData({
owner: address,
skip,
})
if (maybeEmptyComponent) {
......
......@@ -7,7 +7,7 @@ import { NftViewWithContextMenu } from 'wallet/src/components/nfts/NftViewWithCo
import { NftsList } from 'wallet/src/components/nfts/NftsList'
import { NFTItem } from 'wallet/src/features/nfts/types'
export const NftsTab = memo(function _NftsTab({ owner }: { owner: Address }): JSX.Element {
export const NftsTab = memo(function _NftsTab({ owner, skip }: { owner: Address; skip?: boolean }): JSX.Element {
const renderNFTItem = useCallback(
(item: NFTItem) => {
const onPress = (): void => {
......@@ -32,6 +32,7 @@ export const NftsTab = memo(function _NftsTab({ owner }: { owner: Address }): JS
errorStateStyle={defaultEmptyStyle}
owner={owner}
renderNFTItem={renderNFTItem}
skip={skip}
/>
)
})
......
......@@ -3,8 +3,14 @@ import { useDispatch, useSelector } from 'react-redux'
import { useGet5792DappInfo } from 'src/app/hooks/useGet5792DappInfo'
import { ModalName, ModalNameType } from 'uniswap/src/features/telemetry/constants'
import { useEvent } from 'utilities/src/react/hooks'
import { selectHasSeenCreatedSmartWalletModal } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasSeenSmartWalletCreatedWalletModal } from 'wallet/src/features/behaviorHistory/slice'
import {
selectHasSeenCreatedSmartWalletModal,
selectHasShownEip5792Nudge,
} from 'wallet/src/features/behaviorHistory/selectors'
import {
setHasSeenSmartWalletCreatedWalletModal,
setHasShown5792Nudge,
} from 'wallet/src/features/behaviorHistory/slice'
import { useAccountCountChanged } from 'wallet/src/features/wallet/hooks'
import { extractUrlHost } from 'utilities/src/format/urls'
......@@ -12,8 +18,6 @@ import {
SmartWalletDelegationAction,
useSmartWalletDelegationStatus,
} from 'wallet/src/components/smartWallet/smartAccounts/hook'
import { selectHasShownEip5792Nudge } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasShown5792Nudge } from 'wallet/src/features/behaviorHistory/slice'
import { WalletState } from 'wallet/src/state/walletReducer'
type DappInfo = {
......
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/getUniqueId'
import { getUniqueId } from 'utilities/src/device/uniqueId'
import { uniqueIdQuery } from 'utilities/src/device/uniqueIdQuery'
import { logger } from 'utilities/src/logger/logger'
import { useAsyncData } from 'utilities/src/react/hooks'
async function getStatsigUser(): Promise<StatsigUser> {
function makeStatsigUser(userID: string): StatsigUser {
return {
userID: await getUniqueId(),
userID,
appVersion: process.env.VERSION,
custom: {
app: StatsigCustomAppValue.Extension,
......@@ -26,7 +28,7 @@ export function ExtensionStatsigProvider({
children: React.ReactNode
appName: string
}): JSX.Element {
const { data: storedUser } = useAsyncData(getStatsigUser)
const { data: uniqueId } = useQuery(uniqueIdQuery(), SharedQueryClient)
const [initFinished, setInitFinished] = useState(false)
const [user, setUser] = useState<StatsigUser>({
userID: undefined,
......@@ -37,10 +39,10 @@ export function ExtensionStatsigProvider({
})
useEffect(() => {
if (storedUser && initFinished) {
setUser(storedUser)
if (uniqueId && initFinished) {
setUser(makeStatsigUser(uniqueId))
}
}, [storedUser, initFinished])
}, [uniqueId, initFinished])
const onStatsigInit = (): void => {
setInitFinished(true)
......@@ -55,7 +57,8 @@ export function ExtensionStatsigProvider({
}
export async function initStatSigForBrowserScripts(): Promise<void> {
const statsigClient = new StatsigClient(config.statsigApiKey, await getStatsigUser(), statsigBaseConfig)
const uniqueId = await getUniqueId()
const statsigClient = new StatsigClient(config.statsigApiKey, makeStatsigUser(uniqueId), statsigBaseConfig)
await statsigClient.initializeAsync().catch((error) => {
logger.error(error, {
tags: { file: 'StatsigProvider.tsx', function: 'initStatSigForBrowserScripts' },
......
......@@ -6,6 +6,7 @@ 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'
import { useSmartWalletNudges } from 'src/app/context/SmartWalletNudgesContext'
import AppRatingModal from 'src/app/features/appRating/AppRatingModal'
import { useAppRating } from 'src/app/features/appRating/hooks/useAppRating'
import { PortfolioActionButtons } from 'src/app/features/home/PortfolioActionButtons'
......@@ -24,13 +25,17 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/hooks'
import { setNotificationStatus } from 'uniswap/src/features/notifications/slice'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } from 'utilities/src/react/hooks'
import { ONE_MINUTE_MS, ONE_SECOND_MS } from 'utilities/src/time/time'
import { useTimeout } from 'utilities/src/time/timing'
import { NFTS_TAB_DATA_DEPENDENCIES } from 'wallet/src/components/nfts/NftsList'
import { SmartWalletEnabledModal } from 'wallet/src/components/smartWallet/modals/SmartWalletEnabledModal'
import { SmartWalletUpgradeModals } from 'wallet/src/components/smartWallet/modals/SmartWalletUpgradeModal'
import { useOpenSmartWalletNudgeOnCompletedSwap } from 'wallet/src/components/smartWallet/smartAccounts/hook'
import { setIncrementNumPostSwapNudge } from 'wallet/src/features/behaviorHistory/slice'
import { PendingNotificationBadge } from 'wallet/src/features/notifications/components/PendingNotificationBadge'
import { PortfolioBalance } from 'wallet/src/features/portfolio/PortfolioBalance'
import { useHeartbeatReporter, useLastBalancesReporter } from 'wallet/src/features/telemetry/hooks'
......@@ -96,6 +101,18 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
[dispatch, activeAccount.address, setIsSmartWalletEnabledModalOpen],
)
// Handle the smart wallet nudge when a swap transaction is completed
const { openModal } = useSmartWalletNudges()
useOpenSmartWalletNudgeOnCompletedSwap(
useEvent(() => {
if (!activeAccount.address) {
return
}
dispatch(setIncrementNumPostSwapNudge({ walletAddress: address }))
openModal(ModalName.PostSwapSmartWalletNudge)
}),
)
useEffect(() => {
let intervalId: number
const checkExtensionPinnedStatus = async (): Promise<void> => {
......@@ -209,14 +226,14 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element {
hideRight={selectedTab === HomeTabs.Tokens}
isActive={selectedTab === HomeTabs.NFTs}
>
<NftsTab owner={address} />
<NftsTab owner={address} skip={selectedTab !== HomeTabs.NFTs} />
</AnimatedTab>
<AnimatedTab
hideRight={selectedTab !== HomeTabs.Activity}
isActive={selectedTab === HomeTabs.Activity}
>
<ActivityTab address={address} />
<ActivityTab address={address} skip={selectedTab !== HomeTabs.Activity} />
</AnimatedTab>
</>
) : (
......
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'
......@@ -13,7 +14,9 @@ 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 { useAsyncData, useEvent } from 'utilities/src/react/hooks'
import { useEvent } from 'utilities/src/react/hooks'
import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache'
import { queryWithoutCache } from 'utilities/src/reactQuery/queryOptions'
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'
......@@ -28,7 +31,9 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
const { goToNextStep, goToPreviousStep } = useOnboardingSteps()
const { generateAccountsAndImportAddresses, getGeneratedAddresses } = useOnboardingContext()
const { data: generatedAddresses } = useAsyncData(getGeneratedAddresses)
const { data: generatedAddresses } = useQuery(
queryWithoutCache({ queryFn: getGeneratedAddresses, queryKey: [ReactQueryCacheKey.GeneratedAddresses] }),
)
const { importableAccounts, isLoading, showError, refetch } = useImportableAccounts(generatedAddresses)
......
import { useTranslation } from 'react-i18next'
import { Button, Flex, isWeb } from 'ui/src'
import { Button, Flex } from 'ui/src'
import { WarningLabel } from 'uniswap/src/components/modals/WarningModal/types'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { isWeb } from 'utilities/src/platform'
import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext'
type ReviewButtonProps = {
......
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'
......@@ -12,9 +13,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 { useAsyncData, useEvent } from 'utilities/src/react/hooks'
import { useEvent } from 'utilities/src/react/hooks'
import { useBooleanState } from 'utilities/src/react/useBooleanState'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { mnemonicUnlockedQuery } from 'wallet/src/features/wallet/Keyring/queries'
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'
......@@ -153,9 +154,7 @@ function RecoveryPhraseVerificationStep({
const [hasError, setHasError] = useState(false)
const [numberOfWordsVerified, setNumberOfWordsVerified] = useState(0)
const { data: mnemonic, error } = useAsyncData(
useCallback(async () => Keyring.retrieveMnemonicUnlocked(mnemonicId), [mnemonicId]),
)
const { data: mnemonic, error } = useQuery(mnemonicUnlockedQuery(mnemonicId))
if (error) {
// This should never happen. We can't recover from a missing mnemonic.
......
import { useCallback, useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { LayoutChangeEvent } from 'react-native'
import { CopyButton } from 'src/app/components/buttons/CopyButton'
import { Flex, Separator, Text } from 'ui/src'
......@@ -7,8 +8,7 @@ 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 { useAsyncData } from 'utilities/src/react/hooks'
import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring'
import { mnemonicUnlockedQuery } from 'wallet/src/features/wallet/Keyring/queries'
function SeedPhraseColumnGroup({ recoveryPhraseArray }: { recoveryPhraseArray: string[] }): JSX.Element {
const [largestIndexWidth, setLargestIndexWidth] = useState(0)
......@@ -95,9 +95,7 @@ function SeedPhraseWord({
export function SeedPhraseDisplay({ mnemonicId }: { mnemonicId: string }): JSX.Element {
const placeholderWordArrayLength = 12
const recoveryPhraseString = useAsyncData(
useCallback(async () => Keyring.retrieveMnemonicUnlocked(mnemonicId), [mnemonicId]),
).data
const { data: recoveryPhraseString } = useQuery(mnemonicUnlockedQuery(mnemonicId))
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,
ColorTokens,
Flex,
GeneratedIcon,
ScrollView,
Switch,
Text,
TouchableArea,
useSporeColors,
} from 'ui/src'
import { Button, Flex, ScrollView, Text } from 'ui/src'
import {
ArrowUpRight,
Chart,
......@@ -32,12 +23,10 @@ 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'
......@@ -333,124 +322,3 @@ 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, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useSmartWalletNudges } from 'src/app/context/SmartWalletNudgesContext'
import { useState } from 'react'
import { useSelector } from 'react-redux'
import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/contexts/selectors'
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency'
import {
SmartWalletDelegationAction,
useSmartWalletDelegationStatus,
} from 'wallet/src/components/smartWallet/smartAccounts/hook'
import { selectShouldShowPostSwapNudge } from 'wallet/src/features/behaviorHistory/selectors'
import { setIncrementNumPostSwapNudge } from 'wallet/src/features/behaviorHistory/slice'
import { useIsChainSupportedBySmartWallet } from 'wallet/src/features/smartWallet/hooks/useSmartWalletChains'
import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
import { WalletState } from 'wallet/src/state/walletReducer'
export function SwapFlowScreen(): JSX.Element {
const dispatch = useDispatch()
const { navigateBack, locationState } = useExtensionNavigation()
const { defaultChainId } = useEnabledChains()
const account = useActiveAccountWithThrow()
const ignorePersistedFilteredChainIds = !!locationState?.initialTransactionState
const persistedFilteredChainIds = useSelector(selectFilteredChainIds)
const { status: delegationStatus, loading: delegationStatusLoading } = useSmartWalletDelegationStatus({})
const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet)
const inputCurrencyId = useHighestBalanceNativeCurrencyId(
account.address,
!ignorePersistedFilteredChainIds ? persistedFilteredChainIds?.[CurrencyField.INPUT] : undefined,
......@@ -47,46 +31,10 @@ export function SwapFlowScreen(): JSX.Element {
const [initialTransactionState] = useState(() => locationState?.initialTransactionState ?? initialState)
const swapPrefilledState = useSwapPrefilledState(initialTransactionState)
const { openModal, setDappInfo } = useSmartWalletNudges()
const canShowPostSwapNudge = useSelector((state: WalletState) =>
selectShouldShowPostSwapNudge(state, account.address),
)
const isSupportedSmartWalletChain = useIsChainSupportedBySmartWallet(swapPrefilledState?.filteredChainIds.input)
const onSubmitSwap = useCallback(async () => {
if (!isSmartWalletEnabled || delegationStatusLoading) {
return
}
if (
canShowPostSwapNudge &&
delegationStatus === SmartWalletDelegationAction.PromptUpgrade &&
isSupportedSmartWalletChain
) {
openModal(ModalName.PostSwapSmartWalletNudge)
setDappInfo(undefined)
dispatch(setIncrementNumPostSwapNudge({ walletAddress: account.address }))
}
}, [
delegationStatusLoading,
isSmartWalletEnabled,
delegationStatus,
openModal,
setDappInfo,
dispatch,
account.address,
canShowPostSwapNudge,
isSupportedSmartWalletChain,
])
return (
<Flex fill p="$spacing12">
<WalletSwapFlow
prefilledState={swapPrefilledState}
walletNeedsRestore={false}
onClose={navigateBack}
onSubmitSwap={onSubmitSwap}
/>
<WalletSwapFlow prefilledState={swapPrefilledState} walletNeedsRestore={false} onClose={navigateBack} />
</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'
......@@ -53,7 +52,9 @@ export function useIsWalletUnlocked(): boolean | null {
}
}, [checkWalletStatus])
useAsyncData(checkWalletStatus)
useEffect(() => {
checkWalletStatus()
}, [checkWalletStatus])
return isUnlocked
}
import { useCallback, useMemo, useRef } from 'react'
import { useMutation } from '@tanstack/react-query'
import { useEffect, 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'
......@@ -18,7 +19,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 { useAsyncData, usePrevious } from 'utilities/src/react/hooks'
import { useEvent, 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'
......@@ -230,17 +231,27 @@ function LoggedOut(): JSX.Element {
const isOnboarded = useSelector(isOnboardedSelector)
const didOpenOnboarding = useRef(false)
const handleOnboarding = useCallback(async () => {
if (!isOnboarded && !didOpenOnboarding.current) {
const focusOrCreateOnboardingTabMutation = useMutation({
onMutate: () => {
// We keep track of this to avoid opening the onboarding page multiple times if this component remounts.
didOpenOnboarding.current = true
await focusOrCreateOnboardingTab()
},
mutationFn: () => {
return focusOrCreateOnboardingTab()
},
onSuccess: () => {
// Automatically close the pop up after focusing on the onboarding tab.
window.close()
}
}, [isOnboarded])
},
})
const focusOrCreateOnboardingTabEvent = useEvent(focusOrCreateOnboardingTabMutation.mutate)
useAsyncData(handleOnboarding)
useEffect(() => {
if (!focusOrCreateOnboardingTabMutation.isPending && !isOnboarded && !didOpenOnboarding.current) {
focusOrCreateOnboardingTabEvent()
}
}, [focusOrCreateOnboardingTabEvent, isOnboarded, focusOrCreateOnboardingTabMutation.isPending])
// 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/getUniqueId'
import { getUniqueId } from 'utilities/src/device/uniqueId'
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'
......
......@@ -24,6 +24,7 @@ import {
v20Schema,
v21Schema,
v22Schema,
v23Schema,
v2Schema,
v3Schema,
v4Schema,
......@@ -57,6 +58,7 @@ import {
testAddCreatedOnboardingRedesignAccount,
testAddedHapticSetting,
testDeleteWelcomeWalletCard,
testMoveHapticsToUserSettings,
testMoveTokenAndNFTVisibility,
testMovedCurrencySetting,
testMovedLanguageSetting,
......@@ -320,4 +322,8 @@ 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,6 +21,7 @@ import {
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting,
moveDismissedTokenWarnings,
moveHapticsToUserSettings,
moveLanguageSetting,
moveTokenAndNFTVisibility,
moveUserSettings,
......@@ -55,6 +56,7 @@ export const migrations = {
21: migratePendingDappRequestsToRecord,
22: addBatchedTransactions,
23: migrateUnknownBackupAccountsToMaybeManualBackup,
24: moveHapticsToUserSettings,
}
export const EXTENSION_STATE_VERSION = 23
export const EXTENSION_STATE_VERSION = 24
......@@ -254,5 +254,20 @@ export const v22Schema = {
batchedTransactions: {},
}
const v23Schema = v22Schema
export const getSchema = (): typeof v23Schema => v23Schema
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
......@@ -37,6 +37,3 @@ env:
- waitForAnimationToEnd
- runFlow: biometrics-confirm.yaml
- waitForAnimationToEnd
- tapOn:
id: ${output.testIds.SmartWalletUpgradeModalMaybeLater}
- waitForAnimationToEnd
......@@ -2,4 +2,5 @@
<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,4 +2,5 @@
<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"/>
</adaptive-icon>
\ No newline at end of file
<monochrome android:drawable="@drawable/monochrome"/>
</adaptive-icon>
......@@ -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.51.16",
"@tanstack/react-query": "5.77.2",
"@testing-library/react-hooks": "8.0.1",
"@uniswap/analytics": "1.7.0",
"@uniswap/analytics-events": "2.42.0",
......@@ -112,7 +112,6 @@
"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=24.0
MAX_SIZE=23.66
MAX_BUFFER=0.5
# Check OS type and use appropriate stat command
......
......@@ -89,6 +89,7 @@ import {
v84Schema,
v85Schema,
v86Schema,
v87Schema,
v8Schema,
v9Schema,
} from 'src/app/schema'
......@@ -135,6 +136,7 @@ import {
testMovedLanguageSetting,
testMovedTokenWarnings,
testMovedUserSettings,
testMoveHapticsToUserSettings,
testMoveTokenAndNFTVisibility,
testRemoveCreatedOnboardingRedesignAccount,
testRemoveHoldToSwap,
......@@ -1704,4 +1706,8 @@ describe('Redux state migrations', () => {
},
})
})
it('migrates from v87 to v88', () => {
testMoveHapticsToUserSettings(migrations[88], v87Schema)
})
})
......@@ -34,6 +34,7 @@ import {
deleteWelcomeWalletCardBehaviorHistory,
moveCurrencySetting,
moveDismissedTokenWarnings,
moveHapticsToUserSettings,
moveLanguageSetting,
moveTokenAndNFTVisibility,
moveUserSettings,
......@@ -1053,6 +1054,8 @@ export const migrations = {
transactions: newTransactionState,
}
},
88: moveHapticsToUserSettings,
}
export const MOBILE_STATE_VERSION = 87
export const MOBILE_STATE_VERSION = 88
import { DdRum } from '@datadog/mobile-react-native'
import React, { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { BiometricsIconProps, useBiometricsIcon } from 'src/components/icons/useBiometricsIcon'
import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
......@@ -10,34 +9,27 @@ 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 { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
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'
import {
SmartWalletDelegationAction,
useSmartWalletDelegationStatus,
} from 'wallet/src/components/smartWallet/smartAccounts/hook'
import { selectShouldShowPostSwapNudge } from 'wallet/src/features/behaviorHistory/selectors'
import { setIncrementNumPostSwapNudge } from 'wallet/src/features/behaviorHistory/slice'
import { useIsChainSupportedBySmartWallet } from 'wallet/src/features/smartWallet/hooks/useSmartWalletChains'
import { logger } from 'utilities/src/logger/logger'
import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow'
import { useActiveAccount } from 'wallet/src/features/wallet/hooks'
import { setSmartWalletConsent } from 'wallet/src/features/wallet/slice'
import { WalletState } from 'wallet/src/state/walletReducer'
import { invalidateAndRefetchWalletDelegationQueries } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
/* Need to track the swap modal manually until it's integrated in to react-navigation */
const DATADOG_VIEW_KEY = 'global-swap-modal'
export function SwapModal(): JSX.Element {
const appDispatch = useDispatch()
const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet)
const { initialState } = useSelector(selectModalState(ModalName.Swap))
const { hapticFeedback } = useHapticFeedback()
const address = useActiveAccount()?.address
const { status: delegationStatus, loading: delegationStatusLoading } = useSmartWalletDelegationStatus({})
const signerMnemonicAccounts = useSignerAccounts()
const chains = useEnabledChains()
const accountAddresses = signerMnemonicAccounts.map((account) => account.address)
const onClose = useCallback((): void => {
appDispatch(closeModal({ name: ModalName.Swap }))
......@@ -49,7 +41,10 @@ export function SwapModal(): JSX.Element {
const timestamp = Date.now()
DdRum.startView(DATADOG_VIEW_KEY, ModalName.Swap, {}, timestamp).catch(() => undefined)
appDispatch(updateSwapStartTimestamp({ timestamp }))
}, [appDispatch])
invalidateAndRefetchWalletDelegationQueries({ accountAddresses, chainIds: chains.chains }).catch((error) =>
logger.debug('SwapModal', 'useEffect', 'Failed to invalidate and refetch wallet delegation queries', error),
)
}, [appDispatch, accountAddresses, chains.chains])
const { openWalletRestoreModal, walletRestoreType } = useWalletRestore()
......@@ -59,11 +54,6 @@ export function SwapModal(): JSX.Element {
const { trigger: biometricsTrigger } = useBiometricPrompt()
const renderBiometricsIcon = useSwapBiometricsIcon()
const canShowPostSwapNudge = useSelector((state: WalletState) =>
address ? selectShouldShowPostSwapNudge(state, address) : false,
)
const isSupportedSmartWalletChain = useIsChainSupportedBySmartWallet(swapPrefilledState?.filteredChainIds.input)
return (
<WalletSwapFlow
renderBiometricsIcon={renderBiometricsIcon}
......@@ -71,31 +61,7 @@ export function SwapModal(): JSX.Element {
openWalletRestoreModal={openWalletRestoreModal}
prefilledState={swapPrefilledState}
walletNeedsRestore={walletRestoreType === WalletRestoreType.NewDevice}
onSubmitSwap={async () => {
await hapticFeedback.success()
if (!isSmartWalletEnabled || delegationStatusLoading) {
return
}
if (
address &&
canShowPostSwapNudge &&
delegationStatus === SmartWalletDelegationAction.PromptUpgrade &&
isSupportedSmartWalletChain
) {
navigate(ModalName.PostSwapSmartWalletNudge, {
onEnableSmartWallet: () => {
appDispatch(setSmartWalletConsent({ address, smartWalletConsent: true }))
navigate(ModalName.SmartWalletEnabledModal, {
showReconnectDappPrompt: false,
})
},
})
appDispatch(setIncrementNumPostSwapNudge({ walletAddress: address }))
}
}}
onSubmitSwap={hapticFeedback.success}
onClose={onClose}
/>
)
......
......@@ -150,33 +150,18 @@ exports[`AccountSwitcher renders correctly 1`] = `
</View>
</View>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
hitSlop={16}
jestAnimatedStyle={
{
"value": {},
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -189,8 +174,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
testID="copy"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -204,12 +187,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
"borderTopRightRadius": 999999,
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 4,
"justifyContent": "center",
......@@ -224,8 +201,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -464,32 +439,25 @@ exports[`AccountSwitcher renders correctly 1`] = `
</View>
</View>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"marginTop": 16,
"opacity": 1,
......@@ -503,17 +471,9 @@ exports[`AccountSwitcher renders correctly 1`] = `
testID="account-switcher-add-wallet"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 8,
"marginLeft": 24,
......@@ -521,8 +481,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -644,8 +602,6 @@ exports[`AccountSwitcher renders correctly 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.2}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......
......@@ -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,9 +4,10 @@ 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, useCallback, useState } from 'react'
import { Linking } from 'react-native'
import React, { FC, PropsWithChildren, useEffect, useRef, useState } from 'react'
import { EmitterSubscription, Linking } from 'react-native'
import { useDispatch } from 'react-redux'
import { navigationRef } from 'src/app/navigation/navigationRef'
import { RootParamList } from 'src/app/navigation/types'
......@@ -19,7 +20,8 @@ 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 { useAsyncData } from 'utilities/src/react/hooks'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } from 'utilities/src/react/hooks'
import { sleep } from 'utilities/src/time/timing'
interface Props {
......@@ -87,20 +89,47 @@ export const NavigationContainer: FC<PropsWithChildren<Props>> = ({ children, on
const useManageDeepLinks = (): void => {
const dispatch = useDispatch()
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 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 })),
)
const hasRun = useRef(false)
const urlListener = useRef<EmitterSubscription | undefined>()
const deepLinkMutation = useMutation({
mutationFn: async () => {
if (hasRun.current) {
return
}
return urlListener.remove
}, [dispatch])
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 }) =>
dispatch(openDeepLink({ url: event.url, coldStart: false })),
)
},
onMutate: () => {
hasRun.current = true
},
onError: (error) => {
logger.error(error, {
tags: {
file: 'NavigationContainer',
function: 'useManageDeepLinks',
},
})
},
})
useAsyncData(manageDeepLinks)
const deepLinkEvent = useEvent(deepLinkMutation.mutate)
useEffect(() => {
deepLinkEvent()
return () => {
if (urlListener.current) {
urlListener.current.remove()
}
}
}, [deepLinkEvent])
}
......@@ -679,8 +679,22 @@ export const v86Schema = {
batchedTransactions: {},
}
const v87Schema = v86Schema
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
// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer
// export const getSchema = (): RootState => v0Schema
export const getSchema = (): typeof v87Schema => v87Schema
export const getSchema = (): typeof v88Schema => v88Schema
......@@ -11,7 +11,6 @@ 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'
......@@ -20,6 +19,7 @@ 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'
......
......@@ -48,7 +48,7 @@ export function ConnectedDappsList({ backButton, sessions, selectedAddress }: Co
const disconnectSession = useCallback(
async (session: WalletConnectSession, isNotification = true) => {
try {
dispatch(removeSession({ account: address, sessionId: session.id }))
dispatch(removeSession({ sessionId: session.id }))
try {
await wcWeb3Wallet.disconnectSession({
topic: session.id,
......
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Checkbox, Flex, Popover, Text, TouchableArea, useSporeColors } from 'ui/src'
import { DoubleChevronInverted } from 'ui/src/components/icons'
import { iconSizes, spacing } from 'ui/src/theme'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { useEvent } from 'utilities/src/react/hooks'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { OverlappingAccountIcons } from 'wallet/src/components/accounts/OverlappingAccountIcons'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
type SwitchAccountProps = {
allAccountAddresses: string[]
selectedAccountAddresses: string[]
setSelectedAccountAddresses: (addresses: string[]) => void
}
export const AccountSelectPopover = ({
allAccountAddresses,
selectedAccountAddresses,
setSelectedAccountAddresses,
}: SwitchAccountProps): JSX.Element => {
const { t } = useTranslation()
const signerAccounts = useSignerAccounts()
const accountIsSwitchable = signerAccounts.length > 1
const [isOpen, setIsOpen] = useState(false)
const colors = useSporeColors()
const disableDeselect = selectedAccountAddresses.length === 1
const handleAccountSelect = useEvent((address: string) => {
if (selectedAccountAddresses.includes(address)) {
if (disableDeselect) {
return
}
setSelectedAccountAddresses(selectedAccountAddresses.filter((account: string) => account !== address))
} else {
setSelectedAccountAddresses([...selectedAccountAddresses, address])
}
})
return (
<Popover open={isOpen} placement="top-end" offset={spacing.spacing12} onOpenChange={setIsOpen}>
<TouchableArea
disabled={!accountIsSwitchable}
testID={TestID.WCDappSwitchAccount}
onPress={() => accountIsSwitchable && setIsOpen(true)}
>
<Popover.Trigger asChild>
<Flex row alignItems="center" gap="$spacing8" justifyContent="space-between">
<Text color="$neutral2" variant="body3">
{t('dapp.request.approve.label')}
</Text>
<Flex row alignItems="center" justifyContent="flex-end" gap="$spacing4">
<OverlappingAccountIcons accountAddresses={selectedAccountAddresses} iconSize={iconSizes.icon24} />
{accountIsSwitchable && <DoubleChevronInverted color="$neutral3" size={iconSizes.icon16} />}
</Flex>
</Flex>
</Popover.Trigger>
</TouchableArea>
<Popover.Content
elevate
borderRadius="$rounded20"
borderWidth={1}
borderColor="$surface3"
backgroundColor="$surface1"
p="$spacing16"
gap="$gap20"
>
{allAccountAddresses.map((address) => {
const isChecked = selectedAccountAddresses.includes(address)
return (
<Flex
key={address}
row
justifyContent="space-between"
gap="$gap32"
alignItems="center"
borderRadius="$rounded12"
px="$spacing8"
py="$spacing4"
pressStyle={{ backgroundColor: '$surface2' }}
onPress={() => handleAccountSelect(address)}
>
<AddressDisplay
showAccountIcon
address={address}
hideAddressInSubtitle={false}
size={iconSizes.icon24}
textColor="$neutral1"
variant="buttonLabel3"
captionVariant="body4"
/>
<Checkbox
checked={isChecked}
size="$icon.16"
disabled={disableDeselect}
pointerEvents="none"
onCheckedChange={() => handleAccountSelect(address)}
/>
</Flex>
)
})}
<Popover.Arrow backgroundColor={colors.surface1.val} borderColor={colors.surface3.val} />
</Popover.Content>
</Popover>
)
}
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { SwitchAccountOption } from 'src/components/Requests/ScanSheet/SwitchAccountOption'
import { Flex, Text } from 'ui/src'
import { ActionSheetModal } from 'uniswap/src/components/modals/ActionSheetModal'
import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useSignerAccounts } from 'wallet/src/features/wallet/hooks'
type Props = {
activeAccount: Account | null
onPressAccount: (account: Account) => void
onClose: () => void
}
export const PendingConnectionSwitchAccountModal = ({ activeAccount, onPressAccount, onClose }: Props): JSX.Element => {
const { t } = useTranslation()
const signerAccounts = useSignerAccounts()
const options = useMemo(
() =>
signerAccounts.map((account) => {
return {
key: `${ElementName.AccountCard}-${account.address}`,
onPress: () => onPressAccount(account),
render: () => <SwitchAccountOption account={account} activeAccount={activeAccount} />,
}
}),
[signerAccounts, activeAccount, onPressAccount],
)
return (
<ActionSheetModal
header={
<Flex centered gap="$spacing4" py="$spacing16">
<Text variant="buttonLabel2">{t('walletConnect.pending.switchAccount')}</Text>
</Flex>
}
isDismissible={false}
isVisible={true}
name={ModalName.AccountEdit}
options={options}
onClose={onClose}
/>
)
}
import React from 'react'
import { Flex, Separator, Text, Unicon } from 'ui/src'
import { Check } from 'ui/src/components/icons'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { shortenAddress } from 'utilities/src/addresses'
import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText'
import { Account } from 'wallet/src/features/wallet/accounts/types'
import { useDisplayName } from 'wallet/src/features/wallet/hooks'
type Props = {
account: Account
activeAccount: Account | null
}
const ICON_SIZE = 24
export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Element => {
const displayName = useDisplayName(account.address)
return (
<>
<Separator />
<Flex row alignItems="center" justifyContent="space-between" px="$spacing24" py="$spacing8">
<Unicon address={account.address} size={ICON_SIZE} />
<Flex shrink alignItems="center" p="$none">
<DisplayNameText
displayName={displayName}
textProps={{ variant: 'body1', testID: `address-display/name/${displayName?.name}` }}
/>
<Text color="$neutral2" variant="subheading2">
{shortenAddress(account.address)}
</Text>
</Flex>
<Flex height={ICON_SIZE} width={ICON_SIZE}>
{areAddressesEqual(activeAccount?.address, account.address) && <Check color="$accent1" size={ICON_SIZE} />}
</Flex>
</Flex>
</>
)
}
import { useMutation } from '@tanstack/react-query'
import { LinearGradient } from 'expo-linear-gradient'
import { ComponentProps, default as React, useCallback, useMemo } from 'react'
import { ComponentProps, default as React, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
......@@ -8,7 +9,8 @@ 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 { useAsyncData } from 'utilities/src/react/hooks'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } 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'
......@@ -50,6 +52,7 @@ 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,
......@@ -58,15 +61,35 @@ 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) {
await refetch()
refetch()
startPolling(PollingInterval.Fast)
} else {
stopPolling()
}
}, [isVisible, refetch, startPolling, stopPolling])
useAsyncData(controlPolling)
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])
const isPortfolioValueLoading = isNonPollingRequestInFlight(networkStatus)
......
......@@ -14,32 +14,25 @@ exports[`AccountCardItem renders correctly 1`] = `
onPress={[Function]}
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 12,
......@@ -55,17 +48,9 @@ exports[`AccountCardItem renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "flex-start",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 16,
}
......@@ -73,8 +58,6 @@ exports[`AccountCardItem renders correctly 1`] = `
testID="account-item/0x82D56A352367453f74FC0dC7B071b311da373Fa6"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flex": 1,
......@@ -83,8 +66,6 @@ exports[`AccountCardItem renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -96,8 +77,6 @@ exports[`AccountCardItem renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -106,8 +85,6 @@ exports[`AccountCardItem renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......@@ -156,8 +133,6 @@ exports[`AccountCardItem renders correctly 1`] = `
testID="account-icon"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -169,8 +144,6 @@ exports[`AccountCardItem renders correctly 1`] = `
</View>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -180,8 +153,6 @@ exports[`AccountCardItem renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "row",
......@@ -190,8 +161,6 @@ exports[`AccountCardItem renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -207,8 +176,6 @@ exports[`AccountCardItem renders correctly 1`] = `
ellipsizeMode="tail"
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -237,8 +204,6 @@ exports[`AccountCardItem renders correctly 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......
......@@ -44,35 +44,20 @@ exports[`AccountHeader renders correctly 1`] = `
}
>
<View
collapsable={false}
dd-action-name="account-header-avatar"
focusVisibleStyle={{}}
forwardedRef={[Function]}
hitSlop={20}
jestAnimatedStyle={
{
"value": {},
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"alignItems": "center",
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "row",
"opacity": 1,
"transform": [
......@@ -85,8 +70,6 @@ exports[`AccountHeader renders correctly 1`] = `
testID="account-header-avatar"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......@@ -135,8 +118,6 @@ exports[`AccountHeader renders correctly 1`] = `
testID="account-icon"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -160,33 +141,18 @@ exports[`AccountHeader renders correctly 1`] = `
testID="account-header/display-name"
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
hitSlop={20}
jestAnimatedStyle={
{
"value": {},
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"flexGrow": 1,
"opacity": 1,
......@@ -199,8 +165,6 @@ exports[`AccountHeader renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -210,8 +174,6 @@ exports[`AccountHeader renders correctly 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "row",
......@@ -224,8 +186,6 @@ exports[`AccountHeader renders correctly 1`] = `
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -255,32 +215,25 @@ exports[`AccountHeader renders correctly 1`] = `
</Text>
</View>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -293,17 +246,9 @@ exports[`AccountHeader renders correctly 1`] = `
testID="account-header-copy-address"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 4,
}
......@@ -313,8 +258,6 @@ exports[`AccountHeader renders correctly 1`] = `
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -414,33 +357,26 @@ exports[`AccountHeader renders correctly 1`] = `
}
>
<View
collapsable={false}
dd-action-name="Scan"
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......
......@@ -86,32 +86,25 @@ exports[`AccountList renders without error 1`] = `
onPress={[Function]}
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 12,
......@@ -127,17 +120,9 @@ exports[`AccountList renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "flex-start",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 16,
}
......@@ -145,8 +130,6 @@ exports[`AccountList renders without error 1`] = `
testID="account-item/0x82D56A352367453f74FC0dC7B071b311da373Fa6"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flex": 1,
......@@ -155,8 +138,6 @@ exports[`AccountList renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -168,8 +149,6 @@ exports[`AccountList renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -178,8 +157,6 @@ exports[`AccountList renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......@@ -228,8 +205,6 @@ exports[`AccountList renders without error 1`] = `
testID="account-icon"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -241,8 +216,6 @@ exports[`AccountList renders without error 1`] = `
</View>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -252,8 +225,6 @@ exports[`AccountList renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "row",
......@@ -262,8 +233,6 @@ exports[`AccountList renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -279,8 +248,6 @@ exports[`AccountList renders without error 1`] = `
ellipsizeMode="tail"
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -309,8 +276,6 @@ exports[`AccountList renders without error 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......
......@@ -2,35 +2,20 @@
exports[`BackButton renders without error 1`] = `
<View
collapsable={false}
dd-action-name="back"
focusVisibleStyle={{}}
forwardedRef={[Function]}
hitSlop={24}
jestAnimatedStyle={
{
"value": {},
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"alignItems": "center",
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -43,8 +28,6 @@ exports[`BackButton renders without error 1`] = `
testID="back"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -71,8 +54,6 @@ exports[`BackButton renders without error 1`] = `
},
}
}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -107,7 +88,7 @@ exports[`BackButton renders without error 1`] = `
"borderWidth": 0,
},
{
"color": "#222222",
"color": "rgba(19, 19, 19, 0.63)",
"height": 24,
"width": 24,
},
......@@ -122,7 +103,7 @@ exports[`BackButton renders without error 1`] = `
vbWidth={24}
>
<RNSVGGroup
color="#222222"
color="rgba(19, 19, 19, 0.63)"
fill={null}
propList={
[
......@@ -154,8 +135,6 @@ exports[`BackButton renders without error 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......
......@@ -2,32 +2,25 @@
exports[`CloseButton renders without error 1`] = `
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -56,7 +49,6 @@ exports[`CloseButton renders without error 1`] = `
"borderWidth": 0,
},
{
"color": "#222222",
"height": 20,
"width": 20,
},
......@@ -71,7 +63,6 @@ exports[`CloseButton renders without error 1`] = `
vbWidth={16}
>
<RNSVGGroup
color="#222222"
fill={null}
propList={
[
......
......@@ -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
......
......@@ -36,33 +36,18 @@ exports[`FavoriteHeaderRow when editing renders without error 1`] = `
Editing Title
</Text>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
hitSlop={16}
jestAnimatedStyle={
{
"value": {},
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -76,8 +61,6 @@ exports[`FavoriteHeaderRow when editing renders without error 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.2}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -137,33 +120,18 @@ exports[`FavoriteHeaderRow when not editing renders without error 1`] = `
Title
</Text>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
hitSlop={16}
jestAnimatedStyle={
{
"value": {},
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......
......@@ -36,27 +36,31 @@ exports[`FavoriteTokenCard renders without error 1`] = `
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "#FFFFFF",
"backgroundColor": {
"dynamic": {
"dark": "#131313",
"light": "#FFFFFF",
},
},
"borderBottomColor": "rgba(34,34,34,0.05)",
"borderBottomLeftRadius": 16,
"borderBottomRightRadius": 16,
......@@ -90,17 +94,9 @@ exports[`FavoriteTokenCard renders without error 1`] = `
testID="token-box-undefined"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "flex-start",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "column",
"gap": 8,
"paddingBottom": 12,
......@@ -111,8 +107,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "row",
......@@ -122,8 +116,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -134,8 +126,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -146,8 +136,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
testID="shimmer-placeholder"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -155,8 +143,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......@@ -180,8 +166,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -200,22 +184,25 @@ exports[`FavoriteTokenCard renders without error 1`] = `
/>
</View>
<View
aria-disabled={true}
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
disabled={true}
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onLayout={[Function]}
pointerEvents="box-none"
role="button"
style={
{
"alignItems": "center",
"backgroundColor": "rgba(19, 19, 19, 0.35)",
"backgroundColor": {
"dynamic": {
"dark": "rgba(255, 255, 255, 0.38)",
"light": "rgba(19, 19, 19, 0.35)",
},
},
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
......@@ -233,31 +220,21 @@ exports[`FavoriteTokenCard renders without error 1`] = `
"zIndex": 1080,
}
}
tabIndex={-1}
testID="explore/remove-button"
userSelect="none"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
"dynamic": {
"dark": "#1F1F1F",
"light": "#F9F9F9",
"dark": "#131313",
"light": "#FFFFFF",
},
},
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"color": {
"dynamic": {
"dark": "rgba(255, 255, 255, 0.65)",
"light": "rgba(19, 19, 19, 0.63)",
},
},
"flexDirection": "column",
"height": 2,
"width": 10,
......@@ -267,8 +244,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
</View>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -277,8 +252,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -289,8 +262,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
testID="shimmer-placeholder"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -298,8 +269,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......@@ -322,8 +291,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
</View>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -334,8 +301,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
testID="shimmer-placeholder"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -343,8 +308,6 @@ exports[`FavoriteTokenCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......
......@@ -23,28 +23,32 @@ exports[`FavoriteWalletCard renders without error 1`] = `
}
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
disabled={false}
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "#FFFFFF",
"backgroundColor": {
"dynamic": {
"dark": "#131313",
"light": "#FFFFFF",
},
},
"borderBottomColor": "rgba(34,34,34,0.05)",
"borderBottomLeftRadius": 16,
"borderBottomRightRadius": 16,
......@@ -82,16 +86,8 @@ exports[`FavoriteWalletCard renders without error 1`] = `
testID="favorite-wallet-card"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 4,
"justifyContent": "space-between",
......@@ -103,8 +99,6 @@ exports[`FavoriteWalletCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -115,8 +109,6 @@ exports[`FavoriteWalletCard renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......@@ -165,8 +157,6 @@ exports[`FavoriteWalletCard renders without error 1`] = `
testID="account-icon"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -177,8 +167,6 @@ exports[`FavoriteWalletCard renders without error 1`] = `
/>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -191,8 +179,6 @@ exports[`FavoriteWalletCard renders without error 1`] = `
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -215,22 +201,25 @@ exports[`FavoriteWalletCard renders without error 1`] = `
</View>
</View>
<View
aria-disabled={true}
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
disabled={true}
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onLayout={[Function]}
pointerEvents="box-none"
role="button"
style={
{
"alignItems": "center",
"backgroundColor": "rgba(19, 19, 19, 0.35)",
"backgroundColor": {
"dynamic": {
"dark": "rgba(255, 255, 255, 0.38)",
"light": "rgba(19, 19, 19, 0.35)",
},
},
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
......@@ -248,31 +237,21 @@ exports[`FavoriteWalletCard renders without error 1`] = `
"zIndex": 1080,
}
}
tabIndex={-1}
testID="explore/remove-button"
userSelect="none"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
"dynamic": {
"dark": "#1F1F1F",
"light": "#F9F9F9",
"dark": "#131313",
"light": "#FFFFFF",
},
},
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"color": {
"dynamic": {
"dark": "rgba(255, 255, 255, 0.65)",
"light": "rgba(19, 19, 19, 0.63)",
},
},
"flexDirection": "column",
"height": 2,
"width": 10,
......
......@@ -3,28 +3,33 @@
exports[`RemoveButton renders without error 1`] = `
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
disabled={false}
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"alignItems": "center",
"backgroundColor": "rgba(19, 19, 19, 0.35)",
"backgroundColor": {
"dynamic": {
"dark": "rgba(255, 255, 255, 0.38)",
"light": "rgba(19, 19, 19, 0.35)",
},
},
"borderBottomLeftRadius": 999999,
"borderBottomRightRadius": 999999,
"borderTopLeftRadius": 999999,
......@@ -45,8 +50,6 @@ exports[`RemoveButton renders without error 1`] = `
testID="explore/remove-button"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": {
......@@ -59,12 +62,6 @@ exports[`RemoveButton renders without error 1`] = `
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "column",
"height": 2,
"width": 10,
......
......@@ -25,32 +25,25 @@ exports[`SortButton renders without error 1`] = `
}
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -63,17 +56,9 @@ exports[`SortButton renders without error 1`] = `
>
<View
collapsable={false}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 8,
"justifyContent": "center",
......@@ -88,8 +73,6 @@ exports[`SortButton renders without error 1`] = `
lineBreakMode="clip"
maxFontSizeMultiplier={1.2}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -127,8 +110,6 @@ exports[`SortButton renders without error 1`] = `
},
}
}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......
......@@ -6,32 +6,25 @@ exports[`TokenItem renders without error 1`] = `
onPress={[MockFunction]}
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -45,17 +38,9 @@ exports[`TokenItem renders without error 1`] = `
>
<View
collapsable={false}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"flexGrow": 1,
"gap": 12,
......@@ -67,8 +52,6 @@ exports[`TokenItem renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -79,8 +62,6 @@ exports[`TokenItem renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -92,8 +73,6 @@ exports[`TokenItem renders without error 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -114,8 +93,6 @@ exports[`TokenItem renders without error 1`] = `
</Text>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
pointerEvents="auto"
style={
{
......@@ -130,8 +107,6 @@ exports[`TokenItem renders without error 1`] = `
testID="token-logo"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"backgroundColor": "#FFFFFF",
......@@ -173,8 +148,6 @@ exports[`TokenItem renders without error 1`] = `
</View>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flex": 1,
......@@ -188,8 +161,6 @@ exports[`TokenItem renders without error 1`] = `
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -212,8 +183,6 @@ exports[`TokenItem renders without error 1`] = `
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -233,8 +202,6 @@ exports[`TokenItem renders without error 1`] = `
/>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
onLayout={[Function]}
style={
{
......@@ -245,8 +212,6 @@ exports[`TokenItem renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "row",
......@@ -254,8 +219,6 @@ exports[`TokenItem renders without error 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "flex-end",
......@@ -268,8 +231,6 @@ exports[`TokenItem renders without error 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -290,8 +251,6 @@ exports[`TokenItem renders without error 1`] = `
-
</Text>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -303,8 +262,6 @@ exports[`TokenItem renders without error 1`] = `
testID="relative-change"
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -314,8 +271,6 @@ exports[`TokenItem renders without error 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......
import { useMemo } from 'react'
import { AppStackScreenProp } from 'src/app/navigation/types'
import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal'
import { useOnEnableSmartWallet } from 'src/features/smartWallet/hooks/useOnEnableSmartWallet'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { PostSwapSmartWalletNudge } from 'wallet/src/components/smartWallet/modals/PostSwapSmartWalletNudge'
import {
PostSwapSmartWalletNudge,
PostSwapSmartWalletNudgeProps,
} from 'wallet/src/components/smartWallet/modals/PostSwapSmartWalletNudge'
export const PostSwapSmartWalletNudgeScreen = (
props: AppStackScreenProp<typeof ModalName.PostSwapSmartWalletNudge>,
): JSX.Element => {
return <ReactNavigationModal {...props} modalComponent={PostSwapSmartWalletNudge} />
const onEnableSmartWallet = useOnEnableSmartWallet()
const modalComponent = useMemo(() => {
// Create a wrapper component that pre-fills the onEnableSmartWallet prop if it's not defined
return function PostSwapSmartWalletNudgeWrapper(modalProps: PostSwapSmartWalletNudgeProps) {
if (modalProps.onEnableSmartWallet) {
return <PostSwapSmartWalletNudge {...modalProps} />
}
return <PostSwapSmartWalletNudge {...modalProps} onEnableSmartWallet={onEnableSmartWallet} />
}
}, [onEnableSmartWallet])
return <ReactNavigationModal {...props} modalComponent={modalComponent} />
}
import { NativeModules } from 'react-native'
interface RNCloudStorageBackupsManager {
isCloudStorageAvailable: () => Promise<boolean>
deleteCloudStorageMnemonicBackup: (mnemonicId: string) => Promise<boolean>
......@@ -12,7 +14,6 @@ declare module 'react-native' {
RNCloudStorageBackupsManager: RNCloudStorageBackupsManager
}
}
import { NativeModules } from 'react-native'
const { RNCloudStorageBackupsManager } = NativeModules
......
import { hasHardwareAsync, isEnrolledAsync } from 'expo-local-authentication'
import { useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { triggerAuthentication } from 'src/features/biometrics/biometricsSlice'
import { isAndroid } from 'utilities/src/platform'
......@@ -47,22 +48,25 @@ export function useBiometricPrompt<T = undefined>(
} {
const dispatch = useDispatch()
const trigger = async (args?: TriggerArgs<T>): Promise<void> => {
dispatch(
triggerAuthentication({
onSuccess: (params?: unknown) => {
const typedParams = params as T | undefined
if (args?.successCallback) {
args.successCallback(typedParams)
} else if (successCallback) {
successCallback(typedParams)
}
},
onFailure: args?.failureCallback ?? failureCallback,
params: args?.params,
}),
)
}
const trigger = useCallback(
async (args?: TriggerArgs<T>): Promise<void> => {
dispatch(
triggerAuthentication({
onSuccess: (params?: unknown) => {
const typedParams = params as T | undefined
if (args?.successCallback) {
args.successCallback(typedParams)
} else if (successCallback) {
successCallback(typedParams)
}
},
onFailure: args?.failureCallback ?? failureCallback,
params: args?.params,
}),
)
},
[dispatch, successCallback, failureCallback],
)
return { trigger }
}
......
......@@ -223,28 +223,31 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
}
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "rgba(255, 55, 199, 0.08)",
"backgroundColor": {
"dynamic": {
"dark": "rgba(255, 55, 199, 0.08)",
"light": "rgba(255, 55, 199, 0.08)",
},
},
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
......@@ -264,17 +267,9 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 4,
"justifyContent": "center",
......@@ -284,8 +279,6 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.2}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......
......@@ -2,32 +2,26 @@
exports[`renders collection preview card 1`] = `
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
disabled={false}
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"transform": [
......@@ -39,8 +33,6 @@ exports[`renders collection preview card 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -49,12 +41,6 @@ exports[`renders collection preview card 1`] = `
"borderBottomRightRadius": 16,
"borderTopLeftRadius": 16,
"borderTopRightRadius": 16,
"color": {
"dynamic": {
"dark": "#FFFFFF",
"light": "#222222",
},
},
"flexDirection": "row",
"gap": 8,
"justifyContent": "space-between",
......@@ -66,8 +52,6 @@ exports[`renders collection preview card 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -79,8 +63,6 @@ exports[`renders collection preview card 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"borderBottomLeftRadius": 999999,
......@@ -95,8 +77,6 @@ exports[`renders collection preview card 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -121,8 +101,6 @@ exports[`renders collection preview card 1`] = `
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": {
......@@ -145,8 +123,6 @@ exports[`renders collection preview card 1`] = `
</View>
</View>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -155,8 +131,6 @@ exports[`renders collection preview card 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......@@ -167,8 +141,6 @@ exports[`renders collection preview card 1`] = `
}
>
<View
onBlur={[Function]}
onFocus={[Function]}
style={
{
"flexDirection": "column",
......@@ -180,8 +152,6 @@ exports[`renders collection preview card 1`] = `
allowFontScaling={true}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"color": "#131313",
......@@ -327,8 +297,6 @@ exports[`renders collection preview card 1`] = `
},
}
}
onBlur={[Function]}
onFocus={[Function]}
style={
{
"alignItems": "center",
......
......@@ -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/getUniqueId'
import { getUniqueId } from 'utilities/src/device/uniqueId'
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'
......
import { useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { navigate } from 'src/app/navigation/rootNavigation'
import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricAppSettings'
import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import { useActiveAccount } from 'wallet/src/features/wallet/hooks'
import { setSmartWalletConsent } from 'wallet/src/features/wallet/slice'
/**
* Hook for enabling smart wallet with biometrics in modals.
* @returns A function that enables smart wallet with biometrics if needed
*/
export function useOnEnableSmartWallet(): () => void {
const dispatch = useDispatch()
const accountAddress = useActiveAccount()?.address
const { trigger } = useBiometricPrompt()
const { requiredForTransactions: requiresBiometrics } = useBiometricAppSettings()
if (!accountAddress) {
throw new Error('Account address is required')
}
const successAction = useCallback(() => {
dispatch(setSmartWalletConsent({ address: accountAddress, smartWalletConsent: true }))
navigate(ModalName.SmartWalletEnabledModal, {
showReconnectDappPrompt: true,
})
}, [accountAddress, dispatch])
return useCallback(async () => {
if (requiresBiometrics) {
await trigger({
successCallback: successAction,
})
} else {
successAction()
}
}, [requiresBiometrics, successAction, trigger])
}
......@@ -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/getUniqueId'
import { getUniqueId } from 'utilities/src/device/uniqueId'
import { isAndroid } from 'utilities/src/platform'
import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
......
......@@ -175,7 +175,9 @@ export function* handleGetCapabilities({
const detailsMap = delegationStatusResponse.delegationDetails[accountAddress]
if (detailsMap) {
const hasAtLeastOneDelegation = Object.values(detailsMap).some((details) => !!details.currentDelegationAddress)
const hasAtLeastOneDelegation = Object.values(detailsMap).some(
(details) => !!details.currentDelegationAddress && !details.isWalletDelegatedToUniswap,
)
hasNoExistingDelegations = !hasAtLeastOneDelegation
}
......@@ -187,12 +189,6 @@ export function* handleGetCapabilities({
}
if (!hasSmartWalletConsent && !hasShownNudge && hasNoExistingDelegations) {
const onEnableSmartWallet = () => {
navigate(ModalName.SmartWalletEnabledModal, {
showReconnectDappPrompt: true,
})
}
// Update the state to mark that we've shown the nudge
yield* put(
setHasShown5792Nudge({
......@@ -202,7 +198,6 @@ export function* handleGetCapabilities({
)
yield* call(navigate, ModalName.PostSwapSmartWalletNudge, {
onEnableSmartWallet,
dappInfo: {
icon: dappIconUrl,
name: dappName,
......@@ -210,7 +205,8 @@ export function* handleGetCapabilities({
})
}
const capabilities = getCapabilitiesForDelegationStatus(
const capabilities = yield* call(
getCapabilitiesForDelegationStatus,
delegationStatusResponse?.delegationDetails[accountAddress],
hasSmartWalletConsent,
)
......
......@@ -6,7 +6,7 @@ export function fetchDappDetails(
currentState: Readonly<WalletConnectState>,
): { dappIcon: string | null; dappName: string } {
try {
const sessions = Object.values(currentState.byAccount).find((account) => account?.sessions?.[topic])?.sessions
const sessions = currentState.sessions
if (sessions && sessions[topic]) {
const wcSession = sessions[topic]
......
......@@ -4,14 +4,8 @@ import { buildApprovedNamespaces, populateAuthPayload } from '@walletconnect/uti
import { expectSaga } from 'redux-saga-test-plan'
import { handleSessionAuthenticate, handleSessionProposal } from 'src/features/walletConnect/saga'
import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient'
import {
SignRequest,
WalletConnectVerifyStatus,
addPendingSession,
addRequest,
} from 'src/features/walletConnect/walletConnectSlice'
import { WalletConnectVerifyStatus, addPendingSession } from 'src/features/walletConnect/walletConnectSlice'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { EthMethod } from 'uniswap/src/features/dappRequests/types'
import { DappRequestInfo, DappRequestType, EthEvent } from 'uniswap/src/types/walletConnect'
import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
......@@ -121,9 +115,17 @@ describe('WalletConnect Saga', () => {
if (selector === selectActiveAccountAddress) {
return activeAccountAddress
}
// For any other selectors that might access wallet state
return next()
},
})
.withState({
wallet: {
accounts: {
[activeAccountAddress]: { address: activeAccountAddress },
},
},
})
.put(addPendingSession(expectedPendingSession))
.run()
})
......@@ -181,23 +183,6 @@ describe('WalletConnect Saga', () => {
// Mock formatAuthMessage
wcWeb3Wallet.formatAuthMessage = jest.fn().mockReturnValue(mockAuthMessage)
const mockSignRequest: SignRequest = {
type: EthMethod.EthSign,
message: mockAuthMessage,
rawMessage: mockAuthMessage,
sessionId: '789',
internalId: `${UniverseChainId.Mainnet}:789`, // Fix: use actual chainId format without 'eip155:' prefix
chainId: UniverseChainId.Mainnet,
account: activeAccountAddress,
dappRequestInfo: {
name: 'Auth Dapp',
url: 'https://auth-dapp.com',
icon: 'https://auth-dapp.com/icon.png',
requestType: DappRequestType.WalletConnectAuthenticationRequest,
authPayload: mockPopulatedAuthPayload,
},
}
// Run the saga and verify action is dispatched
await expectSaga(handleSessionAuthenticate, mockAuthenticate)
.provide({
......@@ -208,7 +193,14 @@ describe('WalletConnect Saga', () => {
return next()
},
})
.put(addRequest(mockSignRequest))
.withState({
wallet: {
accounts: {
[activeAccountAddress]: { address: activeAccountAddress },
},
},
})
.put.actionType('walletConnect/addRequest')
.run()
})
})
......
......@@ -12,6 +12,7 @@ import {
handleSendCalls,
} from 'src/features/walletConnect/batchedTransactionSaga'
import { fetchDappDetails } from 'src/features/walletConnect/fetchDappDetails'
import { selectAllSessions } from 'src/features/walletConnect/selectors'
import {
getAccountAddressFromEIP155String,
getChainIdFromEIP155String,
......@@ -30,6 +31,7 @@ import {
addRequest,
addSession,
removeSession,
replaceSession,
setHasPendingSessionError,
} from 'src/features/walletConnect/walletConnectSlice'
import { call, fork, put, select, take } from 'typed-redux-saga'
......@@ -43,9 +45,15 @@ import { pushNotification } from 'uniswap/src/features/notifications/slice'
import { AppNotificationType } from 'uniswap/src/features/notifications/types'
import i18n from 'uniswap/src/i18n'
import { DappRequestType, EthEvent, WalletConnectEvent } from 'uniswap/src/types/walletConnect'
import { areAddressesEqual } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors'
import {
selectAccounts,
selectActiveAccountAddress,
selectSignerMnemonicAccounts,
} from 'wallet/src/features/wallet/selectors'
import { setAccountAsActive } from 'wallet/src/features/wallet/slice'
const WC_SUPPORTED_METHODS = [
EthMethod.EthSign,
......@@ -154,8 +162,6 @@ function* cancelErrorSession(dappName: string, chainLabels: string, proposalId:
}
export function* handleSessionProposal(proposal: ProposalTypes.Struct & { verifyContext?: Verify.Context }) {
const activeAccountAddress = yield* select(selectActiveAccountAddress)
const {
id,
proposer: { metadata: dapp },
......@@ -170,9 +176,15 @@ export function* handleSessionProposal(proposal: ProposalTypes.Struct & { verify
return
}
const activeSignerAccounts = yield* select(selectSignerMnemonicAccounts)
const activeSignerAccountAddresses = activeSignerAccounts.map((account) => account.address)
try {
const supportedEip155Chains = ALL_CHAIN_IDS.map((chainId) => `eip155:${chainId}`)
const accounts = supportedEip155Chains.map((chain) => `${chain}:${activeAccountAddress}`)
const accounts = supportedEip155Chains.flatMap((chain) =>
activeSignerAccountAddresses.map((account) => `${chain}:${account}`),
)
const namespaces = buildApprovedNamespaces({
proposal,
......@@ -481,8 +493,8 @@ function* populateActiveSessions() {
},
chains,
namespaces: session.namespaces,
activeAccount: accountAddress,
},
account: accountAddress,
}),
)
}
......@@ -504,10 +516,59 @@ function* fetchPendingSessionRequests() {
}
}
/**
* Monitor wallet account changes and update WC sessions accordingly for sessions that are approved with multiple accounts.
*
* This allows connected Dapps to stay in sync with the currently active wallet account across all supported chains.
*
* Account approvals are per chain, and included as part of the wc session namespaces.
*/
function* monitorAccountChanges() {
while (true) {
const action = yield* take(setAccountAsActive)
const newActiveAccountAddress = action.payload
const allSessions = yield* select(selectAllSessions)
for (const session of Object.values(allSessions)) {
const accounts = session.namespaces.eip155?.accounts
// Update all sessions if new active account is included in the session namespacess
const isNewAccountApprovedInNamespace = accounts?.some((eip155String) => {
const parsedAddress = getAccountAddressFromEIP155String(eip155String)
return areAddressesEqual(parsedAddress, newActiveAccountAddress)
})
if (!isNewAccountApprovedInNamespace) {
continue
}
// Update WC sessions across all supported chains with the new active account
const chains = session.namespaces.eip155?.chains ?? []
yield* call(function* () {
for (const chainId of chains) {
yield* call([wcWeb3Wallet, wcWeb3Wallet.emitSessionEvent], {
topic: session.id,
chainId,
event: {
name: EthEvent.AccountsChanged,
data: [newActiveAccountAddress],
},
})
}
})
// Update the active account in store
yield* put(replaceSession({ wcSession: { ...session, activeAccount: newActiveAccountAddress } }))
}
}
}
export function* walletConnectSaga() {
yield* call(initializeWeb3Wallet)
yield* call(populateActiveSessions)
yield* fork(fetchPendingSessionProposals)
yield* fork(fetchPendingSessionRequests)
yield* fork(watchWalletConnectEvents)
yield* fork(monitorAccountChanges)
}
......@@ -8,22 +8,22 @@ import {
export const makeSelectSessions = (): Selector<MobileState, WalletConnectSession[] | undefined, [Maybe<Address>]> =>
createSelector(
(state: MobileState) => state.walletConnect.byAccount,
(state: MobileState) => state.walletConnect.sessions,
(_: MobileState, address: Maybe<Address>) => address,
(sessionsByAccount, address) => {
(sessions, address) => {
if (!address) {
return undefined
}
const wcAccount = sessionsByAccount[address]
if (!wcAccount) {
return undefined
}
return Object.values(wcAccount.sessions)
// Filter sessions by active account address
return Object.values(sessions).filter((session) => session.activeAccount === address)
},
)
export const selectAllSessions = (state: MobileState): Record<string, WalletConnectSession> => {
return state.walletConnect.sessions
}
export const selectPendingRequests = (state: MobileState): WalletConnectSigningRequest[] => {
return state.walletConnect.pendingRequests
}
......
......@@ -26,16 +26,21 @@ import { GetCallsStatusParams, SendCallsParams } from 'wallet/src/features/dappR
* @return {SessionTypes.Namespaces} session namespaces specifying which accounts, chains, methods, events to complete the pairing
*/
export const getSessionNamespaces = (
account: Address,
accounts: Address[],
proposalNamespaces: ProposalTypes.RequiredNamespaces,
): SessionTypes.Namespaces => {
// Below inspired from https://github.com/WalletConnect/web-examples/blob/main/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx#L63
const namespaces: SessionTypes.Namespaces = {}
Object.entries(proposalNamespaces).forEach(([key, namespace]) => {
Object.entries(proposalNamespaces).forEach(([nameSpaceId, namespace]) => {
const { chains, events, methods } = namespace
namespaces[key] = {
accounts: chains ? chains.map((chain) => `${chain}:${account}`) : [`${key}:${account}`],
const formattedAccounts = !chains
? accounts.map((account) => `${nameSpaceId}:${account}`)
: accounts.flatMap((account) => chains.map((chain) => `${chain}:${account}`))
namespaces[nameSpaceId] = {
accounts: formattedAccounts,
events,
methods,
chains,
......
......@@ -3,6 +3,7 @@ import { ProposalTypes, SessionTypes } from '@walletconnect/types'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { EthMethod, EthSignMethod } from 'uniswap/src/features/dappRequests/types'
import { DappRequestInfo, EthTransaction, UwULinkMethod } from 'uniswap/src/types/walletConnect'
import { logger } from 'utilities/src/logger/logger'
import { Call, Capability } from 'wallet/src/features/dappRequests/types'
export enum WalletConnectVerifyStatus {
......@@ -24,10 +25,12 @@ export type WalletConnectSession = {
chains: UniverseChainId[]
dappRequestInfo: DappRequestInfo
namespaces: SessionTypes.Namespaces
}
interface SessionMapping {
[sessionId: string]: WalletConnectSession
/**
* WC session namespaces can contain approvals for multiple accounts. The active account represents the account that the dapp
* is tracking as the active account based on session events (approve session, change account, etc).
*/
activeAccount: string
}
interface BaseRequest {
......@@ -104,10 +107,8 @@ export const isBatchedTransactionRequest = (
): request is WalletSendCallsEncodedRequest => request.type === EthMethod.WalletSendCalls
export interface WalletConnectState {
byAccount: {
[accountId: string]: {
sessions: SessionMapping
}
sessions: {
[sessionId: string]: WalletConnectSession
}
pendingSession: WalletConnectPendingSession | null
pendingRequests: WalletConnectSigningRequest[]
......@@ -116,7 +117,7 @@ export interface WalletConnectState {
}
export const initialWalletConnectState: Readonly<WalletConnectState> = {
byAccount: {},
sessions: {},
pendingSession: null,
pendingRequests: [],
}
......@@ -125,34 +126,25 @@ const slice = createSlice({
name: 'walletConnect',
initialState: initialWalletConnectState,
reducers: {
addSession: (state, action: PayloadAction<{ account: string; wcSession: WalletConnectSession }>) => {
const { wcSession, account } = action.payload
state.byAccount[account] ??= { sessions: {} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
state.byAccount[account]!.sessions[wcSession.id] = wcSession
addSession: (state, action: PayloadAction<{ wcSession: WalletConnectSession }>) => {
const { wcSession } = action.payload
state.sessions[wcSession.id] = wcSession
state.pendingSession = null
},
removeSession: (state, action: PayloadAction<{ sessionId: string; account?: string }>) => {
const { sessionId, account } = action.payload
replaceSession: (state, action: PayloadAction<{ wcSession: WalletConnectSession }>) => {
const { wcSession } = action.payload
state.sessions[wcSession.id] = wcSession
},
removeSession: (state, action: PayloadAction<{ sessionId: string }>) => {
const { sessionId } = action.payload
// If account address is known, delete directly
if (account) {
const wcAccount = state.byAccount[account]
if (wcAccount) {
delete wcAccount.sessions[sessionId]
}
return
if (!state.sessions[sessionId]) {
logger.warn('walletConnect/walletConnectSlice.ts', 'removeSession', `Session ${sessionId} doesnt exist`)
}
// If account address is not known (handling `session_delete` events),
// iterate over each account and delete the sessionId
Object.keys(state.byAccount).forEach((accountAddress) => {
const wcAccount = state.byAccount[accountAddress]
if (wcAccount && wcAccount.sessions[sessionId]) {
delete wcAccount.sessions[sessionId]
}
})
delete state.sessions[sessionId]
},
addPendingSession: (state, action: PayloadAction<{ wcSession: WalletConnectPendingSession }>) => {
......@@ -185,6 +177,7 @@ const slice = createSlice({
export const {
addSession,
replaceSession,
removeSession,
addPendingSession,
removePendingSession,
......
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'
......@@ -13,9 +14,7 @@ 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'
......@@ -34,7 +33,7 @@ export function DevScreen(): JSX.Element {
const activeAccount = useActiveAccount()
const [rtlEnabled, setRTLEnabled] = useState(I18nManager.isRTL)
const sortedMnemonicAccounts = useSelector(selectSortedSignerMnemonicAccounts)
const { data: deviceId } = useAsyncData(getUniqueId)
const deviceId = getUniqueIdSync()
const onPressResetTokenWarnings = (): void => {
dispatch(resetDismissedWarnings())
......
......@@ -15,7 +15,7 @@ import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext'
import { FiatOnRampCountryListModal } from 'src/features/fiatOnRamp/FiatOnRampCountryListModal'
import { FiatOnRampTokenSelectorModal } from 'src/features/fiatOnRamp/FiatOnRampTokenSelector'
import { OffRampPopover } from 'src/features/fiatOnRamp/OffRampPopover'
import { Flex, isWeb, useIsDarkMode, useIsShortMobileDevice } from 'ui/src'
import { Flex, useIsDarkMode, useIsShortMobileDevice } from 'ui/src'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext'
import { HandleBar } from 'uniswap/src/components/modals/HandleBar'
......@@ -62,7 +62,7 @@ import { CurrencyField } from 'uniswap/src/types/currency'
import { FiatOnRampScreens } from 'uniswap/src/types/screens/mobile'
import { currencyIdToAddress } from 'uniswap/src/utils/currencyId'
import { truncateToMaxDecimals } from 'utilities/src/format/truncateToMaxDecimals'
import { isIOS } from 'utilities/src/platform'
import { isIOS, isWeb } from 'utilities/src/platform'
import { usePrevious } from 'utilities/src/react/hooks'
import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
......
......@@ -44,7 +44,7 @@ import { useHomeScreenTracking } from 'src/screens/HomeScreen/useHomeScreenTrack
import { useHomeScrollRefs } from 'src/screens/HomeScreen/useHomeScrollRefs'
import { useOpenBackupReminderModal } from 'src/utils/useOpenBackupReminderModal'
import { Flex, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src'
import { SMART_WALLET_UPGRADE_VIDEO } from 'ui/src/assets'
import { SMART_WALLET_UPGRADE_FALLBACK, SMART_WALLET_UPGRADE_VIDEO } from 'ui/src/assets'
import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex'
import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions'
import { spacing } from 'ui/src/theme'
......@@ -57,10 +57,15 @@ import { ModalName, SectionName, SectionNameType } from 'uniswap/src/features/te
import { useAppInsets } from 'uniswap/src/hooks/useAppInsets'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { MobileScreens } from 'uniswap/src/types/screens/mobile'
import { logger } from 'utilities/src/logger/logger'
import { useEvent } from 'utilities/src/react/hooks'
import { SmartWalletUpgradeModals } from 'wallet/src/components/smartWallet/modals/SmartWalletUpgradeModal'
import { useOpenSmartWalletNudgeOnCompletedSwap } from 'wallet/src/components/smartWallet/smartAccounts/hook'
import { selectHasSeenCreatedSmartWalletModal } from 'wallet/src/features/behaviorHistory/selectors'
import { setHasSeenSmartWalletCreatedWalletModal } from 'wallet/src/features/behaviorHistory/slice'
import {
setHasSeenSmartWalletCreatedWalletModal,
setIncrementNumPostSwapNudge,
} from 'wallet/src/features/behaviorHistory/slice'
import { PortfolioBalance } from 'wallet/src/features/portfolio/PortfolioBalance'
import { useHeartbeatReporter, useLastBalancesReporter } from 'wallet/src/features/telemetry/hooks'
import { useAccountCountChanged, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks'
......@@ -283,14 +288,28 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
)
}, [showEmptyWalletState, activeAccount.address, isSignerAccount, onPressViewOnlyLabel, viewOnlyLabel, promoBanner])
const MemoizedVideo = useMemo(
() => (
const [hasVideoError, setVideoHasError] = useState(false)
const MemoizedVideo = useMemo(() => {
if (hasVideoError) {
return undefined
}
return (
<Flex borderRadius="$rounded16" width="100%" aspectRatio={16 / 9} overflow="hidden">
<Video source={SMART_WALLET_UPGRADE_VIDEO} resizeMode="cover" style={{ width: '100%', height: '100%' }} />
<Video
source={SMART_WALLET_UPGRADE_VIDEO}
poster={SMART_WALLET_UPGRADE_FALLBACK}
resizeMode="cover"
style={{ width: '100%', height: '100%' }}
onError={(error) => {
logger.warn('HomeScreen', 'MemoizedVideo', 'video error', error)
setVideoHasError(true)
}}
/>
</Flex>
),
[],
)
)
}, [hasVideoError])
const paddingTop = headerHeight + TAB_BAR_HEIGHT + (showEmptyWalletState ? 0 : TAB_STYLES.tabListInner.paddingTop)
const paddingBottom = insets.bottom + SWAP_BUTTON_HEIGHT + TAB_STYLES.tabListInner.paddingBottom + spacing.spacing12
......@@ -529,6 +548,32 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
}),
)
useOpenSmartWalletNudgeOnCompletedSwap(
useEvent(() => {
if (!activeAccount.address) {
return
}
navigate(ModalName.PostSwapSmartWalletNudge, {
onEnableSmartWallet: async () => {
const successAction = (): void => {
dispatch(setSmartWalletConsent({ address: activeAccount.address, smartWalletConsent: true }))
navigate(ModalName.SmartWalletEnabledModal, {
showReconnectDappPrompt: false,
})
}
if (requiresBiometrics) {
await trigger({ successCallback: successAction })
} else {
successAction()
}
},
})
dispatch(setIncrementNumPostSwapNudge({ walletAddress: activeAccount.address }))
}),
)
return (
<Screen edges={['left', 'right']} onLayout={hideSplashScreen}>
<View style={TAB_STYLES.container}>
......
......@@ -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,
loading,
isLoading,
isSmartContractAddress,
isValidSmartContract,
}: {
validAddress: string | null
name: string | null
walletExists: boolean
loading: boolean
isLoading: boolean
isSmartContractAddress: boolean
isValidSmartContract: boolean
}): boolean => {
return (!!validAddress || !!name) && !walletExists && !loading && (!isSmartContractAddress || isValidSmartContract)
return (!!validAddress || !!name) && !walletExists && !isLoading && (!isSmartContractAddress || isValidSmartContract)
}
const getErrorText = ({
walletExists,
isSmartContractAddress,
loading,
isLoading,
t,
}: {
walletExists: boolean
isSmartContractAddress: boolean
loading: boolean
isLoading: 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 (!loading) {
} else if (!isLoading) {
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 } = useIsSmartContractAddress(
const { isSmartContractAddress, loading: isLoading } = useIsSmartContractAddress(
(validAddress || resolvedAddress) ?? undefined,
defaultChainId,
)
......@@ -115,12 +115,12 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX
validAddress,
name,
walletExists,
loading,
isLoading,
isSmartContractAddress,
isValidSmartContract,
})
const errorText = !isValid ? getErrorText({ walletExists, isSmartContractAddress, loading, t }) : undefined
const errorText = !isValid ? getErrorText({ walletExists, isSmartContractAddress, isLoading, t }) : undefined
const onSubmit = useCallback(async () => {
if (isValid && value) {
......
......@@ -392,32 +392,25 @@ exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = `
}
>
<View
collapsable={false}
focusVisibleStyle={{}}
forwardedRef={[Function]}
jestAnimatedStyle={
hitSlop={
{
"value": {},
"bottom": 5,
"left": 5,
"right": 5,
"top": 5,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onLayout={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
{
"backgroundColor": "transparent",
"borderBottomLeftRadius": 12,
"borderBottomRightRadius": 12,
"borderTopLeftRadius": 12,
"borderTopRightRadius": 12,
"flexDirection": "column",
"opacity": 1,
"paddingBottom": 4,
......
......@@ -28,7 +28,6 @@ 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,
......@@ -61,6 +60,7 @@ 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'
......
This diff is collapsed.
import { getTestSelector } from '../../utils'
describe('Uni tags support', () => {
beforeEach(() => {
const unitagSpy = cy.spy().as('unitagSpy')
cy.intercept(/gateway.uniswap.org\/v2\/address/, (req) => {
unitagSpy(req)
})
cy.visit('/swap')
})
it('shows address if no Unitag or ENS exists', () => {
const unusedAccount = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [unusedAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains('0xF030...69Fb').should('be.visible')
})
})
it('shows Unitag, followed by address, if Unitag exists but not ENS', () => {
cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' })
const accountWithUnitag = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountWithUnitag]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains('hayden').should('be.visible')
cy.contains('0xF030...69Fb').should('be.visible')
})
})
it('shows ENS, followed by address, if ENS exists but not Unitag', () => {
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const haydenENS = 'hayden.eth'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains(haydenENS).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
})
it('shows Unitag and more option if user has both Unitag and ENS', () => {
cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' })
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const haydenUnitag = 'hayden'
const haydenENS = 'hayden.eth'
cy.get(getTestSelector('web3-status-connected')).click()
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
cy.get(getTestSelector('account-drawer-status')).within(() => {
cy.contains(haydenUnitag).should('be.visible')
cy.contains('0x50EC...79C3').should('be.visible')
})
cy.get(getTestSelector('secondary-identifiers')).trigger('mouseover').click()
cy.get(getTestSelector('secondary-identifiers-dropdown')).within(() => {
cy.contains(haydenENS)
cy.contains('0x50EC...79C3')
})
})
})
{
"data": {
"portfolios": [
{
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
"assetActivities": [],
"__typename": "Portfolio"
}
]
},
"errors": []
}
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"username": "hayden"
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -35,8 +35,9 @@ 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 fiat on-ramp transactions in the local store,
// Include the externalsessionIDs of all FOR 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],
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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