diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index c4434fc21675..cd8efcb49496 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -18,6 +18,10 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(blockaid) import { SIGNING_METHODS } from '../../../shared/constants/transaction'; +import { + REDESIGN_APPROVAL_TYPES, + REDESIGN_TRANSACTION_TYPES, +} from '../../../ui/pages/confirmations/utils/confirm'; import { getBlockaidMetricsProps } from '../../../ui/helpers/utils/metrics'; ///: END:ONLY_INCLUDE_IF import { getSnapAndHardwareInfoForMetrics } from './snap-keyring/metrics'; @@ -129,6 +133,7 @@ let globalRateLimitCount = 0; * that should be tracked for methods rate limited by random sample. * @param {Function} opts.getAccountType * @param {Function} opts.getDeviceModel + * @param {Function} opts.isConfirmationRedesignEnabled * @param {RestrictedControllerMessenger} opts.snapAndHardwareMessenger * @param {AppStateController} opts.appStateController * @param {number} [opts.globalRateLimitTimeout] - time, in milliseconds, of the sliding @@ -148,6 +153,7 @@ export default function createRPCMethodTrackingMiddleware({ globalRateLimitMaxAmount = 10, // max of events in the globalRateLimitTimeout window. pass 0 for no global rate limit getAccountType, getDeviceModel, + isConfirmationRedesignEnabled, snapAndHardwareMessenger, ///: BEGIN:ONLY_INCLUDE_IF(blockaid) appStateController, @@ -252,6 +258,19 @@ export default function createRPCMethodTrackingMiddleware({ } ///: END:ONLY_INCLUDE_IF + const isConfirmationRedesign = + isConfirmationRedesignEnabled() && + [...REDESIGN_APPROVAL_TYPES, ...REDESIGN_TRANSACTION_TYPES].find( + (type) => type === method, + ); + + if (isConfirmationRedesign) { + eventProperties.ui_customizations = [ + ...(eventProperties.ui_customizations || []), + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ]; + } + const snapAndHardwareInfo = await getSnapAndHardwareInfoForMetrics( getAccountType, getDeviceModel, @@ -265,9 +284,10 @@ export default function createRPCMethodTrackingMiddleware({ if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const { isSIWEMessage } = detectSIWE({ data }); if (isSIWEMessage) { - eventProperties.ui_customizations = ( - eventProperties.ui_customizations || [] - ).concat(MetaMetricsEventUiCustomization.Siwe); + eventProperties.ui_customizations = [ + ...(eventProperties.ui_customizations || []), + MetaMetricsEventUiCustomization.Siwe, + ]; } } } catch (e) { diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index beb4705fca4d..347ef963b1b6 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -2,6 +2,7 @@ import { errorCodes } from 'eth-rpc-errors'; import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { + MetaMetricsEventCategory, MetaMetricsEventName, MetaMetricsEventUiCustomization, } from '../../../shared/constants/metametrics'; @@ -43,6 +44,7 @@ const createHandler = (opts) => globalRateLimitTimeout: 0, globalRateLimitMaxAmount: 0, appStateController, + isConfirmationRedesignEnabled: () => false, ...opts, }); @@ -156,7 +158,7 @@ describe('createRPCMethodTrackingMiddleware', () => { await handler(req, res, next); expect(trackEvent).toHaveBeenCalledTimes(1); expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, @@ -197,7 +199,7 @@ describe('createRPCMethodTrackingMiddleware', () => { * */ expect(trackEvent.mock.calls[0][0]).toStrictEqual({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, @@ -225,7 +227,7 @@ describe('createRPCMethodTrackingMiddleware', () => { await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureApproved, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, @@ -249,7 +251,7 @@ describe('createRPCMethodTrackingMiddleware', () => { await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRejected, properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN, @@ -271,7 +273,7 @@ describe('createRPCMethodTrackingMiddleware', () => { await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.PermissionsApproved, properties: { method: MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS }, referrer: { url: 'some.dapp' }, @@ -416,6 +418,63 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); + it('should track Confirmation Redesign through ui_customizations prop if enabled', async () => { + const req = { + method: MESSAGE_TYPE.PERSONAL_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next, executeMiddlewareStack } = getNext(); + const handler = createHandler({ + isConfirmationRedesignEnabled: () => true, + }); + + await handler(req, res, next); + await executeMiddlewareStack(); + + expect(trackEvent).toHaveBeenCalledTimes(2); + + expect(trackEvent.mock.calls[1][0]).toMatchObject({ + category: MetaMetricsEventCategory.InpageProvider, + event: MetaMetricsEventName.SignatureApproved, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + ui_customizations: [ + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ], + }, + referrer: { url: 'some.dapp' }, + }); + }); + + it('should not track Confirmation Redesign through ui_customizations prop if not enabled', async () => { + const req = { + method: MESSAGE_TYPE.PERSONAL_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next, executeMiddlewareStack } = getNext(); + const handler = createHandler(); + + await handler(req, res, next); + await executeMiddlewareStack(); + + expect(trackEvent).toHaveBeenCalledTimes(2); + + expect(trackEvent.mock.calls[1][0]).toMatchObject({ + category: MetaMetricsEventCategory.InpageProvider, + event: MetaMetricsEventName.SignatureApproved, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + }, + referrer: { url: 'some.dapp' }, + }); + }); + it('should track Sign-in With Ethereum (SIWE) message if detected', async () => { const req = { method: MESSAGE_TYPE.PERSONAL_SIGN, @@ -437,7 +496,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureApproved, properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN, @@ -466,7 +525,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureFailed, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, @@ -493,7 +552,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(1); expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f5287cb565bb..1e6a5aaf64ac 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4895,6 +4895,14 @@ export default class MetamaskController extends EventEmitter { ); ///: END:ONLY_INCLUDE_IF + const isConfirmationRedesignEnabled = () => { + return ( + process.env.ENABLE_CONFIRMATION_REDESIGN && + this.preferencesController.store.getState().preferences + .redesignedConfirmations + ); + }; + engine.push( createRPCMethodTrackingMiddleware({ trackEvent: this.metaMetricsController.trackEvent.bind( @@ -4905,6 +4913,7 @@ export default class MetamaskController extends EventEmitter { ), getAccountType: this.getAccountType.bind(this), getDeviceModel: this.getDeviceModel.bind(this), + isConfirmationRedesignEnabled, snapAndHardwareMessenger: this.controllerMessenger.getRestricted({ name: 'SnapAndHardwareMessenger', allowedActions: [ diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index fc722c42ccc5..fd571b5ebfe0 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -797,6 +797,7 @@ export enum MetaMetricsEventUiCustomization { FlaggedAsSafetyUnknown = 'flagged_as_safety_unknown', FlaggedAsWarning = 'flagged_as_warning', GasEstimationFailed = 'gas_estimation_failed', + RedesignedConfirmation = 'redesigned_confirmation', SecurityAlertError = 'security_alert_error', Siwe = 'sign_in_with_ethereum', } diff --git a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts index 850a6eb89a3f..5e9a03955e52 100644 --- a/ui/pages/confirmations/hooks/useCurrentConfirmation.ts +++ b/ui/pages/confirmations/hooks/useCurrentConfirmation.ts @@ -1,6 +1,5 @@ import { ApprovalRequest } from '@metamask/approval-controller'; import { ApprovalType } from '@metamask/controller-utils'; -import { TransactionType } from '@metamask/transaction-controller'; import { Json } from '@metamask/utils'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -11,6 +10,7 @@ import { pendingConfirmationsSelector, unconfirmedTransactionsHashSelector, } from '../../../selectors'; +import { REDESIGN_APPROVAL_TYPES, REDESIGN_TRANSACTION_TYPES } from '../utils'; type Approval = ApprovalRequest>; @@ -38,7 +38,9 @@ const useCurrentConfirmation = () => { ) { return; } + let pendingConfirmation: Approval | undefined; + if (paramsTransactionId) { if (paramsTransactionId === currentConfirmation?.id) { return; @@ -47,6 +49,7 @@ const useCurrentConfirmation = () => { ({ id: confirmId }) => confirmId === paramsTransactionId, ); } + if (!pendingConfirmation) { if (!latestPendingConfirmation) { setCurrentConfirmation(undefined); @@ -54,6 +57,7 @@ const useCurrentConfirmation = () => { } pendingConfirmation = latestPendingConfirmation; } + if (pendingConfirmation.id !== currentConfirmation?.id) { const unconfirmedTransaction = unconfirmedTransactions[pendingConfirmation.id]; @@ -61,15 +65,21 @@ const useCurrentConfirmation = () => { setCurrentConfirmation(undefined); return; } - if ( - pendingConfirmation.type !== ApprovalType.PersonalSign && - pendingConfirmation.type !== ApprovalType.EthSignTypedData && - unconfirmedTransaction.type !== TransactionType.contractInteraction - ) { + + const isConfirmationRedesignType = + REDESIGN_APPROVAL_TYPES.find( + (type) => type === pendingConfirmation?.type, + ) || + REDESIGN_TRANSACTION_TYPES.find( + (type) => type === unconfirmedTransaction?.type, + ); + + if (!isConfirmationRedesignType) { setCurrentConfirmation(undefined); return; } - if (pendingConfirmation.type === ApprovalType.PersonalSign) { + + if (pendingConfirmation?.type === ApprovalType.PersonalSign) { const { siwe } = unconfirmedTransaction.msgParams; if (siwe?.isSIWEMessage) { setCurrentConfirmation(undefined); diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index 8f09c54460a1..fa56b8249ee8 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -1,8 +1,19 @@ import { ApprovalRequest } from '@metamask/approval-controller'; import { ApprovalType } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; import { Json } from '@metamask/utils'; -const SignatureApprovalTypes = [ +export const REDESIGN_APPROVAL_TYPES = [ + ApprovalType.EthSignTypedData, + ApprovalType.PersonalSign, + ApprovalType.Transaction, +] as const; + +export const REDESIGN_TRANSACTION_TYPES = [ + TransactionType.contractInteraction, +] as const; + +const SIGNATURE_APPROVAL_TYPES = [ ApprovalType.EthSign, ApprovalType.PersonalSign, ApprovalType.EthSignTypedData, @@ -10,4 +21,4 @@ const SignatureApprovalTypes = [ export const isSignatureApprovalRequest = ( request: ApprovalRequest>, -) => SignatureApprovalTypes.includes(request.type as ApprovalType); +) => SIGNATURE_APPROVAL_TYPES.includes(request.type as ApprovalType);