Skip to content

Commit 13ae7c4

Browse files
ci(release): publish latest release
1 parent b76593f commit 13ae7c4

File tree

15 files changed

+346
-85
lines changed

15 files changed

+346
-85
lines changed

RELEASE

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
IPFS hash of the deployment:
2-
- CIDv0: `QmY1cbcRmdUtaHxg3Dk7iu3FQAoy2DtYFogHrCd1CEEBcX`
3-
- CIDv1: `bafybeiepwrzciksdcvpgt4u4sss6hgh5b6zrofqlz4m5gh5myhsa226ofq`
2+
- CIDv0: `QmZmRjJXcRL1HVbuFBLX23qqmQydzmqGsLmb94qrqTU9A7`
3+
- CIDv1: `bafybeifjzfti2rq27be42zfwqnturszxqyecs4zxklxxr4pejgcgi72eja`
44

55
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
66

@@ -10,9 +10,14 @@ You can also access the Uniswap Interface from an IPFS gateway.
1010
Your Uniswap settings are never remembered across different URLs.
1111

1212
IPFS gateways:
13-
- https://bafybeiepwrzciksdcvpgt4u4sss6hgh5b6zrofqlz4m5gh5myhsa226ofq.ipfs.dweb.link/
14-
- [ipfs://QmY1cbcRmdUtaHxg3Dk7iu3FQAoy2DtYFogHrCd1CEEBcX/](ipfs://QmY1cbcRmdUtaHxg3Dk7iu3FQAoy2DtYFogHrCd1CEEBcX/)
13+
- https://bafybeifjzfti2rq27be42zfwqnturszxqyecs4zxklxxr4pejgcgi72eja.ipfs.dweb.link/
14+
- [ipfs://QmZmRjJXcRL1HVbuFBLX23qqmQydzmqGsLmb94qrqTU9A7/](ipfs://QmZmRjJXcRL1HVbuFBLX23qqmQydzmqGsLmb94qrqTU9A7/)
1515

16-
### 5.114.1 (2025-10-24)
16+
## 5.115.0 (2025-10-24)
17+
18+
19+
### Features
20+
21+
* **web:** special case metamask dual vm connection flow (#24756) (#24789) b21eafd
1722

1823

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web/5.114.1
1+
web/5.115.0

apps/web/src/components/TopLevelModals/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default function TopLevelModals() {
4848
<ModalRenderer modalName={ModalName.Help} />
4949
<ModalRenderer modalName={ModalName.OffchainActivity} />
5050
<ModalRenderer modalName={ModalName.ReceiveCryptoModal} />
51+
<ModalRenderer modalName={ModalName.PendingWalletConnection} />
5152
</>
5253
)
5354
}
@@ -81,6 +82,7 @@ export default function TopLevelModals() {
8182
<ModalRenderer modalName={ModalName.Send} />
8283
<ModalRenderer modalName={ModalName.BridgedAsset} componentProps={bridgedAssetModalProps} />
8384
<ModalRenderer modalName={ModalName.Wormhole} componentProps={wormholeModalProps} />
85+
<ModalRenderer modalName={ModalName.PendingWalletConnection} />
8486
</>
8587
)
8688
}

apps/web/src/components/TopLevelModals/modalRegistry.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { createLazy } from 'utils/lazyWithRetry'
77

88
const AddressClaimModal = createLazy(() => import('components/claim/AddressClaimModal'))
99
const ConnectedAccountBlocked = createLazy(() => import('components/ConnectedAccountBlocked'))
10+
const PendingWalletConnectionModal = createLazy(
11+
() => import('components/WalletModal/PendingWalletConnectionModal/PendingWalletConnectionModal'),
12+
)
1013
const UniwalletModal = createLazy(() => import('components/AccountDrawer/UniwalletModal'))
1114
const Banners = createLazy(() =>
1215
import('components/Banner/shared/Banners').then((module) => ({ default: module.Banners })),
@@ -124,6 +127,10 @@ export const modalRegistry: ModalRegistry = {
124127
component: GetTheAppModal,
125128
shouldMount: () => true,
126129
},
130+
[ModalName.PendingWalletConnection]: {
131+
component: PendingWalletConnectionModal,
132+
shouldMount: () => true,
133+
},
127134
[ModalName.PrivacyPolicy]: {
128135
component: PrivacyPolicyModal,
129136
shouldMount: (state) => state.application.openModal?.name === ModalName.PrivacyPolicy,
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import {
2+
getWalletRequiresSeparatePrompt,
3+
useHasAcceptedSolanaConnectionPrompt,
4+
} from 'components/WalletModal/PendingWalletConnectionModal/state'
5+
import { WalletIconWithRipple } from 'components/WalletModal/WalletIconWithRipple'
6+
import { useConnectionStatus } from 'features/accounts/store/hooks'
7+
import { ExternalWallet } from 'features/accounts/store/types'
8+
import { useConnectWallet } from 'features/wallet/connection/hooks/useConnectWallet'
9+
import { useEffect, useMemo, useState } from 'react'
10+
import { useTranslation } from 'react-i18next'
11+
import { AnimatePresence, Button, Flex, HeightAnimator, Text } from 'ui/src'
12+
import SOLANA_ICON from 'ui/src/assets/logos/png/solana-logo.png'
13+
import { CloseIconWithHover } from 'ui/src/components/icons/CloseIconWithHover'
14+
import { Modal } from 'uniswap/src/components/modals/Modal'
15+
import { Platform } from 'uniswap/src/features/platforms/types/Platform'
16+
import { ModalName } from 'uniswap/src/features/telemetry/constants'
17+
import { useEvent } from 'utilities/src/react/hooks'
18+
import { useDebounce } from 'utilities/src/time/timing'
19+
20+
/**
21+
* Debounces resets of the the Solana acceptance flag to prevent the modal from reacting to resets before it has had a chance to close.
22+
* This solves a connection edge case where Solana is rejected in a dual VM connection, so the flag is reset, but the modal should close first.
23+
*/
24+
function useHasAcceptedSolanaConnectionPromptWithBuffer() {
25+
const { hasAcceptedSolanaConnectionPrompt: value } = useHasAcceptedSolanaConnectionPrompt()
26+
const debouncedValue = useDebounce(value, 100)
27+
28+
// We only want to debounce resets / when the value becomes false
29+
if (value === false) {
30+
return debouncedValue
31+
}
32+
33+
return value
34+
}
35+
36+
/** Returns a wallet IF it's currently connecting AND requires separate EVM/SVM prompts (e.g. MetaMask). */
37+
function useApplicablePendingWallet() {
38+
const { pendingWallet, isConnecting } = useConnectWallet()
39+
40+
if (!isConnecting || !pendingWallet || !getWalletRequiresSeparatePrompt(pendingWallet.id)) {
41+
return undefined
42+
}
43+
44+
return pendingWallet
45+
}
46+
47+
/**
48+
* Tracks which wallet needs the Solana prompt. Persists after EVM completes to keep modal open.
49+
* Different from useApplicablePendingWallet which becomes undefined once connection starts.
50+
*/
51+
function useSolanaWalletToPrompt(applicablePendingWallet: ExternalWallet | undefined) {
52+
const hasAcceptedSolanaConnectionPrompt = useHasAcceptedSolanaConnectionPromptWithBuffer()
53+
54+
const [solanaWalletToPrompt, setSolanaWalletToPrompt] = useState<ExternalWallet>()
55+
56+
const isMultiPlatformConnection = !useConnectWallet().variables?.individualPlatform
57+
58+
// Set a flag to keep the modal open if the solana prompt should be shown
59+
useEffect(() => {
60+
if (applicablePendingWallet && !hasAcceptedSolanaConnectionPrompt && isMultiPlatformConnection) {
61+
setSolanaWalletToPrompt(applicablePendingWallet)
62+
}
63+
}, [hasAcceptedSolanaConnectionPrompt, applicablePendingWallet, isMultiPlatformConnection])
64+
65+
const resetSolanaWalletToPrompt = useEvent(() => {
66+
setSolanaWalletToPrompt(undefined)
67+
})
68+
69+
return { solanaWalletToPrompt, resetSolanaWalletToPrompt }
70+
}
71+
72+
/** Modal for dual-VM wallets (MetaMask) that shows connection status and prompts for Solana opt-in. */
73+
export default function PendingWalletConnectionModal() {
74+
const applicablePendingWallet = useApplicablePendingWallet()
75+
const { reset: resetConnectionQuery } = useConnectWallet()
76+
77+
const { solanaWalletToPrompt, resetSolanaWalletToPrompt } = useSolanaWalletToPrompt(applicablePendingWallet)
78+
79+
const closeModal = useEvent(() => {
80+
resetConnectionQuery()
81+
resetSolanaWalletToPrompt()
82+
})
83+
84+
const modalContent = useModalContent({ showSolanaPrompt: Boolean(solanaWalletToPrompt) })
85+
86+
const isOpen = Boolean(applicablePendingWallet) || (Boolean(solanaWalletToPrompt) && !!modalContent)
87+
88+
return (
89+
<Modal name={ModalName.PendingWalletConnection} isModalOpen={isOpen} onClose={closeModal}>
90+
<Flex fill alignItems="flex-end">
91+
<CloseIconWithHover onClose={closeModal} size="$icon.20" />
92+
</Flex>
93+
<HeightAnimator useInitialHeight animation="200ms">
94+
<Flex width="100%" alignItems="center" gap="$spacing24">
95+
<WalletIconWithRipple
96+
src={modalContent?.icon}
97+
alt={`${applicablePendingWallet?.name}-pending-modal-icon`}
98+
size={48}
99+
showRipple={modalContent?.animate}
100+
/>
101+
<Flex width="100%" fill position="relative" minHeight={60}>
102+
<AnimatePresence initial={false}>
103+
<Flex
104+
width="100%"
105+
position="absolute"
106+
top={0}
107+
left={0}
108+
right={0}
109+
alignItems="center"
110+
key={modalContent?.key}
111+
animation="200ms"
112+
enterStyle={{ opacity: 0 }}
113+
exitStyle={{ opacity: 0 }}
114+
gap="$spacing8"
115+
>
116+
<Text variant="subheading1" color="$neutral1">
117+
{modalContent?.title}
118+
</Text>
119+
<Text variant="body2" color="$neutral2">
120+
{modalContent?.description}
121+
</Text>
122+
</Flex>
123+
</AnimatePresence>
124+
</Flex>
125+
<UserInput solanaWalletToPrompt={solanaWalletToPrompt} resetModalState={resetSolanaWalletToPrompt} />
126+
</Flex>
127+
</HeightAnimator>
128+
</Modal>
129+
)
130+
}
131+
132+
function useModalContent(params: { showSolanaPrompt: boolean }) {
133+
const { showSolanaPrompt } = params
134+
135+
const { t } = useTranslation()
136+
const { pendingWallet } = useConnectWallet()
137+
138+
const evmConnecting = useConnectionStatus(Platform.EVM).isConnecting
139+
const svmConnecting = useConnectionStatus(Platform.SVM).isConnecting
140+
141+
const content = useMemo(() => {
142+
if (evmConnecting) {
143+
return {
144+
key: 'evm-connecting',
145+
title: t('wallet.connecting.title.evm', { walletName: pendingWallet?.name }),
146+
description: t('wallet.connecting.description'),
147+
icon: pendingWallet?.icon,
148+
animate: true,
149+
}
150+
}
151+
152+
if (showSolanaPrompt) {
153+
return {
154+
key: 'solana-prompt',
155+
title: t('wallet.connecting.solanaPrompt', { walletName: pendingWallet?.name }),
156+
description: t('wallet.connecting.solanaPrompt.description'),
157+
icon: SOLANA_ICON,
158+
animate: false,
159+
}
160+
}
161+
162+
if (svmConnecting) {
163+
return {
164+
key: 'svm-connecting',
165+
title: t('wallet.connecting.title.svm', { walletName: pendingWallet?.name }),
166+
description: t('wallet.connecting.description'),
167+
icon: SOLANA_ICON,
168+
animate: true,
169+
}
170+
}
171+
172+
return undefined
173+
}, [showSolanaPrompt, evmConnecting, svmConnecting, pendingWallet?.name, pendingWallet?.icon, t])
174+
175+
return content
176+
}
177+
178+
function UserInput(props: { solanaWalletToPrompt: ExternalWallet | undefined; resetModalState: () => void }) {
179+
const { t } = useTranslation()
180+
const { solanaWalletToPrompt, resetModalState } = props
181+
const { connectWallet, isConnecting } = useConnectWallet()
182+
const { setHasAcceptedSolanaConnectionPrompt } = useHasAcceptedSolanaConnectionPrompt()
183+
184+
const connectSolana = useEvent(() => {
185+
if (solanaWalletToPrompt) {
186+
setHasAcceptedSolanaConnectionPrompt(true)
187+
connectWallet({ wallet: solanaWalletToPrompt, individualPlatform: Platform.SVM })
188+
resetModalState()
189+
}
190+
})
191+
192+
return (
193+
<AnimatePresence>
194+
{solanaWalletToPrompt && !isConnecting && (
195+
<Flex width="100%" animation="200ms" enterStyle={{ opacity: 0, y: 10 }} exitStyle={{ opacity: 0, y: 10 }}>
196+
<Flex width="100%" row gap="$spacing8">
197+
<Button fill size="small" emphasis="secondary" onPress={resetModalState}>
198+
{t('common.button.skip')}
199+
</Button>
200+
<Button fill size="small" emphasis="primary" onPress={connectSolana}>
201+
{t('wallet.connecting.solanaPrompt.button')}
202+
</Button>
203+
</Flex>
204+
</Flex>
205+
)}
206+
</AnimatePresence>
207+
)
208+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useAtom } from 'jotai'
2+
import { atomWithStorage } from 'jotai/utils'
3+
import { CONNECTION_PROVIDER_IDS } from 'uniswap/src/constants/web3'
4+
5+
/** Wallets that require separate user consent for EVM vs SVM connections (currently just MetaMask). */
6+
const SEPARATE_PROMPT_WALLET_IDS = new Set<string>([CONNECTION_PROVIDER_IDS.METAMASK_RDNS])
7+
8+
export function getWalletRequiresSeparatePrompt(walletId: string) {
9+
return SEPARATE_PROMPT_WALLET_IDS.has(walletId)
10+
}
11+
12+
/** Tracks if user has accepted the Solana connection prompt for a wallet that requires separate user consent for EVM vs SVM connections. */
13+
const hasAcceptedSolanaConnectionPromptAtom = atomWithStorage<boolean>('hasAcceptedSolanaConnectionPrompt', false)
14+
15+
export function useHasAcceptedSolanaConnectionPrompt() {
16+
const [hasAcceptedSolanaConnectionPrompt, setHasAcceptedSolanaConnectionPrompt] = useAtom(
17+
hasAcceptedSolanaConnectionPromptAtom,
18+
)
19+
20+
return {
21+
hasAcceptedSolanaConnectionPrompt,
22+
setHasAcceptedSolanaConnectionPrompt,
23+
}
24+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react'
2+
import { Flex, Image, PulseRipple, useSporeColors } from 'ui/src'
3+
4+
interface WalletIconWithRippleProps {
5+
src?: string
6+
alt?: string
7+
size?: number
8+
showRipple?: boolean
9+
}
10+
11+
export function WalletIconWithRipple({
12+
src,
13+
alt,
14+
size = 48,
15+
showRipple = false,
16+
}: WalletIconWithRippleProps): JSX.Element {
17+
const colors = useSporeColors()
18+
19+
// Add small padding to account for ripple expansion
20+
// Ripple scales to 1.5x, so we need extra space of 0.25x on each side
21+
const margin = size * 0.25
22+
23+
return (
24+
<Flex position="relative" width={size} height={size} style={{ margin }}>
25+
<Flex position="absolute" centered fill>
26+
<PulseRipple rippleColor={showRipple ? colors.accent1.val : 'transparent'} size={size} />
27+
</Flex>
28+
29+
<Image src={src} alt={alt} width={size} height={size} borderRadius="$roundedFull" />
30+
</Flex>
31+
)
32+
}

0 commit comments

Comments
 (0)