diff --git a/app/components/Views/confirmations/Confirm/Confirm.test.tsx b/app/components/Views/confirmations/Confirm/Confirm.test.tsx
index 3b0f7cec116..6aba8da4231 100644
--- a/app/components/Views/confirmations/Confirm/Confirm.test.tsx
+++ b/app/components/Views/confirmations/Confirm/Confirm.test.tsx
@@ -8,6 +8,11 @@ import {
} from '../../../../util/test/confirm-data-helpers';
import Confirm from './index';
+jest.mock('../../../../util/address', () => ({
+ ...jest.requireActual('../../../../util/address'),
+ getAddressAccountType: (str: string) => str,
+}));
+
jest.mock('react-native-gzip', () => ({
deflate: (str: string) => str,
}));
@@ -51,7 +56,7 @@ describe('Confirm', () => {
expect(queryByText('This is a deceptive request')).toBeNull();
});
- it('should render blockaid banner is confirmation has blockaid error response', async () => {
+ it('should render blockaid banner if confirmation has blockaid error response', async () => {
const typedSignApproval =
typedSignV1ConfirmationState.engine.backgroundState.ApprovalController
.pendingApprovals['7e62bcb1-a4e9-11ef-9b51-ddf21c91a998'];
diff --git a/app/components/Views/confirmations/components/Confirm/Footer/Footer.test.tsx b/app/components/Views/confirmations/components/Confirm/Footer/Footer.test.tsx
index 4139a44c779..da6d2c945d3 100644
--- a/app/components/Views/confirmations/components/Confirm/Footer/Footer.test.tsx
+++ b/app/components/Views/confirmations/components/Confirm/Footer/Footer.test.tsx
@@ -7,17 +7,21 @@ import { fireEvent } from '@testing-library/react-native';
const mockConfirmSpy = jest.fn();
const mockRejectSpy = jest.fn();
-jest.mock('../../../hooks/useApprovalRequest', () => () => ({
- onConfirm: mockConfirmSpy,
- onReject: mockRejectSpy,
+jest.mock('../../../hooks/useConfirmActions', () => ({
+ useConfirmActions: () => ({
+ onConfirm: mockConfirmSpy,
+ onReject: mockRejectSpy,
+ }),
}));
describe('Footer', () => {
- it('should match snapshot for personal sign', async () => {
- const container = renderWithProvider(, {
+ it('should render correctly', async () => {
+ const { getByText, getAllByRole } = renderWithProvider(, {
state: personalSignatureConfirmationState,
});
- expect(container).toMatchSnapshot();
+ expect(getByText('Reject')).toBeDefined();
+ expect(getByText('Confirm')).toBeDefined();
+ expect(getAllByRole('button')).toHaveLength(2);
});
it('should call onConfirm when confirm button is clicked', async () => {
diff --git a/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx b/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx
index 4ccccc46b9a..02aac3ff9ae 100644
--- a/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx
+++ b/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx
@@ -4,11 +4,11 @@ import { View } from 'react-native';
import { strings } from '../../../../../../../locales/i18n';
import StyledButton from '../../../../../../components/UI/StyledButton';
import { useStyles } from '../../../../../../component-library/hooks';
-import useApprovalRequest from '../../../hooks/useApprovalRequest';
+import { useConfirmActions } from '../../../hooks/useConfirmActions';
import styleSheet from './Footer.styles';
const Footer = () => {
- const { onConfirm, onReject } = useApprovalRequest();
+ const { onConfirm, onReject } = useConfirmActions();
const { styles } = useStyles(styleSheet, {});
return (
diff --git a/app/components/Views/confirmations/components/Confirm/Footer/__snapshots__/Footer.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/Footer/__snapshots__/Footer.test.tsx.snap
deleted file mode 100644
index a289fd77735..00000000000
--- a/app/components/Views/confirmations/components/Confirm/Footer/__snapshots__/Footer.test.tsx.snap
+++ /dev/null
@@ -1,130 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Footer should match snapshot for personal sign 1`] = `
-
-
-
- Reject
-
-
-
-
-
- Confirm
-
-
-
-`;
diff --git a/app/components/Views/confirmations/hooks/useConfirmActions.test.ts b/app/components/Views/confirmations/hooks/useConfirmActions.test.ts
new file mode 100644
index 00000000000..31a512d6997
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useConfirmActions.test.ts
@@ -0,0 +1,43 @@
+import Engine from '../../../../core/Engine';
+import { renderHookWithProvider } from '../../../../util/test/renderWithProvider';
+import { personalSignatureConfirmationState } from '../../../../util/test/confirm-data-helpers';
+import { useConfirmActions } from './useConfirmActions';
+
+jest.mock('../../../../core/Engine', () => ({
+ acceptPendingApproval: jest.fn(),
+ rejectPendingApproval: jest.fn(),
+}));
+
+const mockCaptureSignatureMetrics = jest.fn();
+jest.mock('./useSignatureMetrics', () => ({
+ useSignatureMetrics: () => ({
+ captureSignatureMetrics: mockCaptureSignatureMetrics,
+ }),
+}));
+
+const flushPromises = async () => await new Promise(process.nextTick);
+
+describe('useConfirmAction', () => {
+ afterEach(() => {
+ mockCaptureSignatureMetrics.mockClear();
+ });
+
+ it('call required callbacks when confirm button is clicked', async () => {
+ const { result } = renderHookWithProvider(() => useConfirmActions(), {
+ state: personalSignatureConfirmationState,
+ });
+ result?.current?.onConfirm();
+ expect(Engine.acceptPendingApproval).toHaveBeenCalledTimes(1);
+ await flushPromises();
+ expect(mockCaptureSignatureMetrics).toHaveBeenCalledTimes(1);
+ });
+
+ it('call required callbacks when reject button is clicked', async () => {
+ const { result } = renderHookWithProvider(() => useConfirmActions(), {
+ state: personalSignatureConfirmationState,
+ });
+ result?.current?.onReject();
+ expect(Engine.rejectPendingApproval).toHaveBeenCalledTimes(1);
+ expect(mockCaptureSignatureMetrics).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/app/components/Views/confirmations/hooks/useConfirmActions.ts b/app/components/Views/confirmations/hooks/useConfirmActions.ts
new file mode 100644
index 00000000000..01ab81fe5c0
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useConfirmActions.ts
@@ -0,0 +1,38 @@
+import { useCallback } from 'react';
+
+import { MetaMetricsEvents } from '../../../hooks/useMetrics';
+import { isSignatureRequest } from '../utils/confirm';
+import useApprovalRequest from './useApprovalRequest';
+import { useSignatureMetrics } from './useSignatureMetrics';
+
+export const useConfirmActions = () => {
+ const {
+ onConfirm: onRequestConfirm,
+ onReject: onRequestReject,
+ approvalRequest,
+ } = useApprovalRequest();
+ const { captureSignatureMetrics } = useSignatureMetrics();
+
+ const signatureRequest =
+ approvalRequest?.type && isSignatureRequest(approvalRequest?.type);
+
+ const onConfirm = useCallback(async () => {
+ await onRequestConfirm({
+ waitForResult: true,
+ deleteAfterResult: true,
+ handleErrors: false,
+ });
+ if (signatureRequest) {
+ captureSignatureMetrics(MetaMetricsEvents.SIGNATURE_APPROVED);
+ }
+ }, [captureSignatureMetrics, onRequestConfirm, signatureRequest]);
+
+ const onReject = useCallback(() => {
+ onRequestReject();
+ if (signatureRequest) {
+ captureSignatureMetrics(MetaMetricsEvents.SIGNATURE_REJECTED);
+ }
+ }, [captureSignatureMetrics, onRequestReject, signatureRequest]);
+
+ return { onConfirm, onReject };
+};
diff --git a/app/components/Views/confirmations/hooks/useSignatureMetrics.test.ts b/app/components/Views/confirmations/hooks/useSignatureMetrics.test.ts
new file mode 100644
index 00000000000..81b42db4be5
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useSignatureMetrics.test.ts
@@ -0,0 +1,57 @@
+import { MetaMetricsEvents } from '../../../../core/Analytics';
+import { renderHookWithProvider } from '../../../../util/test/renderWithProvider';
+import { useSignatureMetrics } from './useSignatureMetrics';
+
+const mockSigRequest = {
+ type: 'personal_sign',
+ messageParams: {
+ data: '0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765',
+ from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
+ meta: {
+ url: 'https://metamask.github.io/test-dapp/',
+ title: 'E2E Test Dapp',
+ icon: { uri: 'https://metamask.github.io/metamask-fox.svg' },
+ analytics: { request_source: 'In-App-Browser' },
+ },
+ origin: 'metamask.github.io',
+ metamaskId: '76b33b40-7b5c-11ef-bc0a-25bce29dbc09',
+ },
+ chainId: '0x0',
+};
+
+jest.mock('./useSignatureRequest', () => ({
+ useSignatureRequest: () => mockSigRequest,
+}));
+
+jest.mock('../../../../util/address', () => ({
+ getAddressAccountType: (str: string) => str,
+}));
+
+const mockTrackEvent = jest.fn().mockImplementation();
+jest.mock('../../../../core/Analytics', () => ({
+ ...jest.requireActual('../../../../core/Analytics'),
+ MetaMetrics: {
+ getInstance: () => ({ trackEvent: mockTrackEvent }),
+ },
+}));
+
+describe('useSignatureMetrics', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('should capture metrics events correctly', async () => {
+ const { result } = renderHookWithProvider(() => useSignatureMetrics(), {
+ state: {},
+ });
+ // first call for 'SIGNATURE_REQUESTED' event
+ expect(mockTrackEvent).toHaveBeenCalledTimes(1);
+ result?.current?.captureSignatureMetrics(
+ MetaMetricsEvents.SIGNATURE_APPROVED,
+ );
+ expect(mockTrackEvent).toHaveBeenCalledTimes(2);
+ result?.current?.captureSignatureMetrics(
+ MetaMetricsEvents.SIGNATURE_REJECTED,
+ );
+ expect(mockTrackEvent).toHaveBeenCalledTimes(3);
+ });
+});
diff --git a/app/components/Views/confirmations/hooks/useSignatureMetrics.ts b/app/components/Views/confirmations/hooks/useSignatureMetrics.ts
new file mode 100644
index 00000000000..ec345a42083
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useSignatureMetrics.ts
@@ -0,0 +1,76 @@
+import { useCallback, useEffect } from 'react';
+import type { Hex } from '@metamask/utils';
+
+import getDecimalChainId from '../../../../util/networks/getDecimalChainId';
+import { MetricsEventBuilder } from '../../../../core/Analytics/MetricsEventBuilder';
+import { MetaMetrics, MetaMetricsEvents } from '../../../../core/Analytics';
+
+import { getAddressAccountType } from '../../../../util/address';
+import { getBlockaidMetricsParams } from '../../../../util/blockaid';
+import { SecurityAlertResponse } from '../components/BlockaidBanner/BlockaidBanner.types';
+import { getHostFromUrl } from '../utils/generic';
+import { isSignatureRequest } from '../utils/confirm';
+import { useSignatureRequest } from './useSignatureRequest';
+
+interface MessageParamsType {
+ meta: Record;
+ from: string;
+ version: string;
+ securityAlertResponse: SecurityAlertResponse;
+}
+
+const getAnalyticsParams = (
+ messageParams: MessageParamsType,
+ type: string,
+ chainId?: Hex,
+) => {
+ const { meta = {}, from, securityAlertResponse, version } = messageParams;
+
+ return {
+ account_type: getAddressAccountType(from as string),
+ dapp_host_name: getHostFromUrl(meta.url as string) ?? 'N/A',
+ signature_type: type,
+ version: version || 'N/A',
+ chain_id: chainId ? getDecimalChainId(chainId) : '',
+ ui_customizations: ['redesigned_confirmation'],
+ ...(meta.analytics as Record),
+ ...(securityAlertResponse
+ ? getBlockaidMetricsParams(securityAlertResponse)
+ : {}),
+ };
+};
+
+export const useSignatureMetrics = () => {
+ const signatureRequest = useSignatureRequest();
+
+ const { chainId, messageParams, type } = signatureRequest ?? {};
+
+ const captureSignatureMetrics = useCallback(
+ async (
+ event: (typeof MetaMetricsEvents)[keyof typeof MetaMetricsEvents],
+ ) => {
+ if (!type || !isSignatureRequest(type)) {
+ return;
+ }
+
+ MetaMetrics.getInstance().trackEvent(
+ MetricsEventBuilder.createEventBuilder(event)
+ .addProperties(
+ getAnalyticsParams(
+ messageParams as unknown as MessageParamsType,
+ type,
+ chainId,
+ ),
+ )
+ .build(),
+ );
+ },
+ [chainId, messageParams, type],
+ );
+
+ useEffect(() => {
+ captureSignatureMetrics(MetaMetricsEvents.SIGNATURE_REQUESTED);
+ }, [captureSignatureMetrics]);
+
+ return { captureSignatureMetrics };
+};
diff --git a/app/components/Views/confirmations/utils/confirm.test.ts b/app/components/Views/confirmations/utils/confirm.test.ts
new file mode 100644
index 00000000000..4554655176e
--- /dev/null
+++ b/app/components/Views/confirmations/utils/confirm.test.ts
@@ -0,0 +1,14 @@
+import { ApprovalTypes } from '../../../../core/RPCMethods/RPCMethodMiddleware';
+import { isSignatureRequest } from './confirm';
+
+describe('Confirmation utils', () => {
+ describe('isSignatureRequest', () => {
+ it('should return correct value', async () => {
+ expect(isSignatureRequest(ApprovalTypes.PERSONAL_SIGN)).toBeTruthy();
+ expect(
+ isSignatureRequest(ApprovalTypes.ETH_SIGN_TYPED_DATA),
+ ).toBeTruthy();
+ expect(isSignatureRequest(ApprovalTypes.TRANSACTION)).toBeFalsy();
+ });
+ });
+});
diff --git a/app/components/Views/confirmations/utils/confirm.ts b/app/components/Views/confirmations/utils/confirm.ts
new file mode 100644
index 00000000000..6f9b61bdf5c
--- /dev/null
+++ b/app/components/Views/confirmations/utils/confirm.ts
@@ -0,0 +1,8 @@
+import { ApprovalTypes } from '../../../../core/RPCMethods/RPCMethodMiddleware';
+
+export function isSignatureRequest(requestType: string) {
+ return [
+ ApprovalTypes.PERSONAL_SIGN,
+ ApprovalTypes.ETH_SIGN_TYPED_DATA,
+ ].includes(requestType as ApprovalTypes);
+}
diff --git a/app/components/Views/confirmations/utils/generic.test.ts b/app/components/Views/confirmations/utils/generic.test.ts
new file mode 100644
index 00000000000..5326fc6970e
--- /dev/null
+++ b/app/components/Views/confirmations/utils/generic.test.ts
@@ -0,0 +1,10 @@
+import { getHostFromUrl } from './generic';
+
+describe('generic utils', () => {
+ describe('getHostFromUrl', () => {
+ it('should return correct value', async () => {
+ expect(getHostFromUrl('')).toBe(undefined);
+ expect(getHostFromUrl('https://www.dummy.com')).toBe('www.dummy.com');
+ });
+ });
+});
diff --git a/app/components/Views/confirmations/utils/generic.ts b/app/components/Views/confirmations/utils/generic.ts
new file mode 100644
index 00000000000..7ca5b9d1a6a
--- /dev/null
+++ b/app/components/Views/confirmations/utils/generic.ts
@@ -0,0 +1,13 @@
+import Logger from '../../../../util/Logger';
+
+export const getHostFromUrl = (url: string) => {
+ if (!url) {
+ return;
+ }
+ try {
+ return new URL(url).host;
+ } catch (error) {
+ Logger.error(error as Error);
+ }
+ return;
+};
diff --git a/app/components/Views/confirmations/utils/signatures.ts b/app/components/Views/confirmations/utils/signatures.ts
index ada8d3f20f6..04d41b6b19c 100644
--- a/app/components/Views/confirmations/utils/signatures.ts
+++ b/app/components/Views/confirmations/utils/signatures.ts
@@ -32,7 +32,7 @@ const parseTypedDataMessage = (dataToParse: string) => {
const messageValue = extractLargeMessageValue(dataToParse);
if (result.message?.value) {
- result.message.value = messageValue || String(result.message.value);
+ result.message.value = messageValue ?? String(result.message.value);
}
return result;
diff --git a/app/core/EngineService/EngineService.test.ts b/app/core/EngineService/EngineService.test.ts
index 1afd30585b5..b35cbe45aba 100644
--- a/app/core/EngineService/EngineService.test.ts
+++ b/app/core/EngineService/EngineService.test.ts
@@ -76,6 +76,7 @@ jest.mock('../Engine', () => {
UserStorageController: { subscribe: jest.fn() },
NotificationServicesController: { subscribe: jest.fn() },
SelectedNetworkController: { subscribe: jest.fn() },
+ SignatureController: { subscribe: jest.fn() },
},
};
return instance;
diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts
index 0e103d522bc..dd49c701361 100644
--- a/app/core/EngineService/EngineService.ts
+++ b/app/core/EngineService/EngineService.ts
@@ -197,6 +197,10 @@ export class EngineService {
name: 'PPOMController',
key: `${engine.context.PPOMController.name}:stateChange`,
},
+ {
+ name: 'SignatureController',
+ key: `${engine.context.SignatureController.name}:stateChange`,
+ },
];
engine.controllerMessenger.subscribeOnceIf(