Skip to content

Commit 10b1a69

Browse files
tommasinicaieu
andauthored
fix(predict): extended sports outcome grouping, spread ordering, and live pricing cp-7.81.2 (#31934)
## Description Cherry-pick of #31852 into `release/7.81.2-ota`. Extended sports game-details tabs (moneyline, spreads, totals, and future market types) were not grouping or pricing outcomes reliably compared to Polymarket. This PR: 1. **Adds `enabledSportsMarketTypes`** to the `predictExtendedSportsMarkets` remote flag so outcome grouping respects an allowlist of supported market types (defaults to moneyline, spreads, and totals when the field is omitted on an enabled flag). 2. **Fixes spread line ordering** to match Polymarket's ascending/descending display based on the first spread's displayed sign. 3. **Polls REST ask prices** (`entry.sell`) for non-moneyline grouped outcomes while keeping moneyline on live WebSocket best-ask prices; polling pauses while the buy sheet is open. 4. **Refactors game-details outcome UI** into `outcomeGrouping.ts`, `PredictGameOutcomeCards.tsx`, `usePricedOutcomeGroup.ts`, and `utils.ts`, with unit tests colocated by module. **Why:** PRED-957 — corners, team totals, and first-half team totals (and related extended markets) were not showing up or behaving correctly in game details. ## Changelog Fixed extended sports game-details outcome tabs to group enabled market types correctly, order spread lines like Polymarket, and show current buy prices for non-moneyline markets. ## Related issues Fixes: - PRED-957 - PRED-958 ## Manual testing steps See original PR #31852 for full manual testing steps. ## Cherry-pick details | Field | Value | |---|---| | Original PR | #31852 | | Target branch | `release/7.81.2-ota` | | Commits cherry-picked | `5dc7852`, `c96b4a2`, `ac1a070`, `5eb2c08`, `266d944`, `b0a8104`, `498cdfb` | ## Pre-merge author checklist - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using JSDoc format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## 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. Made with [Cursor](https://cursor.com) --------- Co-authored-by: Caainã Jeronimo <caainaje@gmail.com>
1 parent 3e2e9e7 commit 10b1a69

18 files changed

Lines changed: 2604 additions & 1547 deletions
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
2+
import type { PredictMarketGame, PredictOutcomeGroup } from '../../types';
3+
import { useLiveMarketPrices } from '../../hooks/useLiveMarketPrices';
4+
import { isMoneylineLikeMarketType } from '../../constants/sports';
5+
import PredictSportOutcomeCard from '../PredictSportOutcomeCard';
6+
import {
7+
buildButtons,
8+
buildMoneylineButtons,
9+
buildMoneylineSubtitle,
10+
buildSubtitle,
11+
type BuyHandler,
12+
formatOutcomeCardTitle,
13+
getDefaultLineIndex,
14+
getFallbackSportsMarketTypeLabel,
15+
getSportsMarketTypeLabel,
16+
getTranslatedSportsMarketTypeLabel,
17+
sortMoneylineOutcomes,
18+
} from './utils';
19+
20+
const SimpleOutcomeCard = memo(
21+
({
22+
outcome,
23+
title,
24+
onBuyPress,
25+
game,
26+
sportsMarketType,
27+
testID,
28+
}: {
29+
outcome: PredictOutcomeGroup['outcomes'][number];
30+
title: string;
31+
onBuyPress: BuyHandler;
32+
game?: PredictMarketGame;
33+
sportsMarketType?: string;
34+
testID: string;
35+
}) => (
36+
<PredictSportOutcomeCard
37+
title={title}
38+
subtitle={buildSubtitle(outcome)}
39+
buttons={buildButtons(outcome, onBuyPress, game, sportsMarketType)}
40+
testID={testID}
41+
/>
42+
),
43+
);
44+
45+
SimpleOutcomeCard.displayName = 'SimpleOutcomeCard';
46+
47+
const LineOutcomeCard = memo(
48+
({
49+
outcomes,
50+
title,
51+
onBuyPress,
52+
game,
53+
sportsMarketType,
54+
testID,
55+
}: {
56+
outcomes: PredictOutcomeGroup['outcomes'];
57+
title?: string;
58+
onBuyPress: BuyHandler;
59+
game?: PredictMarketGame;
60+
sportsMarketType?: string;
61+
testID: string;
62+
}) => {
63+
const lineIndices = useMemo(
64+
() =>
65+
outcomes
66+
.map((outcome, index) => (outcome.line != null ? index : -1))
67+
.filter((index) => index !== -1),
68+
[outcomes],
69+
);
70+
71+
const lines = useMemo(
72+
() => lineIndices.map((i) => Math.abs(outcomes[i].line ?? 0)),
73+
[lineIndices, outcomes],
74+
);
75+
76+
const defaultSelectedIdx = useMemo(
77+
() => getDefaultLineIndex(outcomes, lineIndices),
78+
[outcomes, lineIndices],
79+
);
80+
81+
const [selectedIdx, setSelectedIdx] = useState(defaultSelectedIdx);
82+
83+
useEffect(() => {
84+
setSelectedIdx(defaultSelectedIdx);
85+
}, [defaultSelectedIdx]);
86+
87+
const handleSelectLine = useCallback(
88+
(_line: number, indexInLines: number) => {
89+
setSelectedIdx(indexInLines);
90+
},
91+
[],
92+
);
93+
94+
const selectedOutcome = useMemo(
95+
() => outcomes[lineIndices[selectedIdx]] ?? outcomes[0],
96+
[outcomes, lineIndices, selectedIdx],
97+
);
98+
99+
if (!selectedOutcome) return null;
100+
101+
return (
102+
<PredictSportOutcomeCard
103+
title={title ?? formatOutcomeCardTitle(selectedOutcome)}
104+
subtitle={buildSubtitle(selectedOutcome)}
105+
buttons={buildButtons(
106+
selectedOutcome,
107+
onBuyPress,
108+
game,
109+
sportsMarketType,
110+
)}
111+
lines={lines}
112+
selectedLine={lines[selectedIdx]}
113+
selectedIndex={selectedIdx}
114+
onSelectLine={handleSelectLine}
115+
testID={testID}
116+
/>
117+
);
118+
},
119+
);
120+
121+
LineOutcomeCard.displayName = 'LineOutcomeCard';
122+
123+
const MoneylineCard = memo(
124+
({
125+
outcomes,
126+
onBuyPress,
127+
game,
128+
title,
129+
testID,
130+
}: {
131+
outcomes: PredictOutcomeGroup['outcomes'];
132+
onBuyPress: BuyHandler;
133+
game?: PredictMarketGame;
134+
title: string;
135+
testID: string;
136+
}) => {
137+
const subtitle = useMemo(
138+
() => buildMoneylineSubtitle(outcomes),
139+
[outcomes],
140+
);
141+
const tokenIds = useMemo(
142+
() =>
143+
sortMoneylineOutcomes(outcomes, game)
144+
.map((outcome) => outcome.tokens[0]?.id)
145+
.filter((id): id is string => Boolean(id)),
146+
[outcomes, game],
147+
);
148+
const { getPrice } = useLiveMarketPrices(tokenIds);
149+
const buttons = useMemo(
150+
() => buildMoneylineButtons(outcomes, onBuyPress, game, getPrice),
151+
[outcomes, onBuyPress, game, getPrice],
152+
);
153+
154+
return (
155+
<PredictSportOutcomeCard
156+
title={title}
157+
subtitle={subtitle}
158+
buttons={buttons}
159+
buttonLayout="inlineNoSeparator"
160+
testID={testID}
161+
/>
162+
);
163+
},
164+
);
165+
166+
MoneylineCard.displayName = 'MoneylineCard';
167+
168+
const SubgroupCards = memo(
169+
({
170+
subgroup,
171+
onBuyPress,
172+
game,
173+
groupKey,
174+
index,
175+
}: {
176+
subgroup: PredictOutcomeGroup;
177+
onBuyPress: BuyHandler;
178+
game?: PredictMarketGame;
179+
groupKey: string;
180+
index: number;
181+
}) => {
182+
const translatedTitle = getTranslatedSportsMarketTypeLabel(subgroup.key);
183+
const firstOutcomeTitle = subgroup.outcomes[0]
184+
? formatOutcomeCardTitle(subgroup.outcomes[0])
185+
: undefined;
186+
const title = getFallbackSportsMarketTypeLabel(
187+
subgroup.key,
188+
translatedTitle ?? firstOutcomeTitle,
189+
);
190+
const testID = `${groupKey}-${subgroup.key}-${index}`;
191+
192+
if (
193+
isMoneylineLikeMarketType(subgroup.key) &&
194+
subgroup.outcomes.length > 1
195+
) {
196+
return (
197+
<MoneylineCard
198+
outcomes={subgroup.outcomes}
199+
onBuyPress={onBuyPress}
200+
game={game}
201+
title={title}
202+
testID={testID}
203+
/>
204+
);
205+
}
206+
207+
if (subgroup.outcomes.length === 1) {
208+
return (
209+
<SimpleOutcomeCard
210+
outcome={subgroup.outcomes[0]}
211+
title={title}
212+
onBuyPress={onBuyPress}
213+
game={game}
214+
sportsMarketType={subgroup.key}
215+
testID={testID}
216+
/>
217+
);
218+
}
219+
220+
return (
221+
<LineOutcomeCard
222+
outcomes={subgroup.outcomes}
223+
title={translatedTitle}
224+
onBuyPress={onBuyPress}
225+
game={game}
226+
sportsMarketType={subgroup.key}
227+
testID={testID}
228+
/>
229+
);
230+
},
231+
);
232+
233+
SubgroupCards.displayName = 'SubgroupCards';
234+
235+
export const OutcomesContent = memo(
236+
({
237+
group,
238+
onBuyPress,
239+
game,
240+
}: {
241+
group: PredictOutcomeGroup;
242+
onBuyPress: BuyHandler;
243+
game?: PredictMarketGame;
244+
}) => {
245+
if (group.subgroups && group.subgroups.length > 0) {
246+
return (
247+
<>
248+
{group.subgroups.map((subgroup, index) => (
249+
<SubgroupCards
250+
key={subgroup.key}
251+
subgroup={subgroup}
252+
onBuyPress={onBuyPress}
253+
game={game}
254+
groupKey={group.key}
255+
index={index}
256+
/>
257+
))}
258+
</>
259+
);
260+
}
261+
262+
const firstType = group.outcomes[0]?.sportsMarketType;
263+
if (
264+
firstType &&
265+
isMoneylineLikeMarketType(firstType) &&
266+
group.outcomes.length > 1
267+
) {
268+
return (
269+
<MoneylineCard
270+
outcomes={group.outcomes}
271+
onBuyPress={onBuyPress}
272+
game={game}
273+
title={getSportsMarketTypeLabel(
274+
firstType,
275+
formatOutcomeCardTitle(group.outcomes[0]),
276+
)}
277+
testID={`${group.key}-moneyline`}
278+
/>
279+
);
280+
}
281+
282+
return (
283+
<>
284+
{group.outcomes.map((outcome, index) => (
285+
<SimpleOutcomeCard
286+
key={outcome.id}
287+
outcome={outcome}
288+
title={formatOutcomeCardTitle(outcome)}
289+
onBuyPress={onBuyPress}
290+
game={game}
291+
sportsMarketType={outcome.sportsMarketType}
292+
testID={`${group.key}-outcome-${index}`}
293+
/>
294+
))}
295+
</>
296+
);
297+
},
298+
);
299+
300+
OutcomesContent.displayName = 'OutcomesContent';

0 commit comments

Comments
 (0)