Skip to content

feat(predict): display outcomes in the outcomes tab with buy flow and sticky headers#28933

Merged
caieu merged 13 commits into
mainfrom
PRED-810-phase-7-display-outcomes-in-the-outcomes-tab
Apr 17, 2026
Merged

feat(predict): display outcomes in the outcomes tab with buy flow and sticky headers#28933
caieu merged 13 commits into
mainfrom
PRED-810-phase-7-display-outcomes-in-the-outcomes-tab

Conversation

@caieu
Copy link
Copy Markdown
Contributor

@caieu caieu commented Apr 16, 2026

Description

Sports game detail screens had a mock outcomes tab with empty cards. This PR replaces the mocks with real outcomeGroups data, renders functional outcome cards grouped by market type, wires buy flow navigation, and adds a sticky tab bar + chip list header.

Provider layer:

  • Added line and shortTitle fields to outcome types so buttons show compact labels (team abbreviations, O/U + line) while the buy screen preserves full names
  • Token titles for O/U markets now map Yes/NoOver/Under
  • Outcome sorting within standalone groups (points, assists, etc.) prioritizes volume descending
  • Unified all group keys to use underscores (was mixed hyphens/underscores)

UI layer:

  • New PredictGameOutcomesTab component renders outcome cards from real data: composite groups show one card per subgroup (with line selector for multi-line markets), standalone groups show one card per outcome
  • Moneyline buttons use team colors; all others use neutral styling
  • Buy flow wired via usePredictActionGuard + usePredictNavigation
  • Tab bar and chip list combined into a single sticky ScrollView child
  • Chip selection scrolls outcomes to top
  • Chip state moved to useGameDetailsTabs hook

Changelog

CHANGELOG entry: null

Related issues

Fixes: PRED-810
PRED-811

Manual testing steps

Feature: Display outcomes in the outcomes tab

  Scenario: user views outcome cards for a sports game
    Given the user opens a sports game detail screen (NBA/Soccer) with the extended sports markets flag enabled

    When user sees the Outcomes tab
    Then the "Game Lines" chip is selected by default
    And Moneyline, Spreads, and Totals cards render with correct titles and prices
    And Moneyline buttons show team abbreviations with team colors
    And Spreads buttons show abbreviation + signed line (e.g., "ORL +1.5")
    And Totals buttons show "O 220.5" / "U 220.5" with neutral styling

  Scenario: user switches outcome group chips
    Given the user is viewing the Outcomes tab

    When user taps the "Points" chip
    Then the outcomes list scrolls to the top
    And player name cards render without the ": Points O/U" suffix
    And buttons show "O {line}" / "U {line}" labels
    And outcomes are sorted by volume descending

  Scenario: user taps an outcome button to buy
    Given the user is viewing outcome cards

    When user taps any outcome button
    Then the buy preview screen opens
    And the buy screen shows full team name or "Over"/"Under" (not abbreviated)
    And the correct market title and price are displayed

  Scenario: sticky tab bar and chip list
    Given the user is viewing the Outcomes tab

    When user scrolls down through outcome cards
    Then the tab bar and chip list stick together at the top of the screen
    And content does not bleed through the sticky header

  Scenario: feature flag is off
    Given the extended sports markets flag is disabled

    When user opens a sports game detail screen
    Then no chip list or outcome tabs are rendered
    And no sticky header behavior is applied

Screenshots/Recordings

Before

N/A

After

Screen.Recording.2026-04-16.at.16.37.42.mov

Pre-merge author checklist

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

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 both UI navigation (buy flow) and provider parsing/types for outcome/token data, so regressions could impact what markets render and which outcome/token gets passed into purchase.

Overview
Replaces the mock sports market Outcomes tab with real outcomeGroups rendering via new PredictGameOutcomesTab, including subgroup cards (with optional line selection), volume subtitles, moneyline team-color buttons, and buy buttons that trigger guarded navigateToBuyPreview.

Updates game details to show a combined sticky header (tab bar + chip list), moves chip state/visibility into useGameDetailsTabs, and adds chip auto-scroll behavior (calculateChipScrollX) plus chip-selection scroll-to-top logic for the outcomes list.

Extends the Polymarket parsing/types to support PredictOutcome.line and PredictOutcomeToken.shortTitle, maps O/U Yes/No titles to Over/Under, adjusts outcome sorting tiebreakers, and standardizes outcome-group keys to underscore format; adds/updates unit tests and English i18n entries for group and market-type labels.

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

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

@metamaskbotv2 metamaskbotv2 Bot added the team-predict Predict team label Apr 16, 2026
@github-actions github-actions Bot added size-XL and removed size-L labels Apr 16, 2026
@caieu caieu marked this pull request as ready for review April 16, 2026 21:00
@caieu caieu requested a review from a team as a code owner April 16, 2026 21:00
@caieu caieu changed the title feat(predict): display outcomes in the outcomes tab with buy flow and sticky headers feat(predict): display outcomes in the outcomes tab with buy flow and sticky headers cp-7.74.0 Apr 16, 2026
@MarioAslau
Copy link
Copy Markdown
Contributor

Good quality PR with strong test coverage (91.5% on new code per SonarCloud). The architecture is clean with proper memo'd sub-components, well-structured hooks, and thorough i18n. A few issues to address below.


HIGH Severity (Should fix before merge)

1. minHeight reads a stale ref, producing an oversized content area on initial render

stickyHeaderY.current is initialized to 0 and updated asynchronously via onLayout. Since ref mutations don't trigger re-renders, minHeight equals windowHeight on the first render — producing unnecessary scrollable empty space below the outcomes list until some other state change forces a re-render.

File: PredictGameDetailsContent.tsx

// ref initialized to 0
const stickyHeaderY = useRef(0);

// used in render — but ref never triggers re-render
<Box style={{ minHeight: windowHeight - stickyHeaderY.current }}>

Suggested fix: Convert to state so the layout measurement triggers a re-render:

const [stickyHeaderY, setStickyHeaderY] = useState(0);
const handleStickyHeaderLayout = useCallback(
  (e) => {
    setStickyHeaderY(e.nativeEvent.layout.y);
  },
  [],
);
// ...
<Box style={{ minHeight: windowHeight - stickyHeaderY }}>

MEDIUM Severity (Worth addressing)

2. Sorting behavior change in buildOutcomeGroups is a silent breaking change

File: polymarket/utils.ts

The outcome sorting within standalone groups changed from a combined score to volume-first with liquidity as tiebreaker:

-      const aScore = (a.liquidity ?? 0) + a.volume;
-      const bScore = (b.liquidity ?? 0) + b.volume;
-      return bScore - aScore;
+      const volumeDiff = b.volume - a.volume;
+      if (volumeDiff !== 0) return volumeDiff;
+      return (b.liquidity ?? 0) - (a.liquidity ?? 0);

This changes ordering for markets where a low-volume high-liquidity market previously ranked above a high-volume low-liquidity one. If intentional, please confirm downstream consumers are unaffected and call it out explicitly in the PR description.

3. formatOutcomeCardTitle title parsing is fragile

File: PredictGameOutcomesTab.tsx

const formatOutcomeCardTitle = (outcome: PredictOutcome): string => {
  const raw = outcome.groupItemTitle || outcome.title;
  if (raw.includes('O/U') && raw.includes(': ')) {
    return raw.split(': ')[0].trim();
  }
  return raw;
};

Takes everything before the first : when the title contains O/U. If a player/team name ever contains a colon (e.g. "St. Louis: Points O/U 0.5"), this would truncate incorrectly. A more targeted regex like /^(.+?):\s+\w+ O\/U/ would be safer.

4. LineOutcomeCard internal state may not reset on group switches

File: PredictGameOutcomesTab.tsx

LineOutcomeCard uses useState(outcomes[0]?.line) to track the selected line. If React reuses the same component instance when the parent group changes (e.g., same subgroup key spreads across different chip groups), the selectedLine state won't reset — potentially showing incorrect line/price data.

Suggested fix: Add a useEffect to reset when outcomes change:

useEffect(() => {
  setSelectedLine(outcomes[0]?.line);
}, [outcomes]);

Or ensure the parent provides a key prop that incorporates the group key to force remounting.

5. No-op function created when onBuyPress is undefined

File: PredictGameOutcomesTab.tsx

onPress: onBuyPress ? () => onBuyPress(outcome, token) : () => undefined,

Creates a new no-op function reference on every render. Should be undefined instead to avoid unnecessary press handling downstream:

onPress: onBuyPress ? () => onBuyPress(outcome, token) : undefined,

LOW Severity (Nice-to-have)

6. CONTENT_PADDING magic number coupled to Tailwind class

File: PredictChipList.tsx

/** Must match the `px-4` applied to ScrollView contentContainerStyle. */
const CONTENT_PADDING = 16;

If someone changes px-4 to px-3, this constant silently goes out of sync. Consider co-locating or deriving from the same source.

7. Duplicated i18n keys between outcome_groups and sports_market_types

File: en.json

Several labels are identical across both sections (points, assists, rebounds, goalscorers, etc.). Consider sharing keys or adding a comment explaining why duplication is intentional.

8. calculateChipScrollX exported from component file

File: PredictChipList.tsx

Exported solely for unit testing. Could be extracted to a utility file like PredictChipList.utils.ts for cleaner separation of concerns.

9. Non-obvious effect + ref guard pattern

File: PredictGameDetailsContent.tsx

useEffect(() => {
  if (!pendingChipScroll.current) return;
  pendingChipScroll.current = false;
  // ...scroll logic...
}, [activeChipKey]);

The pendingChipScroll ref guard correctly prevents firing on initial render, but this pattern can be unintuitive for future maintainers. A brief inline comment explaining the intent would help.

10. Large test file could extract shared helpers

File: PredictGameOutcomesTab.test.tsx (848 lines)

Coverage is excellent. The shared test helpers (createToken, createOutcome, createGroup, toGroupMap, mockGame) could be extracted to a shared test utility file to reduce duplication across test files.

11. Merge conflict risk on useGameDetailsTabs

The hook's interface changes (adding outcomeGroups param). If other PRs touch useGameDetailsTabs concurrently, merge conflicts are likely. Just a heads-up.

@caieu caieu changed the title feat(predict): display outcomes in the outcomes tab with buy flow and sticky headers cp-7.74.0 feat(predict): display outcomes in the outcomes tab with buy flow and sticky headers Apr 16, 2026
@github-actions github-actions Bot added the risk-medium Moderate testing recommended · Possible bug introduction risk label Apr 16, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePredictions, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePredict
  • Risk Level: medium
  • AI Confidence: 88%
click to see 🤖 AI reasoning details

E2E Test Selection:
All 15 changed files are within the Predict/Polymarket feature area (app/components/UI/Predict/) plus localization (en.json). The changes are substantial UI and logic improvements to the sports market game details view:

  1. PredictChipList.tsx: Added auto-scroll behavior when selecting outcome group chips - new layout tracking with refs and scroll-to-chip calculation logic.

  2. PredictGameDetailsContent.tsx: Major refactor integrating chip list as a sticky header, scroll management with InteractionManager, window height-based min-height for content area.

  3. PredictGameDetailsTabsContent.tsx: Replaced mock outcome groups with real data from groupMap, replaced placeholder OutcomesContent with new PredictGameOutcomesTab, added buy press navigation.

  4. PredictGameOutcomesTab.tsx: New component rendering sport outcome cards with line selection (O/U markets), team colors for moneyline markets, spread formatting, and buy press handling.

  5. useGameDetailsTabs.ts: Moved chip state management here from TabsContent, added groupMap, showChips logic.

  6. utils.ts: Added shortTitle support for outcome tokens (team abbreviations, O/U labels), fixed over/under market detection, improved sorting (volume-first then liquidity).

  7. constants.ts: Fixed group key naming from hyphens to underscores (e.g., 'first-half' → 'first_half'), aligning with i18n keys.

  8. types/index.ts: Added line field to PredictOutcome, shortTitle to PredictOutcomeToken.

  9. en.json: Added sports_market_types section and additional outcome_groups entries.

Tag selection rationale:

  • SmokePredictions: Direct changes to Predict/Polymarket game details UI and data processing - core functionality being modified
  • SmokeWalletPlatform: Per SmokePredictions tag description, Predictions is a section inside the Trending tab; changes to Predictions views affect Trending
  • SmokeConfirmations: Per SmokePredictions tag description, opening/closing positions are on-chain transactions requiring confirmations

The changes are self-contained within the Predict feature area with no impact on shared navigation, controllers, Engine, or other wallet features.

Performance Test Selection:
The changes include significant UI component updates to the Predict game details view: new PredictGameOutcomesTab component with line selection state, chip list auto-scroll with layout tracking, sticky header with InteractionManager for scroll coordination, and window height-based min-height calculations. These rendering changes to the prediction market detail screens could impact the performance of loading and displaying prediction market content, which is directly measured by @PerformancePredict.

View GitHub Actions results

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

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 2 potential issues.

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 9083f8e. Configure here.

activePositions={activePositions}
claimablePositions={claimablePositions}
/>
<Box style={{ minHeight: windowHeight - stickyHeaderY }}>
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.

minHeight formula prevents sticky header from activating

Medium Severity

minHeight: windowHeight - stickyHeaderY subtracts the y-position of the sticky header (height of scoreboard + chart above it), producing a content area that's too short for the sticky header to ever activate. For example, with windowHeight=800 and stickyHeaderY=300, the total ScrollView content becomes ~880px, allowing only ~80px of scroll — far less than the 300px needed for the header to reach the top and stick. The formula likely needs to use windowHeight alone (or subtract the sticky header's own height, not its y-position) to guarantee enough scrollable area.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9083f8e. Configure here.

str
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
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.

Duplicated toTitleCase logic across two files

Low Severity

The toTitleCase helper in PredictGameOutcomesTab.tsx is identical to the inline title-casing fallback in outcomeGroupLabel.ts — both split on underscores, capitalize each word, and join with spaces. Extracting a shared utility would reduce duplication and ensure consistent behavior if the logic ever changes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9083f8e. Configure here.

@sonarqubecloud
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
12 value mismatches detected (expected — fixture represents an existing user).
View details

Copy link
Copy Markdown
Contributor

@MarioAslau MarioAslau left a comment

Choose a reason for hiding this comment

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

Thank you for addressing the comments. LGTM !

@caieu caieu added this pull request to the merge queue Apr 17, 2026
Merged via the queue into main with commit e0a6b8c Apr 17, 2026
104 of 105 checks passed
@caieu caieu deleted the PRED-810-phase-7-display-outcomes-in-the-outcomes-tab branch April 17, 2026 13:17
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 17, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.75.0 Issue or pull request that will be included in release 7.75.0 label Apr 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.75.0 Issue or pull request that will be included in release 7.75.0 risk-medium Moderate testing recommended · Possible bug introduction risk size-XL team-predict Predict team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants