Skip to content

Commit a362d3a

Browse files
authored
CP-13769: Hide successful toast for single chain swaps (#3683)
1 parent f454b88 commit a362d3a

File tree

10 files changed

+349
-47
lines changed

10 files changed

+349
-47
lines changed

packages/core-mobile/app/new/features/swap/utils/buildRequestContext.test.ts

Lines changed: 141 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { TransferStepDetails } from '@avalabs/fusion-sdk'
2+
import { isAvalancheChainId } from 'services/network/utils/isAvalancheNetwork'
3+
import { getChainIdFromCaip2 } from 'utils/caip2ChainIds'
24
import { RequestContext } from 'store/rpc/types'
35
import { buildRequestContext } from './buildRequestContext'
46

7+
jest.mock('services/network/utils/isAvalancheNetwork', () => ({
8+
isAvalancheChainId: jest.fn()
9+
}))
10+
jest.mock('utils/caip2ChainIds', () => ({
11+
getChainIdFromCaip2: jest.fn()
12+
}))
13+
14+
const mockIsAvalancheChainId = isAvalancheChainId as jest.Mock
15+
const mockGetChainIdFromCaip2 = getChainIdFromCaip2 as jest.Mock
16+
517
const AVAX_CHAIN_ID = 'eip155:43114'
618
const BTC_CHAIN_ID = 'bip122:000000000019d6689c085ae165831e93'
19+
const SOLANA_CHAIN_ID = 'solana:mainnet'
720

821
function makeStepDetails(
922
overrides: Partial<
@@ -16,8 +29,8 @@ function makeStepDetails(
1629
const {
1730
currentSignature = 1,
1831
requiredSignatures = 1,
19-
sourceChainId = AVAX_CHAIN_ID,
20-
targetChainId = AVAX_CHAIN_ID
32+
sourceChainId = SOLANA_CHAIN_ID,
33+
targetChainId = SOLANA_CHAIN_ID
2134
} = overrides
2235

2336
return {
@@ -32,26 +45,33 @@ function makeStepDetails(
3245
}
3346

3447
describe('buildRequestContext', () => {
35-
describe(`${RequestContext.TOASTS_AND_CONFETTI_DISABLED}`, () => {
36-
it('is false for a single-step same-chain swap', () => {
48+
beforeEach(() => {
49+
// Default: Solana — getChainIdFromCaip2 returns undefined for non-EVM chains,
50+
// so isAvalancheChainId is never reached (short-circuit in the implementation)
51+
mockGetChainIdFromCaip2.mockReturnValue(undefined)
52+
mockIsAvalancheChainId.mockReset()
53+
})
54+
55+
describe(`${RequestContext.SUPPRESS_TX_FEEDBACK}`, () => {
56+
it('is absent for a single-step same-chain swap', () => {
3757
const ctx = buildRequestContext(
3858
makeStepDetails({ currentSignature: 1, requiredSignatures: 1 })
3959
)
40-
expect(ctx[RequestContext.TOASTS_AND_CONFETTI_DISABLED]).toBe(false)
60+
expect(ctx[RequestContext.SUPPRESS_TX_FEEDBACK]).toBeUndefined()
4161
})
4262

4363
it('is true for an intermediate step (currentSignature < requiredSignatures)', () => {
4464
const ctx = buildRequestContext(
4565
makeStepDetails({ currentSignature: 1, requiredSignatures: 2 })
4666
)
47-
expect(ctx[RequestContext.TOASTS_AND_CONFETTI_DISABLED]).toBe(true)
67+
expect(ctx[RequestContext.SUPPRESS_TX_FEEDBACK]).toBe(true)
4868
})
4969

50-
it('is false for the final step of a multi-step same-chain swap', () => {
70+
it('is absent for the final step of a multi-step same-chain swap', () => {
5171
const ctx = buildRequestContext(
5272
makeStepDetails({ currentSignature: 2, requiredSignatures: 2 })
5373
)
54-
expect(ctx[RequestContext.TOASTS_AND_CONFETTI_DISABLED]).toBe(false)
74+
expect(ctx[RequestContext.SUPPRESS_TX_FEEDBACK]).toBeUndefined()
5575
})
5676

5777
it('is true for a cross-chain swap (final step)', () => {
@@ -63,7 +83,7 @@ describe('buildRequestContext', () => {
6383
targetChainId: BTC_CHAIN_ID
6484
})
6585
)
66-
expect(ctx[RequestContext.TOASTS_AND_CONFETTI_DISABLED]).toBe(true)
86+
expect(ctx[RequestContext.SUPPRESS_TX_FEEDBACK]).toBe(true)
6787
})
6888

6989
it('is true for a cross-chain intermediate step', () => {
@@ -75,7 +95,118 @@ describe('buildRequestContext', () => {
7595
targetChainId: BTC_CHAIN_ID
7696
})
7797
)
78-
expect(ctx[RequestContext.TOASTS_AND_CONFETTI_DISABLED]).toBe(true)
98+
expect(ctx[RequestContext.SUPPRESS_TX_FEEDBACK]).toBe(true)
99+
})
100+
})
101+
102+
describe(`${RequestContext.IMMEDIATE_SENT_TOAST}`, () => {
103+
it('is true for a non-Avalanche same-chain final step', () => {
104+
const ctx = buildRequestContext(makeStepDetails())
105+
expect(ctx[RequestContext.IMMEDIATE_SENT_TOAST]).toBe(true)
106+
})
107+
108+
it('is absent for an Avalanche same-chain final step (ApprovalController handles it)', () => {
109+
mockGetChainIdFromCaip2.mockReturnValue(43114)
110+
mockIsAvalancheChainId.mockReturnValue(true)
111+
112+
const ctx = buildRequestContext(
113+
makeStepDetails({
114+
sourceChainId: AVAX_CHAIN_ID,
115+
targetChainId: AVAX_CHAIN_ID
116+
})
117+
)
118+
expect(ctx[RequestContext.IMMEDIATE_SENT_TOAST]).toBeUndefined()
119+
})
120+
121+
it('is absent for a cross-chain swap (SUPPRESS_TX_FEEDBACK handles it)', () => {
122+
const ctx = buildRequestContext(
123+
makeStepDetails({
124+
sourceChainId: AVAX_CHAIN_ID,
125+
targetChainId: BTC_CHAIN_ID
126+
})
127+
)
128+
expect(ctx[RequestContext.IMMEDIATE_SENT_TOAST]).toBeUndefined()
129+
})
130+
131+
it('is absent for an intermediate step (SUPPRESS_TX_FEEDBACK handles it)', () => {
132+
const ctx = buildRequestContext(
133+
makeStepDetails({ currentSignature: 1, requiredSignatures: 2 })
134+
)
135+
expect(ctx[RequestContext.IMMEDIATE_SENT_TOAST]).toBeUndefined()
136+
})
137+
})
138+
139+
describe(`${RequestContext.CONFETTI_DISABLED}`, () => {
140+
it('is true for a non-Avalanche same-chain final step', () => {
141+
const ctx = buildRequestContext(makeStepDetails())
142+
expect(ctx[RequestContext.CONFETTI_DISABLED]).toBe(true)
143+
})
144+
145+
it('is absent for an Avalanche same-chain final step (confetti fires in onTransactionPending)', () => {
146+
mockGetChainIdFromCaip2.mockReturnValue(43114)
147+
mockIsAvalancheChainId.mockReturnValue(true)
148+
149+
const ctx = buildRequestContext(
150+
makeStepDetails({
151+
sourceChainId: AVAX_CHAIN_ID,
152+
targetChainId: AVAX_CHAIN_ID
153+
})
154+
)
155+
expect(ctx[RequestContext.CONFETTI_DISABLED]).toBeUndefined()
156+
})
157+
158+
it('is absent for a cross-chain swap (SUPPRESS_TX_FEEDBACK handles it)', () => {
159+
const ctx = buildRequestContext(
160+
makeStepDetails({
161+
sourceChainId: AVAX_CHAIN_ID,
162+
targetChainId: BTC_CHAIN_ID
163+
})
164+
)
165+
expect(ctx[RequestContext.CONFETTI_DISABLED]).toBeUndefined()
166+
})
167+
168+
it('is absent for an intermediate step (SUPPRESS_TX_FEEDBACK handles it)', () => {
169+
const ctx = buildRequestContext(
170+
makeStepDetails({ currentSignature: 1, requiredSignatures: 2 })
171+
)
172+
expect(ctx[RequestContext.CONFETTI_DISABLED]).toBeUndefined()
173+
})
174+
})
175+
176+
describe(`${RequestContext.SUCCESS_TOAST_DISABLED}`, () => {
177+
it('is true for a non-Avalanche same-chain final step', () => {
178+
const ctx = buildRequestContext(makeStepDetails())
179+
expect(ctx[RequestContext.SUCCESS_TOAST_DISABLED]).toBe(true)
180+
})
181+
182+
it('is absent for an Avalanche same-chain final step (ApprovalController handles it)', () => {
183+
mockGetChainIdFromCaip2.mockReturnValue(43114)
184+
mockIsAvalancheChainId.mockReturnValue(true)
185+
186+
const ctx = buildRequestContext(
187+
makeStepDetails({
188+
sourceChainId: AVAX_CHAIN_ID,
189+
targetChainId: AVAX_CHAIN_ID
190+
})
191+
)
192+
expect(ctx[RequestContext.SUCCESS_TOAST_DISABLED]).toBeUndefined()
193+
})
194+
195+
it('is absent for a cross-chain swap (SUPPRESS_TX_FEEDBACK handles it)', () => {
196+
const ctx = buildRequestContext(
197+
makeStepDetails({
198+
sourceChainId: AVAX_CHAIN_ID,
199+
targetChainId: BTC_CHAIN_ID
200+
})
201+
)
202+
expect(ctx[RequestContext.SUCCESS_TOAST_DISABLED]).toBeUndefined()
203+
})
204+
205+
it('is absent for an intermediate step (SUPPRESS_TX_FEEDBACK handles it)', () => {
206+
const ctx = buildRequestContext(
207+
makeStepDetails({ currentSignature: 1, requiredSignatures: 2 })
208+
)
209+
expect(ctx[RequestContext.SUCCESS_TOAST_DISABLED]).toBeUndefined()
79210
})
80211
})
81212
})

packages/core-mobile/app/new/features/swap/utils/buildRequestContext.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { TransferStepDetails } from '@avalabs/fusion-sdk'
2+
import { isAvalancheChainId } from 'services/network/utils/isAvalancheNetwork'
3+
import { getChainIdFromCaip2 } from 'utils/caip2ChainIds'
24
import { RequestContext } from 'store/rpc/types'
35

46
export function buildRequestContext({
@@ -10,9 +12,28 @@ export function buildRequestContext({
1012
quote.sourceChain.chainId !== quote.targetChain.chainId
1113
const isIntermediateTransaction = currentSignature < requiredSignatures
1214

15+
if (isCrossChainSwap || isIntermediateTransaction) {
16+
// Suppress all transaction feedback — no toasts or confetti at any stage
17+
return { [RequestContext.SUPPRESS_TX_FEEDBACK]: true }
18+
}
19+
20+
const numericChainId = getChainIdFromCaip2(quote.sourceChain.chainId)
21+
const isAvalanche =
22+
numericChainId !== undefined && isAvalancheChainId(numericChainId)
23+
24+
if (isAvalanche) {
25+
// ApprovalController handles Avalanche in-app swaps via isInAppAvalancheRequest:
26+
// onTransactionPending → "Transaction sent" + confetti
27+
// onTransactionConfirmed → nothing
28+
return {}
29+
}
30+
31+
// Non-Avalanche same-chain final step (e.g. Solana): show "Transaction sent"
32+
// immediately and suppress the success toast and confetti — the notification
33+
// center tracks the authoritative final status
1334
return {
14-
// Suppresses all toasts and confetti for cross-chain swaps and intermediate steps
15-
[RequestContext.TOASTS_AND_CONFETTI_DISABLED]:
16-
isCrossChainSwap || isIntermediateTransaction
35+
[RequestContext.IMMEDIATE_SENT_TOAST]: true,
36+
[RequestContext.SUCCESS_TOAST_DISABLED]: true,
37+
[RequestContext.CONFETTI_DISABLED]: true
1738
}
1839
}

packages/core-mobile/app/store/rpc/types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ export enum RequestContext {
182182
// used to signal that the in-app review logic should be triggered
183183
IN_APP_REVIEW = 'inAppReview',
184184

185-
// used to suppress all toasts and confetti for intermediate or cross-chain swap steps
186-
TOASTS_AND_CONFETTI_DISABLED = 'toastsAndConfettiDisabled'
185+
// used to suppress all transaction feedback (toasts, confetti, in-app review) for
186+
// intermediate or cross-chain swap steps
187+
SUPPRESS_TX_FEEDBACK = 'suppressTxFeedback',
188+
189+
// used to suppress only the success toast on confirmation, while still allowing the
190+
// pending toast — confetti is controlled separately by CONFETTI_DISABLED
191+
SUCCESS_TOAST_DISABLED = 'successToastDisabled',
192+
193+
// used to show "Transaction sent" immediately in onTransactionPending instead of a
194+
// pending toast — used when no confirmed toast will follow (e.g. Fusion same-chain swap)
195+
IMMEDIATE_SENT_TOAST = 'immediateSentToast'
187196
}

packages/core-mobile/app/vmModule/ApprovalController/ApprovalController.test.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import { promptForAppReviewAfterSuccessfulTransaction } from 'features/appReview
1111
import AnalyticsService from 'services/analytics/AnalyticsService'
1212
import { getAddressForChainId } from 'store/rpc/handlers/wc_sessionRequest/utils'
1313
import {
14-
isToastsAndConfettiEnabled,
14+
isTxFeedbackEnabled,
1515
isInAppAvalancheRequest,
1616
isConfettiEnabled,
1717
isInAppReview,
18+
isSuccessToastEnabled,
19+
isImmediateSentToast,
1820
showConfetti
1921
} from '../utils/requestContext'
2022
import { onApprove } from './onApprove'
@@ -24,10 +26,12 @@ import { approvalController } from './ApprovalController'
2426
// ─── Mocks ────────────────────────────────────────────────────────────────────
2527

2628
jest.mock('../utils/requestContext', () => ({
27-
isToastsAndConfettiEnabled: jest.fn(() => true),
29+
isTxFeedbackEnabled: jest.fn(() => true),
2830
isInAppAvalancheRequest: jest.fn(() => false),
2931
isConfettiEnabled: jest.fn(() => true),
3032
isInAppReview: jest.fn(() => false),
33+
isSuccessToastEnabled: jest.fn(() => true),
34+
isImmediateSentToast: jest.fn(() => false),
3135
showConfetti: jest.fn()
3236
}))
3337

@@ -87,10 +91,12 @@ jest.mock('store/rpc/handlers/wc_sessionRequest/utils', () => ({
8791

8892
// ─── Typed mock aliases ────────────────────────────────────────────────────────
8993

90-
const mockIsToastsAndConfettiEnabled = isToastsAndConfettiEnabled as jest.Mock
94+
const mockIsTxFeedbackEnabled = isTxFeedbackEnabled as jest.Mock
9195
const mockIsInAppAvalancheRequest = isInAppAvalancheRequest as jest.Mock
9296
const mockIsConfettiEnabled = isConfettiEnabled as jest.Mock
9397
const mockIsInAppReview = isInAppReview as jest.Mock
98+
const mockIsSuccessToastEnabled = isSuccessToastEnabled as jest.Mock
99+
const mockIsImmediateSentToast = isImmediateSentToast as jest.Mock
94100
const mockShowConfetti = showConfetti as jest.Mock
95101
const mockIsInAppRequest = isInAppRequest as jest.Mock
96102
const mockOnApprove = onApprove as jest.Mock
@@ -177,10 +183,12 @@ const populateSigningAddressCache = async (
177183
describe('ApprovalController', () => {
178184
beforeEach(() => {
179185
jest.clearAllMocks()
180-
mockIsToastsAndConfettiEnabled.mockReturnValue(true)
186+
mockIsTxFeedbackEnabled.mockReturnValue(true)
181187
mockIsInAppAvalancheRequest.mockReturnValue(false)
182188
mockIsConfettiEnabled.mockReturnValue(true)
183189
mockIsInAppReview.mockReturnValue(false)
190+
mockIsSuccessToastEnabled.mockReturnValue(true)
191+
mockIsImmediateSentToast.mockReturnValue(false)
184192
mockIsInAppRequest.mockReturnValue(false)
185193
})
186194

@@ -193,8 +201,8 @@ describe('ApprovalController', () => {
193201
explorerLink: 'https://example.com'
194202
})
195203

196-
it('does nothing when toasts and confetti are disabled', () => {
197-
mockIsToastsAndConfettiEnabled.mockReturnValue(false)
204+
it('does nothing when SUPPRESS_TX_FEEDBACK is set', () => {
205+
mockIsTxFeedbackEnabled.mockReturnValue(false)
198206

199207
approvalController.onTransactionPending(pendingArgs(makeRequest()))
200208

@@ -235,6 +243,17 @@ describe('ApprovalController', () => {
235243
})
236244
expect(transactionSnackbar.success).not.toHaveBeenCalled()
237245
})
246+
247+
it('shows "Transaction sent" immediately when IMMEDIATE_SENT_TOAST is set (e.g. Fusion same-chain swap)', () => {
248+
mockIsImmediateSentToast.mockReturnValue(true)
249+
250+
approvalController.onTransactionPending(pendingArgs(makeRequest()))
251+
252+
expect(transactionSnackbar.success).toHaveBeenCalledWith({
253+
message: 'Transaction sent'
254+
})
255+
expect(transactionSnackbar.pending).not.toHaveBeenCalled()
256+
})
238257
})
239258

240259
// ── onTransactionConfirmed ────────────────────────────────────────────────
@@ -249,8 +268,8 @@ describe('ApprovalController', () => {
249268
request
250269
})
251270

252-
it('skips UI effects when toasts and confetti are disabled', () => {
253-
mockIsToastsAndConfettiEnabled.mockReturnValue(false)
271+
it('skips all feedback when SUPPRESS_TX_FEEDBACK is set', () => {
272+
mockIsTxFeedbackEnabled.mockReturnValue(false)
254273

255274
approvalController.onTransactionConfirmed(confirmedArgs(makeRequest()))
256275

@@ -261,8 +280,8 @@ describe('ApprovalController', () => {
261280
).not.toHaveBeenCalled()
262281
})
263282

264-
it('still fires analytics when toasts and confetti are disabled', async () => {
265-
mockIsToastsAndConfettiEnabled.mockReturnValue(false)
283+
it('still fires analytics when SUPPRESS_TX_FEEDBACK is set', async () => {
284+
mockIsTxFeedbackEnabled.mockReturnValue(false)
266285
const request = makeDappRequest(RpcMethod.ETH_SEND_TRANSACTION)
267286
await populateSigningAddressCache(request)
268287

@@ -298,6 +317,16 @@ describe('ApprovalController', () => {
298317
expect(transactionSnackbar.success).not.toHaveBeenCalled()
299318
})
300319

320+
it('skips only the success toast when SUCCESS_TOAST_DISABLED is set (confetti controlled separately)', () => {
321+
mockIsSuccessToastEnabled.mockReturnValue(false)
322+
mockIsInAppRequest.mockReturnValue(true)
323+
324+
approvalController.onTransactionConfirmed(confirmedArgs(makeRequest()))
325+
326+
expect(transactionSnackbar.success).not.toHaveBeenCalled()
327+
expect(mockShowConfetti).toHaveBeenCalledTimes(1)
328+
})
329+
301330
it('shows success toast with explorerLink for non-Avalanche requests', () => {
302331
const request = makeRequest()
303332
const explorerLink = 'https://explorer.example.com'

0 commit comments

Comments
 (0)