Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
122 changes: 106 additions & 16 deletions app/components/UI/Card/Views/CardWelcome/CardWelcome.styles.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,120 @@
import { StyleSheet } from 'react-native';
import { Platform, StyleSheet, Dimensions } from 'react-native';
import { colors as importedColors } from '../../../../../styles/common';
import { Theme } from '@metamask/design-tokens';

const createStyles = (theme: Theme, deviceWidth: number) =>
// Responsive scaling utilities
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');

// Platform-specific base dimensions
const BASE_WIDTH = 375;
const BASE_HEIGHT_IOS = 812; // iPhone X/11/12/13/14/15 Pro base
const BASE_HEIGHT_ANDROID = 736; // Common Android base

const MIN_SCREEN_HEIGHT_FOR_SMALL_SCREEN_STYLES = 750;

// Calculate platform-aware scaling factors
const isIOS = Platform.OS === 'ios';
const baseHeight = isIOS ? BASE_HEIGHT_IOS : BASE_HEIGHT_ANDROID;

const widthScale = screenWidth / BASE_WIDTH;
const heightScale = screenHeight / baseHeight;

// Use more conservative scaling to prevent excessive padding
const scale = Math.min(widthScale, heightScale);
const conservativeScale = Math.min(scale, 1.2); // Cap scaling at 120%

// Platform-aware responsive scaling functions
const scaleSize = (size: number) => Math.ceil(size * conservativeScale);
const scaleFont = (size: number) => Math.ceil(size * conservativeScale);

// For vertical spacing, use percentage of available height instead of pure scaling
const scaleVertical = (size: number) => {
// Use percentage of screen height for more consistent spacing
const percentage = size / baseHeight;
return Math.ceil(screenHeight * percentage);
};

const scaleHorizontal = (size: number) => Math.ceil(size * widthScale);

const createStyles = (theme: Theme) =>
StyleSheet.create({
safeAreaView: {
pageContainer: {
flex: 1,
position: 'relative',
maxHeight: '100%',
width: '100%',
backgroundColor: theme.colors.accent03.dark,
},
container: {
imageContainer: {
flex: 1,
backgroundColor: theme.colors.background.default,
paddingHorizontal: 16,
justifyContent: 'space-between',
},
wrapper: {
alignItems: 'center',
justifyContent: 'center',
marginBottom: scaleVertical(16),
},
imageWrapper: {
image: {
width: '100%',
height: '100%',
resizeMode: 'cover',
},
contentContainer: {
flex: 1,
},
headerContainer: {
alignItems: 'center',
paddingHorizontal: scaleHorizontal(16),
paddingVertical: scaleVertical(16),
},
image: {
width: deviceWidth * 0.9,
height: deviceWidth,
title: {
fontFamily: Platform.OS === 'ios' ? 'MM Poly' : 'MM Poly Regular',
fontWeight: '400',
// make it smaller on smaller screens
fontSize:
screenHeight < MIN_SCREEN_HEIGHT_FOR_SMALL_SCREEN_STYLES ? 40 : 50,
lineHeight:
screenHeight < MIN_SCREEN_HEIGHT_FOR_SMALL_SCREEN_STYLES ? 40 : 50, // 100% of font size
letterSpacing: 0,
textAlign: 'center',
paddingTop: scaleVertical(
screenHeight < MIN_SCREEN_HEIGHT_FOR_SMALL_SCREEN_STYLES ? 8 : 12,
),
color: theme.colors.accent03.light,
},
titleDescription: {
// make it smaller on smaller screens
fontSize:
screenHeight < MIN_SCREEN_HEIGHT_FOR_SMALL_SCREEN_STYLES ? 14 : 16,
paddingTop: scaleVertical(10),
paddingHorizontal: scaleHorizontal(8),
textAlign: 'center',
fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', // Default system font
fontWeight: '500',
lineHeight: 24, // Line Height BodyMd
letterSpacing: 0,
color: theme.colors.accent03.light,
},
footerContainer: {
display: 'flex',
rowGap: scaleVertical(8),
paddingHorizontal: scaleHorizontal(30),
},
getStartedButton: {
borderRadius: scaleSize(12),
backgroundColor: importedColors.white,
},
getStartedButtonText: {
color: importedColors.btnBlack,
fontWeight: '600',
fontSize: scaleFont(16),
},
notNowButton: {
borderRadius: scaleSize(12),
backgroundColor: importedColors.transparent,
borderWidth: 0,
},
button: {
marginTop: 48,
marginBottom: 32,
notNowButtonText: {
color: importedColors.white,
fontWeight: '500',
fontSize: scaleFont(16),
},
});

Expand Down
99 changes: 55 additions & 44 deletions app/components/UI/Card/Views/CardWelcome/CardWelcome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ import CardWelcome from './CardWelcome';
import { CardWelcomeSelectors } from '../../../../../../e2e/selectors/Card/CardWelcome.selectors';
import { strings } from '../../../../../../locales/i18n';
import Routes from '../../../../../constants/navigation/Routes';
import { MetaMetricsEvents } from '../../../../hooks/useMetrics';

// Mocks
const mockNavigate = jest.fn();
const mockTrackEvent = jest.fn();
const mockBuild = jest.fn();
const mockAddProperties = jest.fn(() => ({ build: mockBuild }));
const mockCreateEventBuilder = jest.fn(() => ({
addProperties: mockAddProperties,
}));

jest.mock('@react-navigation/native', () => {
const actual = jest.requireActual('@react-navigation/native');
Expand All @@ -20,25 +27,31 @@ jest.mock('@react-navigation/native', () => {
};
});

jest.mock('../../../../hooks/useMetrics', () => ({
useMetrics: () => ({
trackEvent: mockTrackEvent,
createEventBuilder: mockCreateEventBuilder,
}),
MetaMetricsEvents: {
CARD_VIEWED: 'Card Viewed',
CARD_BUTTON_CLICKED: 'Card Button Clicked',
},
}));

jest.mock('../../../../../../locales/i18n', () => ({
strings: (key: string) => {
const map: Record<string, string> = {
'card.card_onboarding.title': 'Enable MetaMask Card features',
'card.card_onboarding.description':
'Change your spending token and network by signing in with your Crypto Life email and password.',
'card.card_onboarding.verify_account_button': 'Sign in',
'card.card_onboarding.non_cardholder_title': 'Welcome to MetaMask Card',
'card.card_onboarding.non_cardholder_description':
'MetaMask Card is the free and easy way to spend your crypto, with rich onchain rewards.',
'card.card_onboarding.non_cardholder_verify_account_button':
'Get started',
'card.card': 'Card',
'card.card_onboarding.apply_now_button': 'Sign in',
'predict.gtm_content.not_now': 'Not now',
};
return map[key] || key;
},
}));

jest.mock('../../../../../images/metal-card.png', () => 1);
jest.mock('../../../../../images/mm-card-welcome.png', () => 1);

jest.mock('../../../../../util/theme', () => ({
useTheme: () => ({ colors: { background: { default: '#fff' } } }),
Expand All @@ -57,9 +70,11 @@ describe('CardWelcome', () => {
beforeEach(() => {
jest.clearAllMocks();
mockNavigate.mockClear();
mockTrackEvent.mockClear();
mockCreateEventBuilder.mockClear();
});

describe('Non-cardholder flow', () => {
describe('Render', () => {
beforeEach(() => {
store = createTestStore({ cardholderAccounts: [] });
});
Expand All @@ -81,9 +96,10 @@ describe('CardWelcome', () => {
expect(
getByTestId(CardWelcomeSelectors.VERIFY_ACCOUNT_BUTTON),
).toBeTruthy();
expect(getByTestId('predict-gtm-not-now-button')).toBeTruthy();
});

it('displays non-cardholder title when no cardholder accounts exist', () => {
it('displays correct title and description', () => {
const { getByTestId } = render(
<Provider store={store}>
<CardWelcome />
Expand All @@ -92,69 +108,62 @@ describe('CardWelcome', () => {

expect(
getByTestId(CardWelcomeSelectors.WELCOME_TO_CARD_TITLE_TEXT),
).toHaveTextContent(strings('card.card_onboarding.non_cardholder_title'));
).toHaveTextContent(strings('card.card_onboarding.title'));
expect(
getByTestId(CardWelcomeSelectors.WELCOME_TO_CARD_DESCRIPTION_TEXT),
).toHaveTextContent(strings('card.card_onboarding.description'));
});

it('displays non-cardholder description when no cardholder accounts exist', () => {
const { getByTestId } = render(
it('tracks view event on mount', () => {
render(
<Provider store={store}>
<CardWelcome />
</Provider>,
);

expect(
getByTestId(CardWelcomeSelectors.WELCOME_TO_CARD_DESCRIPTION_TEXT),
).toHaveTextContent(
strings('card.card_onboarding.non_cardholder_description'),
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
MetaMetricsEvents.CARD_VIEWED,
);
expect(mockTrackEvent).toHaveBeenCalled();
});
});

it('navigates to onboarding root when verify account button pressed', () => {
describe('Interactions', () => {
it('navigates to wallet home when "Not Now" is pressed', () => {
store = createTestStore();
const { getByTestId } = render(
<Provider store={store}>
<CardWelcome />
</Provider>,
);

fireEvent.press(getByTestId(CardWelcomeSelectors.VERIFY_ACCOUNT_BUTTON));
fireEvent.press(getByTestId('predict-gtm-not-now-button'));

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.ONBOARDING.ROOT);
expect(mockNavigate).toHaveBeenCalledWith(Routes.WALLET.HOME);
});
});

describe('Cardholder flow', () => {
beforeEach(() => {
store = createTestStore({
cardholderAccounts: ['0x1234567890abcdef'],
});
});

it('displays cardholder title when cardholder accounts exist', () => {
describe('Navigation Flow', () => {
it('navigates to onboarding root when verify account button pressed (Non-cardholder)', () => {
store = createTestStore({ cardholderAccounts: [] });
const { getByTestId } = render(
<Provider store={store}>
<CardWelcome />
</Provider>,
);

expect(
getByTestId(CardWelcomeSelectors.WELCOME_TO_CARD_TITLE_TEXT),
).toHaveTextContent(strings('card.card_onboarding.title'));
});
fireEvent.press(getByTestId(CardWelcomeSelectors.VERIFY_ACCOUNT_BUTTON));

it('displays cardholder description when cardholder accounts exist', () => {
const { getByTestId } = render(
<Provider store={store}>
<CardWelcome />
</Provider>,
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.ONBOARDING.ROOT);
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
MetaMetricsEvents.CARD_BUTTON_CLICKED,
);

expect(
getByTestId(CardWelcomeSelectors.WELCOME_TO_CARD_DESCRIPTION_TEXT),
).toHaveTextContent(strings('card.card_onboarding.description'));
});

it('navigates to authentication when verify account button pressed', () => {
it('navigates to authentication when verify account button pressed (Cardholder)', () => {
store = createTestStore({
cardholderAccounts: ['0x1234567890abcdef'],
});
const { getByTestId } = render(
<Provider store={store}>
<CardWelcome />
Expand All @@ -163,8 +172,10 @@ describe('CardWelcome', () => {

fireEvent.press(getByTestId(CardWelcomeSelectors.VERIFY_ACCOUNT_BUTTON));

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.AUTHENTICATION);
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
MetaMetricsEvents.CARD_BUTTON_CLICKED,
);
});
});
});
Loading
Loading