Skip to content

Commit 6187643

Browse files
chore(runway): cherry-pick fix(predict): cp-7.63.0 game picks not showing for claimable positions (#25229)
- fix(predict): cp-7.63.0 game picks not showing for claimable positions (#25220) ## **Description** Fixed an issue where positions/picks were not visible for game markets after the market was closed when users had claimable positions. **Root Cause:** The `PredictPicks` component was only checking for live positions when determining whether to render. When a market closed and positions became claimable (no longer "live"), the component would return `null` even though there were claimable positions to display. **Solution:** 1. Added a second `usePredictPositions` hook call with `claimable: true` to fetch claimable positions 2. Updated render condition to show component when either live OR claimable positions exist 3. Conditionally hide Cash Out button for claimable positions (since they can only be claimed, not cashed out) 4. Updated `PredictSportCardFooter` to also render claimable positions ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/PRED-551 ## **Manual testing steps** ```gherkin Feature: Game picks display for claimable positions Scenario: user views game picks after market closes with claimable positions Given user has an open position on a game market And the game market has ended and positions are now claimable When user navigates to the market details Then user sees their position(s) in the "Your picks" section And the Cash Out button is not displayed for claimable positions And the Claim button is available ``` ## **Screenshots/Recordings** ### **Before** Positions/picks section was hidden when market closed and positions became claimable. ### **After** Positions/picks section now displays for both live and claimable positions, with Cash Out button only shown for non-claimable positions. <img width="371" height="761" alt="Screenshot 2026-01-26 at 11 57 12 AM" src="https://github.com/user-attachments/assets/574590a4-1f62-491d-9577-a6f38f4b86d7" /> <img width="365" height="766" alt="Screenshot 2026-01-26 at 11 57 02 AM" src="https://github.com/user-attachments/assets/d0921cba-96d7-45bd-be34-13640ee021b8" /> ## **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] > Ensures users see their picks after markets close by including claimable positions alongside live ones and adjusting actions accordingly. > > - PredictPicks: adds a second `usePredictPositions` call with `claimable: true`, renders when either `livePositions` or `claimablePositions` exist, and passes both to `PredictPickItem`; `Cash out` button is hidden for `claimable` positions > - PredictSportCardFooter: also fetches `claimablePositions`, renders them via `PredictPicksForCard`, and shows claim CTA with aggregated `claimableAmount` > - Tests: comprehensive unit tests added/updated for live vs claimable flows, hook parameters, optimistic states, navigation, and action guards > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit afdbf84. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [c560e11](c560e11) Co-authored-by: Luis Taniça <matallui@gmail.com>
1 parent 9d961df commit 6187643

6 files changed

Lines changed: 419 additions & 195 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import React from 'react';
2+
import { fireEvent, render, screen } from '@testing-library/react-native';
3+
import PredictPickItem from './PredictPickItem';
4+
import { PredictPositionStatus, type PredictPosition } from '../../types';
5+
import { formatPrice } from '../../utils/format';
6+
7+
import { usePredictOptimisticPositionRefresh } from '../../hooks/usePredictOptimisticPositionRefresh';
8+
9+
jest.mock('../../hooks/usePredictOptimisticPositionRefresh');
10+
jest.mock('../../utils/format');
11+
12+
const mockUsePredictOptimisticPositionRefresh =
13+
usePredictOptimisticPositionRefresh as jest.MockedFunction<
14+
typeof usePredictOptimisticPositionRefresh
15+
>;
16+
const mockFormatPrice = formatPrice as jest.MockedFunction<typeof formatPrice>;
17+
18+
const createMockPosition = (
19+
overrides: Partial<PredictPosition> = {},
20+
): PredictPosition => ({
21+
id: 'position-1',
22+
providerId: 'polymarket',
23+
marketId: 'market-1',
24+
outcomeId: 'outcome-1',
25+
outcomeTokenId: '0',
26+
icon: 'https://example.com/icon.png',
27+
title: 'Will BTC hit 100k?',
28+
outcome: 'Yes',
29+
outcomeIndex: 0,
30+
amount: 10,
31+
price: 0.67,
32+
status: PredictPositionStatus.OPEN,
33+
size: 50,
34+
cashPnl: 15.5,
35+
percentPnl: 5.25,
36+
initialValue: 100,
37+
currentValue: 115.5,
38+
avgPrice: 0.5,
39+
claimable: false,
40+
endDate: '2025-12-31T00:00:00Z',
41+
...overrides,
42+
});
43+
44+
describe('PredictPickItem', () => {
45+
const mockOnCashOut = jest.fn();
46+
47+
beforeEach(() => {
48+
jest.clearAllMocks();
49+
mockUsePredictOptimisticPositionRefresh.mockImplementation(
50+
({ position }) => position as PredictPosition,
51+
);
52+
mockFormatPrice.mockImplementation(
53+
(value: number | string, _options?: { maximumDecimals?: number }) => {
54+
const num = typeof value === 'string' ? parseFloat(value) : value;
55+
if (isNaN(num)) return '$0.00';
56+
return `$${num.toFixed(2)}`;
57+
},
58+
);
59+
});
60+
61+
afterEach(() => {
62+
jest.clearAllMocks();
63+
});
64+
65+
describe('rendering', () => {
66+
it('renders position info with initialValue and outcome', () => {
67+
const position = createMockPosition({ initialValue: 50, outcome: 'Yes' });
68+
69+
render(
70+
<PredictPickItem
71+
position={position}
72+
onCashOut={mockOnCashOut}
73+
testID="test-pick"
74+
/>,
75+
);
76+
77+
expect(screen.getByText(/\$50\.00 on/)).toBeOnTheScreen();
78+
expect(screen.getByText(/Yes/)).toBeOnTheScreen();
79+
});
80+
81+
it('renders positive cashPnl value with SuccessDefault color', () => {
82+
const position = createMockPosition({ id: 'pos-1', cashPnl: 25.75 });
83+
84+
render(
85+
<PredictPickItem
86+
position={position}
87+
onCashOut={mockOnCashOut}
88+
testID="test-pick"
89+
/>,
90+
);
91+
92+
const pnlText = screen.getByTestId('predict-picks-pnl-pos-1');
93+
expect(pnlText).toBeOnTheScreen();
94+
expect(screen.getByText('$25.75')).toBeOnTheScreen();
95+
});
96+
97+
it('renders negative cashPnl value with ErrorDefault color', () => {
98+
const position = createMockPosition({ id: 'pos-1', cashPnl: -10.5 });
99+
100+
render(
101+
<PredictPickItem
102+
position={position}
103+
onCashOut={mockOnCashOut}
104+
testID="test-pick"
105+
/>,
106+
);
107+
108+
expect(screen.getByText('$-10.50')).toBeOnTheScreen();
109+
});
110+
});
111+
112+
describe('Cash Out button', () => {
113+
it('renders Cash Out button when position is not claimable', () => {
114+
const position = createMockPosition({ id: 'pos-1', claimable: false });
115+
116+
render(
117+
<PredictPickItem
118+
position={position}
119+
onCashOut={mockOnCashOut}
120+
testID="test-pick"
121+
/>,
122+
);
123+
124+
expect(
125+
screen.getByTestId('predict-picks-cash-out-button-pos-1'),
126+
).toBeOnTheScreen();
127+
expect(screen.getByText('Cash out')).toBeOnTheScreen();
128+
});
129+
130+
it('does not render Cash Out button when position is claimable', () => {
131+
const position = createMockPosition({ id: 'pos-1', claimable: true });
132+
133+
render(
134+
<PredictPickItem
135+
position={position}
136+
onCashOut={mockOnCashOut}
137+
testID="test-pick"
138+
/>,
139+
);
140+
141+
expect(
142+
screen.queryByTestId('predict-picks-cash-out-button-pos-1'),
143+
).toBeNull();
144+
expect(screen.queryByText('Cash out')).toBeNull();
145+
});
146+
147+
it('calls onCashOut with position when Cash Out button is pressed', () => {
148+
const position = createMockPosition({ id: 'pos-1', claimable: false });
149+
150+
render(
151+
<PredictPickItem
152+
position={position}
153+
onCashOut={mockOnCashOut}
154+
testID="test-pick"
155+
/>,
156+
);
157+
158+
fireEvent.press(
159+
screen.getByTestId('predict-picks-cash-out-button-pos-1'),
160+
);
161+
162+
expect(mockOnCashOut).toHaveBeenCalledTimes(1);
163+
expect(mockOnCashOut).toHaveBeenCalledWith(position);
164+
});
165+
});
166+
167+
describe('optimistic updates', () => {
168+
it('renders Skeleton when position is optimistic', () => {
169+
const position = createMockPosition({
170+
id: 'pos-1',
171+
claimable: false,
172+
});
173+
mockUsePredictOptimisticPositionRefresh.mockReturnValue({
174+
...position,
175+
optimistic: true,
176+
});
177+
178+
render(
179+
<PredictPickItem
180+
position={position}
181+
onCashOut={mockOnCashOut}
182+
testID="test-pick"
183+
/>,
184+
);
185+
186+
expect(screen.queryByTestId('predict-picks-pnl-pos-1')).toBeNull();
187+
});
188+
189+
it('disables Cash Out button when position is optimistic', () => {
190+
const position = createMockPosition({
191+
id: 'pos-1',
192+
claimable: false,
193+
});
194+
mockUsePredictOptimisticPositionRefresh.mockReturnValue({
195+
...position,
196+
optimistic: true,
197+
});
198+
199+
render(
200+
<PredictPickItem
201+
position={position}
202+
onCashOut={mockOnCashOut}
203+
testID="test-pick"
204+
/>,
205+
);
206+
207+
const button = screen.getByTestId('predict-picks-cash-out-button-pos-1');
208+
expect(button.props.accessibilityState?.disabled).toBe(true);
209+
});
210+
});
211+
212+
describe('formatPrice calls', () => {
213+
it('calls formatPrice for position initialValue', () => {
214+
const position = createMockPosition({ initialValue: 15.75 });
215+
216+
render(
217+
<PredictPickItem
218+
position={position}
219+
onCashOut={mockOnCashOut}
220+
testID="test-pick"
221+
/>,
222+
);
223+
224+
expect(mockFormatPrice).toHaveBeenCalledWith(15.75, {
225+
maximumDecimals: 2,
226+
});
227+
});
228+
229+
it('calls formatPrice for cashPnl', () => {
230+
const position = createMockPosition({ cashPnl: 1234.56 });
231+
232+
render(
233+
<PredictPickItem
234+
position={position}
235+
onCashOut={mockOnCashOut}
236+
testID="test-pick"
237+
/>,
238+
);
239+
240+
expect(mockFormatPrice).toHaveBeenNthCalledWith(2, 1234.56, {
241+
maximumDecimals: 2,
242+
});
243+
});
244+
});
245+
});

app/components/UI/Predict/components/PredictPicks/PredictPickItem.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,19 @@ const PredictPickItem: React.FC<PredictPickItemProps> = ({
6161
</Text>
6262
)}
6363
</Box>
64-
<Button
65-
variant={ButtonVariant.Secondary}
66-
twClassName="py-3 px-4 light:bg-muted/5"
67-
onPress={() => onCashOut(currentPosition)}
68-
isDisabled={isOptimistic}
69-
testID={`predict-picks-cash-out-button-${position.id}`}
70-
>
71-
<Text variant={TextVariant.BodyMd} twClassName="font-medium">
72-
{strings('predict.cash_out')}
73-
</Text>
74-
</Button>
64+
{!position.claimable && (
65+
<Button
66+
variant={ButtonVariant.Secondary}
67+
twClassName="light:bg-muted/5"
68+
onPress={() => onCashOut(currentPosition)}
69+
isDisabled={isOptimistic}
70+
testID={`predict-picks-cash-out-button-${position.id}`}
71+
>
72+
<Text variant={TextVariant.BodyMd} twClassName="font-medium">
73+
{strings('predict.cash_out')}
74+
</Text>
75+
</Button>
76+
)}
7577
</Box>
7678
);
7779
};

0 commit comments

Comments
 (0)