diff --git a/app/components/UI/Card/Views/CardWelcome/CardWelcome.styles.ts b/app/components/UI/Card/Views/CardWelcome/CardWelcome.styles.ts index 8f2771b8dd6..8c99821fef3 100644 --- a/app/components/UI/Card/Views/CardWelcome/CardWelcome.styles.ts +++ b/app/components/UI/Card/Views/CardWelcome/CardWelcome.styles.ts @@ -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), }, }); diff --git a/app/components/UI/Card/Views/CardWelcome/CardWelcome.test.tsx b/app/components/UI/Card/Views/CardWelcome/CardWelcome.test.tsx index 7cac336f11f..86d686b565d 100644 --- a/app/components/UI/Card/Views/CardWelcome/CardWelcome.test.tsx +++ b/app/components/UI/Card/Views/CardWelcome/CardWelcome.test.tsx @@ -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'); @@ -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 = { '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' } } }), @@ -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: [] }); }); @@ -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( @@ -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( , ); - 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( , ); - 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( , ); - 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( - - - , + 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( @@ -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, + ); }); }); }); diff --git a/app/components/UI/Card/Views/CardWelcome/CardWelcome.tsx b/app/components/UI/Card/Views/CardWelcome/CardWelcome.tsx index e351f7c0fe3..22ea8d30001 100644 --- a/app/components/UI/Card/Views/CardWelcome/CardWelcome.tsx +++ b/app/components/UI/Card/Views/CardWelcome/CardWelcome.tsx @@ -1,6 +1,6 @@ import { useNavigation } from '@react-navigation/native'; -import React, { useCallback, useMemo, useEffect } from 'react'; -import { Image, useWindowDimensions, View } from 'react-native'; +import React, { useCallback, useEffect } from 'react'; +import { Image, View } from 'react-native'; import { strings } from '../../../../../../locales/i18n'; import Button, { @@ -9,10 +9,9 @@ import Button, { ButtonWidthTypes, } from '../../../../../component-library/components/Buttons/Button'; import Text, { - TextColor, TextVariant, } from '../../../../../component-library/components/Texts/Text'; -import MM_CARDS_MOCKUP from '../../../../../images/mm-cards-mockup.png'; +import MM_CARDS_WELCOME from '../../../../../images/mm-card-welcome.png'; import { useTheme } from '../../../../../util/theme'; import createStyles from './CardWelcome.styles'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -22,14 +21,14 @@ import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; import { CardActions, CardScreens } from '../../util/metrics'; import { selectHasCardholderAccounts } from '../../../../../core/redux/slices/card'; import { useSelector } from 'react-redux'; +import ButtonBase from '../../../../../component-library/components/Buttons/Button/foundation/ButtonBase'; const CardWelcome = () => { const { trackEvent, createEventBuilder } = useMetrics(); const { navigate } = useNavigation(); const hasCardholderAccounts = useSelector(selectHasCardholderAccounts); const theme = useTheme(); - const deviceWidth = useWindowDimensions().width; - const styles = createStyles(theme, deviceWidth); + const styles = createStyles(theme); useEffect(() => { trackEvent( @@ -41,25 +40,9 @@ const CardWelcome = () => { ); }, [trackEvent, createEventBuilder]); - const cardWelcomeCopies = useMemo(() => { - if (hasCardholderAccounts) { - return { - title: strings('card.card_onboarding.title'), - description: strings('card.card_onboarding.description'), - verify_account_button: strings( - 'card.card_onboarding.verify_account_button', - ), - }; - } - - return { - title: strings('card.card_onboarding.non_cardholder_title'), - description: strings('card.card_onboarding.non_cardholder_description'), - verify_account_button: strings( - 'card.card_onboarding.non_cardholder_verify_account_button', - ), - }; - }, [hasCardholderAccounts]); + const handleClose = useCallback(() => { + navigate(Routes.WALLET.HOME); + }, [navigate]); const handleButtonPress = useCallback(() => { trackEvent( @@ -78,43 +61,74 @@ const CardWelcome = () => { }, [hasCardholderAccounts, navigate, trackEvent, createEventBuilder]); return ( - - - - - - + + + {/* Header Section */} + - {cardWelcomeCopies.title} + {strings('card.card_onboarding.title')} - {cardWelcomeCopies.description} + {strings('card.card_onboarding.description')} + -