diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 8bc400aa92a..c389cbeb637 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -35,6 +35,8 @@ ignores: # ESBuild is used for AI E2E script compilation - 'esbuild' - 'esbuild-register' + # xml2js is used in .github/scripts/ for E2E test report processing + - 'xml2js' # Used in scripts/repack for CI optimization - '@expo/repack-app' @@ -44,19 +46,11 @@ ignores: ## Unused dependencies to investigate - '@babel/preset-env' - '@babel/runtime' - - '@cucumber/message-streams' - - '@cucumber/messages' - '@metamask/mobile-provider' - - '@rpii/wdio-html-reporter' - '@testing-library/react' - '@testing-library/react-hooks' - '@types/jest' - '@types/react-native-video' - - '@wdio/appium-service' - - '@wdio/browserstack-service' - - '@wdio/junit-reporter' - - '@wdio/local-runner' - - '@wdio/spec-reporter' - 'appium' - 'assert' - 'babel-core' @@ -68,7 +62,6 @@ ignores: - 'execa' - 'jetifier' - 'metro-react-native-babel-preset' - - 'prettier-plugin-gherkin' - 'react-native-svg-asset-plugin' - 'regenerator-runtime' - 'prettier-2' diff --git a/.github/workflows/needs-e2e-build.yml b/.github/workflows/needs-e2e-build.yml index 77df2ca36d5..035619cd97c 100644 --- a/.github/workflows/needs-e2e-build.yml +++ b/.github/workflows/needs-e2e-build.yml @@ -98,8 +98,6 @@ jobs: - 'app/**' # Shared app files - 'e2e/**' # E2E test files (separate from mobile builds) - 'sentry*.properties*' # Sentry configs - - 'wdio/**' # WebDriver test files - - 'wdio.conf.js' # WebDriver config - 'appwright/**' # Appwright test files - 'scripts/build.sh' # Build script changes - 'scripts/setup.mjs' # Setup script changes diff --git a/.yarn/patches/@metamask-assets-controllers-npm-92.0.0-ea998cb0bd.patch b/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch similarity index 100% rename from .yarn/patches/@metamask-assets-controllers-npm-92.0.0-ea998cb0bd.patch rename to .yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.styles.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.styles.tsx index 2d4b7023f4e..94c39b9a09d 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.styles.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.styles.tsx @@ -6,7 +6,7 @@ const styleSheet = (params: { theme: Theme }) => { const { colors } = theme; return StyleSheet.create({ tokenDetailsContainer: { - marginTop: 24, + marginTop: 16, gap: 24, }, contentWrapper: { diff --git a/app/components/UI/AssetOverview/TokenDetails/__snapshots__/TokenDetails.test.tsx.snap b/app/components/UI/AssetOverview/TokenDetails/__snapshots__/TokenDetails.test.tsx.snap index efa97f86db5..22c68e0ca69 100644 --- a/app/components/UI/AssetOverview/TokenDetails/__snapshots__/TokenDetails.test.tsx.snap +++ b/app/components/UI/AssetOverview/TokenDetails/__snapshots__/TokenDetails.test.tsx.snap @@ -5,7 +5,7 @@ exports[`TokenDetails should render correctly 1`] = ` style={ { "gap": 24, - "marginTop": 24, + "marginTop": 16, } } > diff --git a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap index 1b81b50867e..85b9d2cd203 100644 --- a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap +++ b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap @@ -860,7 +860,7 @@ exports[`AssetOverview should render native balances when non evm network is sel style={ { "gap": 24, - "marginTop": 24, + "marginTop": 16, } } > diff --git a/app/components/UI/Bridge/components/TransactionDetails/BridgeStepDescription.test.tsx b/app/components/UI/Bridge/components/TransactionDetails/BridgeStepDescription.test.tsx index f5558c31627..1a668267ffe 100644 --- a/app/components/UI/Bridge/components/TransactionDetails/BridgeStepDescription.test.tsx +++ b/app/components/UI/Bridge/components/TransactionDetails/BridgeStepDescription.test.tsx @@ -12,6 +12,7 @@ import { TransactionStatus, CHAIN_IDS, } from '@metamask/transaction-controller'; +import { fontStyles } from '../../../../../styles/common'; describe('BridgeStepDescription', () => { const mockStep = { @@ -128,7 +129,7 @@ describe('BridgeStepDescription', () => { const textElement = getByText(/ETH/); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Medium', + fontStyles.medium.fontFamily, ); }); @@ -157,7 +158,7 @@ describe('BridgeStepDescription', () => { expect(textElement.props.style).toHaveProperty('color', '#121314'); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Regular', + fontStyles.normal.fontFamily, ); }); @@ -175,7 +176,7 @@ describe('BridgeStepDescription', () => { expect(textElement.props.style).toHaveProperty('color', '#121314'); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Regular', + fontStyles.normal.fontFamily, ); }); @@ -194,7 +195,7 @@ describe('BridgeStepDescription', () => { expect(textElement.props.style).toHaveProperty('color', '#121314'); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Regular', + fontStyles.normal.fontFamily, ); }); @@ -215,7 +216,7 @@ describe('BridgeStepDescription', () => { expect(textElement.props.style).toHaveProperty('color', '#121314'); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Medium', + fontStyles.medium.fontFamily, ); }); @@ -236,7 +237,7 @@ describe('BridgeStepDescription', () => { expect(textElement.props.style).toHaveProperty('color', '#121314'); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Medium', + fontStyles.medium.fontFamily, ); }); @@ -285,7 +286,7 @@ describe('BridgeStepDescription', () => { expect(textElement.props.style).toHaveProperty('color', '#686e7d'); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Regular', + fontStyles.normal.fontFamily, ); }); @@ -299,7 +300,7 @@ describe('BridgeStepDescription', () => { expect(textElement.props.style).toHaveProperty('color', '#686e7d'); expect(textElement.props.style).toHaveProperty( 'fontFamily', - 'Geist Regular', + fontStyles.normal.fontFamily, ); }); diff --git a/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.styles.ts b/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.styles.ts index 7b30e08cdc1..0f2ebbc2001 100644 --- a/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.styles.ts +++ b/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.styles.ts @@ -1,18 +1,25 @@ import { StyleSheet } from 'react-native'; import { Theme } from '../../../../../util/theme/models'; -const styleSheet = (params: { theme: Theme }) => - StyleSheet.create({ +const styleSheet = (params: { + theme: Theme; + vars: { userHasLendingPositions: boolean }; +}) => { + const { vars, theme } = params; + const { userHasLendingPositions } = vars; + + return StyleSheet.create({ container: { flexDirection: 'row', justifyContent: 'space-between', + paddingTop: 14, gap: 16, }, buttonsContainer: { marginTop: 16, padding: 16, borderRadius: 12, - backgroundColor: params.theme.colors.background.section, + backgroundColor: theme.colors.background.section, }, button: { flex: 1, @@ -26,19 +33,18 @@ const styleSheet = (params: { theme: Theme }) => marginLeft: 16, alignSelf: 'center', }, - ethLogo: { - width: 32, - height: 32, - borderRadius: 16, - overflow: 'hidden', + musdConversionCta: { + paddingTop: 16, + paddingBottom: userHasLendingPositions ? 8 : 0, }, EarnEmptyStateCta: { - paddingTop: 8, + paddingTop: 16, }, earnings: { paddingHorizontal: 16, paddingTop: 16, }, }); +}; export default styleSheet; diff --git a/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.test.tsx b/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.test.tsx index e4a77e0a45e..54d3ad26757 100644 --- a/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.test.tsx +++ b/app/components/UI/Earn/components/EarnLendingBalance/EarnLendingBalance.test.tsx @@ -11,6 +11,7 @@ import { useTokenPricePercentageChange } from '../../../Tokens/hooks/useTokenPri import { TokenI } from '../../../Tokens/types'; import { EARN_EXPERIENCES } from '../../constants/experiences'; import { + selectIsMusdConversionFlowEnabledFlag, selectPooledStakingEnabledFlag, selectPooledStakingServiceInterruptionBannerEnabledFlag, selectStablecoinLendingEnabledFlag, @@ -18,6 +19,8 @@ import { } from '../../selectors/featureFlags'; import { EarnTokenDetails } from '../../types/lending.types'; import { EARN_EMPTY_STATE_CTA_TEST_ID } from '../EmptyStateCta'; +import { useMusdConversionTokens } from '../../hooks/useMusdConversionTokens'; +import { EARN_TEST_IDS } from '../../constants/testIds'; const mockNavigate = jest.fn(); const mockDaiMainnet: EarnTokenDetails = { @@ -121,7 +124,15 @@ jest.mock('../../hooks/useEarnings', () => ({ jest.mock('../../hooks/useEarnTokens'); jest.mock('../../../Tokens/hooks/useTokenPricePercentageChange'); +jest.mock('../../hooks/useMusdConversionTokens', () => ({ + __esModule: true, + useMusdConversionTokens: jest.fn().mockReturnValue({ + isConversionToken: jest.fn().mockReturnValue(false), + }), +})); + jest.mock('../../selectors/featureFlags', () => ({ + selectIsMusdConversionFlowEnabledFlag: jest.fn(), selectPooledStakingEnabledFlag: jest.fn(), selectStablecoinLendingEnabledFlag: jest.fn(), selectStablecoinLendingServiceInterruptionBannerEnabledFlag: jest.fn(), @@ -161,6 +172,12 @@ describe('EarnLendingBalance', () => { beforeEach(() => { jest.clearAllMocks(); + ( + selectIsMusdConversionFlowEnabledFlag as jest.MockedFunction< + typeof selectIsMusdConversionFlowEnabledFlag + > + ).mockReturnValue(false); + ( selectStablecoinLendingEnabledFlag as jest.MockedFunction< typeof selectStablecoinLendingEnabledFlag @@ -441,4 +458,114 @@ describe('EarnLendingBalance', () => { expect(toJSON()).toMatchSnapshot(); }); + + it('hides mUSD conversion CTA when feature flag is disabled', () => { + ( + selectIsMusdConversionFlowEnabledFlag as jest.MockedFunction< + typeof selectIsMusdConversionFlowEnabledFlag + > + ).mockReturnValue(false); + + ( + useMusdConversionTokens as jest.MockedFunction< + typeof useMusdConversionTokens + > + ).mockReturnValue({ + isConversionToken: jest.fn().mockReturnValue(true), + tokenFilter: jest.fn().mockReturnValue([]), + tokens: [], + }); + + const { queryByTestId } = renderWithProvider( + , + { state: mockInitialState }, + ); + + expect( + queryByTestId(EARN_TEST_IDS.MUSD.ASSET_OVERVIEW_CONVERSION_CTA), + ).toBeNull(); + }); + + it('hides mUSD conversion CTA when asset is not a conversion token', () => { + ( + selectIsMusdConversionFlowEnabledFlag as jest.MockedFunction< + typeof selectIsMusdConversionFlowEnabledFlag + > + ).mockReturnValue(true); + + ( + useMusdConversionTokens as jest.MockedFunction< + typeof useMusdConversionTokens + > + ).mockReturnValue({ + isConversionToken: jest.fn().mockReturnValue(false), + tokenFilter: jest.fn().mockReturnValue([]), + tokens: [], + }); + + const { queryByTestId } = renderWithProvider( + , + { state: mockInitialState }, + ); + + expect( + queryByTestId(EARN_TEST_IDS.MUSD.ASSET_OVERVIEW_CONVERSION_CTA), + ).toBeNull(); + }); + + it('favors mUSD conversion CTA over lending empty state CTA when both conditions are met', () => { + const mockEmptyReceiptToken = { + ...mockADAIMainnet, + balanceMinimalUnit: '0', + balanceFormatted: '0 ADAI', + balanceFiatNumber: 0, + }; + + ( + selectIsMusdConversionFlowEnabledFlag as jest.MockedFunction< + typeof selectIsMusdConversionFlowEnabledFlag + > + ).mockReturnValue(true); + + ( + useMusdConversionTokens as jest.MockedFunction< + typeof useMusdConversionTokens + > + ).mockReturnValue({ + isConversionToken: jest.fn().mockReturnValue(true), + tokenFilter: jest.fn().mockReturnValue([]), + tokens: [], + }); + + ( + earnSelectors.selectEarnToken as jest.MockedFunction< + typeof earnSelectors.selectEarnToken + > + ).mockReturnValue(mockDaiMainnet); + + ( + earnSelectors.selectEarnOutputToken as jest.MockedFunction< + typeof earnSelectors.selectEarnOutputToken + > + ).mockReturnValue(undefined); + + ( + earnSelectors.selectEarnTokenPair as jest.MockedFunction< + typeof earnSelectors.selectEarnTokenPair + > + ).mockReturnValue({ + outputToken: mockEmptyReceiptToken, + earnToken: mockDaiMainnet, + }); + + const { getByTestId, queryByTestId } = renderWithProvider( + , + { state: mockInitialState }, + ); + + expect( + getByTestId(EARN_TEST_IDS.MUSD.ASSET_OVERVIEW_CONVERSION_CTA), + ).toBeOnTheScreen(); + expect(queryByTestId(EARN_EMPTY_STATE_CTA_TEST_ID)).toBeNull(); + }); }); diff --git a/app/components/UI/Earn/components/EarnLendingBalance/__snapshots__/EarnLendingBalance.test.tsx.snap b/app/components/UI/Earn/components/EarnLendingBalance/__snapshots__/EarnLendingBalance.test.tsx.snap index 2303e80d5eb..a0e40a85bbc 100644 --- a/app/components/UI/Earn/components/EarnLendingBalance/__snapshots__/EarnLendingBalance.test.tsx.snap +++ b/app/components/UI/Earn/components/EarnLendingBalance/__snapshots__/EarnLendingBalance.test.tsx.snap @@ -9,6 +9,7 @@ exports[`EarnLendingBalance does renders earnings for output tokens 1`] = ` "flexDirection": "row", "gap": 16, "justifyContent": "space-between", + "paddingTop": 14, }, { "backgroundColor": "#f3f5f9", @@ -288,10 +289,10 @@ exports[`EarnLendingBalance renders balance and buttons when user has lending po style={ { "backgroundColor": "#ffffff", - "borderRadius": 16, - "height": 32, + "borderRadius": 20, + "height": 40, "overflow": "hidden", - "width": 32, + "width": 40, } } testID="receipt-token-balance-asset-logo" @@ -484,6 +485,7 @@ exports[`EarnLendingBalance renders balance and buttons when user has lending po "flexDirection": "row", "gap": 16, "justifyContent": "space-between", + "paddingTop": 14, }, { "backgroundColor": "#f3f5f9", diff --git a/app/components/UI/Earn/components/EarnLendingBalance/index.tsx b/app/components/UI/Earn/components/EarnLendingBalance/index.tsx index 670eac0a82c..1ae6b64c5d8 100644 --- a/app/components/UI/Earn/components/EarnLendingBalance/index.tsx +++ b/app/components/UI/Earn/components/EarnLendingBalance/index.tsx @@ -33,12 +33,16 @@ import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance'; import { useTokenPricePercentageChange } from '../../../Tokens/hooks/useTokenPricePercentageChange'; import { TokenI } from '../../../Tokens/types'; import { EARN_EXPERIENCES } from '../../constants/experiences'; -import { selectStablecoinLendingEnabledFlag } from '../../selectors/featureFlags'; +import { + selectIsMusdConversionFlowEnabledFlag, + selectStablecoinLendingEnabledFlag, +} from '../../selectors/featureFlags'; import Earnings from '../Earnings'; import EarnEmptyStateCta from '../EmptyStateCta'; import styleSheet from './EarnLendingBalance.styles'; import { trace, TraceName } from '../../../../../util/trace'; -import { useTheme } from '../../../../../util/theme'; +import { useMusdConversionTokens } from '../../hooks/useMusdConversionTokens'; +import MusdConversionAssetOverviewCta from '../Musd/MusdConversionAssetOverviewCta'; export const EARN_LENDING_BALANCE_TEST_IDS = { RECEIPT_TOKEN_BALANCE_ASSET_LOGO: 'receipt-token-balance-asset-logo', @@ -53,9 +57,13 @@ export interface EarnLendingBalanceProps { const { selectEarnTokenPair, selectEarnOutputToken } = earnSelectors; const EarnLendingBalance = ({ asset }: EarnLendingBalanceProps) => { + const isMusdConversionFlowEnabled = useSelector( + selectIsMusdConversionFlowEnabledFlag, + ); + + const { isConversionToken } = useMusdConversionTokens(); + const { trackEvent, createEventBuilder } = useMetrics(); - const theme = useTheme(); - const { styles } = useStyles(styleSheet, { theme }); const networkConfigurationByChainId = useSelector((state: RootState) => selectNetworkConfigurationByChainId(state, asset.chainId as Hex), @@ -89,6 +97,10 @@ const EarnLendingBalance = ({ asset }: EarnLendingBalanceProps) => { [earnToken?.balanceMinimalUnit], ); + const { styles } = useStyles(styleSheet, { + userHasLendingPositions, + }); + const emitLendingActionButtonMetaMetric = ( action: 'deposit' | 'withdrawal', ) => { @@ -166,6 +178,32 @@ const EarnLendingBalance = ({ asset }: EarnLendingBalanceProps) => { if (!isStablecoinLendingEnabled) return null; + const renderCta = () => { + // Favour the mUSD Conversion CTA over the lending empty state CTA + const shouldRenderMusdConversionAssetOverviewCta = + isMusdConversionFlowEnabled && isConversionToken(asset); + + if (shouldRenderMusdConversionAssetOverviewCta) { + return ( + + + + ); + } + + const shouldRenderLendingEmptyStateCta = + !isAssetReceiptToken && !userHasLendingPositions; + + if (shouldRenderLendingEmptyStateCta) { + return ( + + + + ); + } + return null; + }; + return ( // Receipt Token Balance @@ -194,7 +232,7 @@ const EarnLendingBalance = ({ asset }: EarnLendingBalanceProps) => { { )} - {/* Empty State CTA */} - {!isAssetReceiptToken && !userHasLendingPositions && ( - - - - )} + {renderCta()} {/* Buttons */} - - {userHasLendingPositions && receiptToken && ( -