Skip to content

Commit 9d82768

Browse files
authored
feat(predict): add version gating to market highlights and filter closed markets (#25680)
## **Description** Enhances the `PredictMarketHighlightsFlag` feature with version gating and closed market filtering: 1. **Version Gating**: `PredictMarketHighlightsFlag` now extends `VersionGatedFeatureFlag`, adding `minimumVersion` support. Highlights are only fetched when the user's app version meets the minimum requirement (7.64.0). 2. **Closed Market Filtering**: Highlighted markets that are closed or resolved are now filtered out, ensuring only open markets are displayed at the top of category lists. ## **Changelog** CHANGELOG entry: null ## **Related issues** N/A - Internal improvement ## **Manual testing steps** ```gherkin Feature: Predict Market Highlights Scenario: User views category with highlighted markets Given the user is on the Predict tab And the remote feature flag has highlights configured for a category And the user's app version meets the minimum version requirement When user navigates to that category Then highlighted markets appear at the top of the list And only open markets are shown in the highlights Scenario: Highlights filtered when version requirement not met Given the user is on the Predict tab And the remote feature flag has minimumVersion set to a higher version When user navigates to a category with highlights Then no highlighted markets are prepended And only regular market list is shown Scenario: Closed markets are not highlighted Given a highlighted market has been closed or resolved When user navigates to that category Then that market is not shown in the highlights section ``` ## **Screenshots/Recordings** N/A - Backend logic change, no UI changes ## **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.
1 parent ad6e853 commit 9d82768

5 files changed

Lines changed: 173 additions & 14 deletions

File tree

app/components/UI/Predict/constants/flags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const DEFAULT_LIVE_SPORTS_FLAG: PredictLiveSportsFlag = {
2424
export const DEFAULT_MARKET_HIGHLIGHTS_FLAG: PredictMarketHighlightsFlag = {
2525
enabled: false,
2626
highlights: [],
27+
minimumVersion: '7.64.0',
2728
};
2829

2930
export const DEFAULT_HOT_TAB_FLAG: PredictHotTabFlag = {

app/components/UI/Predict/controllers/PredictController.test.ts

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const DEFAULT_REMOTE_FEATURE_FLAG_STATE = {
5959
},
6060
predictMarketHighlights: {
6161
enabled: false,
62+
minimumVersion: '0.0.0',
6263
highlights: [],
6364
},
6465
},
@@ -71,6 +72,11 @@ const DEFAULT_NETWORK_CLIENT = {
7172
},
7273
};
7374

75+
// Mock react-native-device-info for version gating
76+
jest.mock('react-native-device-info', () => ({
77+
getVersion: jest.fn().mockReturnValue('99.0.0'),
78+
}));
79+
7480
// Mock DevLogger (default export)
7581
jest.mock('../../../../core/SDKConnect/utils/DevLogger', () => ({
7682
__esModule: true,
@@ -1811,11 +1817,16 @@ describe('PredictController', () => {
18111817
title: `Market ${id}`,
18121818
category,
18131819
outcomes: ['YES', 'NO'],
1820+
status: 'open',
18141821
});
18151822

18161823
const createFlagState = (flag: {
18171824
enabled: boolean;
1818-
highlights: { category: string; markets: string[] }[];
1825+
minimumVersion?: string;
1826+
highlights: {
1827+
category: string;
1828+
markets: string[];
1829+
}[];
18191830
}) => ({
18201831
remoteFeatureFlags: {
18211832
predictFeeCollection: {
@@ -1829,7 +1840,10 @@ describe('PredictController', () => {
18291840
enabled: false,
18301841
leagues: [],
18311842
},
1832-
predictMarketHighlights: flag,
1843+
predictMarketHighlights: {
1844+
...flag,
1845+
minimumVersion: flag.minimumVersion ?? '0.0.0',
1846+
},
18331847
},
18341848
cacheTimestamp: Date.now(),
18351849
});
@@ -2335,6 +2349,137 @@ describe('PredictController', () => {
23352349
},
23362350
);
23372351
});
2352+
2353+
it('filters out closed highlighted markets', async () => {
2354+
const regularMarkets = [createMockMarket('regular-1')];
2355+
const closedHighlightedMarket = {
2356+
...createMockMarket('highlight-1'),
2357+
status: 'closed',
2358+
};
2359+
const openHighlightedMarket = {
2360+
...createMockMarket('highlight-2'),
2361+
status: 'open',
2362+
};
2363+
2364+
await withController(
2365+
async ({ controller }) => {
2366+
mockPolymarketProvider.getMarkets.mockResolvedValue(
2367+
regularMarkets as any,
2368+
);
2369+
mockPolymarketProvider.getMarketsByIds.mockResolvedValue([
2370+
closedHighlightedMarket,
2371+
openHighlightedMarket,
2372+
] as any);
2373+
2374+
const result = await controller.getMarkets({
2375+
providerId: 'polymarket',
2376+
category: 'trending',
2377+
offset: 0,
2378+
});
2379+
2380+
expect(result).toHaveLength(2);
2381+
expect(result[0].id).toBe('highlight-2');
2382+
expect(result[1].id).toBe('regular-1');
2383+
expect(result.find((m) => m.id === 'highlight-1')).toBeUndefined();
2384+
},
2385+
{
2386+
mocks: {
2387+
getRemoteFeatureFlagState: jest.fn().mockReturnValue(
2388+
createFlagState({
2389+
enabled: true,
2390+
highlights: [
2391+
{
2392+
category: 'trending',
2393+
markets: ['highlight-1', 'highlight-2'],
2394+
},
2395+
],
2396+
}),
2397+
),
2398+
},
2399+
},
2400+
);
2401+
});
2402+
2403+
it('filters out resolved highlighted markets', async () => {
2404+
const regularMarkets = [createMockMarket('regular-1')];
2405+
const resolvedHighlightedMarket = {
2406+
...createMockMarket('highlight-1'),
2407+
status: 'resolved',
2408+
};
2409+
2410+
await withController(
2411+
async ({ controller }) => {
2412+
mockPolymarketProvider.getMarkets.mockResolvedValue(
2413+
regularMarkets as any,
2414+
);
2415+
mockPolymarketProvider.getMarketsByIds.mockResolvedValue([
2416+
resolvedHighlightedMarket,
2417+
] as any);
2418+
2419+
const result = await controller.getMarkets({
2420+
providerId: 'polymarket',
2421+
category: 'trending',
2422+
offset: 0,
2423+
});
2424+
2425+
expect(result).toHaveLength(1);
2426+
expect(result[0].id).toBe('regular-1');
2427+
},
2428+
{
2429+
mocks: {
2430+
getRemoteFeatureFlagState: jest.fn().mockReturnValue(
2431+
createFlagState({
2432+
enabled: true,
2433+
highlights: [
2434+
{
2435+
category: 'trending',
2436+
markets: ['highlight-1'],
2437+
},
2438+
],
2439+
}),
2440+
),
2441+
},
2442+
},
2443+
);
2444+
});
2445+
2446+
it('skips highlights when version requirement not met', async () => {
2447+
const regularMarkets = [createMockMarket('regular-1')];
2448+
2449+
await withController(
2450+
async ({ controller }) => {
2451+
mockPolymarketProvider.getMarkets.mockResolvedValue(
2452+
regularMarkets as any,
2453+
);
2454+
2455+
const result = await controller.getMarkets({
2456+
providerId: 'polymarket',
2457+
category: 'trending',
2458+
offset: 0,
2459+
});
2460+
2461+
expect(result).toHaveLength(1);
2462+
expect(result[0].id).toBe('regular-1');
2463+
expect(mockPolymarketProvider.getMarketsByIds).not.toHaveBeenCalled();
2464+
},
2465+
{
2466+
mocks: {
2467+
getRemoteFeatureFlagState: jest.fn().mockReturnValue(
2468+
createFlagState({
2469+
enabled: true,
2470+
minimumVersion: '999.0.0',
2471+
highlights: [
2472+
{
2473+
category: 'trending',
2474+
markets: ['highlight-1'],
2475+
},
2476+
],
2477+
}),
2478+
),
2479+
},
2480+
},
2481+
);
2482+
});
23382483
});
23392484

23402485
describe('updateStateForTesting', () => {

app/components/UI/Predict/controllers/PredictController.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ import {
107107
PredictLiveSportsFlag,
108108
PredictMarketHighlightsFlag,
109109
} from '../types/flags';
110+
import {
111+
VersionGatedFeatureFlag,
112+
validatedVersionGatedFeatureFlag,
113+
} from '../../../../util/remoteFeatureFlag';
110114

111115
/**
112116
* State shape for PredictController
@@ -518,11 +522,19 @@ export class PredictController extends BaseController<
518522
? filterSupportedLeagues(liveSportsFlag.leagues ?? [])
519523
: [];
520524

521-
const marketHighlightsFlag =
522-
(remoteFeatureFlagState.remoteFeatureFlags
523-
.predictMarketHighlights as unknown as
524-
| PredictMarketHighlightsFlag
525-
| undefined) ?? DEFAULT_MARKET_HIGHLIGHTS_FLAG;
525+
const rawMarketHighlightsFlag = remoteFeatureFlagState.remoteFeatureFlags
526+
.predictMarketHighlights as unknown as
527+
| PredictMarketHighlightsFlag
528+
| undefined;
529+
530+
const isHighlightsFlagValid = validatedVersionGatedFeatureFlag(
531+
rawMarketHighlightsFlag as unknown as VersionGatedFeatureFlag,
532+
);
533+
534+
const marketHighlightsFlag: PredictMarketHighlightsFlag =
535+
isHighlightsFlagValid && rawMarketHighlightsFlag
536+
? rawMarketHighlightsFlag
537+
: DEFAULT_MARKET_HIGHLIGHTS_FLAG;
526538

527539
const paramsWithLiveSports = { ...params, liveSportsLeagues };
528540

@@ -538,10 +550,7 @@ export class PredictController extends BaseController<
538550

539551
const isFirstPage = !params.offset || params.offset === 0;
540552
const shouldFetchHighlights =
541-
marketHighlightsFlag.enabled &&
542-
isFirstPage &&
543-
params.category &&
544-
!params.q;
553+
isHighlightsFlagValid && isFirstPage && params.category && !params.q;
545554

546555
if (shouldFetchHighlights) {
547556
const highlightedMarketIds =
@@ -554,12 +563,16 @@ export class PredictController extends BaseController<
554563
params.providerId ?? 'polymarket',
555564
);
556565

557-
const highlightedMarkets =
566+
const fetchedHighlightedMarkets =
558567
(await provider?.getMarketsByIds?.(
559568
highlightedMarketIds,
560569
liveSportsLeagues,
561570
)) ?? [];
562571

572+
const highlightedMarkets = fetchedHighlightedMarkets.filter(
573+
(market) => market.status === 'open',
574+
);
575+
563576
const highlightedIdSet = new Set(highlightedMarkets.map((m) => m.id));
564577
markets = markets.filter(
565578
(market) => !highlightedIdSet.has(market.id),

app/components/UI/Predict/mocks/remoteFeatureFlagMocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const mockedPredictFeatureFlagsEnabledState: Record<
1515

1616
export const mockPredictMarketHighlightsFlag: PredictMarketHighlightsFlag = {
1717
enabled: true,
18+
minimumVersion: '0.0.0',
1819
highlights: [
1920
{
2021
category: 'trending',

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ export interface PredictMarketHighlight {
1919
markets: string[];
2020
}
2121

22-
export interface PredictMarketHighlightsFlag {
23-
enabled: boolean;
22+
export interface PredictMarketHighlightsFlag extends VersionGatedFeatureFlag {
2423
highlights: PredictMarketHighlight[];
2524
}
2625

0 commit comments

Comments
 (0)