diff --git a/app/components/Approvals/TransactionApproval/TransactionApproval.test.tsx b/app/components/Approvals/TransactionApproval/TransactionApproval.test.tsx index dd97d0722fb..5fcaf50275d 100644 --- a/app/components/Approvals/TransactionApproval/TransactionApproval.test.tsx +++ b/app/components/Approvals/TransactionApproval/TransactionApproval.test.tsx @@ -7,7 +7,15 @@ import { TransactionApproval, TransactionModalType, } from './TransactionApproval'; - +import { useConfirmationRedesignEnabled } from '../../Views/confirmations/hooks/useConfirmationRedesignEnabled'; +import renderWithProvider from '../../../util/test/renderWithProvider'; + +jest.mock( + '../../Views/confirmations/hooks/useConfirmationRedesignEnabled', + () => ({ + useConfirmationRedesignEnabled: jest.fn(), + }), +); jest.mock('../../Views/confirmations/hooks/useApprovalRequest'); jest.mock('../../UI/QRHardware/withQRHardwareAwareness', () => @@ -30,6 +38,13 @@ const mockApprovalRequest = (approvalRequest?: ApprovalRequest) => { describe('TransactionApproval', () => { beforeEach(() => { jest.resetAllMocks(); + ( + useConfirmationRedesignEnabled as jest.MockedFn< + typeof useConfirmationRedesignEnabled + > + ).mockReturnValue({ + isRedesignedEnabled: false, + }); }); it('renders approval component if transaction type is dapp', () => { @@ -79,9 +94,9 @@ describe('TransactionApproval', () => { it('returns null if no approval request', () => { mockApprovalRequest(undefined); - const wrapper = shallow(); + const { toJSON } = renderWithProvider(, {}); - expect(wrapper).toMatchSnapshot(); + expect(toJSON()).toMatchInlineSnapshot(`null`); }); it('returns null if incorrect approval request type', () => { @@ -91,9 +106,9 @@ describe('TransactionApproval', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - const wrapper = shallow(); + const { toJSON } = renderWithProvider(, {}); - expect(wrapper).toMatchSnapshot(); + expect(toJSON()).toMatchInlineSnapshot(`null`); }); it('returns null if incorrect transaction type', () => { @@ -103,8 +118,34 @@ describe('TransactionApproval', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - const wrapper = shallow(); + const { toJSON } = renderWithProvider( + , + {}, + ); - expect(wrapper).toMatchSnapshot(); + expect(toJSON()).toMatchInlineSnapshot(`null`); + }); + + it('returns null if redesign is enabled', () => { + mockApprovalRequest({ + type: ApprovalTypes.TRANSACTION, + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + ( + useConfirmationRedesignEnabled as jest.MockedFn< + typeof useConfirmationRedesignEnabled + > + ).mockReturnValue({ + isRedesignedEnabled: true, + }); + + const { toJSON } = renderWithProvider( + , + {}, + ); + + expect(toJSON()).toMatchInlineSnapshot(`null`); }); }); diff --git a/app/components/Approvals/TransactionApproval/TransactionApproval.tsx b/app/components/Approvals/TransactionApproval/TransactionApproval.tsx index 9c63edba82c..96055d0e77d 100644 --- a/app/components/Approvals/TransactionApproval/TransactionApproval.tsx +++ b/app/components/Approvals/TransactionApproval/TransactionApproval.tsx @@ -6,6 +6,7 @@ import Approve from '../../Views/confirmations/ApproveView/Approve'; import QRSigningModal from '../../UI/QRHardware/QRSigningModal'; import withQRHardwareAwareness from '../../UI/QRHardware/withQRHardwareAwareness'; import { IQRState } from '../../UI/QRHardware/types'; +import { useConfirmationRedesignEnabled } from '../../Views/confirmations/hooks/useConfirmationRedesignEnabled'; export enum TransactionModalType { Transaction = 'transaction', @@ -24,6 +25,7 @@ export interface TransactionApprovalProps { const TransactionApprovalInternal = (props: TransactionApprovalProps) => { const { approvalRequest } = useApprovalRequest(); + const { isRedesignedEnabled } = useConfirmationRedesignEnabled(); const [modalVisible, setModalVisible] = useState(false); const { onComplete: propsOnComplete } = props; @@ -32,7 +34,10 @@ const TransactionApprovalInternal = (props: TransactionApprovalProps) => { propsOnComplete(); }, [propsOnComplete]); - if (approvalRequest?.type !== ApprovalTypes.TRANSACTION && !modalVisible) { + if ( + (approvalRequest?.type !== ApprovalTypes.TRANSACTION && !modalVisible) || + isRedesignedEnabled + ) { return null; } diff --git a/app/components/Approvals/TransactionApproval/__snapshots__/TransactionApproval.test.tsx.snap b/app/components/Approvals/TransactionApproval/__snapshots__/TransactionApproval.test.tsx.snap index 200f9d6b402..11d8aa1b92b 100644 --- a/app/components/Approvals/TransactionApproval/__snapshots__/TransactionApproval.test.tsx.snap +++ b/app/components/Approvals/TransactionApproval/__snapshots__/TransactionApproval.test.tsx.snap @@ -27,9 +27,3 @@ exports[`TransactionApproval renders approve component if transaction type is tr modalVisible={true} /> `; - -exports[`TransactionApproval returns null if incorrect approval request type 1`] = `""`; - -exports[`TransactionApproval returns null if incorrect transaction type 1`] = `""`; - -exports[`TransactionApproval returns null if no approval request 1`] = `""`; diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index 3ae652a718b..2862f0c5f97 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -1,6 +1,7 @@ import { useNavigation } from '@react-navigation/native'; import React, { useCallback, useEffect } from 'react'; import { View } from 'react-native'; +import { useSelector } from 'react-redux'; import { strings } from '../../../../../../locales/i18n'; import Button, { ButtonSize, @@ -20,14 +21,25 @@ import useStakingInputHandlers from '../../hooks/useStakingInput'; import InputDisplay from '../../components/InputDisplay'; import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics'; +import usePoolStakedDeposit from '../../hooks/usePoolStakedDeposit'; import { formatEther } from 'ethers/lib/utils'; import { EVENT_PROVIDERS, EVENT_LOCATIONS } from '../../constants/events'; +import { selectConfirmationRedesignFlags } from '../../../../../selectors/featureFlagController'; +import { selectSelectedInternalAccount } from '../../../../../selectors/accountsController'; const StakeInputView = () => { const title = strings('stake.stake_eth'); const navigation = useNavigation(); const { styles, theme } = useStyles(styleSheet, {}); const { trackEvent, createEventBuilder } = useMetrics(); + const { attemptDepositTransaction } = usePoolStakedDeposit(); + const confirmationRedesignFlags = useSelector( + selectConfirmationRedesignFlags, + ); + const isStakingDepositRedesignedEnabled = + confirmationRedesignFlags?.staking_transactions; + const activeAccount = useSelector(selectSelectedInternalAccount); + const { isEth, @@ -62,7 +74,15 @@ const StakeInputView = () => { }); }; - const handleStakePress = useCallback(() => { + const handleStakePress = useCallback(async () => { + if (isStakingDepositRedesignedEnabled) { + await attemptDepositTransaction( + amountWei.toString(), + activeAccount?.address as string, + ); + return; + } + if (isHighGasCostImpact()) { trackEvent( createEventBuilder( @@ -126,6 +146,9 @@ const StakeInputView = () => { amountEth, estimatedGasFeeWei, getDepositTxGasPercentage, + isStakingDepositRedesignedEnabled, + activeAccount, + attemptDepositTransaction, ]); const handleMaxButtonPress = () => { diff --git a/app/components/Views/confirmations/Confirm/Confirm.test.tsx b/app/components/Views/confirmations/Confirm/Confirm.test.tsx index c6eebfb4b9b..9a828957243 100644 --- a/app/components/Views/confirmations/Confirm/Confirm.test.tsx +++ b/app/components/Views/confirmations/Confirm/Confirm.test.tsx @@ -5,6 +5,7 @@ import { personalSignatureConfirmationState, securityAlertResponse, typedSignV1ConfirmationState, + stakingDepositConfirmationState, } from '../../../../util/test/confirm-data-helpers'; // eslint-disable-next-line import/no-namespace import * as ConfirmationRedesignEnabled from '../hooks/useConfirmationRedesignEnabled'; @@ -45,7 +46,21 @@ jest.mock('@react-navigation/native', () => ({ })); describe('Confirm', () => { - it('should render correct information for personal sign', () => { + it('renders flat confirmation', async () => { + const { getByTestId } = renderWithProvider(, { + state: stakingDepositConfirmationState, + }); + expect(getByTestId('flat-confirmation-container')).toBeDefined(); + }); + + it('renders modal confirmation', async () => { + const { getByTestId } = renderWithProvider(, { + state: typedSignV1ConfirmationState, + }); + expect(getByTestId('modal-confirmation-container')).toBeDefined(); + }); + + it('renders correct information for personal sign', async () => { const { getAllByRole, getByText } = renderWithProvider(, { state: personalSignatureConfirmationState, }); @@ -60,7 +75,7 @@ describe('Confirm', () => { expect(getAllByRole('button')).toHaveLength(2); }); - it('should render correct information for typed sign v1', () => { + it('renders correct information for typed sign v1', async () => { const { getAllByRole, getAllByText, getByText, queryByText } = renderWithProvider(, { state: typedSignV1ConfirmationState, @@ -74,7 +89,7 @@ describe('Confirm', () => { expect(queryByText('This is a deceptive request')).toBeNull(); }); - it('should render blockaid banner if confirmation has blockaid error response', () => { + it('renders blockaid banner if confirmation has blockaid error response', async () => { const { getByText } = renderWithProvider(, { state: { ...typedSignV1ConfirmationState, diff --git a/app/components/Views/confirmations/Confirm/Confirm.tsx b/app/components/Views/confirmations/Confirm/Confirm.tsx index ea943f87609..367dd910955 100644 --- a/app/components/Views/confirmations/Confirm/Confirm.tsx +++ b/app/components/Views/confirmations/Confirm/Confirm.tsx @@ -4,7 +4,6 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { TransactionType } from '@metamask/transaction-controller'; import { useStyles } from '../../../../component-library/hooks'; -import AccountNetworkInfo from '../components/Confirm/AccountNetworkInfo'; import BottomModal from '../components/UI/BottomModal'; import Footer from '../components/Confirm/Footer'; import Info from '../components/Confirm/Info'; @@ -13,13 +12,12 @@ import SignatureBlockaidBanner from '../components/Confirm/SignatureBlockaidBann import Title from '../components/Confirm/Title'; import useApprovalRequest from '../hooks/useApprovalRequest'; import { useConfirmationRedesignEnabled } from '../hooks/useConfirmationRedesignEnabled'; - +import { useTransactionMetadataRequest } from '../hooks/useTransactionMetadataRequest'; import styleSheet from './Confirm.styles'; // todo: if possible derive way to dynamically check if confirmation should be rendered flat -// todo: unit test coverage to be added once we have flat confirmations in place -const FLAT_CONFIRMATIONS: TransactionType[] = [ - // To be filled with flat confirmations +const FLAT_TRANSACTION_CONFIRMATIONS: TransactionType[] = [ + TransactionType.stakingDeposit, ]; const ConfirmWrapped = ({ @@ -35,7 +33,6 @@ const ConfirmWrapped = ({ contentContainerStyle={styles.scrollableSection} > - @@ -45,10 +42,11 @@ const ConfirmWrapped = ({ const Confirm = () => { const { approvalRequest } = useApprovalRequest(); + const transactionMetadata = useTransactionMetadataRequest(); const { isRedesignedEnabled } = useConfirmationRedesignEnabled(); - const isFlatConfirmation = FLAT_CONFIRMATIONS.includes( - approvalRequest?.type as TransactionType, + const isFlatConfirmation = FLAT_TRANSACTION_CONFIRMATIONS.includes( + transactionMetadata?.type as TransactionType, ); const { styles } = useStyles(styleSheet, { isFlatConfirmation }); @@ -59,14 +57,20 @@ const Confirm = () => { if (isFlatConfirmation) { return ( - + ); } return ( - + diff --git a/app/components/Views/confirmations/components/Confirm/Info/Info.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/Info.test.tsx index 8c77f4240eb..87831c3d618 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/Info.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/Info.test.tsx @@ -10,6 +10,21 @@ import { Text } from 'react-native'; const MockText = Text; jest.mock('./QRInfo', () => () => QR Scanning Component); +jest.mock('../../../../../../core/Engine', () => ({ + getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }), + context: { + KeyringController: { + state: { + keyrings: [], + }, + getOrAddQRKeyring: jest.fn(), + }, + }, + controllerMessenger: { + subscribe: jest.fn(), + }, +})); + describe('Info', () => { it('renders correctly for personal sign', () => { const { getByText } = renderWithProvider(, { diff --git a/app/components/Views/confirmations/components/Confirm/Info/Info.tsx b/app/components/Views/confirmations/components/Confirm/Info/Info.tsx index 025c5b3bfbd..6a346724ce5 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/Info.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/Info.tsx @@ -1,23 +1,41 @@ import { TransactionType } from '@metamask/transaction-controller'; +import { ApprovalType } from '@metamask/controller-utils'; import React from 'react'; import useApprovalRequest from '../../../hooks/useApprovalRequest'; +import { useTransactionMetadataRequest } from '../../../hooks/useTransactionMetadataRequest'; import { useQRHardwareContext } from '../../../context/QRHardwareContext/QRHardwareContext'; import PersonalSign from './PersonalSign'; import QRInfo from './QRInfo'; import TypedSignV1 from './TypedSignV1'; import TypedSignV3V4 from './TypedSignV3V4'; +import StakingDeposit from './StakingDeposit'; + +interface ConfirmationInfoComponentRequest { + signatureRequestVersion?: string; + transactionType?: TransactionType; +} const ConfirmationInfoComponentMap = { [TransactionType.personalSign]: () => PersonalSign, - [TransactionType.signTypedData]: (approvalRequestVersion: string) => { - if (approvalRequestVersion === 'V1') return TypedSignV1; + [TransactionType.signTypedData]: ({ + signatureRequestVersion, + }: ConfirmationInfoComponentRequest) => { + if (signatureRequestVersion === 'V1') return TypedSignV1; return TypedSignV3V4; }, + [ApprovalType.Transaction]: ({ + transactionType, + }: ConfirmationInfoComponentRequest) => { + if (transactionType === TransactionType.stakingDeposit) + return StakingDeposit; + return null; + }, }; const Info = () => { const { approvalRequest } = useApprovalRequest(); + const transactionMetadata = useTransactionMetadataRequest(); const { isSigningQRObject } = useQRHardwareContext(); if (!approvalRequest?.type) { @@ -31,11 +49,12 @@ const Info = () => { const { requestData } = approvalRequest ?? { requestData: {}, }; - const approvalRequestVersion = requestData?.version; + const signatureRequestVersion = requestData?.version; + const transactionType = transactionMetadata?.type; - const InfoComponent: React.FC = ConfirmationInfoComponentMap[ + const InfoComponent = ConfirmationInfoComponentMap[ approvalRequest?.type as keyof typeof ConfirmationInfoComponentMap - ](approvalRequestVersion); + ]({ signatureRequestVersion, transactionType }) as React.FC; return ; }; diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.test.tsx index 66d28fdb89d..92a9fcdc6cb 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.test.tsx @@ -8,6 +8,21 @@ import { } from '../../../../../../../util/test/confirm-data-helpers'; import PersonalSign from './PersonalSign'; +jest.mock('../../../../../../../core/Engine', () => ({ + getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }), + context: { + KeyringController: { + state: { + keyrings: [], + }, + getOrAddQRKeyring: jest.fn(), + }, + }, + controllerMessenger: { + subscribe: jest.fn(), + }, +})); + describe('PersonalSign', () => { it('should render correctly', async () => { const { getByText } = renderWithProvider(, { @@ -39,7 +54,7 @@ describe('PersonalSign', () => { expect(getByText('URL')).toBeDefined(); expect(getAllByText('metamask.github.io')).toHaveLength(2); expect(getByText('Network')).toBeDefined(); - expect(getByText('Ethereum Mainnet')).toBeDefined(); + expect(getAllByText('Ethereum Mainnet')).toHaveLength(2); expect(getByText('Account')).toBeDefined(); expect(getAllByText('0x8Eeee...73D12')).toBeDefined(); expect(getByText('Version')).toBeDefined(); diff --git a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx index 59e8862475f..0f7f692d6fb 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/PersonalSign/PersonalSign.tsx @@ -1,6 +1,7 @@ import React from 'react'; import useApprovalRequest from '../../../../hooks/useApprovalRequest'; +import AccountNetworkInfo from '../../AccountNetworkInfo'; import InfoRowOrigin from '../Shared/InfoRowOrigin'; import Message from './Message'; @@ -13,6 +14,7 @@ const PersonalSign = () => { return ( <> + diff --git a/app/components/Views/confirmations/components/Confirm/Info/StakingDeposit/StakingDeposit.tsx b/app/components/Views/confirmations/components/Confirm/Info/StakingDeposit/StakingDeposit.tsx new file mode 100644 index 00000000000..1f7b5b4b474 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/StakingDeposit/StakingDeposit.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Text } from 'react-native'; + +const StakingDeposit = () => Staking Deposit; + +export default StakingDeposit; diff --git a/app/components/Views/confirmations/components/Confirm/Info/StakingDeposit/index.ts b/app/components/Views/confirmations/components/Confirm/Info/StakingDeposit/index.ts new file mode 100644 index 00000000000..33c500bb3a5 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/StakingDeposit/index.ts @@ -0,0 +1 @@ +export { default } from './StakingDeposit'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx index 260d551a2d5..2880947db4f 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx @@ -4,6 +4,21 @@ import renderWithProvider from '../../../../../../../util/test/renderWithProvide import { typedSignV1ConfirmationState } from '../../../../../../../util/test/confirm-data-helpers'; import TypedSignV1 from './TypedSignV1'; +jest.mock('../../../../../../../core/Engine', () => ({ + getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }), + context: { + KeyringController: { + state: { + keyrings: [], + }, + getOrAddQRKeyring: jest.fn(), + }, + }, + controllerMessenger: { + subscribe: jest.fn(), + }, +})); + describe('TypedSignV1', () => { it('should contained required text', async () => { const { getByText, getAllByText } = renderWithProvider(, { diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.tsx index 4c162c81bec..e92504e6b0a 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.tsx @@ -1,6 +1,7 @@ import React from 'react'; import useApprovalRequest from '../../../../hooks/useApprovalRequest'; +import AccountNetworkInfo from '../../AccountNetworkInfo'; import InfoRowOrigin from '../Shared/InfoRowOrigin'; import Message from './Message'; @@ -13,6 +14,7 @@ const TypedSignV1 = () => { return ( <> + diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx index 41167c7502f..a62cd30d022 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx @@ -10,11 +10,21 @@ import TypedSignV3V4 from './TypedSignV3V4'; jest.mock('../../../../../../../core/Engine', () => ({ resetState: jest.fn(), + getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }), context: { + KeyringController: { + state: { + keyrings: [], + }, + getOrAddQRKeyring: jest.fn(), + }, NetworkController: { findNetworkClientIdByChainId: () => 123, }, }, + controllerMessenger: { + subscribe: jest.fn(), + }, })); jest.mock('../../../../hooks/useTokenDecimalsInTypedSignRequest', () => ({ diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx index 2caee661061..8f728780297 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx @@ -1,10 +1,12 @@ import React from 'react'; +import AccountNetworkInfo from '../../AccountNetworkInfo'; import InfoRowOrigin from '../Shared/InfoRowOrigin'; import Message from './Message'; import TypedSignV3V4Simulation from './Simulation'; const TypedSignV3V4 = () => ( <> + diff --git a/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx b/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx index 725d332f132..1534a4cba1a 100644 --- a/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx +++ b/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx @@ -12,6 +12,7 @@ interface BottomModalProps { children: ReactChild; onClose?: () => void; hideBackground?: boolean; + testID?: string; } /** @@ -22,7 +23,8 @@ const BottomModal = ({ canCloseOnBackdropClick = true, children, hideBackground, - onClose + onClose, + testID, }: BottomModalProps) => { const { colors } = useTheme(); const { styles } = useStyles(styleSheet, {}); @@ -42,6 +44,7 @@ const BottomModal = ({ onSwipeComplete={onClose} swipeDirection={'down'} propagateSwipe + testID={testID} > diff --git a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.test.ts b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.test.ts index 8b195400d40..3cc846c9f06 100644 --- a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.test.ts +++ b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.test.ts @@ -1,10 +1,21 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; +import { merge, cloneDeep } from 'lodash'; + // eslint-disable-next-line import/no-namespace -import * as AddressUtils from '../../../../util/address'; +import { isExternalHardwareAccount } from '../../../../util/address'; import { renderHookWithProvider } from '../../../../util/test/renderWithProvider'; -import { personalSignatureConfirmationState } from '../../../../util/test/confirm-data-helpers'; - +import { + personalSignatureConfirmationState, + stakingDepositConfirmationState, +} from '../../../../util/test/confirm-data-helpers'; import { useConfirmationRedesignEnabled } from './useConfirmationRedesignEnabled'; +jest.mock('../../../../util/address', () => ({ + ...jest.requireActual('../../../../util/address'), + isExternalHardwareAccount: jest.fn(), +})); + jest.mock('../../../../core/Engine', () => ({ getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }), context: { @@ -21,24 +32,152 @@ jest.mock('../../../../core/Engine', () => ({ })); describe('useConfirmationRedesignEnabled', () => { - it('return true for personal sign request', () => { - const { result } = renderHookWithProvider( - () => useConfirmationRedesignEnabled(), - { - state: personalSignatureConfirmationState, - }, - ); - expect(result?.current.isRedesignedEnabled).toBeTruthy(); + describe('signature confirmations', () => { + beforeEach(() => { + jest.clearAllMocks(); + (isExternalHardwareAccount as jest.Mock).mockReturnValue(true); + }); + + it('returns true for personal sign request', async () => { + (isExternalHardwareAccount as jest.Mock).mockReturnValue(false); + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state: personalSignatureConfirmationState, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(true); + }); + + it('returns false for external accounts', async () => { + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state: personalSignatureConfirmationState, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(false); + }); + + it('returns false when remote flag is disabled', async () => { + const state = merge(personalSignatureConfirmationState, { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + confirmation_redesign: { + signatures: false, + }, + }, + }, + }, + }, + }); + + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(false); + }); }); - it('return false for external accounts', () => { - jest.spyOn(AddressUtils, 'isExternalHardwareAccount').mockReturnValue(true); - const { result } = renderHookWithProvider( - () => useConfirmationRedesignEnabled(), - { - state: personalSignatureConfirmationState, - }, - ); - expect(result?.current.isRedesignedEnabled).toBeFalsy(); + describe('transaction redesigned confirmations', () => { + describe('staking confirmations', () => { + describe('staking deposit', () => { + it('returns true when enabled', async () => { + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state: stakingDepositConfirmationState, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(true); + }); + + it('returns false when remote flag is disabled', async () => { + const withDisabledFlag = cloneDeep(stakingDepositConfirmationState); + withDisabledFlag.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags.confirmation_redesign.staking_transactions = + false; + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state: withDisabledFlag, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(false); + }); + + it('returns false when transactionMeta is not present', async () => { + const withoutTransactions = cloneDeep( + stakingDepositConfirmationState, + ); + withoutTransactions.engine.backgroundState.TransactionController.transactions = + []; + + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state: withoutTransactions, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(false); + }); + + it('returns false when approval type is not transaction', async () => { + const approvalId = + stakingDepositConfirmationState.engine.backgroundState + .TransactionController.transactions[0].id; + + const state = merge(stakingDepositConfirmationState, { + engine: { + backgroundState: { + ApprovalController: { + pendingApprovals: { + [approvalId]: { + type: 'not_transaction' as ApprovalType, + }, + }, + }, + }, + }, + }); + + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(false); + }); + + it('only redesign if transactions is staking deposit', async () => { + const withBridgeTransaction = cloneDeep( + stakingDepositConfirmationState, + ); + withBridgeTransaction.engine.backgroundState.TransactionController.transactions[0].type = + TransactionType.bridge; + + const { result } = renderHookWithProvider( + useConfirmationRedesignEnabled, + { + state: withBridgeTransaction, + }, + ); + + expect(result.current.isRedesignedEnabled).toBe(false); + }); + }); + }); }); }); diff --git a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts index 9091297b254..7ea755f3d0f 100644 --- a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts +++ b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts @@ -1,28 +1,100 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; +import { ApprovalType } from '@metamask/controller-utils'; -import { ApprovalTypes } from '../../../../core/RPCMethods/RPCMethodMiddleware'; import { isExternalHardwareAccount } from '../../../../util/address'; -import { selectRemoteFeatureFlags } from '../../../../selectors/featureFlagController'; +import { + type ConfirmationRedesignRemoteFlags, + selectConfirmationRedesignFlags, +} from '../../../../selectors/featureFlagController'; +import { useTransactionMetadataRequest } from './useTransactionMetadataRequest'; import useApprovalRequest from './useApprovalRequest'; +const REDESIGNED_SIGNATURE_TYPES = [ + ApprovalType.EthSignTypedData, + ApprovalType.PersonalSign, +]; + +const REDESIGNED_TRANSACTION_TYPES = [TransactionType.stakingDeposit]; + +function isRedesignedSignature({ + approvalRequestType, + confirmationRedesignFlags, + fromAddress, +}: { + approvalRequestType: ApprovalType; + confirmationRedesignFlags: ConfirmationRedesignRemoteFlags; + fromAddress: string; +}) { + return ( + confirmationRedesignFlags?.signatures && + // following condition will ensure that user is redirected to old designs for hardware wallets + !isExternalHardwareAccount(fromAddress) && + approvalRequestType && + REDESIGNED_SIGNATURE_TYPES.includes(approvalRequestType as ApprovalType) + ); +} + +function isRedesignedTransaction({ + approvalRequestType, + confirmationRedesignFlags, + transactionMetadata, +}: { + approvalRequestType: ApprovalType; + confirmationRedesignFlags: ConfirmationRedesignRemoteFlags; + transactionMetadata?: TransactionMeta; +}) { + const isTransactionTypeRedesigned = REDESIGNED_TRANSACTION_TYPES.includes( + transactionMetadata?.type as TransactionType, + ); + + if ( + !isTransactionTypeRedesigned || + approvalRequestType !== ApprovalType.Transaction || + !transactionMetadata + ) { + return false; + } + + if (transactionMetadata.type === TransactionType.stakingDeposit) { + return confirmationRedesignFlags?.staking_transactions; + } + + return false; +} + export const useConfirmationRedesignEnabled = () => { const { approvalRequest } = useApprovalRequest(); - const { confirmation_redesign } = useSelector(selectRemoteFeatureFlags); + const transactionMetadata = useTransactionMetadataRequest(); + const confirmationRedesignFlags = useSelector( + selectConfirmationRedesignFlags, + ); - const approvalRequestType = approvalRequest?.type; + const approvalRequestType = approvalRequest?.type as ApprovalType; const fromAddress = approvalRequest?.requestData?.from; const isRedesignedEnabled = useMemo( () => - (confirmation_redesign as Record)?.signatures && - // following condition will ensure that user is redirected to old designs for ledger - !isExternalHardwareAccount(fromAddress) && - approvalRequestType && - [ApprovalTypes.PERSONAL_SIGN, ApprovalTypes.ETH_SIGN_TYPED_DATA].includes( - approvalRequestType as ApprovalTypes, - ), - [approvalRequestType, confirmation_redesign, fromAddress], + isRedesignedSignature({ + approvalRequestType, + confirmationRedesignFlags, + fromAddress, + }) || + isRedesignedTransaction({ + approvalRequestType, + confirmationRedesignFlags, + transactionMetadata, + }), + [ + approvalRequestType, + confirmationRedesignFlags, + fromAddress, + transactionMetadata, + ], ); return { isRedesignedEnabled }; diff --git a/app/components/Views/confirmations/hooks/useTransactionMetadataRequest.test.ts b/app/components/Views/confirmations/hooks/useTransactionMetadataRequest.test.ts new file mode 100644 index 00000000000..ade4ba3f219 --- /dev/null +++ b/app/components/Views/confirmations/hooks/useTransactionMetadataRequest.test.ts @@ -0,0 +1,47 @@ +import { merge } from 'lodash'; + +import { useTransactionMetadataRequest } from './useTransactionMetadataRequest'; +import { renderHookWithProvider } from '../../../../util/test/renderWithProvider'; +import { + personalSignatureConfirmationState, + stakingDepositConfirmationState, +} from '../../../../util/test/confirm-data-helpers'; + +describe('useTransactionMetadataRequest', () => { + it('returns transaction metadata', () => { + const { result } = renderHookWithProvider(useTransactionMetadataRequest, { + state: stakingDepositConfirmationState, + }); + + expect(result.current).toEqual( + stakingDepositConfirmationState.engine.backgroundState + .TransactionController.transactions[0], + ); + }); + + it('returns undefined when approval type is not Transaction', () => { + const { result } = renderHookWithProvider(useTransactionMetadataRequest, { + state: personalSignatureConfirmationState, + }); + + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when transaction metadata is not found', () => { + const state = merge(personalSignatureConfirmationState, { + engine: { + backgroundState: { + TransactionController: { + transactions: [], + }, + }, + }, + }); + + const { result } = renderHookWithProvider(useTransactionMetadataRequest, { + state, + }); + + expect(result.current).toBeUndefined(); + }); +}); diff --git a/app/components/Views/confirmations/hooks/useTransactionMetadataRequest.ts b/app/components/Views/confirmations/hooks/useTransactionMetadataRequest.ts new file mode 100644 index 00000000000..fffe6a78d0a --- /dev/null +++ b/app/components/Views/confirmations/hooks/useTransactionMetadataRequest.ts @@ -0,0 +1,25 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionMeta } from '@metamask/transaction-controller'; + +import { useSelector } from 'react-redux'; + +import { selectTransactionMetadataById } from '../../../../selectors/transactionController'; +import { RootState } from '../../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test'; +import useApprovalRequest from './useApprovalRequest'; + +export function useTransactionMetadataRequest() { + const { approvalRequest } = useApprovalRequest(); + + const transactionMetadata = useSelector((state: RootState) => + selectTransactionMetadataById(state, approvalRequest?.id as string), + ); + + if ( + approvalRequest?.type === ApprovalType.Transaction && + !transactionMetadata + ) { + return undefined; + } + + return transactionMetadata as TransactionMeta; +} diff --git a/app/selectors/featureFlagController/index.ts b/app/selectors/featureFlagController/index.ts index f6b211db457..a5ec99b0a5c 100644 --- a/app/selectors/featureFlagController/index.ts +++ b/app/selectors/featureFlagController/index.ts @@ -2,8 +2,27 @@ import { createSelector } from 'reselect'; import { StateWithPartialEngine } from './types'; import { isRemoteFeatureFlagOverrideActivated } from '../../core/Engine/controllers/RemoteFeatureFlagController/utils'; -export const selectRemoteFeatureFlagControllerState = (state: StateWithPartialEngine) => - state.engine.backgroundState.RemoteFeatureFlagController; +export interface ConfirmationRedesignRemoteFlags { + signatures: boolean; + staking_transactions: boolean; +} + +function getFeatureFlagValue( + envValue: string | undefined, + remoteValue: boolean, +): boolean { + if (envValue === 'true') { + return true; + } + if (envValue === 'false') { + return false; + } + return remoteValue; +} + +export const selectRemoteFeatureFlagControllerState = ( + state: StateWithPartialEngine, +) => state.engine.backgroundState.RemoteFeatureFlagController; export const selectRemoteFeatureFlags = createSelector( selectRemoteFeatureFlagControllerState, @@ -12,4 +31,30 @@ export const selectRemoteFeatureFlags = createSelector( return {}; } return remoteFeatureFlagControllerState?.remoteFeatureFlags ?? {}; - }); + }, +); + +export const selectConfirmationRedesignFlags = createSelector( + selectRemoteFeatureFlags, + (remoteFeatureFlags) => { + const confirmationRedesignFlags = + (remoteFeatureFlags?.confirmation_redesign as unknown as ConfirmationRedesignRemoteFlags) ?? + {}; + + const isStakingTransactionsEnabled = getFeatureFlagValue( + process.env.FEATURE_FLAG_REDESIGNED_STAKING_TRANSACTIONS, + confirmationRedesignFlags.staking_transactions, + ); + + const isSignaturesEnabled = getFeatureFlagValue( + process.env.FEATURE_FLAG_REDESIGNED_SIGNATURES, + confirmationRedesignFlags.signatures, + ); + + return { + ...confirmationRedesignFlags, + staking_transactions: isStakingTransactionsEnabled, + signatures: isSignaturesEnabled, + }; + }, +); diff --git a/app/selectors/transactionController.ts b/app/selectors/transactionController.ts index 7cfa4d6cb35..a52e102dfb8 100644 --- a/app/selectors/transactionController.ts +++ b/app/selectors/transactionController.ts @@ -27,3 +27,9 @@ export const selectSwapsTransactions = createSelector( //@ts-expect-error - This is populated at the app level, the TransactionController is not aware of this property transactionControllerState.swapsTransactions ?? {}, ); + +export const selectTransactionMetadataById = createDeepEqualSelector( + selectTransactionsStrict, + (_: RootState, id: string) => id, + (transactions, id) => transactions.find((tx) => tx.id === id), +); diff --git a/app/util/test/confirm-data-helpers.ts b/app/util/test/confirm-data-helpers.ts index 0be508d1c02..9dc3c7253fe 100644 --- a/app/util/test/confirm-data-helpers.ts +++ b/app/util/test/confirm-data-helpers.ts @@ -1,4 +1,3 @@ -import { Hex } from '@metamask/utils'; import { MessageParamsPersonal, MessageParamsTyped, @@ -6,9 +5,20 @@ import { SignatureRequestStatus, SignatureRequestType, } from '@metamask/signature-controller'; +import { Hex } from '@metamask/utils'; +import { TransactionControllerState } from '@metamask/transaction-controller'; import { backgroundState } from './initial-root-state'; +export const confirmationRedesignRemoteFlagsState = { + remoteFeatureFlags: { + confirmation_redesign: { + signatures: true, + staking_transactions: true, + }, + }, +}; + export const personalSignSignatureRequest = { chainId: '0x1', type: SignatureRequestType.PersonalSign, @@ -61,11 +71,7 @@ export const personalSignatureConfirmationState = { approvalFlows: [], }, RemoteFeatureFlagController: { - remoteFeatureFlags: { - confirmation_redesign: { - signatures: true, - }, - }, + ...confirmationRedesignRemoteFlagsState, }, SignatureController: { signatureRequests: { @@ -226,11 +232,7 @@ export const typedSignV1ConfirmationState = { }, }, RemoteFeatureFlagController: { - remoteFeatureFlags: { - confirmation_redesign: { - signatures: true, - }, - }, + ...confirmationRedesignRemoteFlagsState, }, }, }, @@ -326,9 +328,7 @@ export const typedSignV3ConfirmationState = { }, RemoteFeatureFlagController: { remoteFeatureFlags: { - confirmation_redesign: { - signatures: true, - }, + ...confirmationRedesignRemoteFlagsState, }, }, }, @@ -432,3 +432,69 @@ export const securityAlertResponse = { }, chainId: '0x1', }; + +export const stakingDepositConfirmationState = { + engine: { + backgroundState: { + ...backgroundState, + ApprovalController: { + pendingApprovals: { + '699ca2f0-e459-11ef-b6f6-d182277cf5e1': { + expectsResult: true, + id: '699ca2f0-e459-11ef-b6f6-d182277cf5e1', + origin: 'metamask', + requestData: { txId: '699ca2f0-e459-11ef-b6f6-d182277cf5e1' }, + requestState: null, + time: 1738825814816, + type: 'transaction', + }, + }, + pendingApprovalCount: 1, + approvalFlows: [], + }, + TransactionController: { + transactions: [ + { + actionId: undefined, + chainId: '0x1', + dappSuggestedGasFees: undefined, + defaultGasEstimates: { + estimateType: 'medium', + gas: '0x1a5bd', + gasPrice: undefined, + maxFeePerGas: '0x74594b20', + maxPriorityFeePerGas: '0x1dcd6500', + }, + deviceConfirmedOn: 'metamask_mobile', + gasLimitNoBuffer: '0x11929', + id: '699ca2f0-e459-11ef-b6f6-d182277cf5e1', + isFirstTimeInteraction: undefined, + networkClientId: 'mainnet', + origin: 'metamask', + originalGasEstimate: '0x1a5bd', + securityAlertResponse: undefined, + simulationFails: undefined, + status: 'unapproved', + time: 1738825814687, + txParams: { + data: '0xf9609f08000000000000000000000000dc47789de4ceff0e8fe9d15d728af7f17550c1640000000000000000000000000000000000000000000000000000000000000000', + from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164', + gas: '0x1a5bd', + maxFeePerGas: '0x74594b20', + maxPriorityFeePerGas: '0x1dcd6500', + to: '0x4fef9d741011476750a243ac70b9789a63dd47df', + value: '0x5af3107a4000', + }, + type: 'stakingDeposit', + userEditedGasLimit: false, + userFeeLevel: 'medium', + verifiedOnBlockchain: false, + }, + ], + } as unknown as TransactionControllerState, + RemoteFeatureFlagController: { + ...confirmationRedesignRemoteFlagsState, + }, + }, + }, +};