Skip to content

Commit 0fcafbb

Browse files
chore(runway): cherry-pick feat(predict): cp-7.62.0 improve PredictGameChart with market prop and smart timeframe defaults (#24896)
- feat(predict): cp-7.62.0 improve PredictGameChart with market prop and smart timeframe defaults (#24858) ## **Description** This PR improves the PredictGameChart component for live NFL games by: 1. **Refactoring to accept a `market` prop** - Instead of requiring `tokenIds` and `seriesConfig` to be passed separately, the component now derives these internally from the market object, simplifying the API. 2. **Adding smart timeframe defaults based on game status**: - `ongoing` games → Default to `'live'` timeframe with live price updates - `scheduled` games → Default to `'6h'` timeframe (no live updates) - `ended` games → Default to `'max'` timeframe with disabled selector, using timestamp-based queries 3. **Supporting time-range price history queries** - For ended games, the chart now queries price history using `startTs`/`endTs` (game start/end timestamps) instead of interval-based queries, with 2-minute fidelity for detailed game period visualization. 4. **Showing disabled timeframe selector for ended games** - Instead of hiding the selector entirely, it's now shown but disabled with "Max" selected, providing better UX feedback. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: N/A (internal improvement) ## **Manual testing steps** ```gherkin Feature: PredictGameChart smart timeframe defaults Scenario: User views chart for ongoing game Given user is on a game details page for an ongoing NFL game When the page loads Then the chart defaults to "Live" timeframe And the timeframe selector buttons are enabled Scenario: User views chart for scheduled game Given user is on a game details page for a scheduled NFL game When the page loads Then the chart defaults to "6H" timeframe And the timeframe selector buttons are enabled Scenario: User views chart for ended game Given user is on a game details page for a completed NFL game When the page loads Then the chart defaults to "Max" timeframe And the timeframe selector buttons are visible but disabled And the chart shows the full game period with detailed price history ``` ## **Screenshots/Recordings** ### **Before** N/A - Internal refactor ### **After** N/A - Internal refactor ## **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](https://jsdoc.app/) 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Modernizes the game chart API and behavior, and wires backend support for time-range queries. > > - Refactors `PredictGameChart` to accept `market` and derive `tokenIds`/`seriesConfig`; adds `'1h'` timeframe and smart defaults: `ongoing` → `live`, `scheduled` → `6h`, `ended` → `max` > - Disables live updates and the timeframe selector for ended games; passes `disabledTimeframeSelector` through `PredictGameChartContent` > - For ended games, fetches price history via `startTs/endTs` (game start/end) with 2-minute fidelity; otherwise uses interval-based fetching > - Extends `usePredictPriceHistory` and `PolymarketProvider.getPriceHistory` to accept `startTs/endTs`; updates logging and dependencies > - Updates types: add `PredictMarketGame.endTime`, Polymarket API `finishedTimestamp`; maps to `endTime` in `gameParser` > - Simplifies `PredictGameDetailsContent` to render chart via `market` prop > - Comprehensive test updates and snapshot adjustments for new props, defaults, and disabled selector > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3fb3ae2. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [cb1b076](cb1b076) Co-authored-by: Luis Taniça <matallui@gmail.com>
1 parent c15e817 commit 0fcafbb

12 files changed

Lines changed: 355 additions & 285 deletions

File tree

app/components/UI/Predict/components/PredictGameChart/PredictGameChart.tsx

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React, {
55
useCallback,
66
useRef,
77
} from 'react';
8-
import { PredictPriceHistoryInterval } from '../../types';
8+
import { PredictGameStatus, PredictPriceHistoryInterval } from '../../types';
99
import { usePredictPriceHistory } from '../../hooks/usePredictPriceHistory';
1010
import { useLiveMarketPrices } from '../../hooks/useLiveMarketPrices';
1111
import PredictGameChartContent from './PredictGameChartContent';
@@ -14,54 +14,111 @@ import {
1414
GameChartSeries,
1515
GameChartDataPoint,
1616
ChartTimeframe,
17+
GameChartSeriesConfig,
1718
} from './PredictGameChart.types';
1819

1920
const TIMEFRAME_TO_INTERVAL: Record<
20-
ChartTimeframe,
21+
Exclude<ChartTimeframe, 'live'>,
2122
PredictPriceHistoryInterval
2223
> = {
23-
live: PredictPriceHistoryInterval.ONE_HOUR,
24+
'1h': PredictPriceHistoryInterval.ONE_HOUR,
2425
'6h': PredictPriceHistoryInterval.SIX_HOUR,
2526
'1d': PredictPriceHistoryInterval.ONE_DAY,
2627
max: PredictPriceHistoryInterval.MAX,
2728
};
2829

2930
const FIDELITY_BY_TIMEFRAME: Record<ChartTimeframe, number> = {
3031
live: 1,
32+
'1h': 1,
3133
'6h': 5,
3234
'1d': 15,
3335
max: 60,
3436
};
3537

38+
const ENDED_GAME_FIDELITY = 2;
39+
3640
const getMinuteTimestamp = (timestamp: number): number =>
3741
Math.floor(timestamp / 60000) * 60000;
3842

39-
/**
40-
* Converts a timestamp to milliseconds.
41-
* Detects if timestamp is in seconds (10 digits) or milliseconds (13 digits).
42-
* Unix timestamps in seconds are typically < 10 billion (until year 2286).
43-
*/
4443
const toMilliseconds = (timestamp: number): number =>
4544
timestamp < 10_000_000_000 ? timestamp * 1000 : timestamp;
4645

46+
const toUnixSeconds = (isoString: string): number =>
47+
Math.floor(new Date(isoString).getTime() / 1000);
48+
49+
const getDefaultTimeframe = (
50+
gameStatus: PredictGameStatus | undefined,
51+
): ChartTimeframe => {
52+
switch (gameStatus) {
53+
case 'ongoing':
54+
return 'live';
55+
case 'scheduled':
56+
return '6h';
57+
case 'ended':
58+
return 'max';
59+
default:
60+
return '6h';
61+
}
62+
};
63+
4764
const PredictGameChart: React.FC<PredictGameChartProps> = ({
48-
tokenIds,
49-
seriesConfig,
65+
market,
5066
providerId = 'polymarket',
5167
testID,
5268
}) => {
53-
const [timeframe, setTimeframe] = useState<ChartTimeframe>('live');
69+
const game = market.game;
70+
const gameStatus = game?.status;
71+
const isGameEnded = gameStatus === 'ended';
72+
const isGameOngoing = gameStatus === 'ongoing';
73+
74+
const tokenIds = useMemo(() => {
75+
const tokens = market.outcomes[0]?.tokens ?? [];
76+
return tokens.map((t) => t.id);
77+
}, [market.outcomes]);
78+
79+
const seriesConfig: [GameChartSeriesConfig, GameChartSeriesConfig] | null =
80+
useMemo(() => {
81+
if (!game) return null;
82+
return [
83+
{ label: game.awayTeam.abbreviation, color: game.awayTeam.color },
84+
{ label: game.homeTeam.abbreviation, color: game.homeTeam.color },
85+
];
86+
}, [game]);
87+
88+
const [timeframe, setTimeframe] = useState<ChartTimeframe>(() =>
89+
getDefaultTimeframe(gameStatus),
90+
);
5491
const [liveChartData, setLiveChartData] = useState<GameChartSeries[]>([]);
5592
const initialDataLoadedRef = useRef<boolean>(false);
5693

57-
const isLive = timeframe === 'live';
58-
const interval = TIMEFRAME_TO_INTERVAL[timeframe];
59-
const fidelity = FIDELITY_BY_TIMEFRAME[timeframe];
94+
const isLive = timeframe === 'live' && isGameOngoing;
95+
const disabledTimeframeSelector = isGameEnded;
96+
97+
const { startTs, endTs } = useMemo(() => {
98+
if (!isGameEnded || !game?.startTime) {
99+
return { startTs: undefined, endTs: undefined };
100+
}
101+
const start = toUnixSeconds(game.startTime);
102+
const end = game.endTime ? toUnixSeconds(game.endTime) : undefined;
103+
return { startTs: start, endTs: end };
104+
}, [isGameEnded, game?.startTime, game?.endTime]);
105+
106+
const interval = useMemo(() => {
107+
if (isGameEnded) return undefined;
108+
if (timeframe === 'live') return PredictPriceHistoryInterval.ONE_HOUR;
109+
return TIMEFRAME_TO_INTERVAL[timeframe];
110+
}, [isGameEnded, timeframe]);
111+
112+
const fidelity = isGameEnded
113+
? ENDED_GAME_FIDELITY
114+
: FIDELITY_BY_TIMEFRAME[timeframe];
60115

61116
const { priceHistories, isFetching, errors, refetch } =
62117
usePredictPriceHistory({
63118
marketIds: tokenIds,
64119
interval,
120+
startTs,
121+
endTs,
65122
fidelity,
66123
providerId,
67124
enabled: tokenIds.length === 2,
@@ -72,7 +129,7 @@ const PredictGameChart: React.FC<PredictGameChartProps> = ({
72129
});
73130

74131
const historicalChartData: GameChartSeries[] = useMemo(() => {
75-
if (priceHistories.length < 2) return [];
132+
if (priceHistories.length < 2 || !seriesConfig) return [];
76133

77134
return tokenIds.map((_tokenId, index) => {
78135
const history = priceHistories[index] ?? [];
@@ -189,6 +246,10 @@ const PredictGameChart: React.FC<PredictGameChartProps> = ({
189246
? (errors.find((error) => error !== null) ?? null)
190247
: null;
191248

249+
if (!game || !seriesConfig) {
250+
return null;
251+
}
252+
192253
return (
193254
<PredictGameChartContent
194255
data={chartData}
@@ -197,6 +258,7 @@ const PredictGameChart: React.FC<PredictGameChartProps> = ({
197258
onRetry={handleRetry}
198259
timeframe={timeframe}
199260
onTimeframeChange={handleTimeframeChange}
261+
disabledTimeframeSelector={disabledTimeframeSelector}
200262
testID={testID}
201263
/>
202264
);

app/components/UI/Predict/components/PredictGameChart/PredictGameChart.types.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
1-
/**
2-
* Data point for game chart series
3-
*/
1+
import { PredictMarket } from '../../types';
2+
43
export interface GameChartDataPoint {
54
timestamp: number;
65
value: number;
76
}
87

9-
/**
10-
* Series configuration for game chart
11-
*/
128
export interface GameChartSeries {
139
label: string;
1410
color: string;
1511
data: GameChartDataPoint[];
1612
}
1713

18-
/**
19-
* Available chart timeframes
20-
*/
21-
export type ChartTimeframe = 'live' | '6h' | '1d' | 'max';
14+
export type ChartTimeframe = 'live' | '1h' | '6h' | '1d' | 'max';
2215

2316
export interface GameChartSeriesConfig {
2417
label: string;
@@ -32,12 +25,12 @@ export interface PredictGameChartContentProps {
3225
onRetry?: () => void;
3326
timeframe?: ChartTimeframe;
3427
onTimeframeChange?: (timeframe: ChartTimeframe) => void;
28+
disabledTimeframeSelector?: boolean;
3529
testID?: string;
3630
}
3731

3832
export interface PredictGameChartProps {
39-
tokenIds: [string, string];
40-
seriesConfig: [GameChartSeriesConfig, GameChartSeriesConfig];
33+
market: PredictMarket;
4134
providerId?: string;
4235
testID?: string;
4336
}

0 commit comments

Comments
 (0)