Skip to content

Commit bb4de90

Browse files
feat: Implement redesigned staking confirmation entry point (#13361)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR implements entry point for staking deposit confirmation. ## **Related issues** Fixes: MetaMask/MetaMask-planning#4078 ## **Manual testing steps** N/A ## **Screenshots/Recordings** https://github.com/user-attachments/assets/376554f6-a869-4cec-aa9b-f7e0b28414b5 <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 - [X] 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. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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. --------- Co-authored-by: Nico MASSART <[email protected]>
1 parent 7cf4457 commit bb4de90

24 files changed

+656
-84
lines changed

app/components/Approvals/TransactionApproval/TransactionApproval.test.tsx

+48-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import {
77
TransactionApproval,
88
TransactionModalType,
99
} from './TransactionApproval';
10-
10+
import { useConfirmationRedesignEnabled } from '../../Views/confirmations/hooks/useConfirmationRedesignEnabled';
11+
import renderWithProvider from '../../../util/test/renderWithProvider';
12+
13+
jest.mock(
14+
'../../Views/confirmations/hooks/useConfirmationRedesignEnabled',
15+
() => ({
16+
useConfirmationRedesignEnabled: jest.fn(),
17+
}),
18+
);
1119
jest.mock('../../Views/confirmations/hooks/useApprovalRequest');
1220

1321
jest.mock('../../UI/QRHardware/withQRHardwareAwareness', () =>
@@ -30,6 +38,13 @@ const mockApprovalRequest = (approvalRequest?: ApprovalRequest<any>) => {
3038
describe('TransactionApproval', () => {
3139
beforeEach(() => {
3240
jest.resetAllMocks();
41+
(
42+
useConfirmationRedesignEnabled as jest.MockedFn<
43+
typeof useConfirmationRedesignEnabled
44+
>
45+
).mockReturnValue({
46+
isRedesignedEnabled: false,
47+
});
3348
});
3449

3550
it('renders approval component if transaction type is dapp', () => {
@@ -79,9 +94,9 @@ describe('TransactionApproval', () => {
7994
it('returns null if no approval request', () => {
8095
mockApprovalRequest(undefined);
8196

82-
const wrapper = shallow(<TransactionApproval />);
97+
const { toJSON } = renderWithProvider(<TransactionApproval />, {});
8398

84-
expect(wrapper).toMatchSnapshot();
99+
expect(toJSON()).toMatchInlineSnapshot(`null`);
85100
});
86101

87102
it('returns null if incorrect approval request type', () => {
@@ -91,9 +106,9 @@ describe('TransactionApproval', () => {
91106
// eslint-disable-next-line @typescript-eslint/no-explicit-any
92107
} as any);
93108

94-
const wrapper = shallow(<TransactionApproval />);
109+
const { toJSON } = renderWithProvider(<TransactionApproval />, {});
95110

96-
expect(wrapper).toMatchSnapshot();
111+
expect(toJSON()).toMatchInlineSnapshot(`null`);
97112
});
98113

99114
it('returns null if incorrect transaction type', () => {
@@ -103,8 +118,34 @@ describe('TransactionApproval', () => {
103118
// eslint-disable-next-line @typescript-eslint/no-explicit-any
104119
} as any);
105120

106-
const wrapper = shallow(<TransactionApproval transactionType="invalid" />);
121+
const { toJSON } = renderWithProvider(
122+
<TransactionApproval transactionType="invalid" />,
123+
{},
124+
);
107125

108-
expect(wrapper).toMatchSnapshot();
126+
expect(toJSON()).toMatchInlineSnapshot(`null`);
127+
});
128+
129+
it('returns null if redesign is enabled', () => {
130+
mockApprovalRequest({
131+
type: ApprovalTypes.TRANSACTION,
132+
// TODO: Replace "any" with type
133+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
134+
} as any);
135+
136+
(
137+
useConfirmationRedesignEnabled as jest.MockedFn<
138+
typeof useConfirmationRedesignEnabled
139+
>
140+
).mockReturnValue({
141+
isRedesignedEnabled: true,
142+
});
143+
144+
const { toJSON } = renderWithProvider(
145+
<TransactionApproval transactionType={TransactionModalType.Dapp} />,
146+
{},
147+
);
148+
149+
expect(toJSON()).toMatchInlineSnapshot(`null`);
109150
});
110151
});

app/components/Approvals/TransactionApproval/TransactionApproval.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Approve from '../../Views/confirmations/ApproveView/Approve';
66
import QRSigningModal from '../../UI/QRHardware/QRSigningModal';
77
import withQRHardwareAwareness from '../../UI/QRHardware/withQRHardwareAwareness';
88
import { IQRState } from '../../UI/QRHardware/types';
9+
import { useConfirmationRedesignEnabled } from '../../Views/confirmations/hooks/useConfirmationRedesignEnabled';
910

1011
export enum TransactionModalType {
1112
Transaction = 'transaction',
@@ -24,6 +25,7 @@ export interface TransactionApprovalProps {
2425

2526
const TransactionApprovalInternal = (props: TransactionApprovalProps) => {
2627
const { approvalRequest } = useApprovalRequest();
28+
const { isRedesignedEnabled } = useConfirmationRedesignEnabled();
2729
const [modalVisible, setModalVisible] = useState(false);
2830
const { onComplete: propsOnComplete } = props;
2931

@@ -32,7 +34,10 @@ const TransactionApprovalInternal = (props: TransactionApprovalProps) => {
3234
propsOnComplete();
3335
}, [propsOnComplete]);
3436

35-
if (approvalRequest?.type !== ApprovalTypes.TRANSACTION && !modalVisible) {
37+
if (
38+
(approvalRequest?.type !== ApprovalTypes.TRANSACTION && !modalVisible) ||
39+
isRedesignedEnabled
40+
) {
3641
return null;
3742
}
3843

app/components/Approvals/TransactionApproval/__snapshots__/TransactionApproval.test.tsx.snap

-6
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,3 @@ exports[`TransactionApproval renders approve component if transaction type is tr
2727
modalVisible={true}
2828
/>
2929
`;
30-
31-
exports[`TransactionApproval returns null if incorrect approval request type 1`] = `""`;
32-
33-
exports[`TransactionApproval returns null if incorrect transaction type 1`] = `""`;
34-
35-
exports[`TransactionApproval returns null if no approval request 1`] = `""`;

app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useNavigation } from '@react-navigation/native';
22
import React, { useCallback, useEffect } from 'react';
33
import { View } from 'react-native';
4+
import { useSelector } from 'react-redux';
45
import { strings } from '../../../../../../locales/i18n';
56
import Button, {
67
ButtonSize,
@@ -20,14 +21,25 @@ import useStakingInputHandlers from '../../hooks/useStakingInput';
2021
import InputDisplay from '../../components/InputDisplay';
2122
import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics';
2223
import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics';
24+
import usePoolStakedDeposit from '../../hooks/usePoolStakedDeposit';
2325
import { formatEther } from 'ethers/lib/utils';
2426
import { EVENT_PROVIDERS, EVENT_LOCATIONS } from '../../constants/events';
27+
import { selectConfirmationRedesignFlags } from '../../../../../selectors/featureFlagController';
28+
import { selectSelectedInternalAccount } from '../../../../../selectors/accountsController';
2529

2630
const StakeInputView = () => {
2731
const title = strings('stake.stake_eth');
2832
const navigation = useNavigation();
2933
const { styles, theme } = useStyles(styleSheet, {});
3034
const { trackEvent, createEventBuilder } = useMetrics();
35+
const { attemptDepositTransaction } = usePoolStakedDeposit();
36+
const confirmationRedesignFlags = useSelector(
37+
selectConfirmationRedesignFlags,
38+
);
39+
const isStakingDepositRedesignedEnabled =
40+
confirmationRedesignFlags?.staking_transactions;
41+
const activeAccount = useSelector(selectSelectedInternalAccount);
42+
3143

3244
const {
3345
isEth,
@@ -62,7 +74,15 @@ const StakeInputView = () => {
6274
});
6375
};
6476

65-
const handleStakePress = useCallback(() => {
77+
const handleStakePress = useCallback(async () => {
78+
if (isStakingDepositRedesignedEnabled) {
79+
await attemptDepositTransaction(
80+
amountWei.toString(),
81+
activeAccount?.address as string,
82+
);
83+
return;
84+
}
85+
6686
if (isHighGasCostImpact()) {
6787
trackEvent(
6888
createEventBuilder(
@@ -126,6 +146,9 @@ const StakeInputView = () => {
126146
amountEth,
127147
estimatedGasFeeWei,
128148
getDepositTxGasPercentage,
149+
isStakingDepositRedesignedEnabled,
150+
activeAccount,
151+
attemptDepositTransaction,
129152
]);
130153

131154
const handleMaxButtonPress = () => {

app/components/Views/confirmations/Confirm/Confirm.test.tsx

+18-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
personalSignatureConfirmationState,
66
securityAlertResponse,
77
typedSignV1ConfirmationState,
8+
stakingDepositConfirmationState,
89
} from '../../../../util/test/confirm-data-helpers';
910
// eslint-disable-next-line import/no-namespace
1011
import * as ConfirmationRedesignEnabled from '../hooks/useConfirmationRedesignEnabled';
@@ -45,7 +46,21 @@ jest.mock('@react-navigation/native', () => ({
4546
}));
4647

4748
describe('Confirm', () => {
48-
it('should render correct information for personal sign', () => {
49+
it('renders flat confirmation', async () => {
50+
const { getByTestId } = renderWithProvider(<Confirm />, {
51+
state: stakingDepositConfirmationState,
52+
});
53+
expect(getByTestId('flat-confirmation-container')).toBeDefined();
54+
});
55+
56+
it('renders modal confirmation', async () => {
57+
const { getByTestId } = renderWithProvider(<Confirm />, {
58+
state: typedSignV1ConfirmationState,
59+
});
60+
expect(getByTestId('modal-confirmation-container')).toBeDefined();
61+
});
62+
63+
it('renders correct information for personal sign', async () => {
4964
const { getAllByRole, getByText } = renderWithProvider(<Confirm />, {
5065
state: personalSignatureConfirmationState,
5166
});
@@ -60,7 +75,7 @@ describe('Confirm', () => {
6075
expect(getAllByRole('button')).toHaveLength(2);
6176
});
6277

63-
it('should render correct information for typed sign v1', () => {
78+
it('renders correct information for typed sign v1', async () => {
6479
const { getAllByRole, getAllByText, getByText, queryByText } =
6580
renderWithProvider(<Confirm />, {
6681
state: typedSignV1ConfirmationState,
@@ -74,7 +89,7 @@ describe('Confirm', () => {
7489
expect(queryByText('This is a deceptive request')).toBeNull();
7590
});
7691

77-
it('should render blockaid banner if confirmation has blockaid error response', () => {
92+
it('renders blockaid banner if confirmation has blockaid error response', async () => {
7893
const { getByText } = renderWithProvider(<Confirm />, {
7994
state: {
8095
...typedSignV1ConfirmationState,

app/components/Views/confirmations/Confirm/Confirm.tsx

+14-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { SafeAreaView } from 'react-native-safe-area-context';
44
import { TransactionType } from '@metamask/transaction-controller';
55

66
import { useStyles } from '../../../../component-library/hooks';
7-
import AccountNetworkInfo from '../components/Confirm/AccountNetworkInfo';
87
import BottomModal from '../components/UI/BottomModal';
98
import Footer from '../components/Confirm/Footer';
109
import Info from '../components/Confirm/Info';
@@ -13,13 +12,12 @@ import SignatureBlockaidBanner from '../components/Confirm/SignatureBlockaidBann
1312
import Title from '../components/Confirm/Title';
1413
import useApprovalRequest from '../hooks/useApprovalRequest';
1514
import { useConfirmationRedesignEnabled } from '../hooks/useConfirmationRedesignEnabled';
16-
15+
import { useTransactionMetadataRequest } from '../hooks/useTransactionMetadataRequest';
1716
import styleSheet from './Confirm.styles';
1817

1918
// todo: if possible derive way to dynamically check if confirmation should be rendered flat
20-
// todo: unit test coverage to be added once we have flat confirmations in place
21-
const FLAT_CONFIRMATIONS: TransactionType[] = [
22-
// To be filled with flat confirmations
19+
const FLAT_TRANSACTION_CONFIRMATIONS: TransactionType[] = [
20+
TransactionType.stakingDeposit,
2321
];
2422

2523
const ConfirmWrapped = ({
@@ -35,7 +33,6 @@ const ConfirmWrapped = ({
3533
contentContainerStyle={styles.scrollableSection}
3634
>
3735
<SignatureBlockaidBanner />
38-
<AccountNetworkInfo />
3936
<Info />
4037
</ScrollView>
4138
</View>
@@ -45,10 +42,11 @@ const ConfirmWrapped = ({
4542

4643
const Confirm = () => {
4744
const { approvalRequest } = useApprovalRequest();
45+
const transactionMetadata = useTransactionMetadataRequest();
4846
const { isRedesignedEnabled } = useConfirmationRedesignEnabled();
4947

50-
const isFlatConfirmation = FLAT_CONFIRMATIONS.includes(
51-
approvalRequest?.type as TransactionType,
48+
const isFlatConfirmation = FLAT_TRANSACTION_CONFIRMATIONS.includes(
49+
transactionMetadata?.type as TransactionType,
5250
);
5351

5452
const { styles } = useStyles(styleSheet, { isFlatConfirmation });
@@ -59,14 +57,20 @@ const Confirm = () => {
5957

6058
if (isFlatConfirmation) {
6159
return (
62-
<SafeAreaView style={styles.flatContainer}>
60+
<SafeAreaView
61+
style={styles.flatContainer}
62+
testID="flat-confirmation-container"
63+
>
6364
<ConfirmWrapped styles={styles} />
6465
</SafeAreaView>
6566
);
6667
}
6768

6869
return (
69-
<BottomModal canCloseOnBackdropClick={false}>
70+
<BottomModal
71+
canCloseOnBackdropClick={false}
72+
testID="modal-confirmation-container"
73+
>
7074
<View style={styles.modalContainer} testID={approvalRequest?.type}>
7175
<ConfirmWrapped styles={styles} />
7276
</View>

app/components/Views/confirmations/components/Confirm/Info/Info.test.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ import { Text } from 'react-native';
1010
const MockText = Text;
1111
jest.mock('./QRInfo', () => () => <MockText>QR Scanning Component</MockText>);
1212

13+
jest.mock('../../../../../../core/Engine', () => ({
14+
getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }),
15+
context: {
16+
KeyringController: {
17+
state: {
18+
keyrings: [],
19+
},
20+
getOrAddQRKeyring: jest.fn(),
21+
},
22+
},
23+
controllerMessenger: {
24+
subscribe: jest.fn(),
25+
},
26+
}));
27+
1328
describe('Info', () => {
1429
it('renders correctly for personal sign', () => {
1530
const { getByText } = renderWithProvider(<Info />, {

app/components/Views/confirmations/components/Confirm/Info/Info.tsx

+24-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
import { TransactionType } from '@metamask/transaction-controller';
2+
import { ApprovalType } from '@metamask/controller-utils';
23
import React from 'react';
34

45
import useApprovalRequest from '../../../hooks/useApprovalRequest';
6+
import { useTransactionMetadataRequest } from '../../../hooks/useTransactionMetadataRequest';
57
import { useQRHardwareContext } from '../../../context/QRHardwareContext/QRHardwareContext';
68
import PersonalSign from './PersonalSign';
79
import QRInfo from './QRInfo';
810
import TypedSignV1 from './TypedSignV1';
911
import TypedSignV3V4 from './TypedSignV3V4';
12+
import StakingDeposit from './StakingDeposit';
13+
14+
interface ConfirmationInfoComponentRequest {
15+
signatureRequestVersion?: string;
16+
transactionType?: TransactionType;
17+
}
1018

1119
const ConfirmationInfoComponentMap = {
1220
[TransactionType.personalSign]: () => PersonalSign,
13-
[TransactionType.signTypedData]: (approvalRequestVersion: string) => {
14-
if (approvalRequestVersion === 'V1') return TypedSignV1;
21+
[TransactionType.signTypedData]: ({
22+
signatureRequestVersion,
23+
}: ConfirmationInfoComponentRequest) => {
24+
if (signatureRequestVersion === 'V1') return TypedSignV1;
1525
return TypedSignV3V4;
1626
},
27+
[ApprovalType.Transaction]: ({
28+
transactionType,
29+
}: ConfirmationInfoComponentRequest) => {
30+
if (transactionType === TransactionType.stakingDeposit)
31+
return StakingDeposit;
32+
return null;
33+
},
1734
};
1835

1936
const Info = () => {
2037
const { approvalRequest } = useApprovalRequest();
38+
const transactionMetadata = useTransactionMetadataRequest();
2139
const { isSigningQRObject } = useQRHardwareContext();
2240

2341
if (!approvalRequest?.type) {
@@ -31,11 +49,12 @@ const Info = () => {
3149
const { requestData } = approvalRequest ?? {
3250
requestData: {},
3351
};
34-
const approvalRequestVersion = requestData?.version;
52+
const signatureRequestVersion = requestData?.version;
53+
const transactionType = transactionMetadata?.type;
3554

36-
const InfoComponent: React.FC = ConfirmationInfoComponentMap[
55+
const InfoComponent = ConfirmationInfoComponentMap[
3756
approvalRequest?.type as keyof typeof ConfirmationInfoComponentMap
38-
](approvalRequestVersion);
57+
]({ signatureRequestVersion, transactionType }) as React.FC;
3958

4059
return <InfoComponent />;
4160
};

0 commit comments

Comments
 (0)