Skip to content

Commit bd42563

Browse files
fix(rewards): improve VIP splash layout on small screens (#32187)
## **Description** The VIP splash screen used fixed sizing (38px title, full-size fox image, tall footer buttons) that felt crowded on compact devices. The referee splash variant is especially tight because it adds "Referred by" footer text above the action buttons. This PR adds a responsive layout for screens shorter than 750px, following the same breakpoint pattern used in `CardWelcome`: - Scale the gradient title from 38px to 28px - Scale the fox hero image down (max 260px wide, preserving aspect ratio) and reduce its top margin - Compact the footer with tighter gaps/padding and shorter primary (`h-10`) and secondary (`h-8`) buttons `mt-24` and `mt-[18px]` spacing for the title and description are unchanged. ## **Changelog** CHANGELOG entry: Fixed VIP splash screen layout on smaller devices ## **Related issues** Refs: N/A ## **Manual testing steps** ```gherkin Feature: VIP splash responsive layout Scenario: VIP splash renders proportionally on a small device Given the app is running on a simulator with screen height below 750px (e.g. iPhone SE) And the user navigates to the VIP splash screen When the splash screen is displayed Then the title uses the smaller 28px font size And the fox image is scaled down And the footer buttons use the compact heights Scenario: VIP referee splash fits with referral footer on a small device Given the app is running on a small simulator And the user is a VIP referee who has not yet accepted the invite When the referee splash screen is displayed Then the "Referred by" text, Continue button, and Not now button fit without crowding the title Scenario: VIP splash layout is unchanged on large devices Given the app is running on a simulator with screen height at or above 750px (e.g. iPhone 15 Pro Max) When the VIP splash screen is displayed Then the title, fox image, and footer buttons use the original sizing ``` - [x] `yarn jest VipSplashGradientTitle.test.tsx VipSplashScreenLayout.test.tsx --coverage=false` ## **Screenshots/Recordings** N/A — layout changes are best verified on small vs. large simulators; screenshots can be added after manual QA. ### **Before** N/A ### **After** iPhone 13 Mini <img width="1080" height="2340" alt="Simulator Screenshot - iPhone 13 mini - 2026-06-22 at 16 16 03" src="https://github.com/user-attachments/assets/458c7dd1-a257-40f1-822d-038a3b732e24" /> iPhone 14 <img width="1179" height="2556" alt="Simulator Screenshot - E2E Test - 2026-06-22 at 16 19 45" src="https://github.com/user-attachments/assets/9ff4e841-d6c2-4a5d-aa03-def3727d6d25" /> ## **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 - [ ] 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 ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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] > **Low Risk** > UI-only responsive styling in the VIP rewards splash with no auth, data, or navigation logic changes; behavior on tall screens is preserved. > > **Overview** > Adds a **750px screen-height breakpoint** so the VIP splash layout compacts on shorter devices while leaving large-screen sizing unchanged. > > **`VipSplashGradientTitle`** now reads `useWindowDimensions` and switches title `fontSize`/`lineHeight` from **38px** to **28px** below the threshold (replacing fixed Tailwind `text-[38px]`). > > **`VipSplashScreenLayout`** applies the same breakpoint to scale the fox hero (**max 260px** wide, aspect ratio preserved), reduce fox top margin and footer gap/padding, and use shorter primary (**h-10**) and secondary (**h-8**) buttons. Shared sizing values live in **`Vip.constants`**, with unit tests mocking window dimensions for both breakpoints. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c67c98e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7a23062 commit bd42563

5 files changed

Lines changed: 188 additions & 12 deletions

File tree

app/components/UI/Rewards/components/Vip/Vip.constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export const VIP_SPLASH_BACKGROUND_GRADIENT_COLORS = [
99
];
1010
export const VIP_SPLASH_TITLE_GRADIENT_COLORS = ['#FFE181', '#F7F0D6'];
1111
export const VIP_SPLASH_ACCEPT_BUTTON_BACKGROUND = '#1E1C17';
12+
export const VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES = 750;
13+
export const VIP_SPLASH_TITLE_FONT_SIZE = 38;
14+
export const VIP_SPLASH_TITLE_FONT_SIZE_SMALL = 28;
15+
export const VIP_SPLASH_FOX_WIDTH = 320;
16+
export const VIP_SPLASH_FOX_HEIGHT = 381;
1217
export const VIP_GOLD_BACKGROUND_GRADIENT_COLORS = ['transparent', '#CBAE6950'];
1318
export const VIP_GOLD_TIER_GRADIENT_COLORS = [
1419
'rgba(234, 215, 151, 0.12)',

app/components/UI/Rewards/components/Vip/VipSplashGradientTitle.test.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import React from 'react';
2+
import { useWindowDimensions } from 'react-native';
23
import { render } from '@testing-library/react-native';
34
import VipSplashGradientTitle from './VipSplashGradientTitle';
5+
import {
6+
VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES,
7+
VIP_SPLASH_TITLE_FONT_SIZE,
8+
VIP_SPLASH_TITLE_FONT_SIZE_SMALL,
9+
} from './Vip.constants';
10+
11+
jest.mock('react-native/Libraries/Utilities/useWindowDimensions', () => ({
12+
__esModule: true,
13+
default: jest.fn(() => ({
14+
width: 375,
15+
height: 812,
16+
scale: 2,
17+
fontScale: 1,
18+
})),
19+
}));
420

521
jest.mock('@metamask/design-system-react-native', () => {
622
const ReactActual = jest.requireActual('react');
@@ -9,11 +25,13 @@ jest.mock('@metamask/design-system-react-native', () => {
925
return {
1026
Text: ({
1127
children,
28+
style,
1229
testID,
1330
}: {
1431
children?: React.ReactNode;
32+
style?: unknown;
1533
testID?: string;
16-
}) => ReactActual.createElement(Text, { testID }, children),
34+
}) => ReactActual.createElement(Text, { style, testID }, children),
1735
TextVariant: { DisplayMd: 'displayMd' },
1836
};
1937
});
@@ -58,6 +76,15 @@ jest.mock('../../../../../../locales/i18n', () => ({
5876
}));
5977

6078
describe('VipSplashGradientTitle', () => {
79+
beforeEach(() => {
80+
jest.mocked(useWindowDimensions).mockReturnValue({
81+
width: 375,
82+
height: 812,
83+
scale: 2,
84+
fontScale: 1,
85+
});
86+
});
87+
6188
it('renders the shared splash title with the provided testID', () => {
6289
const { getAllByText, getByTestId } = render(
6390
<VipSplashGradientTitle testID="vip-splash-title" />,
@@ -66,4 +93,41 @@ describe('VipSplashGradientTitle', () => {
6693
expect(getByTestId('vip-splash-title')).toBeOnTheScreen();
6794
expect(getAllByText('WELCOME\nTO GOLD FOX\nCOLLECTIVE')).toHaveLength(2);
6895
});
96+
97+
it('uses the default title font size on larger screens', () => {
98+
const { getByTestId } = render(
99+
<VipSplashGradientTitle testID="vip-splash-title" />,
100+
);
101+
102+
expect(getByTestId('vip-splash-title').props.style).toEqual(
103+
expect.arrayContaining([
104+
expect.objectContaining({
105+
fontSize: VIP_SPLASH_TITLE_FONT_SIZE,
106+
lineHeight: VIP_SPLASH_TITLE_FONT_SIZE,
107+
}),
108+
]),
109+
);
110+
});
111+
112+
it('uses the smaller title font size on small screens', () => {
113+
jest.mocked(useWindowDimensions).mockReturnValue({
114+
width: 320,
115+
height: VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES - 1,
116+
scale: 2,
117+
fontScale: 1,
118+
});
119+
120+
const { getByTestId } = render(
121+
<VipSplashGradientTitle testID="vip-splash-title" />,
122+
);
123+
124+
expect(getByTestId('vip-splash-title').props.style).toEqual(
125+
expect.arrayContaining([
126+
expect.objectContaining({
127+
fontSize: VIP_SPLASH_TITLE_FONT_SIZE_SMALL,
128+
lineHeight: VIP_SPLASH_TITLE_FONT_SIZE_SMALL,
129+
}),
130+
]),
131+
);
132+
});
69133
});

app/components/UI/Rewards/components/Vip/VipSplashGradientTitle.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import MaskedView from '@react-native-masked-view/masked-view';
22
import React from 'react';
3+
import { useWindowDimensions } from 'react-native';
34
import LinearGradient from 'react-native-linear-gradient';
45
import { Text, TextVariant } from '@metamask/design-system-react-native';
56
import { useTailwind } from '@metamask/design-system-twrnc-preset';
67
import { strings } from '../../../../../../locales/i18n';
7-
import { VIP_SPLASH_TITLE_GRADIENT_COLORS } from './Vip.constants';
8+
import {
9+
VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES,
10+
VIP_SPLASH_TITLE_FONT_SIZE,
11+
VIP_SPLASH_TITLE_FONT_SIZE_SMALL,
12+
VIP_SPLASH_TITLE_GRADIENT_COLORS,
13+
} from './Vip.constants';
814

915
const titleColorStyle = { color: VIP_SPLASH_TITLE_GRADIENT_COLORS[0] };
1016
const titleFontStyle = {
@@ -22,11 +28,20 @@ const VipSplashGradientTitle: React.FC<VipSplashGradientTitleProps> = ({
2228
testID,
2329
}) => {
2430
const tw = useTailwind();
31+
const { height: screenHeight } = useWindowDimensions();
2532
const title = strings('rewards.vip.splash_title');
33+
const titleFontSize =
34+
screenHeight < VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES
35+
? VIP_SPLASH_TITLE_FONT_SIZE_SMALL
36+
: VIP_SPLASH_TITLE_FONT_SIZE;
2637
const titleStyle = tw.style(
27-
'text-center text-[38px] leading-[38px] pt-[6px]',
38+
'text-center pt-[6px]',
2839
titleFontStyle,
2940
titleColorStyle,
41+
{
42+
fontSize: titleFontSize,
43+
lineHeight: titleFontSize,
44+
},
3045
);
3146

3247
return (

app/components/UI/Rewards/components/Vip/VipSplashScreenLayout.test.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import React from 'react';
2+
import { useWindowDimensions, Text } from 'react-native';
23
import { fireEvent, render } from '@testing-library/react-native';
3-
import { Text } from 'react-native';
44
import VipSplashScreenLayout, {
55
VIP_SPLASH_SCREEN_TEST_IDS,
66
} from './VipSplashScreenLayout';
7+
import {
8+
VIP_SPLASH_FOX_HEIGHT,
9+
VIP_SPLASH_FOX_WIDTH,
10+
VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES,
11+
} from './Vip.constants';
12+
13+
jest.mock('react-native/Libraries/Utilities/useWindowDimensions', () => ({
14+
__esModule: true,
15+
default: jest.fn(() => ({
16+
width: 375,
17+
height: 812,
18+
scale: 2,
19+
fontScale: 1,
20+
})),
21+
}));
722

823
jest.mock('@metamask/design-system-react-native', () => {
924
const ReactActual = jest.requireActual('react');
@@ -105,6 +120,12 @@ const defaultProps = {
105120
describe('VipSplashScreenLayout', () => {
106121
beforeEach(() => {
107122
jest.clearAllMocks();
123+
jest.mocked(useWindowDimensions).mockReturnValue({
124+
width: 375,
125+
height: 812,
126+
scale: 2,
127+
fontScale: 1,
128+
});
108129
});
109130

110131
it('renders the shared splash content and primary button label', () => {
@@ -154,4 +175,50 @@ describe('VipSplashScreenLayout', () => {
154175
expect(getByTestId('vip-splash-footer')).toBeOnTheScreen();
155176
expect(getByText('Referred by TESTCODE')).toBeOnTheScreen();
156177
});
178+
179+
it('uses the default fox image dimensions on larger screens', () => {
180+
const { getByTestId } = render(<VipSplashScreenLayout {...defaultProps} />);
181+
182+
expect(getByTestId(VIP_SPLASH_SCREEN_TEST_IDS.FOX).props).toMatchObject({
183+
width: VIP_SPLASH_FOX_WIDTH,
184+
height: VIP_SPLASH_FOX_HEIGHT,
185+
});
186+
});
187+
188+
it('scales the fox image down on small screens', () => {
189+
jest.mocked(useWindowDimensions).mockReturnValue({
190+
width: 320,
191+
height: VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES - 1,
192+
scale: 2,
193+
fontScale: 1,
194+
});
195+
196+
const { getByTestId } = render(<VipSplashScreenLayout {...defaultProps} />);
197+
const expectedWidth = 260;
198+
199+
expect(getByTestId(VIP_SPLASH_SCREEN_TEST_IDS.FOX).props).toMatchObject({
200+
width: expectedWidth,
201+
height: Math.round(
202+
expectedWidth * (VIP_SPLASH_FOX_HEIGHT / VIP_SPLASH_FOX_WIDTH),
203+
),
204+
});
205+
});
206+
207+
it('uses compact button heights on small screens', () => {
208+
jest.mocked(useWindowDimensions).mockReturnValue({
209+
width: 320,
210+
height: VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES - 1,
211+
scale: 2,
212+
fontScale: 1,
213+
});
214+
215+
const { getByTestId } = render(<VipSplashScreenLayout {...defaultProps} />);
216+
217+
expect(
218+
getByTestId(VIP_SPLASH_SCREEN_TEST_IDS.ACCEPT_BUTTON).props.style,
219+
).toEqual(expect.arrayContaining(['h-10']));
220+
expect(
221+
getByTestId(VIP_SPLASH_SCREEN_TEST_IDS.NOT_NOW_BUTTON).props.style,
222+
).toEqual(expect.arrayContaining(['h-8']));
223+
});
157224
});

app/components/UI/Rewards/components/Vip/VipSplashScreenLayout.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { type ReactNode } from 'react';
2-
import { Image, Pressable } from 'react-native';
1+
import React, { type ReactNode, useMemo } from 'react';
2+
import { Image, Pressable, useWindowDimensions } from 'react-native';
33
import LinearGradient from 'react-native-linear-gradient';
44
import { SafeAreaView } from 'react-native-safe-area-context';
55
import {
@@ -17,6 +17,9 @@ import {
1717
VIP_GOLD_TEXT_MUTED,
1818
VIP_SPLASH_ACCEPT_BUTTON_BACKGROUND,
1919
VIP_SPLASH_BACKGROUND_GRADIENT_COLORS,
20+
VIP_SPLASH_FOX_HEIGHT,
21+
VIP_SPLASH_FOX_WIDTH,
22+
VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES,
2023
} from './Vip.constants';
2124
import VipSplashGradientTitle from './VipSplashGradientTitle';
2225

@@ -72,6 +75,22 @@ const VipSplashScreenLayout: React.FC<VipSplashScreenLayoutProps> = ({
7275
footerContent,
7376
}) => {
7477
const tw = useTailwind();
78+
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
79+
const isSmallScreen =
80+
screenHeight < VIP_SPLASH_MIN_SCREEN_HEIGHT_FOR_SMALL_STYLES;
81+
const foxDimensions = useMemo(() => {
82+
if (!isSmallScreen) {
83+
return { width: VIP_SPLASH_FOX_WIDTH, height: VIP_SPLASH_FOX_HEIGHT };
84+
}
85+
86+
const foxWidth = Math.min(screenWidth - 32, 260);
87+
return {
88+
width: foxWidth,
89+
height: Math.round(
90+
foxWidth * (VIP_SPLASH_FOX_HEIGHT / VIP_SPLASH_FOX_WIDTH),
91+
),
92+
};
93+
}, [isSmallScreen, screenWidth]);
7594

7695
return (
7796
<LinearGradient
@@ -97,20 +116,23 @@ const VipSplashScreenLayout: React.FC<VipSplashScreenLayoutProps> = ({
97116
</Text>
98117
<Image
99118
source={VipSplashFox}
100-
style={tw.style('mt-[54px]')}
119+
style={tw.style(isSmallScreen ? 'mt-6' : 'mt-[54px]')}
101120
testID={testIDs.fox}
102-
width={320}
103-
height={381}
121+
width={foxDimensions.width}
122+
height={foxDimensions.height}
104123
/>
105124
</Box>
106125

107-
<Box twClassName="gap-[18px] px-4 pb-[18px]">
126+
<Box
127+
twClassName={`px-4 ${isSmallScreen ? 'gap-3 pb-3' : 'gap-[18px] pb-[18px]'}`}
128+
>
108129
{footerContent}
109130
<Pressable
110131
accessibilityRole="button"
111132
onPress={onPrimaryPress}
112133
style={tw.style(
113-
'h-12 items-center justify-center rounded-[10px] border',
134+
'items-center justify-center rounded-[10px] border',
135+
isSmallScreen ? 'h-10' : 'h-12',
114136
primaryButtonBorderStyle,
115137
primaryButtonBackgroundStyle,
116138
)}
@@ -127,7 +149,10 @@ const VipSplashScreenLayout: React.FC<VipSplashScreenLayoutProps> = ({
127149
<Pressable
128150
accessibilityRole="button"
129151
onPress={onNotNow}
130-
style={tw.style('h-9 items-center justify-center')}
152+
style={tw.style(
153+
'items-center justify-center',
154+
isSmallScreen ? 'h-8' : 'h-9',
155+
)}
131156
testID={testIDs.notNowButton}
132157
>
133158
<Text

0 commit comments

Comments
 (0)