Skip to content

Commit 44dccc3

Browse files
authored
feat(predict): add feature flag and data types for Live NFL sports (MetaMask#24454)
## **Description** Adds the foundational feature flag and TypeScript types for the Predict Live NFL feature. This is Task 00 from the Live NFL implementation plan and unblocks all subsequent development work. **Reason for change:** The Live NFL feature requires new data structures to represent game state (teams, scores, periods, game status) and a feature flag to gate the rollout. **Solution:** - Added `selectPredictLiveNflEnabled` feature flag selector with remote config and local env fallback (`MM_PREDICT_LIVE_NFL_ENABLED`) - Added game-related TypeScript types: `PredictSportsLeague`, `PredictGameStatus`, `PredictSportTeam`, `PredictMarketGame`, `GameUpdate`, `PriceUpdate` - Extended `PredictMarket` type with optional `game` property - Updated navigation params with `isGame` flag - Added Polymarket API types for team and game event data ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: N/A (internal infrastructure task) ## **Manual testing steps** ```gherkin Feature: Live NFL Feature Flag and Types Scenario: Feature flag returns true when remote flag is enabled Given the remote feature flag predictLiveNflEnabled is set to enabled with valid version When selectPredictLiveNflEnabled selector is called Then it returns true Scenario: Feature flag falls back to local env var Given the remote feature flag is not configured And MM_PREDICT_LIVE_NFL_ENABLED environment variable is set to "true" When selectPredictLiveNflEnabled selector is called Then it returns true Scenario: Types compile without errors Given the new types are added to the codebase When TypeScript compilation runs Then no type errors are reported ``` ## **Screenshots/Recordings** ### **Before** N/A - Infrastructure task (no UI changes) ### **After** N/A - Infrastructure task (no UI changes) **Test Results:** ``` PASS app/components/UI/Predict/selectors/featureFlags/index.test.ts Predict Feature Flag Selectors selectPredictLiveNflEnabled ✓ returns true for enabled version-gated flag with valid version remote flag precedence ✓ returns true when remote flag enabled overrides local flag false ✓ returns false when remote flag disabled overrides local flag true ✓ returns false when app version below minimum required version local flag fallback ✓ falls back to local flag when remote flag is invalid ✓ returns false from local flag when remote flag is null ✓ falls back to local flag when remote feature flags are empty ✓ returns false from local flag when controller is undefined Test Suites: 1 passed, 1 total Tests: 27 passed, 27 total ``` ## **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 - [ ] 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] > Establishes the foundation for Live NFL in Predict with a version-gated feature flag and new game data structures. > > - Adds `selectPredictLiveNflEnabled` selector (remote `predictLiveNflEnabled` with env fallback `MM_PREDICT_LIVE_NFL_ENABLED`) and comprehensive tests > - Extends Predict types: `PredictSportsLeague`, `PredictGameStatus`, `PredictSportTeam`, `PredictMarketGame`, `GameUpdate`, `PriceUpdate`; adds optional `game` to `PredictMarket` > - Updates navigation params: `PredictMarketDetails` now supports `isGame` > - Expands Polymarket API types with `PolymarketApiTeam` and `PolymarketApiGameEvent` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 1592a1c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 032f7ad commit 44dccc3

5 files changed

Lines changed: 260 additions & 1 deletion

File tree

app/components/UI/Predict/providers/polymarket/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,23 @@ export interface OrderBook {
342342
tick_size: string;
343343
neg_risk: boolean;
344344
}
345+
346+
export interface PolymarketApiTeam {
347+
id: string;
348+
name: string;
349+
logo: string;
350+
abbreviation: string;
351+
color: string;
352+
alias: string;
353+
}
354+
355+
export interface PolymarketApiGameEvent {
356+
gameId?: string;
357+
startTime?: string;
358+
score?: string;
359+
elapsed?: string;
360+
period?: string;
361+
live?: boolean;
362+
ended?: boolean;
363+
closed?: boolean;
364+
}

app/components/UI/Predict/selectors/featureFlags/index.test.ts

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { selectPredictEnabledFlag } from '.';
1+
import { selectPredictEnabledFlag, selectPredictLiveNflEnabled } from '.';
22
import mockedEngine from '../../../../../core/__mocks__/MockedEngine';
33
import {
44
mockedState,
@@ -32,6 +32,7 @@ describe('Predict Feature Flag Selectors', () => {
3232
beforeEach(() => {
3333
jest.clearAllMocks();
3434
delete process.env.MM_PREDICT_ENABLED;
35+
delete process.env.MM_PREDICT_LIVE_NFL_ENABLED;
3536
mockHasMinimumRequiredVersion = jest.spyOn(
3637
remoteFeatureFlagModule,
3738
'hasMinimumRequiredVersion',
@@ -41,6 +42,7 @@ describe('Predict Feature Flag Selectors', () => {
4142

4243
afterEach(() => {
4344
delete process.env.MM_PREDICT_ENABLED;
45+
delete process.env.MM_PREDICT_LIVE_NFL_ENABLED;
4446
mockHasMinimumRequiredVersion?.mockRestore();
4547
});
4648

@@ -193,6 +195,174 @@ describe('Predict Feature Flag Selectors', () => {
193195
});
194196
});
195197

198+
describe('selectPredictLiveNflEnabled', () => {
199+
it('returns true for enabled version-gated flag with valid version', () => {
200+
mockHasMinimumRequiredVersion.mockReturnValue(true);
201+
const stateWithEnabledRemoteFlag = {
202+
engine: {
203+
backgroundState: {
204+
RemoteFeatureFlagController: {
205+
remoteFeatureFlags: {
206+
predictLiveNflEnabled: {
207+
enabled: true,
208+
minimumVersion: '1.0.0',
209+
},
210+
},
211+
cacheTimestamp: 0,
212+
},
213+
},
214+
},
215+
};
216+
217+
const result = selectPredictLiveNflEnabled(stateWithEnabledRemoteFlag);
218+
219+
expect(result).toBe(true);
220+
});
221+
222+
describe('remote flag precedence', () => {
223+
it('returns true when remote flag enabled overrides local flag false', () => {
224+
mockHasMinimumRequiredVersion.mockReturnValue(true);
225+
process.env.MM_PREDICT_LIVE_NFL_ENABLED = 'false';
226+
const stateWithEnabledRemoteFlag = {
227+
engine: {
228+
backgroundState: {
229+
RemoteFeatureFlagController: {
230+
remoteFeatureFlags: {
231+
predictLiveNflEnabled: {
232+
enabled: true,
233+
minimumVersion: '1.0.0',
234+
},
235+
},
236+
cacheTimestamp: 0,
237+
},
238+
},
239+
},
240+
};
241+
242+
const result = selectPredictLiveNflEnabled(stateWithEnabledRemoteFlag);
243+
244+
expect(result).toBe(true);
245+
});
246+
247+
it('returns false when remote flag disabled overrides local flag true', () => {
248+
mockHasMinimumRequiredVersion.mockReturnValue(true);
249+
process.env.MM_PREDICT_LIVE_NFL_ENABLED = 'true';
250+
const stateWithDisabledRemoteFlag = {
251+
engine: {
252+
backgroundState: {
253+
RemoteFeatureFlagController: {
254+
remoteFeatureFlags: {
255+
predictLiveNflEnabled: {
256+
enabled: false,
257+
minimumVersion: '1.0.0',
258+
},
259+
},
260+
cacheTimestamp: 0,
261+
},
262+
},
263+
},
264+
};
265+
266+
const result = selectPredictLiveNflEnabled(stateWithDisabledRemoteFlag);
267+
268+
expect(result).toBe(false);
269+
});
270+
271+
it('returns false when app version below minimum required version', () => {
272+
mockHasMinimumRequiredVersion.mockReturnValue(false);
273+
process.env.MM_PREDICT_LIVE_NFL_ENABLED = 'true';
274+
const stateWithVersionCheckFailure = {
275+
engine: {
276+
backgroundState: {
277+
RemoteFeatureFlagController: {
278+
remoteFeatureFlags: {
279+
predictLiveNflEnabled: {
280+
enabled: true,
281+
minimumVersion: '99.0.0',
282+
},
283+
},
284+
cacheTimestamp: 0,
285+
},
286+
},
287+
},
288+
};
289+
290+
const result = selectPredictLiveNflEnabled(
291+
stateWithVersionCheckFailure,
292+
);
293+
294+
expect(result).toBe(false);
295+
});
296+
});
297+
298+
describe('local flag fallback', () => {
299+
it('falls back to local flag when remote flag is invalid', () => {
300+
const stateWithInvalidRemoteFlag = {
301+
engine: {
302+
backgroundState: {
303+
RemoteFeatureFlagController: {
304+
remoteFeatureFlags: {
305+
predictLiveNflEnabled: {
306+
enabled: 'invalid',
307+
minimumVersion: 123,
308+
},
309+
},
310+
cacheTimestamp: 0,
311+
},
312+
},
313+
},
314+
};
315+
316+
const result = selectPredictLiveNflEnabled(stateWithInvalidRemoteFlag);
317+
318+
expect(result).toBe(false);
319+
});
320+
321+
it('returns false from local flag when remote flag is null', () => {
322+
process.env.MM_PREDICT_LIVE_NFL_ENABLED = 'false';
323+
const stateWithNullRemoteFlag = {
324+
engine: {
325+
backgroundState: {
326+
RemoteFeatureFlagController: {
327+
remoteFeatureFlags: {
328+
predictLiveNflEnabled: null,
329+
},
330+
cacheTimestamp: 0,
331+
},
332+
},
333+
},
334+
};
335+
336+
const result = selectPredictLiveNflEnabled(stateWithNullRemoteFlag);
337+
338+
expect(result).toBe(false);
339+
});
340+
341+
it('falls back to local flag when remote feature flags are empty', () => {
342+
const result = selectPredictLiveNflEnabled(mockedEmptyFlagsState);
343+
344+
expect(result).toBe(false);
345+
});
346+
347+
it('returns false from local flag when controller is undefined', () => {
348+
process.env.MM_PREDICT_LIVE_NFL_ENABLED = 'false';
349+
const stateWithUndefinedController = {
350+
engine: {
351+
backgroundState: {
352+
RemoteFeatureFlagController: undefined,
353+
},
354+
},
355+
};
356+
357+
const result = selectPredictLiveNflEnabled(
358+
stateWithUndefinedController,
359+
);
360+
361+
expect(result).toBe(false);
362+
});
363+
});
364+
});
365+
196366
describe('predictTradingEnabled remote feature flag validation', () => {
197367
const validRemoteFlag: VersionGatedFeatureFlag = {
198368
enabled: true,

app/components/UI/Predict/selectors/featureFlags/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,23 @@ export const selectPredictGtmOnboardingModalEnabledFlag = createSelector(
3939
return validatedVersionGatedFeatureFlag(remoteFlag) ?? localFlag;
4040
},
4141
);
42+
43+
/**
44+
* Selector for Predict Live NFL feature enablement
45+
*
46+
* Uses version-gated feature flag `predictLiveNflEnabled` from remote config.
47+
* Falls back to local environment variable MM_PREDICT_LIVE_NFL_ENABLED if remote flag
48+
* is unavailable or invalid.
49+
*
50+
* @returns {boolean} True if feature is enabled and version requirement is met
51+
*/
52+
export const selectPredictLiveNflEnabled = createSelector(
53+
selectRemoteFeatureFlags,
54+
(remoteFeatureFlags) => {
55+
const localFlag = process.env.MM_PREDICT_LIVE_NFL_ENABLED === 'true';
56+
const remoteFlag =
57+
remoteFeatureFlags?.predictLiveNflEnabled as unknown as VersionGatedFeatureFlag;
58+
59+
return validatedVersionGatedFeatureFlag(remoteFlag) ?? localFlag;
60+
},
61+
);

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export type PredictMarket = {
9898
outcomes: PredictOutcome[];
9999
liquidity: number;
100100
volume: number;
101+
game?: PredictMarketGame;
101102
};
102103

103104
export type PredictSeries = {
@@ -111,6 +112,53 @@ export type PredictCategory =
111112
| 'crypto'
112113
| 'politics';
113114

115+
// Sports league types
116+
export type PredictSportsLeague = 'nfl';
117+
118+
// Game status
119+
export type PredictGameStatus = 'scheduled' | 'ongoing' | 'ended';
120+
121+
// Team data
122+
export interface PredictSportTeam {
123+
id: string;
124+
name: string;
125+
logo: string;
126+
abbreviation: string; // e.g., "SEA", "DEN"
127+
color: string; // Team primary color (hex)
128+
alias: string; // Team alias (e.g., "Seahawks")
129+
}
130+
131+
// Game data attached to market
132+
export interface PredictMarketGame {
133+
id: string;
134+
startTime: string;
135+
status: PredictGameStatus;
136+
league: PredictSportsLeague;
137+
elapsed: string; // Game clock
138+
period: string; // Current period (Q1, Q2, HT, Q3, Q4, OT, FT)
139+
score: string; // "away-home" format (e.g., "21-14")
140+
homeTeam: PredictSportTeam;
141+
awayTeam: PredictSportTeam;
142+
turn?: string; // Team abbreviation with possession
143+
}
144+
145+
// Live update types for WebSocket data
146+
export interface GameUpdate {
147+
gameId: string;
148+
score: string;
149+
elapsed: string;
150+
period: string;
151+
status: PredictGameStatus;
152+
turn?: string;
153+
}
154+
155+
export interface PriceUpdate {
156+
tokenId: string;
157+
price: number;
158+
bestBid: number;
159+
bestAsk: number;
160+
}
161+
114162
export type PredictOutcome = {
115163
id: string;
116164
providerId: string;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface PredictNavigationParamList extends ParamListBase {
3030
entryPoint?: PredictEntryPoint;
3131
title?: string;
3232
image?: string;
33+
isGame?: boolean;
3334
};
3435
PredictSellPreview: {
3536
market: PredictMarket;

0 commit comments

Comments
 (0)