Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/components/UI/Predict/constants/feedTabs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PredictCategory } from '../types';
import { PREDICT_WORLD_CUP_FEED_PARAM } from './worldCupTabs';

export type PredictTabKey = PredictCategory;

Expand All @@ -21,9 +22,15 @@ export const PREDICT_HOT_TAB: PredictTabConfig = {
labelKey: 'predict.category.hot',
};

export const PREDICT_WORLD_CUP_TAB: PredictTabConfig = {
key: PREDICT_WORLD_CUP_FEED_PARAM,
labelKey: 'predict.world_cup.title',
};

export const PREDICT_ALL_TABS: readonly PredictTabConfig[] = [
...PREDICT_BASE_TABS,
PREDICT_HOT_TAB,
PREDICT_WORLD_CUP_TAB,
];

const PREDICT_TAB_KEYS = PREDICT_ALL_TABS.map((tab) => tab.key);
Expand Down
124 changes: 118 additions & 6 deletions app/components/UI/Predict/hooks/usePredictTabs.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { renderHook, act } from '@testing-library/react-native';
import { DEFAULT_PREDICT_WORLD_CUP_FLAG } from '../constants/flags';
import { buildPredictWorldCupAllQuery } from '../utils/worldCup';
import { usePredictTabs } from './usePredictTabs';

const mockRouteParams: { tab?: string } = {};
Expand All @@ -13,9 +15,29 @@ const mockHotTabFlag: { enabled: boolean; queryParams: string | undefined } = {
enabled: false,
queryParams: undefined,
};
let mockIsWorldCupMainFeedTabEnabled = false;
let mockWorldCupConfig = DEFAULT_PREDICT_WORLD_CUP_FLAG;

jest.mock('react-redux', () => ({
useSelector: () => mockHotTabFlag,
useSelector: (selector: string) => {
switch (selector) {
case 'selectPredictHotTabFlag':
return mockHotTabFlag;
case 'selectPredictWorldCupMainFeedTabEnabledFlag':
return mockIsWorldCupMainFeedTabEnabled;
case 'selectPredictWorldCupConfig':
return mockWorldCupConfig;
default:
return undefined;
}
},
}));

jest.mock('../selectors/featureFlags', () => ({
selectPredictHotTabFlag: 'selectPredictHotTabFlag',
selectPredictWorldCupConfig: 'selectPredictWorldCupConfig',
selectPredictWorldCupMainFeedTabEnabledFlag:
'selectPredictWorldCupMainFeedTabEnabledFlag',
}));

jest.mock('../../../../../locales/i18n', () => ({
Expand All @@ -28,6 +50,8 @@ describe('usePredictTabs', () => {
mockRouteParams.tab = undefined;
mockHotTabFlag.enabled = false;
mockHotTabFlag.queryParams = undefined;
mockIsWorldCupMainFeedTabEnabled = false;
mockWorldCupConfig = DEFAULT_PREDICT_WORLD_CUP_FLAG;
});

describe('tabs array', () => {
Expand All @@ -47,6 +71,42 @@ describe('usePredictTabs', () => {
expect(result.current.tabs[0].key).toBe('hot');
expect(result.current.tabs[1].key).toBe('trending');
});

it('does not include World Cup tab when main feed tab flag is disabled', () => {
const { result } = renderHook(() => usePredictTabs());

expect(result.current.tabs.map((tab) => tab.key)).not.toContain(
'world-cup',
);
});

it('includes World Cup tab at beginning when enabled', () => {
mockIsWorldCupMainFeedTabEnabled = true;

const { result } = renderHook(() => usePredictTabs());

expect(result.current.tabs).toHaveLength(7);
expect(result.current.tabs[0]).toEqual({
key: 'world-cup',
label: 'predict.world_cup.title',
customQueryParams: buildPredictWorldCupAllQuery(mockWorldCupConfig),
});
expect(result.current.tabs[1].key).toBe('trending');
});

it('places World Cup before Hot when both tabs are enabled', () => {
mockHotTabFlag.enabled = true;
mockHotTabFlag.queryParams = 'test=value';
mockIsWorldCupMainFeedTabEnabled = true;

const { result } = renderHook(() => usePredictTabs());

expect(result.current.tabs.map((tab) => tab.key).slice(0, 3)).toEqual([
'world-cup',
'hot',
'trending',
]);
});
});

describe('activeIndex', () => {
Expand All @@ -56,6 +116,15 @@ describe('usePredictTabs', () => {
expect(result.current.activeIndex).toBe(0);
});

it('defaults to 0 (World Cup) when World Cup tab is enabled and no tab is requested', () => {
mockIsWorldCupMainFeedTabEnabled = true;

const { result } = renderHook(() => usePredictTabs());

expect(result.current.activeIndex).toBe(0);
expect(result.current.tabs[0].key).toBe('world-cup');
});

it('sets active index to requested tab position', () => {
mockRouteParams.tab = 'crypto';

Expand All @@ -72,6 +141,25 @@ describe('usePredictTabs', () => {
expect(result.current.activeIndex).toBe(0);
});

it('defaults to trending when World Cup tab is requested but disabled', () => {
mockRouteParams.tab = 'world-cup';

const { result } = renderHook(() => usePredictTabs());

expect(result.current.activeIndex).toBe(0);
expect(result.current.initialTabKey).toBe('trending');
});

it('sets active index to requested World Cup tab when enabled', () => {
mockRouteParams.tab = 'world-cup';
mockIsWorldCupMainFeedTabEnabled = true;

const { result } = renderHook(() => usePredictTabs());

expect(result.current.activeIndex).toBe(0);
expect(result.current.initialTabKey).toBe('world-cup');
});

it('updates when setActiveIndex is called', () => {
const { result } = renderHook(() => usePredictTabs());

Expand Down Expand Up @@ -184,20 +272,44 @@ describe('usePredictTabs', () => {
});
});

describe('hotTabQueryParams', () => {
it('returns undefined when hot tab is disabled', () => {
describe('tab custom query params', () => {
it('does not add custom query params to base tabs', () => {
const { result } = renderHook(() => usePredictTabs());

expect(result.current.hotTabQueryParams).toBeUndefined();
expect(
result.current.tabs.every((tab) => tab.customQueryParams === undefined),
).toBe(true);
});

it('returns query params when hot tab is enabled', () => {
it('adds query params to the hot tab when enabled', () => {
mockHotTabFlag.enabled = true;
mockHotTabFlag.queryParams = 'test=value';

const { result } = renderHook(() => usePredictTabs());

expect(result.current.hotTabQueryParams).toBe('test=value');
expect(result.current.tabs[0]).toEqual({
key: 'hot',
label: 'predict.category.hot',
customQueryParams: 'test=value',
});
});

it('adds exact World Cup query params to the World Cup tab when enabled', () => {
mockIsWorldCupMainFeedTabEnabled = true;
mockWorldCupConfig = {
...DEFAULT_PREDICT_WORLD_CUP_FLAG,
enabled: true,
showMainFeedTab: true,
tagSlug: 'custom-world-cup',
};

const { result } = renderHook(() => usePredictTabs());

expect(result.current.tabs[0]).toEqual({
key: 'world-cup',
label: 'predict.world_cup.title',
customQueryParams: buildPredictWorldCupAllQuery(mockWorldCupConfig),
});
});
});

Expand Down
38 changes: 32 additions & 6 deletions app/components/UI/Predict/hooks/usePredictTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,42 @@ import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useRoute, RouteProp } from '@react-navigation/native';
import { strings } from '../../../../../locales/i18n';
import { selectPredictHotTabFlag } from '../selectors/featureFlags';
import {
selectPredictHotTabFlag,
selectPredictWorldCupConfig,
selectPredictWorldCupMainFeedTabEnabledFlag,
} from '../selectors/featureFlags';
import {
PREDICT_BASE_TABS,
PREDICT_HOT_TAB,
PREDICT_WORLD_CUP_TAB,
isPredictTabKey,
type PredictTabKey,
} from '../constants/feedTabs';
import type { PredictNavigationParamList } from '../types/navigation';
import { buildPredictWorldCupAllQuery } from '../utils/worldCup';

export interface FeedTab {
key: PredictTabKey;
label: string;
customQueryParams?: string;
}

export interface UsePredictTabsResult {
tabs: FeedTab[];
activeIndex: number;
setActiveIndex: (index: number) => void;
initialTabKey: PredictTabKey;
hotTabQueryParams?: string;
}

export const usePredictTabs = (): UsePredictTabsResult => {
const route =
useRoute<RouteProp<PredictNavigationParamList, 'PredictMarketList'>>();
const hotTabFlag = useSelector(selectPredictHotTabFlag);
const isWorldCupMainFeedTabEnabled = useSelector(
selectPredictWorldCupMainFeedTabEnabledFlag,
);
const worldCupConfig = useSelector(selectPredictWorldCupConfig);

const tabs: FeedTab[] = useMemo(() => {
const baseTabs: FeedTab[] = PREDICT_BASE_TABS.map((tab) => ({
Expand All @@ -39,15 +49,32 @@ export const usePredictTabs = (): UsePredictTabsResult => {
baseTabs.unshift({
key: PREDICT_HOT_TAB.key,
label: strings(PREDICT_HOT_TAB.labelKey),
customQueryParams: hotTabFlag.queryParams,
});
}

return baseTabs;
}, [hotTabFlag.enabled]);
if (isWorldCupMainFeedTabEnabled) {
baseTabs.unshift({
key: PREDICT_WORLD_CUP_TAB.key,
label: strings(PREDICT_WORLD_CUP_TAB.labelKey),
customQueryParams: buildPredictWorldCupAllQuery(worldCupConfig),
});
}

const requestedTabKey = isPredictTabKey(route.params?.tab)
return baseTabs;
}, [
hotTabFlag.enabled,
hotTabFlag.queryParams,
isWorldCupMainFeedTabEnabled,
worldCupConfig,
]);

const requestedValidTabKey = isPredictTabKey(route.params?.tab)
? route.params?.tab
: undefined;
const requestedTabKey = tabs.some((tab) => tab.key === requestedValidTabKey)
? requestedValidTabKey
: undefined;

const initialTabKeyRef = useRef<PredictTabKey>(
requestedTabKey ?? tabs[0].key,
Expand Down Expand Up @@ -109,6 +136,5 @@ export const usePredictTabs = (): UsePredictTabsResult => {
activeIndex,
setActiveIndex,
initialTabKey: initialTabKeyRef.current,
hotTabQueryParams: hotTabFlag.queryParams,
};
};
58 changes: 58 additions & 0 deletions app/components/UI/Predict/providers/polymarket/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
clearClobMarketInfoSessionState,
createApiKey,
deriveApiKey,
fetchEventsFromPolymarketApi,
getAllowance,
getClobMarketInfo,
getClobMarketInfoSafe,
Expand Down Expand Up @@ -204,6 +205,63 @@ describe('polymarket utils', () => {
);
});

describe('fetchEventsFromPolymarketApi', () => {
const mockEventsPaginationResponse = {
ok: true,
json: jest.fn().mockResolvedValue({ data: [] }),
};

beforeEach(() => {
mockEventsPaginationResponse.json.mockClear();
mockFetch.mockResolvedValue(mockEventsPaginationResponse);
});

it('uses exact World Cup custom query params without normal feed filters', async () => {
await fetchEventsFromPolymarketApi({
category: 'world-cup',
limit: 20,
offset: 40,
customQueryParams:
'active=true&archived=false&closed=false&tag_slug=fifa-world-cup&order=volume24hr',
});

expect(mockFetch).toHaveBeenCalledWith(
'https://gamma-api.polymarket.com/events/pagination?limit=20&offset=40&active=true&archived=false&closed=false&tag_slug=fifa-world-cup&order=volume24hr',
);
const requestedUrl = String(mockFetch.mock.calls[0][0]);
expect(requestedUrl).not.toContain('liquidity_min');
expect(requestedUrl).not.toContain('volume_min');
});

it('falls back to default World Cup query params without normal feed filters', async () => {
await fetchEventsFromPolymarketApi({
category: 'world-cup',
limit: 10,
offset: 0,
});

expect(mockFetch).toHaveBeenCalledWith(
'https://gamma-api.polymarket.com/events/pagination?limit=10&offset=0&active=true&archived=false&closed=false&tag_slug=fifa-world-cup&order=volume24hr',
);
const requestedUrl = String(mockFetch.mock.calls[0][0]);
expect(requestedUrl).not.toContain('liquidity_min');
expect(requestedUrl).not.toContain('volume_min');
});

it('keeps Hot category default query on normal feed filters without custom params', async () => {
await fetchEventsFromPolymarketApi({
category: 'hot',
limit: 20,
offset: 0,
});

const requestedUrl = String(mockFetch.mock.calls[0][0]);
expect(requestedUrl).toContain('liquidity_min=10000');
expect(requestedUrl).toContain('volume_min=10000');
expect(requestedUrl).toContain('&order=volume24hr');
});
});

it('creates API keys against the canonical CLOB host', async () => {
mockFetch.mockResolvedValue({
status: 200,
Expand Down
Loading
Loading