Skip to content

Commit 64d0890

Browse files
authored
feat(predict): update sports market card UI (#30195)
## **Description** Updates the Predict sports/game market card UI for World Cup match markets to match the provided Figma designs and aligns the game details footer action buttons with the same button treatment. This includes: - Reworking `PredictMarketSportCard` to use the sports match card layout with team logos, scheduled/live states, draw support, and inline team-colored outcome buttons. - Adding guarded buy-sheet handling directly from the sport card outcome buttons. - Adding an `inlineNoSeparator` action-button layout so `PredictGameDetailsFooter` buttons match the card button format (`CAN 55¢`, `DRAW 24¢`, `BIH 22¢`). - Updating focused tests for sport card rendering, buy actions, live state, and footer/action-button layouts. ## **Changelog** CHANGELOG entry: Updated World Cup prediction cards and game detail action buttons to match the sports match design. ## **Related issues** Fixes: PRED-875, PRED-866 ## **Manual testing steps** ```gherkin Feature: World Cup sports match cards Scenario: user views World Cup match markets Given the Predict World Cup feed contains match/game markets When user opens the World Cup feed Then each game market is displayed with team logos, match schedule/live state, and team-colored outcome buttons Scenario: user opens a World Cup game detail screen Given user taps a World Cup match card When the game details screen opens Then the bottom prediction buttons match the card button treatment with inline label and cents price ``` ## **Screenshots/Recordings** ### **Before** N/A ### **After** https://github.com/user-attachments/assets/37ccf11c-b865-4a86-8c9a-cb84d242a7d2 ## **Validation** - `yarn lint:tsc --pretty false` - Focused ESLint on changed files - `yarn jest app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCard.test.tsx app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.test.tsx --runInBand` - `yarn jest app/components/UI/Predict/components/PredictActionButtons/PredictBetButton.test.tsx app/components/UI/Predict/components/PredictActionButtons/PredictBetButtons.test.tsx app/components/UI/Predict/components/PredictActionButtons/PredictActionButtons.test.tsx app/components/UI/Predict/components/PredictGameDetailsFooter/PredictGameDetailsFooter.test.tsx --runInBand` ## **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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **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] > **Medium Risk** > Moderate risk due to a substantial rewrite of `PredictMarketSportCard` rendering and new guarded buy-sheet trigger paths that could affect navigation and bet entry UX. > > **Overview** > Updates Predict’s sports match presentation by **rewriting `PredictMarketSportCard`** to a new layout with team logos, scheduled vs live states (including live score updates), optional draw support, and inline outcome buttons that can open the preview/buy sheet via guarded actions. > > Extends action button APIs to support a new `inlineNoSeparator` layout plus customizable container/gap styling, and applies this in `PredictGameDetailsFooter` so footer bet buttons match the card’s “TEAM 60¢” format. > > Adds the `PulsingLiveDot` component and adjusts World Cup tabs to use it for live indicators; updates and refocuses tests to cover the new card rendering, buy-button behavior, and button layout variants. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2eb4496. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8fbb9a4 commit 64d0890

12 files changed

Lines changed: 827 additions & 517 deletions

File tree

app/components/UI/Predict/components/PredictActionButtons/PredictActionButtons.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { Box } from '@metamask/design-system-react-native';
33
import PredictBetButtons from './PredictBetButtons';
44
import PredictClaimButton from './PredictClaimButton';
55
import PredictDetailsButtonsSkeleton from '../PredictDetailsButtonsSkeleton';
6-
import { PredictActionButtonsProps } from './PredictActionButtons.types';
6+
import {
7+
PredictActionButtonsProps,
8+
PredictBetButtonLayout,
9+
} from './PredictActionButtons.types';
710
import { PredictMarketStatus, PredictOutcomeToken } from '../../types';
811
import { useLiveMarketPrices } from '../../hooks/useLiveMarketPrices';
912
import { isDrawCapableLeague } from '../../constants/sports';
@@ -35,6 +38,9 @@ const PredictActionButtons: React.FC<PredictActionButtonsProps> = ({
3538
isLoading = false,
3639
isClaimPending = false,
3740
isCarousel,
41+
buttonLayout,
42+
buttonGapClassName,
43+
buttonContainerClassName,
3844
testID = BASE_PREDICT_ACTION_BUTTONS_TEST_IDS.PREDICT_ACTION_BUTTON,
3945
}) => {
4046
const isGameMarket = Boolean(market.game);
@@ -172,6 +178,9 @@ const PredictActionButtons: React.FC<PredictActionButtonsProps> = ({
172178
onBetPress={onBetPress}
173179
testID={testID}
174180
isCarousel={isCarousel}
181+
buttonLayout={buttonLayout}
182+
buttonGapClassName={buttonGapClassName}
183+
buttonContainerClassName={buttonContainerClassName}
175184
/>
176185
);
177186
}
@@ -184,8 +193,19 @@ function PredictBetButtonsContainer(props: {
184193
onBetPress: (token: PredictOutcomeToken) => void;
185194
testID: string;
186195
isCarousel?: boolean;
196+
buttonLayout?: PredictBetButtonLayout;
197+
buttonGapClassName?: string;
198+
buttonContainerClassName?: string;
187199
}) {
188-
const { buttonConfig, onBetPress, testID, isCarousel } = props;
200+
const {
201+
buttonConfig,
202+
onBetPress,
203+
testID,
204+
isCarousel,
205+
buttonLayout,
206+
buttonGapClassName,
207+
buttonContainerClassName = 'w-full mt-4',
208+
} = props;
189209
const { yesToken, drawToken, noToken } = buttonConfig;
190210

191211
const onYesPress = useCallback(
@@ -202,7 +222,7 @@ function PredictBetButtonsContainer(props: {
202222
);
203223

204224
return (
205-
<Box twClassName="w-full mt-4">
225+
<Box twClassName={buttonContainerClassName}>
206226
<PredictBetButtons
207227
yesLabel={buttonConfig.yesLabel}
208228
yesPrice={buttonConfig.yesPrice}
@@ -217,6 +237,8 @@ function PredictBetButtonsContainer(props: {
217237
noTeamColor={buttonConfig.noTeamColor}
218238
testID={`${testID}${PREDICT_ACTION_BUTTONS_TEST_IDS.PREDICT_BET_BUTTON}`}
219239
isCarousel={isCarousel}
240+
layout={buttonLayout}
241+
gapClassName={buttonGapClassName}
220242
/>
221243
</Box>
222244
);

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ButtonBaseSize } from '@metamask/design-system-react-native';
77

88
export type PredictBetButtonVariant = 'yes' | 'no' | 'draw';
99

10-
export type PredictBetButtonLayout = 'inline' | 'stacked';
10+
export type PredictBetButtonLayout = 'inline' | 'inlineNoSeparator' | 'stacked';
1111

1212
export interface PredictBetButtonProps {
1313
label: string;
@@ -36,6 +36,8 @@ export interface PredictBetButtonsProps {
3636
disabled?: boolean;
3737
testID?: string;
3838
isCarousel?: boolean;
39+
layout?: PredictBetButtonLayout;
40+
gapClassName?: string;
3941
}
4042

4143
export interface PredictClaimButtonProps {
@@ -57,4 +59,7 @@ export interface PredictActionButtonsProps {
5759
isClaimPending?: boolean;
5860
testID?: string;
5961
isCarousel?: boolean;
62+
buttonLayout?: PredictBetButtonLayout;
63+
buttonGapClassName?: string;
64+
buttonContainerClassName?: string;
6065
}

app/components/UI/Predict/components/PredictActionButtons/PredictBetButton.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,18 @@ describe('PredictBetButton', () => {
174174
expect(screen.getByText('DRAW · 20¢')).toBeOnTheScreen();
175175
});
176176

177+
it('renders inline with no separator', () => {
178+
const props = createDefaultProps({
179+
label: 'SEA',
180+
price: 70,
181+
layout: 'inlineNoSeparator',
182+
});
183+
184+
renderWithProvider(<PredictBetButton {...props} />);
185+
186+
expect(screen.getByText('SEA 70¢')).toBeOnTheScreen();
187+
});
188+
177189
it('defaults to stacked layout when layout prop is omitted', () => {
178190
const props = createDefaultProps({ label: 'Yes', price: 65 });
179191

app/components/UI/Predict/components/PredictActionButtons/PredictBetButton.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import React from 'react';
2-
import { Button, Text } from '@metamask/design-system-react-native';
2+
import {
3+
Button,
4+
Text,
5+
TextVariant,
6+
} from '@metamask/design-system-react-native';
37
import { useTailwind } from '@metamask/design-system-twrnc-preset';
48
import { useTheme } from '../../../../../util/theme';
59
import { PredictBetButtonProps } from './PredictActionButtons.types';
@@ -41,6 +45,10 @@ const PredictBetButton: React.FC<PredictBetButtonProps> = ({
4145
};
4246

4347
const textStyle = tw.style('font-medium text-center', getTextColor());
48+
const inlineLabel =
49+
layout === 'inlineNoSeparator'
50+
? `${label.toUpperCase()} ${price}¢`
51+
: `${label.toUpperCase()} · ${price}¢`;
4452

4553
return (
4654
<Button
@@ -51,9 +59,15 @@ const PredictBetButton: React.FC<PredictBetButtonProps> = ({
5159
isFullWidth
5260
size={size}
5361
>
54-
{layout === 'inline' ? (
55-
<Text style={textStyle} numberOfLines={1}>
56-
{label.toUpperCase()} · {price}¢
62+
{layout === 'inline' || layout === 'inlineNoSeparator' ? (
63+
<Text
64+
variant={
65+
layout === 'inlineNoSeparator' ? TextVariant.BodySm : undefined
66+
}
67+
style={textStyle}
68+
numberOfLines={1}
69+
>
70+
{inlineLabel}
5771
</Text>
5872
) : (
5973
<>

app/components/UI/Predict/components/PredictActionButtons/PredictBetButtons.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ const PredictBetButtons: React.FC<PredictBetButtonsProps> = ({
2626
disabled = false,
2727
testID = BASE_PREDICT_BET_BUTTONS_TEST_IDS.PREDICT_BET_BUTTON,
2828
isCarousel,
29+
layout,
30+
gapClassName = 'w-full gap-3',
2931
}) => (
30-
<Box flexDirection={BoxFlexDirection.Row} twClassName="w-full gap-3">
32+
<Box flexDirection={BoxFlexDirection.Row} twClassName={gapClassName}>
3133
<Box twClassName="flex-1">
3234
<PredictBetButton
3335
label={yesLabel}
@@ -38,6 +40,7 @@ const PredictBetButtons: React.FC<PredictBetButtonsProps> = ({
3840
disabled={disabled}
3941
testID={`${testID}${PREDICT_BET_BUTTONS_TEST_IDS.PREDICT_BET_BUTTON_YES}`}
4042
size={isCarousel ? ButtonBaseSize.Md : undefined}
43+
layout={layout}
4144
/>
4245
</Box>
4346
{drawLabel !== undefined && drawPrice !== undefined && onDrawPress && (
@@ -50,6 +53,7 @@ const PredictBetButtons: React.FC<PredictBetButtonsProps> = ({
5053
disabled={disabled}
5154
testID={`${testID}${PREDICT_BET_BUTTONS_TEST_IDS.PREDICT_BET_BUTTON_DRAW}`}
5255
size={isCarousel ? ButtonBaseSize.Md : undefined}
56+
layout={layout}
5357
/>
5458
</Box>
5559
)}
@@ -63,6 +67,7 @@ const PredictBetButtons: React.FC<PredictBetButtonsProps> = ({
6367
disabled={disabled}
6468
testID={`${testID}${PREDICT_BET_BUTTONS_TEST_IDS.PREDICT_BET_BUTTON_NO}`}
6569
size={isCarousel ? ButtonBaseSize.Md : undefined}
70+
layout={layout}
6671
/>
6772
</Box>
6873
</Box>

app/components/UI/Predict/components/PredictGameDetailsFooter/PredictGameDetailsFooter.test.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,8 @@ describe('PredictGameDetailsFooter', () => {
187187

188188
renderWithProvider(<PredictGameDetailsFooter {...props} />);
189189

190-
expect(screen.getByText('YES')).toBeOnTheScreen();
191-
expect(screen.getByText('65¢')).toBeOnTheScreen();
192-
expect(screen.getByText('NO')).toBeOnTheScreen();
193-
expect(screen.getByText('35¢')).toBeOnTheScreen();
190+
expect(screen.getByText('YES 65¢')).toBeOnTheScreen();
191+
expect(screen.getByText('NO 35¢')).toBeOnTheScreen();
194192
});
195193

196194
it('renders team buttons for game market', () => {
@@ -200,8 +198,8 @@ describe('PredictGameDetailsFooter', () => {
200198

201199
renderWithProvider(<PredictGameDetailsFooter {...props} />);
202200

203-
expect(screen.getByText('SEA')).toBeOnTheScreen();
204-
expect(screen.getByText('DEN')).toBeOnTheScreen();
201+
expect(screen.getByText('SEA 65¢')).toBeOnTheScreen();
202+
expect(screen.getByText('DEN 35¢')).toBeOnTheScreen();
205203
});
206204

207205
it('calls onBetPress when bet button is pressed', () => {

app/components/UI/Predict/components/PredictGameDetailsFooter/PredictGameDetailsFooter.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ const PredictGameDetailsFooter: React.FC<PredictGameDetailsFooterProps> = ({
106106
claimableAmount={claimableAmount}
107107
isLoading={isLoading}
108108
isClaimPending={isClaimPending}
109+
buttonLayout="inlineNoSeparator"
110+
buttonGapClassName="w-full gap-2"
111+
buttonContainerClassName="w-full mt-2"
109112
testID={`${testID}${PREDICT_GAME_DETAILS_FOOTER_TEST_IDS.ACTION_BUTTONS}`}
110113
/>
111114
</Box>

0 commit comments

Comments
 (0)