Skip to content

CP-14844: Restore the token switch button on the swap screen - #4008

Merged
B0Y3R-AVA merged 10 commits into
mainfrom
CP-14844
Jul 27, 2026
Merged

CP-14844: Restore the token switch button on the swap screen#4008
B0Y3R-AVA merged 10 commits into
mainfrom
CP-14844

Conversation

@onghwan

@onghwan onghwan commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Description

Ticket: CP-14844

  • Restores the from ⇄ to token switch button on the Swap screen. The button existed pre-Fusion and was removed in CP-13847 (CP-13847: Remove the swap button in the middle #3705); this brings it back with the same design (40×40 CircularButton + SwapVertical icon centered over the separator between the "You pay" / "You receive" cards, split separators, hidden while the "You pay" input is focused, testID="swap_vertical_icon" kept for existing e2e locators).
  • The toggle logic is rewritten for the Fusion SwapContext (the old setDestination(SwapSide.SELL) API no longer exists): it swaps fromToken/toToken and clears the amounts so the reversed pair fetches a fresh quote — rates aren't symmetric across services, so the old receive-amount is intentionally not carried over (web carries it over; pre-Fusion mobile cleared it, we follow mobile).
  • Zero-balance guard: switching is blocked with the existing fusionErrors.noDestinationToken error when the current "to" token has no swappable balance. The old implementation checked the tokensWithZeroBalance list (C-Chain/Solana only); this uses getSwappableBalance(toToken) === 0n, which covers all networks and correctly excludes staked/locked P/X-chain balances.
  • No new dependencies.

Stale-quote safety: clearing the amount makes useQuoteStreaming drop bestQuote/allQuotes immediately, so Next can't be pressed with a quote from the previous direction.

Screenshots/Videos

ScreenRecording_07-23-2026.11-45-40_1.MP4

Testing

iOS: 9398
Android: 9399

Dev Testing

  • Open Swap, pick a pair (e.g. AVAX → USDC), enter an amount, tap the circular switch button between the two cards → tokens swap positions, amounts clear, a new quote streams in for the reversed pair.
  • Focus the "You pay" input → the button fades out and the separator joins; blur → it fades back in.
  • Select a "to" token you hold none of, tap switch → blocked with "You don't have any {symbol} token for swap".
  • During an in-flight swap the button is disabled.

QA Testing

  • Verify the switch button behaves the same as the pre-Fusion swap screen, including on Android (button overlaps the card boundary — confirm the touch target works on Fabric).

Checklist

Please check all that apply (if applicable)

  • I have performed a self-review of my code
  • I have verified the code works
  • I have included screenshots / videos of android and ios
  • I have added testing steps
  • I have added/updated necessary unit tests
  • I have updated the documentation

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 23, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restores the pre-Fusion “swap direction” (You pay ⇄ You receive) switch button on the Swap screen and rewires the token/amount swapping behavior to work with the Fusion SwapContext, including the zero-balance guard and “clear amounts to force a fresh quote” behavior.

Changes:

  • Reintroduced the circular token-switch button UI between the “You pay” and “You receive” sections, including focus-driven hide/show behavior.
  • Implemented handleToggleTokens to swap fromToken/toToken, clear amounts, and block switching when the resulting pay-token would have zero swappable balance.
  • Adjusted the separator rendering to match the previous split-separator design when the switch button is visible.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core-mobile/app/new/features/swap/screens/SwapScreen.tsx
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Coverage report ✅

2/2 packages passed thresholds
Thresholds are shown inline against each package baseline.

🟢 @avalabs/core-mobile

St. Category Percentage Covered / Total
🟢 Statements 27.85% (+9.85% vs 18% ▲) 12326/44256
🟢 Branches 23.49% (+11.49% vs 12% ▲) 6045/25734
🟢 Functions 22.12% (+9.12% vs 13% ▲) 2262/10222
🟢 Lines 27.92% (+9.92% vs 18% ▲) 11802/42259

🟢 @avalabs/k2-alpine

St. Category Percentage Covered / Total
🟢 Statements 8.49% (+6.49% vs 2% ▲) 304/3578
🟢 Branches 7.90% (+6.90% vs 1% ▲) 172/2177
🟢 Functions 4.99% (+3.99% vs 1% ▲) 41/821
🟢 Lines 7.82% (+5.82% vs 2% ▲) 257/3283
Artifacts and threshold sources
  • @avalabs/core-mobile: summary core-mobile/coverage/coverage-summary.json, thresholds core-mobile/coverage-thresholds.json
  • @avalabs/k2-alpine: summary k2-alpine/coverage/coverage-summary.json, thresholds k2-alpine/coverage-thresholds.json

Source run: Mobile PR

@B0Y3R-AVA B0Y3R-AVA left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review pass (verified against the branch source, not just the diff). Two pre-merge items below; everything else checked out clean — dep arrays, getSwappableBalance typing, e2e swap_vertical_icon wiring, getButtonBackgroundColor usage, and the FadeIn/FadeOut + LinearTransition mix are all fine.

const handleToggleTokens = useCallback(() => {
// Can't pay with a token the user holds none of.
if (toToken && getSwappableBalance(toToken) === 0n) {
setValidationError(fusionErrors.noDestinationToken(toToken.symbol))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routing the blocked-toggle feedback through validationError wedges the current (unchanged, still-valid) swap: activeError = validationError ?? quoteError feeds canSwap, and noDestinationToken is a blocking FusionQuoteError (no isWarning), so Next disables and the pay field flags invalid even though nothing about the on-screen direction changed.

Nothing clears it in place either — validateInputs and the token-compatibility effect only rerun when the amount or a token changes, so the error sticks until the user edits something unrelated.

Suggestion: surface "can't reverse — no X balance" via a toast/snackbar (or similar transient UI) instead of validationError, so the still-valid current swap isn't gated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 1ed0d01. The zero-balance guard now surfaces the message via showSnackbar (same copy, reusing fusionErrors.noDestinationToken(...).message) instead of setValidationError, so the current direction's swap state (Next button, pay-field validity) is untouched and nothing lingers.

exiting={FadeOut}
style={{
position: 'absolute',
top: -20,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android tap-target concern: this 40×40 TouchableOpacity (CircularButton, no hitSlop) sits at top: -20 inside a host view whose natural layout height is just the 1px separator row. RN on Android doesn't hit-test touches outside the parent's bounds, and zIndex: 100 only affects paint order, not touch — so most (possibly all) of the button may be untappable on Android even though it renders correctly.

Worth verifying on an Android device before merge. If it repros, give the host view real bounds (or render the button in a sibling with actual height) rather than relying on overflow paint.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked both in the RN source and on a device:

  • The "no hit-testing outside parent bounds" behavior is the old architecture's. This app is on RN 0.85 / Fabric, and Fabric's Android TouchTargetHelper.kt hit-tests using overflow insets: touches outside a view's bounds are only rejected for overflow: hidden / clipChildren parents (lines ~191-198), otherwise isTouchPointInViewWithOverflowInset accepts points within the child-overflow region (lines ~272-279). None of the button's ancestors here set overflow: hidden (the card wrappers that do are siblings), and zIndex: 100 also puts this subtree first in hit-test order.
  • Verified on an Android device: the full 40×40 button is tappable, including the half overlapping the "You pay" card.

ruijia1in
ruijia1in previously approved these changes Jul 23, 2026

@ruijia1in ruijia1in left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dev-tested

Copilot AI review requested due to automatic review settings July 23, 2026 18:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread packages/core-mobile/app/new/features/swap/screens/SwapScreen.tsx Outdated
Copilot AI review requested due to automatic review settings July 23, 2026 18:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread packages/core-mobile/app/new/features/swap/screens/SwapScreen.tsx
Copilot AI review requested due to automatic review settings July 23, 2026 18:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 23, 2026 19:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 23, 2026 19:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

packages/core-mobile/app/new/features/swap/screens/SwapScreen.tsx:1788

  • The toggle button is currently enabled whenever !isSwapping, but toToken can be cleared (incompatible networks / same-token guard), which makes toggling a no-op at best and surprising at worst. Disable the button when either token is missing.
                disabled={isSwapping}

packages/core-mobile/app/new/features/swap/screens/SwapScreen.tsx:1783

  • This icon-only CircularButton lacks an accessible label. Add a title (or accessibility label, depending on the component API) so screen readers can announce what the control does.
              <CircularButton
                testID="swap_vertical_icon"
                backgroundColor={swapButtonBackgroundColor}
                style={{

Comment thread packages/core-mobile/app/new/features/swap/screens/SwapScreen.tsx
Copilot AI review requested due to automatic review settings July 23, 2026 20:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/core-mobile/app/new/features/swap/hooks/useMaxSwapAmount/index.ts:61

  • useHoldMaxWhileRecalculating mutates lastKnownRef.current during render. In React 18+/19 (and RN Strict Mode), render can be re-run or abandoned, so render-time ref writes are side effects and can lead to held values being out of sync. Move the ref updates into an effect, and compute the held value purely (guarded by the current tokenKey) during render.
  const lastKnownRef = useRef<{ tokenKey: string; value: bigint }>()
  const tokenKey = fromToken ? getTokenKey(fromToken) : undefined

  if (lastKnownRef.current && lastKnownRef.current.tokenKey !== tokenKey) {
    lastKnownRef.current = undefined
  }
  if (max !== undefined && tokenKey !== undefined) {
    lastKnownRef.current = { tokenKey, value: max }
  }

Comment thread packages/core-mobile/app/new/features/swap/hooks/useMaxSwapAmount/index.ts Outdated
Copilot AI review requested due to automatic review settings July 23, 2026 20:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings July 23, 2026 21:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/core-mobile/app/new/features/swap/hooks/useMaxSwapAmount/index.ts:227

  • isMaxLoading is computed even when the pre-quote prerequisites aren’t met (e.g., toToken/addresses missing). In those states additiveFeeForMax stays undefined, so computeIsMaxLoading returns true and TokenInputWidget will keep the Max button mounted + disabled indefinitely, even though nothing is actually “loading” until the user selects the missing inputs. Gate isMaxLoading on the same prerequisites as usePreQuote so Max hides (or behaves as before) when the route can’t be estimated yet.
  const hasEstimationError =
    (!!feeEstimationError && !isFeeEstimationFetching) || preQuoteFailed

  const isMaxLoading = computeIsMaxLoading({
    fromToken,

Copilot AI review requested due to automatic review settings July 23, 2026 21:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@B0Y3R-AVA
B0Y3R-AVA requested a review from a team as a code owner July 27, 2026 20:29
@B0Y3R-AVA
B0Y3R-AVA merged commit 38a1bdd into main Jul 27, 2026
6 checks passed
@B0Y3R-AVA
B0Y3R-AVA deleted the CP-14844 branch July 27, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants