Skip to content

Commit d19b27f

Browse files
ci(release): publish latest release
1 parent 9f0622c commit d19b27f

12 files changed

Lines changed: 113 additions & 35 deletions

File tree

RELEASE

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
IPFS hash of the deployment:
2-
- CIDv0: `QmUKj6FRWbbb9KzXLektsshwBwFpKtNpoAB5SwERR1KWMw`
3-
- CIDv1: `bafybeicy5f7mprd6hqsd4xo6onxa5fxnroyim6nipgojsda3a55qw47oei`
2+
- CIDv0: `QmSAWSDEHki5zUxyG1dKGXtgXW7uCbGpfCmKxqce5JSC5x`
3+
- CIDv1: `bafybeiby2wkyrd3wzpyy5sv3a6bqs2hvlpur5wmzegylv64uylbw3vni4m`
44

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

@@ -10,5 +10,5 @@ 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://bafybeicy5f7mprd6hqsd4xo6onxa5fxnroyim6nipgojsda3a55qw47oei.ipfs.dweb.link/
14-
- [ipfs://QmUKj6FRWbbb9KzXLektsshwBwFpKtNpoAB5SwERR1KWMw/](ipfs://QmUKj6FRWbbb9KzXLektsshwBwFpKtNpoAB5SwERR1KWMw/)
13+
- https://bafybeiby2wkyrd3wzpyy5sv3a6bqs2hvlpur5wmzegylv64uylbw3vni4m.ipfs.dweb.link/
14+
- [ipfs://QmSAWSDEHki5zUxyG1dKGXtgXW7uCbGpfCmKxqce5JSC5x/](ipfs://QmSAWSDEHki5zUxyG1dKGXtgXW7uCbGpfCmKxqce5JSC5x/)

VERSION

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

apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { memo } from 'react'
33
import { useSelector } from 'react-redux'
44
import { RWAIssuerHeaderDetails } from 'src/components/TokenDetails/rwa/RWAIssuerHeaderDetails'
55
import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext'
6+
import { useFeatureFlaggedProjectTokens } from 'src/components/TokenDetails/useFeatureFlaggedProjectTokens'
67
import { useGatedTokenDetailsRWAMatch } from 'src/components/TokenDetails/useTokenDetailsRWAMatch'
78
import { EM_DASH, Flex, FlexLoader, flexStyles, Shine, Text, TouchableArea } from 'ui/src'
89
import { CopyAlt } from 'ui/src/components/icons'
@@ -31,7 +32,9 @@ export const TokenDetailsHeader = memo(function TokenDetailsHeaderInner(): JSX.E
3132
const token = useTokenBasicInfoPartsFragment({ currencyId }).data
3233
const project = useTokenBasicProjectPartsFragment({ currencyId }).data.project
3334
const projectTokensLoaded = project?.tokens !== undefined
34-
const projectIsMultichain = projectTokensLoaded && isMultichainProjectTokens(project.tokens)
35+
// Gate out unlaunched chains (e.g. Arc/Robinhood) so they don't drive multichain UI on the header.
36+
const featureFlaggedProjectTokens = useFeatureFlaggedProjectTokens(project?.tokens)
37+
const projectIsMultichain = projectTokensLoaded && isMultichainProjectTokens(featureFlaggedProjectTokens)
3538
const isMultichainToken = initialIsMultichainAsset || projectIsMultichain
3639
// need to wait for the project tokens to load before we can open the multichain address sheet
3740
const canOpenMultichainAddressSheet = projectIsMultichain

apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useOrderedMultichainEntries } from 'uniswap/src/components/MultichainTo
1818
import type { MultichainTokenEntry } from 'uniswap/src/components/MultichainTokenDetails/useOrderedMultichainEntries'
1919
import { useTokenProjectUrlsPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments'
2020
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
21+
import { useFeatureFlaggedChainIds } from 'uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds'
2122
import type { UniverseChainId } from 'uniswap/src/features/chains/types'
2223
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
2324
import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils/currencyIdToContractInput'
@@ -47,6 +48,7 @@ function useMultichainTokenEntries(currencyId: string): MultichainTokenEntry[] {
4748
const { data } = GraphQLApi.useTokenProjectsQuery({
4849
variables: { contracts: [contractInput] },
4950
})
51+
const featureFlaggedChainIds = useFeatureFlaggedChainIds()
5052

5153
const entries = useMemo(() => {
5254
const tokens = data?.tokenProjects?.[0]?.tokens
@@ -56,12 +58,13 @@ function useMultichainTokenEntries(currencyId: string): MultichainTokenEntry[] {
5658
const result: MultichainTokenEntry[] = []
5759
for (const token of tokens) {
5860
const chainId = fromGraphQLChain(token.chain)
59-
if (chainId && token.address) {
61+
// Exclude feature-gated chains (e.g. unlaunched Arc/Robinhood) so they don't appear in the network selector.
62+
if (chainId && token.address && featureFlaggedChainIds.includes(chainId)) {
6063
result.push({ chainId, address: token.address, isNative: false })
6164
}
6265
}
6366
return result
64-
}, [data])
67+
}, [data, featureFlaggedChainIds])
6568

6669
return useOrderedMultichainEntries(entries)
6770
}

apps/mobile/src/components/TokenDetails/TokenDetailsStats/TokenDetailsMarketData.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
44
import { useSelector } from 'react-redux'
55
import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext'
66
import { StatsRow } from 'src/components/TokenDetails/TokenDetailsStats/StatsRow'
7+
import { useFeatureFlaggedProjectTokens } from 'src/components/TokenDetails/useFeatureFlaggedProjectTokens'
78
import { useTokenDetailsPreferProjectMarketData } from 'src/components/TokenDetails/useTokenDetailsRWAMatch'
89
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
910
import type { IconProps } from 'ui/src/components/factories/createIcon'
@@ -67,13 +68,17 @@ export const TokenDetailsMarketData = memo(function TokenDetailsMarketDataInner(
6768
}
6869
}, [screenData?.token?.multichainMarket, screenData?.token?.project?.markets])
6970

71+
// Gate out unlaunched chains (e.g. Arc/Robinhood) so they don't appear in the Networks row or
72+
// make the token look multichain.
73+
const featureFlaggedScreenTokens = useFeatureFlaggedProjectTokens(screenData?.token?.project?.tokens)
74+
const featureFlaggedProjectTokens = useFeatureFlaggedProjectTokens(project?.tokens)
75+
7076
const networkChainIds = useMemo((): UniverseChainId[] => {
71-
const projectTokens = screenData?.token?.project?.tokens
72-
if (!projectTokens?.length) {
77+
if (!featureFlaggedScreenTokens.length) {
7378
return [chainId]
7479
}
7580
const chainIds = new Set<UniverseChainId>()
76-
for (const projectToken of projectTokens) {
81+
for (const projectToken of featureFlaggedScreenTokens) {
7782
const id = fromGraphQLChain(projectToken.chain)
7883
if (id !== null) {
7984
chainIds.add(id)
@@ -83,11 +88,11 @@ export const TokenDetailsMarketData = memo(function TokenDetailsMarketDataInner(
8388
return [chainId]
8489
}
8590
return Array.from(chainIds)
86-
}, [chainId, screenData?.token?.project?.tokens])
91+
}, [chainId, featureFlaggedScreenTokens])
8792

8893
const singleNetworkChainId = networkChainIds.length === 1 ? networkChainIds[0] : undefined
8994

90-
const isMultichainToken = isMultichainProjectTokens(project?.tokens)
95+
const isMultichainToken = isMultichainProjectTokens(featureFlaggedProjectTokens)
9196

9297
/** Native currency pages have no contract address to copy / multichain address sheet (see TokenDetailsLinks). */
9398
const hasCopyableContractAddress = useMemo(() => {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { GraphQLApi } from '@universe/api'
2+
import { useMemo } from 'react'
3+
import { useFeatureFlaggedChainIds } from 'uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds'
4+
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
5+
6+
/**
7+
* Filters a token project's on-chain deployments down to feature-flag-enabled chains, so unlaunched
8+
* gated chains (e.g. Arc/Robinhood) don't surface in the mobile TDP multichain UI — header multichain
9+
* state, the market-data Networks row, cross-chain balances, and the address/explorer sheets.
10+
*
11+
* Accepts the loosely-typed GraphQL fragment shape (elements may be undefined, `chain` may be absent)
12+
* and preserves the element type so callers keep fields like `address`.
13+
*/
14+
export function useFeatureFlaggedProjectTokens<T extends { chain?: GraphQLApi.Chain | null }>(
15+
tokens: readonly (T | null | undefined)[] | null | undefined,
16+
): T[] {
17+
const featureFlaggedChainIds = useFeatureFlaggedChainIds()
18+
return useMemo(() => {
19+
const result: T[] = []
20+
for (const token of tokens ?? []) {
21+
if (!token) {
22+
continue
23+
}
24+
const chainId = fromGraphQLChain(token.chain ?? undefined)
25+
if (chainId !== null && featureFlaggedChainIds.includes(chainId)) {
26+
result.push(token)
27+
}
28+
}
29+
return result
30+
}, [tokens, featureFlaggedChainIds])
31+
}

apps/mobile/src/components/TokenDetails/useTokenDetailsCrossChainBalances.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { GraphQLApi } from '@universe/api'
22
import { useMemo } from 'react'
33
import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext'
4+
import { useFeatureFlaggedProjectTokens } from 'src/components/TokenDetails/useFeatureFlaggedProjectTokens'
45
import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances'
56
import { useTokenBasicProjectPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments'
67
import type { DataApiOutageState, PortfolioBalance } from 'uniswap/src/features/dataApi/types'
@@ -13,11 +14,14 @@ export function useTokenDetailsCrossChainBalances({ evmAddress }: { evmAddress:
1314
otherChainBalances: PortfolioBalance[] | null
1415
} & DataApiOutageState {
1516
const { currencyId } = useTokenDetailsContext()
16-
const projectTokens = useTokenBasicProjectPartsFragment({ currencyId }).data.project?.tokens
17+
// Gate out unlaunched chains (e.g. Arc/Robinhood) so they don't surface as cross-chain balances.
18+
const projectTokens = useFeatureFlaggedProjectTokens(
19+
useTokenBasicProjectPartsFragment({ currencyId }).data.project?.tokens,
20+
)
1721

1822
const crossChainTokens = useMemo<CrossChainToken[]>(() => {
19-
return (projectTokens ?? []).flatMap((token) => {
20-
if (!token || !token.chain || token.address === undefined) {
23+
return projectTokens.flatMap((token) => {
24+
if (!token.chain || token.address === undefined) {
2125
return []
2226
}
2327
return [{ address: token.address, chain: token.chain }]

apps/web/src/pages/Liquidity/CreateAuction/XOAuthCallbackPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ export function XOAuthCallbackPage() {
2626
return
2727
}
2828

29-
const storedState = sessionStorage.getItem('x_oauth_state')
30-
sessionStorage.removeItem('x_oauth_state')
29+
// Paired with the localStorage write in useXOAuthFlow — sessionStorage isn't shared with this popup
30+
// when it runs in an isolated browsing-context group under COOP (see the comment there).
31+
const storedState = localStorage.getItem('x_oauth_state')
32+
localStorage.removeItem('x_oauth_state')
3133

3234
if (state !== storedState) {
3335
const message = 'State mismatch — possible CSRF attack'

apps/web/src/pages/Liquidity/CreateAuction/hooks/useXOAuthFlow.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export function useXOAuthFlow(): { connectX: () => void; isLoading: boolean; err
1818
const [error, setError] = useState<string | null>(null)
1919
const popupRef = useRef<Window | null>(null)
2020

21-
// Poll for popup closed without a message (user dismissed), and manage the BroadcastChannel lifetime.
21+
// Listen for the OAuth result on a channel that outlives the popup. The callback posts its result and
22+
// then immediately closes the popup; under COOP `same-origin-allow-popups` (e.g. app.corn-staging.com)
23+
// the popup is in a separate browsing-context group and its message is delivered *after* the popup-closed
24+
// poller below fires. So this listener must NOT be torn down on popup close / loading end, or the late
25+
// cross-group message is dropped and the verification silently never lands.
2226
useEffect(() => {
23-
if (!isLoading) {
24-
return undefined
25-
}
26-
2727
const channel = new BroadcastChannel('x_oauth')
2828

2929
const handler = (event: MessageEvent<XOAuthMessage>) => {
@@ -37,18 +37,27 @@ export function useXOAuthFlow(): { connectX: () => void; isLoading: boolean; err
3737

3838
channel.addEventListener('message', handler)
3939

40+
return () => {
41+
channel.removeEventListener('message', handler)
42+
channel.close()
43+
}
44+
}, [setXVerification])
45+
46+
// Reset loading if the user dismisses the popup without completing the flow. Deliberately does not touch
47+
// the channel above — a late success/error message can still arrive after the popup window closes.
48+
useEffect(() => {
49+
if (!isLoading) {
50+
return undefined
51+
}
52+
4053
const interval = setInterval(() => {
4154
if (popupRef.current?.closed) {
4255
setIsLoading(false)
4356
}
4457
}, 500)
4558

46-
return () => {
47-
clearInterval(interval)
48-
channel.removeEventListener('message', handler)
49-
channel.close()
50-
}
51-
}, [isLoading, setXVerification])
59+
return () => clearInterval(interval)
60+
}, [isLoading])
5261

5362
const connectX = useCallback(() => {
5463
if (!address) {
@@ -60,7 +69,11 @@ export function useXOAuthFlow(): { connectX: () => void; isLoading: boolean; err
6069

6170
XVerificationClient.getXAuthUrl(new GetXAuthUrlRequest({ walletAddress: address }))
6271
.then(({ authUrl, state }) => {
63-
sessionStorage.setItem('x_oauth_state', state)
72+
// Use localStorage, not sessionStorage: the OAuth popup opens cross-origin (x.com), and under
73+
// COOP `same-origin-allow-popups` (set on some deploys, e.g. app.corn-staging.com) it lands in a
74+
// separate browsing-context group with a fresh, unshared sessionStorage. localStorage is shared
75+
// across that boundary, so the callback can read the state back. Cleared on read in the callback.
76+
localStorage.setItem('x_oauth_state', state)
6477

6578
const popup = window.open(authUrl, 'x_oauth', 'width=600,height=700')
6679
popupRef.current = popup

apps/web/src/pages/TokenDetails/TokenDetails.e2e.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,11 @@ test.describe(
8888
await page.goto('/explore/tokens/ethereum/0x123')
8989
await expect(page).toHaveURL('/explore')
9090
})
91+
92+
test('redirect to explore for a feature-gated chain (Arc)', async ({ page }) => {
93+
// Arc is gated behind FeatureFlags.Arc (off by default), so the TDP must not load.
94+
await page.goto('/explore/tokens/arc/0xbef5f6d51cb62b58e6a8f77868681825c6fe21c1')
95+
await expect(page).toHaveURL('/explore')
96+
})
9197
},
9298
)

0 commit comments

Comments
 (0)