Skip to content

Commit 16d3a5e

Browse files
ci(release): publish latest release
1 parent f167cfa commit 16d3a5e

File tree

21 files changed

+501
-66
lines changed

21 files changed

+501
-66
lines changed

RELEASE

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
IPFS hash of the deployment:
2-
- CIDv0: `QmVRMcjw3tePfF64bhmZkfATWDZyHEyhiBa71wyThG7KjC`
3-
- CIDv1: `bafybeidjgzrm5mpdv4nix2457qbay6vzo3l7vjdu4iysvd7cxa2lxbv4o4`
2+
- CIDv0: `QmeDZNuZYccR5NDjknqpTztn6iwPRHALzESa7w3nyLiqrK`
3+
- CIDv1: `bafybeihl5hs3q6eacvtnt7qhdudqjoy57sezujozkiob2yxzsas5dggbyq`
44

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

@@ -10,9 +10,9 @@ 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://bafybeidjgzrm5mpdv4nix2457qbay6vzo3l7vjdu4iysvd7cxa2lxbv4o4.ipfs.dweb.link/
14-
- [ipfs://QmVRMcjw3tePfF64bhmZkfATWDZyHEyhiBa71wyThG7KjC/](ipfs://QmVRMcjw3tePfF64bhmZkfATWDZyHEyhiBa71wyThG7KjC/)
13+
- https://bafybeihl5hs3q6eacvtnt7qhdudqjoy57sezujozkiob2yxzsas5dggbyq.ipfs.dweb.link/
14+
- [ipfs://QmeDZNuZYccR5NDjknqpTztn6iwPRHALzESa7w3nyLiqrK/](ipfs://QmeDZNuZYccR5NDjknqpTztn6iwPRHALzESa7w3nyLiqrK/)
1515

16-
### 5.136.1 (2026-03-04)
16+
### 5.136.2 (2026-03-06)
1717

1818

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web/5.136.1
1+
web/5.136.2
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { runSaga } from 'redux-saga'
2+
import { UniverseChainId } from 'uniswap/src/features/chains/types'
3+
import { addTransaction } from 'uniswap/src/features/transactions/slice'
4+
import type { HandleOnChainStepParams, OnChainTransactionStep } from 'uniswap/src/features/transactions/steps/types'
5+
import { TransactionStepType } from 'uniswap/src/features/transactions/steps/types'
6+
import { TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails'
7+
8+
const mockSendTransaction = vi.fn()
9+
10+
vi.mock('wagmi/actions', () => ({
11+
getConnectorClient: vi.fn().mockResolvedValue({}),
12+
getTransaction: vi.fn(),
13+
}))
14+
15+
vi.mock('~/components/Web3Provider/wagmiConfig', () => ({
16+
wagmiConfig: {},
17+
}))
18+
19+
vi.mock('~/hooks/useEthersProvider', () => ({
20+
clientToProvider: vi.fn().mockReturnValue({
21+
getSigner: vi.fn().mockResolvedValue({
22+
sendTransaction: (...args: unknown[]) => mockSendTransaction(...args),
23+
}),
24+
}),
25+
}))
26+
27+
vi.mock('~/utils/signing', () => ({
28+
signTypedData: vi.fn(),
29+
}))
30+
31+
vi.mock('~/components/Popups/registry', () => ({
32+
popupRegistry: { addPopup: vi.fn() },
33+
}))
34+
35+
vi.mock('~/components/Popups/types', () => ({
36+
PopupType: { Plan: 'Plan' },
37+
}))
38+
39+
vi.mock('@datadog/browser-rum', () => ({
40+
datadogRum: { addAction: vi.fn() },
41+
}))
42+
43+
vi.mock('~/state/activity/utils', () => ({
44+
getRoutingForTransaction: vi.fn().mockReturnValue('CLASSIC'),
45+
}))
46+
47+
vi.mock('@universe/gating', async (importOriginal) => {
48+
const actual = await importOriginal<typeof import('@universe/gating')>()
49+
return {
50+
...actual,
51+
getDynamicConfigValue: vi.fn().mockReturnValue([]),
52+
}
53+
})
54+
55+
describe('handleOnChainStep', () => {
56+
const hash = '0xabc123' as `0x${string}`
57+
const address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' as `0x${string}`
58+
const chainId = UniverseChainId.Mainnet
59+
60+
const step = {
61+
type: TransactionStepType.SwapTransaction,
62+
txRequest: {
63+
to: '0x0000000000000000000000000000000000000001' as `0x${string}`,
64+
data: '0xoriginal',
65+
value: '0x0',
66+
chainId,
67+
},
68+
} as OnChainTransactionStep
69+
70+
const info = {
71+
type: TransactionType.Approve as const,
72+
tokenAddress: '0xtoken',
73+
spender: '0xspender',
74+
approvalAmount: '1000',
75+
}
76+
77+
let handleOnChainStep: typeof import('./utils').handleOnChainStep
78+
79+
beforeAll(async () => {
80+
const utils = await import('./utils')
81+
handleOnChainStep = utils.handleOnChainStep
82+
})
83+
84+
function createParams(overrides: Partial<HandleOnChainStepParams> = {}): HandleOnChainStepParams {
85+
return {
86+
address,
87+
step,
88+
info,
89+
setCurrentStep: vi.fn(),
90+
shouldWaitForConfirmation: false,
91+
ignoreInterrupt: true,
92+
...overrides,
93+
}
94+
}
95+
96+
beforeEach(async () => {
97+
vi.clearAllMocks()
98+
99+
// Force sync submission path by including chainId in blocked list
100+
const gating = await import('@universe/gating')
101+
vi.mocked(gating.getDynamicConfigValue).mockReturnValue([chainId])
102+
103+
// Return modified data to trigger onModification path
104+
mockSendTransaction.mockResolvedValue({
105+
hash,
106+
data: '0xmodified',
107+
nonce: 1,
108+
})
109+
})
110+
111+
it('does not dispatch addTransaction or call onModification when planId is set', async () => {
112+
const onModification = vi.fn()
113+
const params = createParams({
114+
planId: 'plan-123',
115+
onModification,
116+
})
117+
118+
const dispatched: unknown[] = []
119+
await runSaga(
120+
{
121+
dispatch: (action: unknown) => dispatched.push(action),
122+
getState: () => ({ transactions: {} }),
123+
},
124+
handleOnChainStep,
125+
params,
126+
).toPromise()
127+
128+
const addTxActions = dispatched.filter((a: unknown) => (a as { type: string }).type === addTransaction.type)
129+
expect(addTxActions).toHaveLength(0)
130+
expect(onModification).not.toHaveBeenCalled()
131+
})
132+
133+
it('dispatches addTransaction and calls onModification when planId is not set', async () => {
134+
const onModification = vi.fn()
135+
const params = createParams({ onModification })
136+
137+
const dispatched: unknown[] = []
138+
await runSaga(
139+
{
140+
dispatch: (action: unknown) => dispatched.push(action),
141+
getState: () => ({ transactions: {} }),
142+
},
143+
handleOnChainStep,
144+
params,
145+
).toPromise()
146+
147+
const addTxActions = dispatched.filter((a: unknown) => (a as { type: string }).type === addTransaction.type)
148+
expect(addTxActions).toHaveLength(1)
149+
expect(onModification).toHaveBeenCalled()
150+
})
151+
})

apps/web/src/state/sagas/transactions/utils.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,23 +199,23 @@ export function* handleOnChainStep<T extends OnChainTransactionStep>(params: Han
199199
const { hash, data, nonce } = yield* call(submitTransaction, params)
200200
transaction = createTransaction(hash)
201201

202+
// For plans, individual tx state and validation is handled by that backend
202203
if (!planId) {
203204
yield* put(addTransaction(transaction))
204-
}
205-
206-
if (step.txRequest.data !== data && onModification) {
207-
yield* call(onModification, { hash, data, nonce })
205+
if (step.txRequest.data !== data && onModification) {
206+
yield* call(onModification, { hash, data, nonce })
207+
}
208208
}
209209
} else {
210210
const hash = yield* call(submitTransactionAsync, params)
211211
transaction = createTransaction(hash)
212212

213+
// For plans, individual tx state and validation is handled by that backend
213214
if (!planId) {
214215
yield* put(addTransaction(transaction))
215-
}
216-
217-
if (onModification) {
218-
yield* spawn(handleOnModificationAsync, { onModification, hash, step })
216+
if (onModification) {
217+
yield* spawn(handleOnModificationAsync, { onModification, hash, step })
218+
}
219219
}
220220
}
221221

@@ -669,6 +669,17 @@ export function* sendToast(appNotification: AppNotification, planId: string): Sa
669669
)
670670
break
671671
}
672+
case AppNotificationType.Transaction: {
673+
popupRegistry.addPopup(
674+
{
675+
type: PopupType.Plan,
676+
planId,
677+
},
678+
planId,
679+
DEFAULT_TXN_DISMISS_MS,
680+
)
681+
break
682+
}
672683
default: {
673684
logger.warn('swapSaga', 'sendToast', 'Unknown app notification type', appNotification)
674685
break

packages/uniswap/src/components/activity/details/plan/ResumePlanButton.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
33
import { useTranslation } from 'react-i18next'
44
import { Button, SpinningLoader } from 'ui/src'
55
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
6+
import { useIsPriceChangeInterrupted } from 'uniswap/src/features/transactions/swap/plan/intermediaryState/useIsPriceChangeInterrupted'
67
import { useResumePlanMutation } from 'uniswap/src/features/transactions/swap/plan/intermediaryState/useResumePlanMutation'
78
import { PlanTransactionInfo } from 'uniswap/src/features/transactions/types/transactionDetails'
89
import { useEvent } from 'utilities/src/react/hooks'
@@ -16,6 +17,7 @@ export function ResumePlanButton({
1617
}): JSX.Element {
1718
const { planId, inputCurrencyId, outputCurrencyId, inputCurrencyAmountRaw } = typeInfo
1819
const { t } = useTranslation()
20+
const isPriceChangeInterrupted = useIsPriceChangeInterrupted(planId)
1921

2022
const inputCurrencyDecimals = useCurrencyInfo(typeInfo.inputCurrencyId)?.currency.decimals
2123
const inputCurrencyAmount = useMemo(() => {
@@ -53,7 +55,9 @@ export function ResumePlanButton({
5355
icon={isPending ? <SpinningLoader /> : undefined}
5456
onPress={onPress}
5557
>
56-
{t('transaction.status.plan.completeSwap')}
58+
{isPriceChangeInterrupted
59+
? t('transaction.status.plan.priceChange.viewNewPrice')
60+
: t('transaction.status.plan.completeSwap')}
5761
</Button>
5862
)
5963
}

packages/uniswap/src/components/activity/details/transactions/PlanTransactionDetails.tsx

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Flex, Text } from 'ui/src'
2-
import { ArrowDown, InfoCircle } from 'ui/src/components/icons'
1+
import { useTranslation } from 'react-i18next'
2+
import { Flex, styled, Text } from 'ui/src'
3+
import { AlertTriangleFilled, ArrowDown, InfoCircle } from 'ui/src/components/icons'
34
import { iconSizes } from 'ui/src/theme'
45
import {
56
TwoTokenDetails,
@@ -9,6 +10,7 @@ import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo'
910
import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo'
1011
import { useIntermediaryPlanState } from 'uniswap/src/features/transactions/swap/plan/intermediaryState/useIntermediaryPlanState'
1112
import { useIntermediaryPlanStateDescriptor } from 'uniswap/src/features/transactions/swap/plan/intermediaryState/useIntermediaryPlanStateDescriptor'
13+
import { useIsPriceChangeInterrupted } from 'uniswap/src/features/transactions/swap/plan/intermediaryState/useIsPriceChangeInterrupted'
1214
import { PlanTransactionInfo, TransactionStatus } from 'uniswap/src/features/transactions/types/transactionDetails'
1315
import { currencyId } from 'uniswap/src/utils/currencyId'
1416

@@ -64,6 +66,17 @@ export function PlanTransactionDetails({
6466
)
6567
}
6668

69+
const IntermediaryStateCardContainer = styled(Flex, {
70+
row: true,
71+
justifyContent: 'space-between',
72+
backgroundColor: '$surface2',
73+
borderRadius: '$rounded12',
74+
p: '$spacing12',
75+
mx: '$spacing4',
76+
alignItems: 'center',
77+
gap: '$spacing12',
78+
})
79+
6780
/**
6881
* In the case that the plan is interrupted and the user is left with an intermediary token,
6982
* we display a card with the intermediary token and amount.
@@ -84,6 +97,40 @@ const IntermediaryStateCard = ({
8497
typeInfo: PlanTransactionInfo
8598
status: TransactionStatus
8699
}): JSX.Element | null => {
100+
const isPriceChangeInterrupted = useIsPriceChangeInterrupted(typeInfo.planId)
101+
102+
if (isPriceChangeInterrupted) {
103+
return <PriceChangeInterruptedCard />
104+
}
105+
106+
return <IntermediaryTokenStateCard typeInfo={typeInfo} status={status} />
107+
}
108+
109+
function PriceChangeInterruptedCard(): JSX.Element {
110+
const { t } = useTranslation()
111+
112+
return (
113+
<IntermediaryStateCardContainer alignItems="flex-start" justifyContent="flex-start">
114+
<AlertTriangleFilled color="$statusWarning" size="$icon.18" />
115+
<Flex>
116+
<Text color="$statusWarning" variant="body3">
117+
{t('transaction.status.plan.priceChange.title')}
118+
</Text>
119+
<Text flexWrap="wrap" flexShrink={1} color="$neutral2" variant="body4">
120+
{t('transaction.status.plan.priceChange.description')}
121+
</Text>
122+
</Flex>
123+
</IntermediaryStateCardContainer>
124+
)
125+
}
126+
127+
function IntermediaryTokenStateCard({
128+
typeInfo,
129+
status,
130+
}: {
131+
typeInfo: PlanTransactionInfo
132+
status: TransactionStatus
133+
}): JSX.Element | null {
87134
const intermediaryState = useIntermediaryPlanState({ typeInfo, status })
88135
const descriptor = useIntermediaryPlanStateDescriptor({ intermediaryState, status })
89136

@@ -94,16 +141,7 @@ const IntermediaryStateCard = ({
94141
}
95142

96143
return (
97-
<Flex
98-
row
99-
justifyContent="space-between"
100-
backgroundColor="$surface2"
101-
borderRadius="$rounded12"
102-
p="$spacing12"
103-
mx="$spacing4"
104-
alignItems="center"
105-
gap="$spacing12"
106-
>
144+
<IntermediaryStateCardContainer>
107145
<Flex row gap="$spacing12" alignItems="center" flexShrink={1}>
108146
<InfoCircle color="$neutral3" size="$icon.16" />
109147
<Text flexWrap="wrap" flexShrink={1} color="$neutral1" variant="body3">
@@ -113,6 +151,6 @@ const IntermediaryStateCard = ({
113151
<Flex>
114152
<CurrencyLogo hideNetworkLogo currencyInfo={intermediaryCurrencyInfo} size={iconSizes.icon24} />
115153
</Flex>
116-
</Flex>
154+
</IntermediaryStateCardContainer>
117155
)
118156
}

packages/uniswap/src/components/activity/summaries/PlanSummaryItem.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { memo, useMemo } from 'react'
2-
import { useOnRetrySwap } from 'uniswap/src/components/activity/hooks/useOnRetrySwap'
32
import { TransactionSummaryLayout } from 'uniswap/src/components/activity/summaries/TransactionSummaryLayout'
43
import type { SummaryItemProps } from 'uniswap/src/components/activity/types'
54
import { TXN_HISTORY_ICON_SIZE } from 'uniswap/src/components/activity/utils'
@@ -21,7 +20,6 @@ import { getFormattedCurrencyAmount } from 'uniswap/src/utils/currency'
2120
*/
2221
function _PlanSummaryItem({
2322
transaction,
24-
swapCallbacks,
2523
index,
2624
isExternalProfile,
2725
}: SummaryItemProps & {
@@ -34,7 +32,6 @@ function _PlanSummaryItem({
3432
const inputCurrencyInfo = useCurrencyInfo(inputCurrencyId)
3533
const outputCurrencyInfo = useCurrencyInfo(outputCurrencyId)
3634
const formatter = useLocalizationContext()
37-
const onRetry = useOnRetrySwap(transaction, swapCallbacks)
3835

3936
const caption = useMemo(() => {
4037
if (!inputCurrencyInfo || !outputCurrencyInfo) {
@@ -82,14 +79,14 @@ function _PlanSummaryItem({
8279
[inputCurrencyInfo, outputCurrencyInfo, transaction.chainId, status],
8380
)
8481

82+
// TODO(SWAP-2133): Add onRetry prop to support retrying plan transactions in-line.
8583
return (
8684
<TransactionSummaryLayout
8785
caption={caption}
8886
icon={icon}
8987
index={index}
9088
transaction={transaction}
9189
isExternalProfile={isExternalProfile}
92-
onRetry={onRetry}
9390
/>
9491
)
9592
}

0 commit comments

Comments
 (0)