Skip to content

Commit af9ebca

Browse files
authored
feat: Add first time interaction warning (#28435)
<!-- 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 first time interaction feature where it shows an alert if you interact with the address for the first time. Information of the first time interaction is fetched in the transaction controller when the transaction is added to the state. Core PR: MetaMask/core#4895 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28435?quickstart=1) ## **Related issues** Fixes: MetaMask/MetaMask-planning#3040 ## **Manual testing steps** 1. Go to test dapp 2. Use send legacy transaction - make sure you interact here with your account for the first time 3. See warning ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** ![Screenshot 2024-11-13 at 11 51 14](https://github.com/user-attachments/assets/6cc1f481-788c-4945-b190-1448c5a03141) ![Screenshot 2024-11-13 at 11 51 19](https://github.com/user-attachments/assets/98413caa-ef43-4877-a37d-ea0a6da1397f) ## **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 fd3ac16 commit af9ebca

File tree

13 files changed

+198
-23
lines changed

13 files changed

+198
-23
lines changed

app/_locales/en/messages.json

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/scripts/metamask-controller.js

+2
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,8 @@ export default class MetamaskController extends EventEmitter {
19401940
queryEntireHistory: false,
19411941
updateTransactions: false,
19421942
},
1943+
isFirstTimeInteractionEnabled: () =>
1944+
this.preferencesController.state.securityAlertsEnabled,
19431945
isMultichainEnabled: process.env.TRANSACTION_MULTICHAIN,
19441946
isSimulationEnabled: () =>
19451947
this.preferencesController.state.useTransactionSimulations,

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@
355355
"@metamask/snaps-sdk": "^6.12.0",
356356
"@metamask/snaps-utils": "^8.6.0",
357357
"@metamask/solana-wallet-snap": "^0.1.9",
358-
"@metamask/transaction-controller": "^40.0.0",
358+
"@metamask/transaction-controller": "^40.1.0",
359359
"@metamask/user-operation-controller": "^13.0.0",
360360
"@metamask/utils": "^10.0.1",
361361
"@ngraveio/bc-ur": "^1.1.12",

ui/components/app/confirm/info/row/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const TEST_ADDRESS = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1';
22

33
export enum RowAlertKey {
44
EstimatedFee = 'estimatedFee',
5+
FirstTimeInteraction = 'firstTimeInteraction',
56
SigningInWith = 'signingInWith',
67
RequestFrom = 'requestFrom',
78
Resimulation = 'resimulation',

ui/components/app/confirm/info/row/row.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export enum ConfirmInfoRowVariant {
3333

3434
export type ConfirmInfoRowProps = {
3535
label: string;
36-
children: React.ReactNode | string;
36+
children?: React.ReactNode | string;
3737
tooltip?: string;
3838
variant?: ConfirmInfoRowVariant;
3939
style?: React.CSSProperties;
@@ -169,6 +169,7 @@ export const ConfirmInfoRow: React.FC<ConfirmInfoRowProps> = ({
169169
</Box>
170170
</Box>
171171
{expanded &&
172+
children &&
172173
(typeof children === 'string' ? (
173174
<Text marginRight={copyEnabled ? 3 : 0} color={TextColor.inherit}>
174175
{children}

ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ exports[`<TransactionFlowSection /> renders correctly 1`] = `
9494
/>
9595
<div
9696
class="mm-box confirm-info-row mm-box--margin-top-2 mm-box--margin-bottom-2 mm-box--padding-right-2 mm-box--padding-left-2 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-wrap mm-box--justify-content-space-between mm-box--align-items-flex-start mm-box--color-text-default mm-box--rounded-lg"
97-
style="overflow-wrap: anywhere; min-height: 24px; position: relative; flex-direction: column; align-items: flex-start;"
97+
style="overflow-wrap: anywhere; min-height: 24px; position: relative; background: transparent; flex-direction: column;"
9898
>
9999
<div
100-
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start"
100+
class="mm-box mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-center mm-box--align-items-flex-start mm-box--color-text-default"
101101
>
102102
<div
103103
class="mm-box mm-box--display-flex mm-box--align-items-center"

ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ import {
1717
IconColor,
1818
JustifyContent,
1919
} from '../../../../../../helpers/constants/design-system';
20-
import {
21-
ConfirmInfoRow,
22-
ConfirmInfoRowAddress,
23-
} from '../../../../../../components/app/confirm/info/row';
20+
import { ConfirmInfoRowAddress } from '../../../../../../components/app/confirm/info/row';
2421
import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row';
2522
import { RowAlertKey } from '../../../../../../components/app/confirm/info/row/constants';
2623
import { useI18nContext } from '../../../../../../hooks/useI18nContext';
@@ -61,7 +58,9 @@ export const TransactionFlowSection = () => {
6158
alertKey={RowAlertKey.SigningInWith}
6259
label={t('from')}
6360
ownerId={transactionMeta.id}
64-
style={{ flexDirection: FlexDirection.Column }}
61+
style={{
62+
flexDirection: FlexDirection.Column,
63+
}}
6564
>
6665
<Box marginTop={1}>
6766
<ConfirmInfoRowAddress
@@ -77,11 +76,12 @@ export const TransactionFlowSection = () => {
7776
color={IconColor.iconMuted}
7877
/>
7978
{recipientAddress && (
80-
<ConfirmInfoRow
79+
<ConfirmInfoAlertRow
80+
alertKey={RowAlertKey.FirstTimeInteraction}
8181
label={t('to')}
82+
ownerId={transactionMeta.id}
8283
style={{
8384
flexDirection: FlexDirection.Column,
84-
alignItems: AlignItems.flexStart,
8585
}}
8686
>
8787
<Box marginTop={1}>
@@ -90,7 +90,7 @@ export const TransactionFlowSection = () => {
9090
chainId={chainId}
9191
/>
9292
</Box>
93-
</ConfirmInfoRow>
93+
</ConfirmInfoAlertRow>
9494
)}
9595
</Box>
9696
</ConfirmInfoSection>

ui/pages/confirmations/components/simulation-details/simulation-details.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,7 @@ const HeaderWithAlert = ({ transactionId }: { transactionId: string }) => {
110110
paddingLeft: 0,
111111
paddingRight: 0,
112112
}}
113-
>
114-
{/* Intentional fragment */}
115-
<></>
116-
</ConfirmInfoAlertRow>
113+
/>
117114
);
118115
};
119116

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { ApprovalType } from '@metamask/controller-utils';
2+
import {
3+
TransactionMeta,
4+
TransactionStatus,
5+
TransactionType,
6+
} from '@metamask/transaction-controller';
7+
8+
import { getMockConfirmState } from '../../../../../../test/data/confirmations/helper';
9+
import { renderHookWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers';
10+
import { Severity } from '../../../../../helpers/constants/design-system';
11+
import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants';
12+
import { genUnapprovedContractInteractionConfirmation } from '../../../../../../test/data/confirmations/contract-interaction';
13+
import { useFirstTimeInteractionAlert } from './useFirstTimeInteractionAlert';
14+
15+
const ACCOUNT_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc';
16+
const TRANSACTION_ID_MOCK = '123-456';
17+
18+
const CONFIRMATION_MOCK = genUnapprovedContractInteractionConfirmation({
19+
chainId: '0x5',
20+
}) as TransactionMeta;
21+
22+
const TRANSACTION_META_MOCK = {
23+
id: TRANSACTION_ID_MOCK,
24+
chainId: '0x5',
25+
status: TransactionStatus.submitted,
26+
type: TransactionType.contractInteraction,
27+
txParams: {
28+
from: ACCOUNT_ADDRESS,
29+
},
30+
time: new Date().getTime() - 10000,
31+
firstTimeInteraction: true,
32+
} as TransactionMeta;
33+
34+
function runHook({
35+
currentConfirmation,
36+
transactions = [],
37+
}: {
38+
currentConfirmation?: TransactionMeta;
39+
transactions?: TransactionMeta[];
40+
} = {}) {
41+
let pendingApprovals = {};
42+
if (currentConfirmation) {
43+
pendingApprovals = {
44+
[currentConfirmation.id as string]: {
45+
id: currentConfirmation.id,
46+
type: ApprovalType.Transaction,
47+
},
48+
};
49+
transactions.push(currentConfirmation);
50+
}
51+
const state = getMockConfirmState({
52+
metamask: {
53+
pendingApprovals,
54+
transactions,
55+
},
56+
});
57+
const response = renderHookWithConfirmContextProvider(
58+
useFirstTimeInteractionAlert,
59+
state,
60+
);
61+
62+
return response.result.current;
63+
}
64+
65+
describe('useFirstTimeInteractionAlert', () => {
66+
beforeEach(() => {
67+
jest.resetAllMocks();
68+
});
69+
70+
it('returns no alerts if no confirmation', () => {
71+
expect(runHook()).toEqual([]);
72+
});
73+
74+
it('returns no alerts if no transactions', () => {
75+
expect(
76+
runHook({
77+
currentConfirmation: CONFIRMATION_MOCK,
78+
transactions: [],
79+
}),
80+
).toEqual([]);
81+
});
82+
83+
it('returns no alerts if firstTimeInteraction is false', () => {
84+
const notFirstTimeConfirmation = {
85+
...TRANSACTION_META_MOCK,
86+
firstTimeInteraction: false,
87+
};
88+
expect(
89+
runHook({
90+
currentConfirmation: notFirstTimeConfirmation,
91+
}),
92+
).toEqual([]);
93+
});
94+
95+
it('returns no alerts if firstTimeInteraction is undefined', () => {
96+
const notFirstTimeConfirmation = {
97+
...TRANSACTION_META_MOCK,
98+
firstTimeInteraction: undefined,
99+
};
100+
expect(
101+
runHook({
102+
currentConfirmation: notFirstTimeConfirmation,
103+
}),
104+
).toEqual([]);
105+
});
106+
107+
it('returns alert if isFirstTimeInteraction is true', () => {
108+
const firstTimeConfirmation = {
109+
...CONFIRMATION_MOCK,
110+
isFirstTimeInteraction: true,
111+
};
112+
const alerts = runHook({
113+
currentConfirmation: firstTimeConfirmation,
114+
});
115+
116+
expect(alerts).toEqual([
117+
{
118+
actions: [],
119+
field: RowAlertKey.FirstTimeInteraction,
120+
isBlocking: false,
121+
key: 'firstTimeInteractionTitle',
122+
message:
123+
"You're interacting with this address for the first time. Make sure that it's correct before you continue.",
124+
reason: '1st interaction',
125+
severity: Severity.Warning,
126+
},
127+
]);
128+
});
129+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useMemo } from 'react';
2+
import { TransactionMeta } from '@metamask/transaction-controller';
3+
4+
import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts';
5+
import { useI18nContext } from '../../../../../hooks/useI18nContext';
6+
import { Severity } from '../../../../../helpers/constants/design-system';
7+
import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants';
8+
import { useConfirmContext } from '../../../context/confirm';
9+
10+
export function useFirstTimeInteractionAlert(): Alert[] {
11+
const t = useI18nContext();
12+
const { currentConfirmation } = useConfirmContext<TransactionMeta>();
13+
14+
const { isFirstTimeInteraction } = currentConfirmation ?? {};
15+
16+
return useMemo(() => {
17+
// If isFirstTimeInteraction is undefined that means it's either disabled or error in accounts API
18+
// If it's false that means account relationship found
19+
if (!isFirstTimeInteraction) {
20+
return [];
21+
}
22+
23+
return [
24+
{
25+
actions: [],
26+
field: RowAlertKey.FirstTimeInteraction,
27+
isBlocking: false,
28+
key: 'firstTimeInteractionTitle',
29+
message: t('alertMessageFirstTimeInteraction'),
30+
reason: t('alertReasonFirstTimeInteraction'),
31+
severity: Severity.Warning,
32+
},
33+
];
34+
}, [isFirstTimeInteraction, t]);
35+
}

ui/pages/confirmations/hooks/useConfirmationAlerts.ts

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useNoGasPriceAlerts } from './alerts/transactions/useNoGasPriceAlerts';
1111
import { usePendingTransactionAlerts } from './alerts/transactions/usePendingTransactionAlerts';
1212
import { useQueuedConfirmationsAlerts } from './alerts/transactions/useQueuedConfirmationsAlerts';
1313
import { useResimulationAlert } from './alerts/transactions/useResimulationAlert';
14+
import { useFirstTimeInteractionAlert } from './alerts/transactions/useFirstTimeInteractionAlert';
1415
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
1516
import { useSigningOrSubmittingAlerts } from './alerts/transactions/useSigningOrSubmittingAlerts';
1617
///: END:ONLY_INCLUDE_IF
@@ -37,6 +38,7 @@ function useTransactionAlerts(): Alert[] {
3738
const noGasPriceAlerts = useNoGasPriceAlerts();
3839
const pendingTransactionAlerts = usePendingTransactionAlerts();
3940
const resimulationAlert = useResimulationAlert();
41+
const firstTimeInteractionAlert = useFirstTimeInteractionAlert();
4042
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
4143
const signingOrSubmittingAlerts = useSigningOrSubmittingAlerts();
4244
///: END:ONLY_INCLUDE_IF
@@ -52,6 +54,7 @@ function useTransactionAlerts(): Alert[] {
5254
...noGasPriceAlerts,
5355
...pendingTransactionAlerts,
5456
...resimulationAlert,
57+
...firstTimeInteractionAlert,
5558
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
5659
...signingOrSubmittingAlerts,
5760
///: END:ONLY_INCLUDE_IF
@@ -66,6 +69,7 @@ function useTransactionAlerts(): Alert[] {
6669
noGasPriceAlerts,
6770
pendingTransactionAlerts,
6871
resimulationAlert,
72+
firstTimeInteractionAlert,
6973
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
7074
signingOrSubmittingAlerts,
7175
///: END:ONLY_INCLUDE_IF

ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ exports[`Security Tab should match snapshot 1`] = `
126126
>
127127
<span>
128128
129-
This feature alerts you to malicious activity by actively reviewing transaction and signature requests.
129+
This feature alerts you to malicious or unusual activity by actively reviewing transaction and signature requests.
130130
<a
131131
href="https://support.metamask.io/privacy-and-security/how-to-turn-on-security-alerts/"
132132
rel="noreferrer"

yarn.lock

+5-5
Original file line numberDiff line numberDiff line change
@@ -6738,9 +6738,9 @@ __metadata:
67386738
languageName: node
67396739
linkType: hard
67406740

6741-
"@metamask/transaction-controller@npm:^40.0.0":
6742-
version: 40.0.0
6743-
resolution: "@metamask/transaction-controller@npm:40.0.0"
6741+
"@metamask/transaction-controller@npm:^40.1.0":
6742+
version: 40.1.0
6743+
resolution: "@metamask/transaction-controller@npm:40.1.0"
67446744
dependencies:
67456745
"@ethereumjs/common": "npm:^3.2.0"
67466746
"@ethereumjs/tx": "npm:^4.2.0"
@@ -6767,7 +6767,7 @@ __metadata:
67676767
"@metamask/approval-controller": ^7.0.0
67686768
"@metamask/gas-fee-controller": ^22.0.0
67696769
"@metamask/network-controller": ^22.0.0
6770-
checksum: 10/1325f5d264e4351dfeee664bba601d873b2204eb82ccb840ab7934fa27f48e31c5f47a60f0a4b4baa94b41ac801121c498bb28102a65cbe59a0456630d4e0138
6770+
checksum: 10/1057af5b0da2d51e46e7568fc0e7fdbe6aed34a013cf56a5a35ad694cbedcb726a5823bbe70b980d1dc9560138acf9d82ac5f0e06f7d17e11b46abacd466dc42
67716771
languageName: node
67726772
linkType: hard
67736773

@@ -26891,7 +26891,7 @@ __metadata:
2689126891
"@metamask/solana-wallet-snap": "npm:^0.1.9"
2689226892
"@metamask/test-bundler": "npm:^1.0.0"
2689326893
"@metamask/test-dapp": "npm:8.13.0"
26894-
"@metamask/transaction-controller": "npm:^40.0.0"
26894+
"@metamask/transaction-controller": "npm:^40.1.0"
2689526895
"@metamask/user-operation-controller": "npm:^13.0.0"
2689626896
"@metamask/utils": "npm:^10.0.1"
2689726897
"@ngraveio/bc-ur": "npm:^1.1.12"

0 commit comments

Comments
 (0)