Skip to content

Commit cfb5fce

Browse files
authored
feat: add Privacy tab to Settings V2 (#40743)
## **Description** Adds the Privacy tab to Settings V2, migrating privacy-related settings from the legacy `security-tab.component.js` to the new Settings V2 architecture. This work will be broken up into a few PRs to keep them small. This one adds the following: - Created a new Privacy tab (`privacy-tab.tsx`) with a registry-based architecture - Migrated 5 settings items from Settings V1: - **Basic Functionality** - Toggle for external services - **Batch Account Balance Requests** - Multi-account balance checker - **Skip Link Confirmation Screens** - Deep link interstitial setting - **MetaMetrics** - Analytics participation toggle (with complex enable/disable logic) - **Data Collection for Marketing** - Marketing consent toggle [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/40743?quickstart=1) ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/CEUX-911 ## **Manual testing steps** 1. Uncomment the object under `Uncomment to view Settings V2 in Hamburger Menu` 2. Navigate to Settings > Privacy 3. Verify all toggle items render correctly: - Basic Functionality toggle - Batch Account Balance Requests toggle - Skip Link Confirmation Screens toggle - MetaMetrics toggle - Data Collection for Marketing toggle 4. Test toggling each setting and verify state persists 5. Verify MetaMetrics toggle disables Data Collection when turned off 6. Verify Data Collection toggle is disabled when MetaMetrics is off ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** N/A - New feature ### **After** <img width="433" height="766" alt="image" src="https://github.com/user-attachments/assets/2312e6fe-acbe-4643-b8b5-d7ff25d3aff5" /> <img width="434" height="770" alt="image" src="https://github.com/user-attachments/assets/67fd1b38-4b21-428e-9eab-505eddf4f9fa" /> ## **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/main/.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/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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches privacy/analytics consent toggles and related MetaMetrics tracking, so mistakes could change user opt-in/out behavior. Scope is mostly UI wiring to existing actions/selectors with added tests, reducing risk. > > **Overview** > Adds a new **Settings V2 Privacy** page (`/settings-v2/privacy`) and menu entry, implemented as a registry-driven tab with toggles for **Basic Functionality (external services)**, **Batch account balance requests**, **Skip link confirmation screens**, **MetaMetrics participation**, and **Data collection for marketing** (including disable/consent-cascade behavior when MetaMetrics is turned off). > > Updates i18n copy (new V2 descriptions, new skip-link strings, revised marketing-data wording) and adds unit tests/console baselines plus a snapshot update reflecting the new marketing description text. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 026d54b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 9719b34 commit cfb5fce

File tree

14 files changed

+728
-3
lines changed

14 files changed

+728
-3
lines changed

app/_locales/en/messages.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/_locales/en_GB/messages.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/jest/console-baseline-unit.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2847,6 +2847,18 @@
28472847
"ui/pages/settings-v2/preferences-and-display-tab/theme-sub-page.test.tsx": {
28482848
"warn: ⚠️ React Router Future Flag": 2
28492849
},
2850+
"ui/pages/settings-v2/privacy-tab/basic-functionality-item.test.tsx": {
2851+
"React: componentWill* lifecycle deprecations": 1,
2852+
"warn: ⚠️ React Router Future Flag": 2
2853+
},
2854+
"ui/pages/settings-v2/privacy-tab/data-collection-item.test.tsx": {
2855+
"React: componentWill* lifecycle deprecations": 1,
2856+
"warn: ⚠️ React Router Future Flag": 2
2857+
},
2858+
"ui/pages/settings-v2/privacy-tab/metametrics-item.test.tsx": {
2859+
"React: componentWill* lifecycle deprecations": 1,
2860+
"warn: ⚠️ React Router Future Flag": 2
2861+
},
28502862
"ui/pages/settings-v2/settings-v2.test.tsx": {
28512863
"React: componentWill* lifecycle deprecations": 1,
28522864
"warn: ⚠️ React Router Future Flag": 2

ui/helpers/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const THEME_ROUTE = '/settings-v2/preferences-and-display/theme';
2121
export const LANGUAGE_ROUTE = '/settings-v2/preferences-and-display/language';
2222
export const ACCOUNT_IDENTICON_ROUTE =
2323
'/settings-v2/preferences-and-display/account-identicon';
24+
export const PRIVACY_ROUTE = '/settings-v2/privacy';
2425
export const GENERAL_ROUTE = '/settings/general';
2526
export const ADVANCED_ROUTE = '/settings/advanced';
2627
export const DEVELOPER_OPTIONS_ROUTE = '/settings/developer-options';
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { fireEvent, screen } from '@testing-library/react';
2+
import React from 'react';
3+
import configureMockStore from 'redux-mock-store';
4+
import thunk from 'redux-thunk';
5+
import mockState from '../../../../test/data/mock-state.json';
6+
import { enLocale as messages } from '../../../../test/lib/i18n-helpers';
7+
import { renderWithProvider } from '../../../../test/lib/render-helpers-navigate';
8+
import { setBackgroundConnection } from '../../../store/background-connection';
9+
import { BasicFunctionalityToggleItem } from './basic-functionality-item';
10+
11+
const mockToggleExternalServices = jest.fn();
12+
const mockOpenBasicFunctionalityModal = jest.fn();
13+
14+
jest.mock('../../../store/actions', () => ({
15+
...jest.requireActual('../../../store/actions'),
16+
toggleExternalServices: (val: boolean) => {
17+
mockToggleExternalServices(val);
18+
return { type: 'MOCK_ACTION' };
19+
},
20+
}));
21+
22+
jest.mock('../../../ducks/app/app', () => ({
23+
...jest.requireActual('../../../ducks/app/app'),
24+
openBasicFunctionalityModal: () => {
25+
mockOpenBasicFunctionalityModal();
26+
return { type: 'MOCK_OPEN_MODAL' };
27+
},
28+
}));
29+
30+
const backgroundConnectionMock = new Proxy(
31+
{},
32+
{ get: () => jest.fn().mockResolvedValue(undefined) },
33+
);
34+
35+
describe('BasicFunctionalityToggleItem', () => {
36+
beforeEach(() => {
37+
jest.clearAllMocks();
38+
setBackgroundConnection(backgroundConnectionMock as never);
39+
});
40+
41+
it('renders title', () => {
42+
const mockStore = configureMockStore([thunk])(mockState);
43+
renderWithProvider(<BasicFunctionalityToggleItem />, mockStore);
44+
45+
expect(
46+
screen.getByText(messages.basicConfigurationLabel.message),
47+
).toBeInTheDocument();
48+
});
49+
50+
it('renders description with privacy link', () => {
51+
const mockStore = configureMockStore([thunk])(mockState);
52+
renderWithProvider(<BasicFunctionalityToggleItem />, mockStore);
53+
54+
const link = screen.getByRole('link', {
55+
name: messages.privacyMsg.message,
56+
});
57+
expect(link).toBeInTheDocument();
58+
expect(link).toHaveAttribute('href', 'https://consensys.io/privacy-policy');
59+
});
60+
61+
it('renders toggle in enabled state when useExternalServices is true', () => {
62+
const storeEnabled = configureMockStore([thunk])({
63+
...mockState,
64+
metamask: {
65+
...mockState.metamask,
66+
useExternalServices: true,
67+
},
68+
});
69+
renderWithProvider(<BasicFunctionalityToggleItem />, storeEnabled);
70+
71+
expect(screen.getByTestId('basic-functionality-toggle')).toHaveAttribute(
72+
'value',
73+
'true',
74+
);
75+
});
76+
77+
it('renders toggle in disabled state when useExternalServices is false', () => {
78+
const storeDisabled = configureMockStore([thunk])({
79+
...mockState,
80+
metamask: {
81+
...mockState.metamask,
82+
useExternalServices: false,
83+
},
84+
});
85+
renderWithProvider(<BasicFunctionalityToggleItem />, storeDisabled);
86+
87+
expect(screen.getByTestId('basic-functionality-toggle')).toHaveAttribute(
88+
'value',
89+
'false',
90+
);
91+
});
92+
93+
it('opens modal when toggling off', () => {
94+
const storeEnabled = configureMockStore([thunk])({
95+
...mockState,
96+
metamask: {
97+
...mockState.metamask,
98+
useExternalServices: true,
99+
},
100+
});
101+
renderWithProvider(<BasicFunctionalityToggleItem />, storeEnabled);
102+
103+
fireEvent.click(screen.getByTestId('basic-functionality-toggle'));
104+
105+
expect(mockOpenBasicFunctionalityModal).toHaveBeenCalled();
106+
expect(mockToggleExternalServices).not.toHaveBeenCalled();
107+
});
108+
109+
it('calls toggleExternalServices when toggling on', () => {
110+
const storeDisabled = configureMockStore([thunk])({
111+
...mockState,
112+
metamask: {
113+
...mockState.metamask,
114+
useExternalServices: false,
115+
},
116+
});
117+
renderWithProvider(<BasicFunctionalityToggleItem />, storeDisabled);
118+
119+
fireEvent.click(screen.getByTestId('basic-functionality-toggle'));
120+
121+
expect(mockToggleExternalServices).toHaveBeenCalledWith(true);
122+
expect(mockOpenBasicFunctionalityModal).not.toHaveBeenCalled();
123+
});
124+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React, { useContext } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import { TextButton } from '@metamask/design-system-react';
4+
import { useI18nContext } from '../../../hooks/useI18nContext';
5+
import { getUseExternalServices } from '../../../selectors';
6+
import { toggleExternalServices } from '../../../store/actions';
7+
import { openBasicFunctionalityModal } from '../../../ducks/app/app';
8+
import { SettingsToggleItem } from '../../settings/settings-toggle-item';
9+
import { MetaMetricsContext } from '../../../contexts/metametrics';
10+
import {
11+
MetaMetricsEventCategory,
12+
MetaMetricsEventName,
13+
} from '../../../../shared/constants/metametrics';
14+
15+
export const BasicFunctionalityToggleItem = () => {
16+
const t = useI18nContext();
17+
const dispatch = useDispatch();
18+
const { trackEvent } = useContext(MetaMetricsContext);
19+
const useExternalServices = useSelector(getUseExternalServices);
20+
21+
const handleToggle = (value: boolean) => {
22+
if (value) {
23+
dispatch(openBasicFunctionalityModal());
24+
} else {
25+
dispatch(toggleExternalServices(true));
26+
trackEvent({
27+
category: MetaMetricsEventCategory.Settings,
28+
event: MetaMetricsEventName.SettingsUpdated,
29+
properties: {
30+
/* eslint-disable @typescript-eslint/naming-convention */
31+
settings_group: 'security_privacy',
32+
settings_type: 'basic_functionality',
33+
old_value: false,
34+
new_value: true,
35+
was_notifications_on: false,
36+
was_profile_syncing_on: false,
37+
/* eslint-enable @typescript-eslint/naming-convention */
38+
},
39+
});
40+
}
41+
};
42+
43+
const description = t('basicConfigurationDescriptionV2', [
44+
<TextButton asChild key="privacy-link">
45+
<a
46+
href="https://consensys.io/privacy-policy"
47+
target="_blank"
48+
rel="noopener noreferrer"
49+
>
50+
{t('privacyMsg')}
51+
</a>
52+
</TextButton>,
53+
]);
54+
55+
return (
56+
<SettingsToggleItem
57+
title={t('basicConfigurationLabel')}
58+
description={description}
59+
value={useExternalServices}
60+
onToggle={handleToggle}
61+
dataTestId="basic-functionality-toggle"
62+
/>
63+
);
64+
};

0 commit comments

Comments
 (0)