Skip to content

Commit 34259a3

Browse files
authored
chore: MMI adds note to trader support to the new Tx confirmation view (#27214)
## **Description** Adds the MMI transaction "note to trader" functionality to the new transactions redesign, the current one is also kept in order to facilitate backwards compatibility with the current confirmation view (to be removed later). It also adds a few parameters that were missing for MMI like the tx metadata. ![note_to_trader_ss](https://github.com/user-attachments/assets/7a7f123a-f0de-48f9-8646-40aabd5e27b2) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- 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** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.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.
1 parent 5e011f8 commit 34259a3

23 files changed

+355
-93
lines changed

app/scripts/controllers/app-state.js

+6
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,12 @@ export default class AppStateController extends EventEmitter {
531531
});
532532
}
533533

534+
setNoteToTraderMessage(message) {
535+
this.store.updateState({
536+
noteToTraderMessage: message,
537+
});
538+
}
539+
534540
///: END:ONLY_INCLUDE_IF
535541

536542
getSignatureSecurityAlertResponse(securityAlertId) {

app/scripts/controllers/app-state.test.js

+19
Original file line numberDiff line numberDiff line change
@@ -374,5 +374,24 @@ describe('AppStateController', () => {
374374

375375
updateStateSpy.mockRestore();
376376
});
377+
378+
it('set the setNoteToTraderMessage with a message', () => {
379+
appStateController = createAppStateController();
380+
const updateStateSpy = jest.spyOn(
381+
appStateController.store,
382+
'updateState',
383+
);
384+
385+
const mockParams = 'some message';
386+
387+
appStateController.setNoteToTraderMessage(mockParams);
388+
389+
expect(updateStateSpy).toHaveBeenCalledTimes(1);
390+
expect(updateStateSpy).toHaveBeenCalledWith({
391+
noteToTraderMessage: mockParams,
392+
});
393+
394+
updateStateSpy.mockRestore();
395+
});
377396
});
378397
});

app/scripts/metamask-controller.js

+2
Original file line numberDiff line numberDiff line change
@@ -3672,6 +3672,8 @@ export default class MetamaskController extends EventEmitter {
36723672
),
36733673
setCustodianDeepLink:
36743674
appStateController.setCustodianDeepLink.bind(appStateController),
3675+
setNoteToTraderMessage:
3676+
appStateController.setNoteToTraderMessage.bind(appStateController),
36753677
///: END:ONLY_INCLUDE_IF
36763678

36773679
// snaps

test/e2e/playwright/mmi/specs/extension.visual.spec.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,12 @@ test.describe('MMI extension', () => {
152152
await accountsPopup.connectCustodian(
153153
process.env.MMI_E2E_CUSTODIAN_NAME as string,
154154
);
155-
156155
const accountNamesWithCustodian = await accountsPopup.getAccountNames();
157156

158-
expect(
159-
JSON.stringify(accountNamesWithCustodian) ===
160-
JSON.stringify(arrayWithCustodianAccounts),
161-
).toBeTruthy();
157+
const containsAccount = arrayWithCustodianAccounts.some((account) =>
158+
accountNamesWithCustodian.includes(account),
159+
);
160+
expect(containsAccount).toBeTruthy();
162161

163162
await accountsPopup.selectCustodyAccount(accountFrom);
164163
// Check remove custodian token screen (aborted before removed)

ui/components/institutional/note-to-trader/__snapshots__/note-to-trader.test.tsx.snap

+6-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
exports[`NoteToTrader should render the Note to trader component 1`] = `
44
<div>
55
<div
6-
class="mm-box confirm-page-container-content__data"
6+
class="mm-box mm-box--margin-bottom-4 mm-box--padding-0 mm-box--background-color-background-default mm-box--rounded-md"
77
>
88
<div
99
class="mm-box mm-box--padding-4 mm-box--display-flex mm-box--flex-direction-column"
@@ -20,7 +20,7 @@ exports[`NoteToTrader should render the Note to trader component 1`] = `
2020
<p
2121
class="mm-box mm-text note-header__counter mm-text--body-md mm-box--color-text-default"
2222
>
23-
9
23+
0
2424
/
2525
280
2626
</p>
@@ -29,13 +29,13 @@ exports[`NoteToTrader should render the Note to trader component 1`] = `
2929
class="mm-box note-field mm-box--display-flex mm-box--flex-direction-column"
3030
>
3131
<textarea
32+
class="mm-box mm-text mm-textarea mm-textarea--resize-vertical mm-text--body-md mm-box--padding-2 mm-box--padding-top-1 mm-box--padding-right-4 mm-box--padding-bottom-1 mm-box--padding-left-4 mm-box--width-full mm-box--height-full mm-box--color-text-default mm-box--background-color-background-default mm-box--rounded-sm mm-box--border-color-border-default mm-box--border-width-1 box--border-style-solid"
3233
data-testid="transaction-note"
3334
id="transaction-note"
3435
maxlength="280"
35-
placeholder=""
36-
>
37-
some text
38-
</textarea>
36+
placeholder="The approver will see this note when approving the transaction at the custodian."
37+
resize="vertical"
38+
/>
3939
</div>
4040
</div>
4141
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
import {
3+
Display,
4+
FlexDirection,
5+
JustifyContent,
6+
} from '../../../helpers/constants/design-system';
7+
import { Label, Box, Text } from '../../component-library';
8+
9+
type NoteToTraderProps = {
10+
placeholder: string;
11+
maxLength: number;
12+
onChange: (value: string) => void;
13+
noteText: string;
14+
labelText: string;
15+
};
16+
17+
const TabbedNoteToTrader: React.FC<NoteToTraderProps> = ({
18+
placeholder,
19+
maxLength,
20+
onChange,
21+
noteText,
22+
labelText,
23+
}) => {
24+
return (
25+
<Box className="confirm-page-container-content__data">
26+
<Box
27+
display={Display.Flex}
28+
flexDirection={FlexDirection.Column}
29+
padding={4}
30+
>
31+
<Box
32+
className="note-header"
33+
display={Display.Flex}
34+
justifyContent={JustifyContent.spaceBetween}
35+
>
36+
<Label htmlFor="transaction-note">{labelText}</Label>
37+
<Text className="note-header__counter">
38+
{noteText.length}/{maxLength}
39+
</Text>
40+
</Box>
41+
<Box
42+
display={Display.Flex}
43+
flexDirection={FlexDirection.Column}
44+
className="note-field"
45+
>
46+
<textarea
47+
id="transaction-note"
48+
data-testid="transaction-note"
49+
onChange={({ target: { value } }) => onChange(value)}
50+
autoFocus
51+
maxLength={maxLength}
52+
placeholder={placeholder}
53+
value={noteText}
54+
/>
55+
</Box>
56+
</Box>
57+
</Box>
58+
);
59+
};
60+
61+
export default TabbedNoteToTrader;

ui/components/institutional/note-to-trader/note-to-trader.stories.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import React from 'react';
2+
import { Meta } from '@storybook/react';
3+
import { Provider } from 'react-redux';
24
import NoteToTrader from '.';
5+
import { getMockContractInteractionConfirmState } from '../../../../test/data/confirmations/helper';
6+
import configureStore from '../../../store/store';
7+
import { ConfirmContextProvider } from '../../../pages/confirmations/context/confirm';
8+
9+
const store = configureStore(getMockContractInteractionConfirmState());
310

411
export default {
512
title: 'Components/Institutional/NoteToTrader',
613
component: NoteToTrader,
14+
decorators: [
15+
(story: () => Meta<typeof NoteToTrader>) => (
16+
<Provider store={store}>
17+
<ConfirmContextProvider>{story()}</ConfirmContextProvider>
18+
</Provider>
19+
),
20+
],
721
args: {
822
placeholder:
923
'The approver will see this note when approving the transaction at the custodian.',

ui/components/institutional/note-to-trader/note-to-trader.test.tsx

+17-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
import { render, fireEvent } from '@testing-library/react';
1+
import { fireEvent } from '@testing-library/react';
22
import React from 'react';
3+
import configureMockStore from 'redux-mock-store';
4+
import thunk from 'redux-thunk';
5+
import { renderWithConfirmContextProvider } from '../../../../test/lib/confirmations/render-helpers';
6+
import mockState from '../../../../test/data/mock-state.json';
37
import NoteToTrader from './note-to-trader';
48

9+
jest.mock('../../../selectors/institutional/selectors', () => ({
10+
getIsNoteToTraderSupported: () => true,
11+
}));
12+
13+
const middleware = [thunk];
14+
const store = configureMockStore(middleware)(mockState);
15+
516
describe('NoteToTrader', () => {
617
it('should render the Note to trader component', () => {
7-
const props = {
8-
placeholder: '',
9-
maxLength: 280,
10-
noteText: 'some text',
11-
labelText: 'Transaction note',
12-
onChange: jest.fn(),
13-
};
18+
const { getByTestId, container } = renderWithConfirmContextProvider(
19+
<NoteToTrader />,
20+
store,
21+
);
1422

15-
const { getByTestId, container } = render(<NoteToTrader {...props} />);
1623
const transactionNoteInput = getByTestId(
1724
'transaction-note',
1825
) as HTMLInputElement;
1926

2027
fireEvent.change(transactionNoteInput);
21-
expect(transactionNoteInput.value).toBe('some text');
28+
expect(transactionNoteInput.value).toBe('');
2229
expect(transactionNoteInput).toBeDefined();
2330
expect(container).toMatchSnapshot();
2431
});
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,54 @@
1-
import React from 'react';
1+
import React, { useEffect, useState } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
23
import {
4+
BackgroundColor,
5+
BlockSize,
6+
BorderRadius,
37
Display,
48
FlexDirection,
59
JustifyContent,
610
} from '../../../helpers/constants/design-system';
711
import { Label, Box, Text } from '../../component-library';
12+
import { Textarea } from '../../component-library/textarea';
13+
import { setNoteToTraderMessage } from '../../../store/institutional/institution-background';
14+
import {
15+
getIsNoteToTraderSupported,
16+
State,
17+
} from '../../../selectors/institutional/selectors';
18+
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
19+
import { useConfirmContext } from '../../../pages/confirmations/context/confirm';
20+
import { getConfirmationSender } from '../../../pages/confirmations/components/confirm/utils';
21+
import { useI18nContext } from '../../../hooks/useI18nContext';
22+
import { isSignatureTransactionType } from '../../../pages/confirmations/utils';
823

9-
type NoteToTraderProps = {
10-
placeholder: string;
11-
maxLength: number;
12-
onChange: (value: string) => void;
13-
noteText: string;
14-
labelText: string;
15-
};
24+
const NoteToTrader: React.FC = () => {
25+
const dispatch = useDispatch();
26+
const t = useI18nContext();
27+
const [noteText, setNoteText] = useState('');
28+
29+
const { currentConfirmation } = useConfirmContext();
30+
const isSignature = isSignatureTransactionType(currentConfirmation);
31+
const { from } = getConfirmationSender(currentConfirmation);
32+
const fromChecksumHexAddress = toChecksumHexAddress(from || '');
33+
const isNoteToTraderSupported = useSelector((state: State) =>
34+
getIsNoteToTraderSupported(state, fromChecksumHexAddress),
35+
);
1636

17-
const NoteToTrader: React.FC<NoteToTraderProps> = ({
18-
placeholder,
19-
maxLength,
20-
onChange,
21-
noteText,
22-
labelText,
23-
}) => {
24-
return (
25-
<Box className="confirm-page-container-content__data">
37+
useEffect(() => {
38+
const timer = setTimeout(() => {
39+
dispatch(setNoteToTraderMessage(noteText));
40+
}, 700);
41+
42+
return () => clearTimeout(timer);
43+
}, [noteText]);
44+
45+
return isNoteToTraderSupported && !isSignature ? (
46+
<Box
47+
backgroundColor={BackgroundColor.backgroundDefault}
48+
borderRadius={BorderRadius.MD}
49+
padding={0}
50+
marginBottom={4}
51+
>
2652
<Box
2753
display={Display.Flex}
2854
flexDirection={FlexDirection.Column}
@@ -33,29 +59,31 @@ const NoteToTrader: React.FC<NoteToTraderProps> = ({
3359
display={Display.Flex}
3460
justifyContent={JustifyContent.spaceBetween}
3561
>
36-
<Label htmlFor="transaction-note">{labelText}</Label>
62+
<Label htmlFor="transaction-note">{t('transactionNote')}</Label>
3763
<Text className="note-header__counter">
38-
{noteText.length}/{maxLength}
64+
{noteText.length}/{280}
3965
</Text>
4066
</Box>
4167
<Box
4268
display={Display.Flex}
4369
flexDirection={FlexDirection.Column}
4470
className="note-field"
4571
>
46-
<textarea
72+
<Textarea
4773
id="transaction-note"
4874
data-testid="transaction-note"
49-
onChange={({ target: { value } }) => onChange(value)}
50-
autoFocus
51-
maxLength={maxLength}
52-
placeholder={placeholder}
75+
onChange={({ target: { value } }) => setNoteText(value)}
5376
value={noteText}
77+
height={BlockSize.Full}
78+
width={BlockSize.Full}
79+
maxLength={280}
80+
placeholder={t('notePlaceholder')}
81+
padding={2}
5482
/>
5583
</Box>
5684
</Box>
5785
</Box>
58-
);
86+
) : null;
5987
};
6088

6189
export default NoteToTrader;

ui/hooks/useMMIConfirmations.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export function useMMIConfirmations() {
2020
currentConfirmation.type === TransactionType.signTypedData) &&
2121
Boolean(currentConfirmation?.custodyId),
2222
mmiOnSignCallback: () => custodySignFn(currentConfirmation),
23-
mmiOnTransactionCallback: () =>
24-
custodyTransactionFn(currentConfirmation as TransactionMeta),
23+
mmiOnTransactionCallback: (
24+
transactionData: TransactionMeta,
25+
noteToTraderMessage: string,
26+
) => custodyTransactionFn(transactionData, noteToTraderMessage),
2527
};
2628
}

ui/hooks/useMMICustodySendTransaction.test.js

+11-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useMMICustodySendTransaction } from './useMMICustodySendTransaction';
77

88
jest.mock('react-router-dom', () => ({
99
...jest.requireActual('react-router-dom'),
10-
useHistory: jest.fn(),
10+
useHistory: jest.fn().mockReturnValue({ push: jest.fn() }),
1111
}));
1212

1313
jest.mock('react-redux', () => ({
@@ -23,14 +23,20 @@ jest.mock('../store/institutional/institution-background', () => ({
2323
mmiActionsFactory: jest.fn(),
2424
}));
2525

26-
jest.mock('../selectors', () => ({
27-
getAccountType: jest.fn(),
28-
}));
29-
3026
jest.mock('../store/actions', () => ({
3127
updateAndApproveTx: jest.fn(),
3228
}));
3329

30+
jest.mock('../pages/confirmations/context/confirm', () => ({
31+
useConfirmContext: () => ({
32+
currentConfirmation: { from: '0x123' },
33+
}),
34+
}));
35+
36+
jest.mock('../pages/confirmations/components/confirm/utils', () => ({
37+
getConfirmationSender: () => ({ from: '0x123' }),
38+
}));
39+
3440
describe('useMMICustodySendTransaction', () => {
3541
it('handles custody account type', async () => {
3642
const dispatch = jest.fn();

0 commit comments

Comments
 (0)