Skip to content

Commit 3fb3e97

Browse files
authored
feat(predict): update World Cup banner (MetaMask#30480)
## **Description** Updates the Predict World Cup main feed banner to match the latest Figma card design. The banner now renders as a muted card with the World Cup image, title, description, and a filled icon button in the footer. Also updates the Predict World Cup feature flag image override from a single URL to a structured `bannerImage` object containing `{ url, width, height }`, so remote images can preserve their intended aspect ratio while still falling back to a safe default aspect ratio when dimensions are missing or invalid. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: PRED-895 ## **Manual testing steps** ```gherkin Feature: Predict World Cup main feed banner Scenario: user views the Predict feed with World Cup enabled Given the Predict World Cup feature flag is enabled And showMainFeedBanner and showWorldCupScreen are enabled When user opens the Predict feed Then the World Cup banner is displayed as a card with an image, title, description, and arrow button Scenario: user taps the World Cup banner Given the World Cup banner is visible on the Predict feed When user taps the banner Then user is navigated to the World Cup Predictions screen ``` ## **Screenshots/Recordings** ### **Before** N/A ### **After** <img width="350" alt="Simulator Screenshot - mm-blue - 2026-05-20 at 15 16 24" src="https://github.com/user-attachments/assets/088e1035-373e-4f23-bd3e-736d6930ff7b" /> ## **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. ## Testing ```bash yarn jest app/components/UI/Predict/components/PredictWorldCupMainFeedBanner/PredictWorldCupMainFeedBanner.test.tsx app/components/UI/Predict/schemas/flags.test.ts app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts --runInBand ``` Result: 3 test suites passed, 53 tests passed. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate risk because it changes the remote feature-flag schema/type for the World Cup banner image (from `bannerImageUrl` to structured `bannerImage`) and updates banner sizing logic based on provided dimensions, which could impact rendering if remote config is malformed. > > **Overview** > Updates the Predict World Cup main-feed banner to a card-style layout with footer copy (title/description) and a filled arrow `ButtonIcon`, while keeping the same navigation/tracking behavior. > > Reworks the World Cup feature-flag image override from `bannerImageUrl` to a structured `bannerImage` object (`{ url, width, height }`) and introduces `getPredictWorldCupBannerImageAspectRatio` to compute banner height from configured dimensions with a safe default fallback; associated schema/type and unit tests are updated accordingly, and the English banner description string is revised. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 580875f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 1919f0f commit 3fb3e97

8 files changed

Lines changed: 131 additions & 35 deletions

File tree

2.27 MB
Loading

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

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from 'react';
2-
import { StyleSheet } from 'react-native';
2+
import { Dimensions, StyleSheet } from 'react-native';
33
import { fireEvent, render } from '@testing-library/react-native';
44
import { useNavigation } from '@react-navigation/native';
55
import { useSelector } from 'react-redux';
66
import Routes from '../../../../../constants/navigation/Routes';
77
import { DEFAULT_PREDICT_WORLD_CUP_FLAG } from '../../constants/flags';
88
import { PredictEventValues } from '../../constants/eventNames';
99
import PredictWorldCupMainFeedBanner, {
10+
getPredictWorldCupBannerImageAspectRatio,
1011
getPredictWorldCupBannerSource,
1112
} from './PredictWorldCupMainFeedBanner';
1213
import { PredictWorldCupMainFeedBannerSelectorsIDs } from './PredictWorldCupMainFeedBanner.testIds';
@@ -19,19 +20,6 @@ jest.mock('react-redux', () => ({
1920
useSelector: jest.fn(),
2021
}));
2122

22-
jest.mock('react-native', () => {
23-
const actualReactNative = jest.requireActual('react-native');
24-
return {
25-
...actualReactNative,
26-
useWindowDimensions: jest.fn(() => ({
27-
width: 393,
28-
height: 852,
29-
scale: 3,
30-
fontScale: 1,
31-
})),
32-
};
33-
});
34-
3523
const mockUseNavigation = useNavigation as jest.Mock;
3624
const mockUseSelector = useSelector as jest.Mock;
3725
const mockNavigate = jest.fn();
@@ -75,18 +63,24 @@ describe('PredictWorldCupMainFeedBanner', () => {
7563
).toBeOnTheScreen();
7664
});
7765

78-
it('uses the remote banner image URL when configured', () => {
66+
it('uses the remote banner image URL and configured dimensions when configured', () => {
7967
const bannerImageUrl = 'https://example.com/world-cup-banner.png';
8068
mockUseSelector.mockReturnValue({
8169
...enabledConfig,
82-
bannerImageUrl,
70+
bannerImage: {
71+
url: bannerImageUrl,
72+
width: 300,
73+
height: 100,
74+
},
8375
});
8476

8577
const { getByTestId } = render(<PredictWorldCupMainFeedBanner />);
8678
const image = getByTestId(PredictWorldCupMainFeedBannerSelectorsIDs.IMAGE);
8779

8880
expect(image.props.source).toStrictEqual({ uri: bannerImageUrl });
89-
expect(StyleSheet.flatten(image.props.style).height).toBeGreaterThan(0);
81+
expect(StyleSheet.flatten(image.props.style).height).toBeCloseTo(
82+
(Dimensions.get('window').width - 32) / 3,
83+
);
9084
});
9185

9286
it('does not render when the main feed banner is disabled', () => {
@@ -159,10 +153,39 @@ describe('PredictWorldCupMainFeedBanner', () => {
159153
});
160154
});
161155

156+
describe('getPredictWorldCupBannerImageAspectRatio', () => {
157+
it('returns configured image aspect ratio when dimensions are provided', () => {
158+
expect(
159+
getPredictWorldCupBannerImageAspectRatio({
160+
url: 'https://example.com/banner.png',
161+
width: 300,
162+
height: 100,
163+
}),
164+
).toBe(3);
165+
});
166+
167+
it('returns default image aspect ratio when dimensions are missing', () => {
168+
expect(getPredictWorldCupBannerImageAspectRatio()).toBe(2);
169+
});
170+
171+
it('returns default image aspect ratio when dimensions are invalid', () => {
172+
expect(
173+
getPredictWorldCupBannerImageAspectRatio({
174+
url: 'https://example.com/banner.png',
175+
width: 0,
176+
height: -200,
177+
}),
178+
).toBe(2);
179+
});
180+
});
181+
162182
describe('getPredictWorldCupBannerSource', () => {
163183
it('returns a trimmed remote URI source before the fallback image source', () => {
164184
expect(
165-
getPredictWorldCupBannerSource(' https://example.com/banner.png ', 1),
185+
getPredictWorldCupBannerSource(
186+
{ url: ' https://example.com/banner.png ', width: 400, height: 200 },
187+
1,
188+
),
166189
).toStrictEqual({ uri: 'https://example.com/banner.png' });
167190
});
168191

app/components/UI/Predict/components/PredictWorldCupMainFeedBanner/PredictWorldCupMainFeedBanner.tsx

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,23 @@ import {
33
Image,
44
ImageSourcePropType,
55
Pressable,
6+
View,
67
useWindowDimensions,
78
} from 'react-native';
89
import { useNavigation } from '@react-navigation/native';
910
import { useSelector } from 'react-redux';
1011
import { useTailwind } from '@metamask/design-system-twrnc-preset';
12+
import {
13+
ButtonIcon,
14+
ButtonIconSize,
15+
ButtonIconVariant,
16+
FontWeight,
17+
IconName,
18+
Text,
19+
TextColor,
20+
TextVariant,
21+
} from '@metamask/design-system-react-native';
22+
import { strings } from '../../../../../../locales/i18n';
1123
import Routes from '../../../../../constants/navigation/Routes';
1224
import Engine from '../../../../../core/Engine';
1325
import { selectPredictWorldCupConfig } from '../../selectors/featureFlags';
@@ -17,16 +29,16 @@ import { PredictWorldCupMainFeedBannerSelectorsIDs } from './PredictWorldCupMain
1729

1830
import worldCupMainFeedBannerImage from '../../assets/world-cup-main-feed-banner.png';
1931

20-
const WORLD_CUP_BANNER_ASPECT_RATIO = 360 / 177;
32+
const WORLD_CUP_BANNER_DEFAULT_IMAGE_ASPECT_RATIO = 2;
2133
const WORLD_CUP_BANNER_HORIZONTAL_MARGIN = 16;
2234
const WORLD_CUP_BANNER_HORIZONTAL_MARGIN_TOTAL =
2335
WORLD_CUP_BANNER_HORIZONTAL_MARGIN * 2;
2436

2537
export const getPredictWorldCupBannerSource = (
26-
bannerImageUrl?: string,
38+
bannerImage?: PredictWorldCupConfig['bannerImage'],
2739
fallbackImageSource?: ImageSourcePropType,
2840
): ImageSourcePropType | undefined => {
29-
const trimmedBannerImageUrl = bannerImageUrl?.trim();
41+
const trimmedBannerImageUrl = bannerImage?.url.trim();
3042

3143
if (trimmedBannerImageUrl) {
3244
return { uri: trimmedBannerImageUrl };
@@ -35,6 +47,16 @@ export const getPredictWorldCupBannerSource = (
3547
return fallbackImageSource;
3648
};
3749

50+
export const getPredictWorldCupBannerImageAspectRatio = (
51+
bannerImage?: PredictWorldCupConfig['bannerImage'],
52+
): number => {
53+
if (bannerImage && bannerImage.width > 0 && bannerImage.height > 0) {
54+
return bannerImage.width / bannerImage.height;
55+
}
56+
57+
return WORLD_CUP_BANNER_DEFAULT_IMAGE_ASPECT_RATIO;
58+
};
59+
3860
interface PredictWorldCupMainFeedBannerProps {
3961
fallbackImageSource?: ImageSourcePropType | null;
4062
}
@@ -60,7 +82,10 @@ const PredictWorldCupMainFeedBanner: React.FC<
6082
windowWidth - WORLD_CUP_BANNER_HORIZONTAL_MARGIN_TOTAL,
6183
0,
6284
);
63-
const bannerHeight = bannerWidth / WORLD_CUP_BANNER_ASPECT_RATIO;
85+
const bannerImageAspectRatio = getPredictWorldCupBannerImageAspectRatio(
86+
predictWorldCupConfig.bannerImage,
87+
);
88+
const bannerImageHeight = bannerWidth / bannerImageAspectRatio;
6489

6590
const resolvedFallbackImageSource =
6691
fallbackImageSource === undefined
@@ -71,7 +96,7 @@ const PredictWorldCupMainFeedBanner: React.FC<
7196
() =>
7297
shouldRenderBanner(predictWorldCupConfig)
7398
? getPredictWorldCupBannerSource(
74-
predictWorldCupConfig.bannerImageUrl,
99+
predictWorldCupConfig.bannerImage,
75100
resolvedFallbackImageSource,
76101
)
77102
: undefined,
@@ -114,12 +139,38 @@ const PredictWorldCupMainFeedBanner: React.FC<
114139
style={tw.style('mx-4 pb-3')}
115140
testID={PredictWorldCupMainFeedBannerSelectorsIDs.CONTAINER}
116141
>
117-
<Image
118-
source={imageSource}
119-
resizeMode="cover"
120-
testID={PredictWorldCupMainFeedBannerSelectorsIDs.IMAGE}
121-
style={tw.style('w-full rounded-xl', { height: bannerHeight })}
122-
/>
142+
<View style={tw.style('bg-muted rounded-xl overflow-hidden')}>
143+
<Image
144+
source={imageSource}
145+
resizeMode="cover"
146+
testID={PredictWorldCupMainFeedBannerSelectorsIDs.IMAGE}
147+
style={tw.style('w-full rounded-t-xl', { height: bannerImageHeight })}
148+
/>
149+
<View style={tw.style('flex-row items-center justify-between p-3')}>
150+
<View style={tw.style('flex-shrink')}>
151+
<Text
152+
variant={TextVariant.BodyMd}
153+
color={TextColor.TextDefault}
154+
fontWeight={FontWeight.Medium}
155+
>
156+
{strings('predict.world_cup.banner_title')}
157+
</Text>
158+
<Text
159+
variant={TextVariant.BodySm}
160+
color={TextColor.TextAlternative}
161+
>
162+
{strings('predict.world_cup.banner_description')}
163+
</Text>
164+
</View>
165+
<ButtonIcon
166+
accessibilityLabel={strings('predict.world_cup.banner_title')}
167+
iconName={IconName.ArrowRight}
168+
onPress={handlePress}
169+
size={ButtonIconSize.Md}
170+
variant={ButtonIconVariant.Filled}
171+
/>
172+
</View>
173+
</View>
123174
</Pressable>
124175
);
125176
};

app/components/UI/Predict/schemas/flags.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,11 @@ describe('PredictWorldCupSchema', () => {
217217
seriesId: '11433',
218218
tagSlug: 'fifa-world-cup',
219219
gamesTagId: '100639',
220-
bannerImageUrl: 'https://example.com/banner.png',
220+
bannerImage: {
221+
url: 'https://example.com/banner.png',
222+
width: 400,
223+
height: 200,
224+
},
221225
stages: [
222226
{
223227
key: 'group_stage',

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,13 @@ export const PredictWorldCupSchema = defaulted(
7676
string(),
7777
() => DEFAULT_PREDICT_WORLD_CUP_FLAG.gamesTagId,
7878
),
79-
bannerImageUrl: optional(string()),
79+
bannerImage: optional(
80+
object({
81+
url: string(),
82+
width: number(),
83+
height: number(),
84+
}),
85+
),
8086
stages: defaulted(array(PredictWorldCupStageSchema), () => []),
8187
}),
8288
() => DEFAULT_PREDICT_WORLD_CUP_FLAG,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export interface PredictWorldCupConfig extends VersionGatedFeatureFlag {
3737
seriesId: string;
3838
tagSlug: string;
3939
gamesTagId: string;
40-
bannerImageUrl?: string;
40+
bannerImage?: {
41+
url: string;
42+
width: number;
43+
height: number;
44+
};
4145
stages: PredictWorldCupStageConfig[];
4246
}
4347

app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,11 @@ describe('resolvePredictFeatureFlags', () => {
236236
showMainFeedBanner: true,
237237
showMainFeedTab: true,
238238
showWorldCupScreen: true,
239-
bannerImageUrl: 'https://example.com/banner.png',
239+
bannerImage: {
240+
url: 'https://example.com/banner.png',
241+
width: 400,
242+
height: 200,
243+
},
240244
stages: [
241245
{
242246
key: 'group_stage',
@@ -255,7 +259,11 @@ describe('resolvePredictFeatureFlags', () => {
255259
showMainFeedBanner: true,
256260
showMainFeedTab: true,
257261
showWorldCupScreen: true,
258-
bannerImageUrl: 'https://example.com/banner.png',
262+
bannerImage: {
263+
url: 'https://example.com/banner.png',
264+
width: 400,
265+
height: 200,
266+
},
259267
stages: [
260268
{
261269
key: 'group_stage',

locales/languages/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2243,7 +2243,7 @@
22432243
"world_cup": {
22442244
"title": "World Cup",
22452245
"banner_title": "World Cup 2026",
2246-
"banner_description": "Trade on World Cup markets",
2246+
"banner_description": "Trade every match, every moment.",
22472247
"tabs": {
22482248
"all": "All",
22492249
"props": "Props",

0 commit comments

Comments
 (0)