Skip to content

feat: add tab navigation for market, limit, and recurring views - #34803

Merged
GeorgeGkas merged 5 commits into
mainfrom
swaps-4902
Aug 15, 2026
Merged

feat: add tab navigation for market, limit, and recurring views#34803
GeorgeGkas merged 5 commits into
mainfrom
swaps-4902

Conversation

@GeorgeGkas

@GeorgeGkas GeorgeGkas commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

The Bridge/Swaps view is being extended to support new order types (Limit Order, Recurring Buy) alongside the existing Market order flow. Previously, BridgeView rendered a single, monolithic ~1000-line component containing all Market-order UI directly, with no place to slot in additional order-type screens.

This PR introduces a tab system so the Bridge/Swaps screen can host multiple order-type views, gated individually behind feature flags so Limit and Recurring can ship to production disabled and be enabled independently once ready:

  • Extracted all existing Market-order content out of BridgeView/index.tsx into a new BridgeMarketView component (and colocated BridgeMarketView.styles.ts / .utils.ts / BridgeMarketViewFooter, renamed from their BridgeView.* counterparts).
  • Rewrote BridgeView/index.tsx as a thin tab host: it renders a HeaderStandard, a TabsBar (from the shared component-library/components-temp/Tabs), and swaps between BridgeMarketView, the new BridgeLimitOrderView, and BridgeRecurringBuyView placeholder screens based on the selected tab.
  • Tab selection uses startTransition to decouple the immediate tab-press visual feedback (selectedTab) from the more expensive content swap (renderedTab), avoiding dropped frames on tab press.
  • The slippage-settings header icon only appears while the rendered tab is Market, since slippage doesn't apply to limit/recurring orders yet.
  • Added app/components/UI/Bridge/selectors/featureFlags/index.ts with selectBridgeLimitOrderTabEnabledFlag and selectBridgeRecurringBuyTabEnabledFlag. Each selector prefers the remote LaunchDarkly flag (swapsLimitOrder / swapsRecurringBuy, shaped { enabled: boolean }) when present and falls back to a local env override (MM_BRIDGE_LIMIT_ORDER_TAB_ENABLED / MM_BRIDGE_RECURRING_BUY_TAB_ENABLED, added to .js.env.example) for local development.
  • BridgeLimitOrderView and BridgeRecurringBuyView are currently empty placeholder screens (just a themed container with a test ID) since the underlying order-type functionality doesn't exist yet — this PR only lands the navigation shell.
  • If a tab's flag flips off while it's the active/rendered tab (e.g. a remote flag update), the view falls back to the Market tab rather than rendering nothing.
  • Added bridge.tabs.market / bridge.tabs.limit / bridge.tabs.recurring strings to locales/languages/en.json.
  • Updated the tests/component-view/presets/bridge.ts component-view preset to set both new remote flags to { enabled: true } by default so existing tab-behavior component-view tests exercise all three tabs; tests covering the disabled state override them back to { enabled: false }.

Changelog

CHANGELOG entry: null

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/SWAPS-4902

Manual testing steps

Feature: Bridge/Swaps tab navigation for Market, Limit, and Recurring orders

  Background:
    Given I am logged into MetaMask Mobile
    And the MM_BRIDGE_LIMIT_ORDER_TAB_ENABLED and MM_BRIDGE_RECURRING_BUY_TAB_ENABLED env vars (or their remote flag equivalents) are set to "true"
    And I have navigated to the Bridge/Swaps screen

  Scenario: user sees only the Market tab when both new tabs are disabled
    Given both MM_BRIDGE_LIMIT_ORDER_TAB_ENABLED and MM_BRIDGE_RECURRING_BUY_TAB_ENABLED are "false"

    When I open the Bridge/Swaps screen
    Then no tabs bar should be visible
    And the Market order form should be shown directly

  Scenario: user switches to the Limit tab
    Given the Limit tab is enabled and I am on the Bridge/Swaps screen showing the tabs bar

    When user taps the "Limit" tab
    Then the Market order form (source/destination token areas, slippage settings icon) should disappear
    And an empty "Limit" placeholder screen should be shown
    And the slippage settings icon should no longer appear in the header

  Scenario: user switches to the Recurring tab
    Given the Recurring tab is enabled and I am on the Bridge/Swaps screen showing the tabs bar

    When user taps the "Recurring" tab
    Then the Market order form should disappear
    And an empty "Recurring" placeholder screen should be shown

  Scenario: user returns to the Market tab
    Given user has navigated to the "Limit" or "Recurring" tab

    When user taps the "Market" tab
    Then the full Market order form should be restored, including the source/destination token inputs and the slippage settings icon
    And any previously entered source amount should still be present

  Scenario: tab press feels responsive while switching content
    Given user is on the Bridge/Swaps screen with the tabs bar visible

    When user rapidly taps between tabs
    Then the tab highlight should update immediately on each press
    And the underlying screen content should not freeze or drop input responsiveness while it swaps in the background

Screenshots/Recordings

Before

N/A — no screenshot captured for this description. The Bridge/Swaps screen previously showed the Market order form only, with no tabs bar.

After

N/A — no screenshot captured for this description. Recommended: attach a recording of the Bridge/Swaps screen with MM_BRIDGE_LIMIT_ORDER_TAB_ENABLED=true and MM_BRIDGE_RECURRING_BUY_TAB_ENABLED=true, showing the tabs bar and switching between Market/Limit/Recurring.

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs and MetaMask Mobile Coding Standards.
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable
    • Added app/components/UI/Bridge/selectors/featureFlags/index.test.ts and new describe('tabs', ...) cases in BridgeView.view.test.tsx covering tab switching, labels, and content swapping. BridgeLimitOrderView and BridgeRecurringBuyView are untested directly since they are empty placeholder containers with no logic; their rendering is covered indirectly by the BridgeView.view.test.tsx tab tests.
  • I've documented my code using JSDoc format if applicable
    • Added JSDoc to the new feature-flag selectors and to BridgeTabKey.
  • I've applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics
    • Not applicable — this PR only adds navigation/tab-shell code and empty placeholder screens; no new async operations were introduced. Existing SwapViewLoaded tracing in the Market flow is preserved unchanged in BridgeMarketView.

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Touches the main Bridge/Swaps entry screen and quote/reset lifecycle on tab changes, but Limit/Recurring are empty placeholders behind flags defaulting off; market behavior is largely a move/refactor with new navigation shell.

Overview
Refactors Bridge/Swaps from a single monolithic screen into a tab host with Market, Limit, and Recurring views. Existing market swap/bridge UI moves into BridgeMarketView (footer/utils/styles renamed from BridgeView.*); BridgeView now only renders the header, optional TabsBar, and the active tab.

Limit and Recurring are placeholder screens behind swapsLimitOrder / swapsRecurringBuy remote flags (with MM_BRIDGE_* env fallbacks). When only Market is enabled, the tabs bar is hidden. Tab switches use startTransition for snappy highlights vs. heavier content swaps; leaving Market clears amounts and BridgeController quote polling via resetBridgeTokenInputs while keeping selected tokens. Slippage settings in the header show only on Market.

Reviewed by Cursor Bugbot for commit 3439b80. Bugbot is set up for automated code reviews on this repo. Configure here.

@GeorgeGkas
GeorgeGkas requested review from a team as code owners August 14, 2026 11:44
@GeorgeGkas GeorgeGkas added the no-changelog no-changelog Indicates no external facing user changes, therefore no changelog documentation needed label Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamask-ci metamask-ci Bot added the team-swaps-and-bridge Swaps and Bridge team label Aug 14, 2026
Comment thread app/components/UI/Bridge/Views/BridgeView/BridgeMarketView/index.tsx Outdated
Comment thread app/components/UI/Bridge/Views/BridgeView/BridgeView.view.test.tsx
@github-actions github-actions Bot added the risk:high AI analysis: high risk label Aug 14, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.02041% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.16%. Comparing base (d59936b) to head (9bd1843).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
...Bridge/Views/BridgeView/BridgeMarketView/index.tsx 90.86% 10 Missing and 8 partials ⚠️
...pp/components/UI/Bridge/Views/BridgeView/index.tsx 87.87% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #34803      +/-   ##
==========================================
+ Coverage   85.15%   85.16%   +0.01%     
==========================================
  Files        6448     6452       +4     
  Lines      175600   175668      +68     
  Branches    43607    43619      +12     
==========================================
+ Hits       149526   149603      +77     
+ Misses      15902    15899       -3     
+ Partials    10172    10166       -6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Flaky unit test detection

Run history flaky detection

View recent run history

Historical failure rate is a hint, not proof — review each suggestion in context. See the flaky-test-detection skill for the full pattern reference and manual audit workflow.

Failures / runs sampled per window:

File 7d 15d 30d
app/components/UI/Bridge/Views/BridgeView/BridgeMarketView/BridgeMarketViewFooter.test.tsx 0/129 0/172 0/365

AI-detected flaky patterns

app/components/UI/Bridge/Views/BridgeView/BridgeMarketView/BridgeMarketViewFooter.test.tsx

  • J7 — Non-deterministic data: Date.now() (medium)
    • Date.now() is called inside buildActiveQuoteState(), which is invoked at test-body execution time. The quotesLastFetched field is fed into the Redux store state and consumed by selectors or component logic that may compare it against the current wall-clock time (e.g. to decide whether quotes are stale). Under CI load, the gap between when Date.now() is captured and when the component evaluates the value can vary, causing intermittent staleness checks to flip. Pinning the value to a fixed constant eliminates this non-determinism entirely.
    • Suggested fix in app/components/UI/Bridge/Views/BridgeView/BridgeMarketView/BridgeMarketViewFooter.test.tsx:120:
      -function buildActiveQuoteState(
      -  overrides: {
      -    bridgeControllerOverrides?: Record<string, unknown>;
      -    bridgeReducerOverrides?: Record<string, unknown>;
      -  } = {},
      -) {
      -  return createBridgeTestState({
      -    bridgeControllerOverrides: {
      -      quotesLoadingStatus: RequestStatus.FETCHED,
      -      quotes: [mockQuoteWithMetadata],
      -      quotesLastFetched: Date.now(),
      -      ...(overrides.bridgeControllerOverrides ?? {}),
      -    },
      +const FIXED_QUOTES_LAST_FETCHED = 1_700_000_000_000; // fixed epoch ms
      +
      +function buildActiveQuoteState(
      +  overrides: {
      +    bridgeControllerOverrides?: Record<string, unknown>;
      +    bridgeReducerOverrides?: Record<string, unknown>;
      +  } = {},
      +) {
      +  return createBridgeTestState({
      +    bridgeControllerOverrides: {
      +      quotesLoadingStatus: RequestStatus.FETCHED,
      +      quotes: [mockQuoteWithMetadata],
      +      quotesLastFetched: FIXED_QUOTES_LAST_FETCHED,
      +      ...(overrides.bridgeControllerOverrides ?? {}),
      +    },
  • J7 — Non-deterministic data: Date.now() (medium)
    • A second inline call to Date.now() appears in the 'Hardware Wallet Banner' test for quotesLastFetched. Same risk as the one in buildActiveQuoteState: if the component or a selector derives staleness from this timestamp, the test outcome can differ depending on CI timing. Replace with the same fixed constant used elsewhere.
    • Suggested fix in app/components/UI/Bridge/Views/BridgeView/BridgeMarketView/BridgeMarketViewFooter.test.tsx:218:
      -      const testState = createBridgeTestState({
      -        bridgeControllerOverrides: {
      -          quoteRequest: { insufficientBal: false },
      -          quotesLoadingStatus: RequestStatus.FETCHED,
      -          quotes: [mockQuoteWithMetadata],
      -          quotesLastFetched: Date.now(),
      -        },
      +      const testState = createBridgeTestState({
      +        bridgeControllerOverrides: {
      +          quoteRequest: { insufficientBal: false },
      +          quotesLoadingStatus: RequestStatus.FETCHED,
      +          quotes: [mockQuoteWithMetadata],
      +          quotesLastFetched: FIXED_QUOTES_LAST_FETCHED,
      +        },

This check is informational only and does not block merging.

@github-actions github-actions Bot added risk:medium AI analysis: medium risk and removed risk:high AI analysis: high risk labels Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAccounts, SmokeConfirmations, SmokeNetworkAbstractions, SmokeNetworkExpansion, SmokeSwap, SmokeStake, SmokeWalletPlatform, SmokeMoney, SmokePerps, SmokeMultiChainAPI, SmokePredictions, SmokeSeedlessOnboarding, SmokeBrowser, SmokeSnaps, SmokeMMConnect
  • Selected Performance tags: @PerformanceSwaps
  • Risk Level: high
  • AI Confidence: 100%
click to see 🤖 AI reasoning details

E2E Test Selection:
Hard rule (en-locale-change): locales/languages/en.json changed — UI strings and E2E text/label selectors may diverge (including platform casing like Android textAllCaps). Running all tests.

Performance Test Selection:
The BridgeView has been significantly refactored with a new tab-based architecture using startTransition for deferred rendering. The BridgeMarketView now wraps the existing swap/bridge content. This architectural change could affect swap flow performance metrics (quote fetching, token selection, swap execution times) measured by @PerformanceSwaps. The use of startTransition for tab switching is specifically a performance optimization, suggesting the team is aware of render performance concerns in this view.

View GitHub Actions results

@github-actions github-actions Bot added risk:high AI analysis: high risk and removed risk:medium AI analysis: medium risk labels Aug 14, 2026

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3439b80. Configure here.

Comment thread app/components/UI/Bridge/Views/BridgeView/index.tsx
@github-actions

Copy link
Copy Markdown
Contributor

⚡ Performance Test Results

ℹ️ Performance test results are currently non-blocking and will not block this PR.

All tests passed · 2 tests · 1 device

📱 Devices tested (1)

Android: Google Pixel 8 Pro (v14.0)

✅ Passed Tests (2)
Test Platform Device Duration Team Recording
Swap flow - ETH to LINK, SRP 1 + SRP 2 + SRP 3 Android Google Pixel 8 Pro (v14.0) 1.25s @swap-bridge-dev-team 📹 Watch
Cross-chain swap flow - ETH to SOL - 50+ accounts, SRP 1 + SRP 2 + SRP 3 Android Google Pixel 8 Pro (v14.0) 5.38s @swap-bridge-dev-team 📹 Watch

Branch: swaps-4902 · Build: E2E · Commit: 45ad35b · View full run

@sonarqubecloud

Copy link
Copy Markdown


const BridgeMarketViewContent = ({
latestSourceBalance,
}: BridgeMarketViewContentProps) => {

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.

this file is copied as is from BridgeView/index.ts without changes.

@GeorgeGkas
GeorgeGkas added this pull request to the merge queue Aug 15, 2026
Merged via the queue into main with commit aaf3289 Aug 15, 2026
115 of 117 checks passed
@GeorgeGkas
GeorgeGkas deleted the swaps-4902 branch August 15, 2026 20:45
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 15, 2026
@metamask-ci metamask-ci Bot added the release-8.9.0 Issue or pull request that will be included in release 8.9.0 label Aug 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

no-changelog no-changelog Indicates no external facing user changes, therefore no changelog documentation needed release-8.9.0 Issue or pull request that will be included in release 8.9.0 risk:high AI analysis: high risk size-XL team-swaps-and-bridge Swaps and Bridge team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants