Skip to content

Commit

Permalink
feat: add privacy mode (#28021)
Browse files Browse the repository at this point in the history
<!--
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**

Adds a privacy mode toggle (an eye icon next to the main balance) that
hides all sensitive information/token balances

**UPDATE**
Here is feedback from @amandaye0h and has been currently implemented in
this PR

[Figma](https://www.figma.com/design/aMYisczaJyEsYl1TYdcPUL/Portfolio-View?node-id=6219-62460&t=aeTv5cenoUPUrg1c-4)

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28021?quickstart=1)

## **Related issues**

Fixes:
MetaMask/MetaMask-planning#3416
MetaMask/MetaMask-planning#3418
MetaMask/MetaMask-planning#3419

## **Manual testing steps**

1. Go to the Wallet page
2. Click on the new Eye icon next to the balance
3. All balances should be hidden
4. Click on the Eye icon once again
5. All balances should be shown

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**


https://github.com/user-attachments/assets/2950ac0c-593d-4daa-aa5d-3e6c3a2d5598


https://github.com/user-attachments/assets/6371c2a2-04fa-48a3-8744-991a1540d5f2

<img width="496" alt="Screenshot 2024-10-22 at 18 43 19"
src="https://github.com/user-attachments/assets/d7c2f681-75c7-4be0-921b-f3c5186f8d4a">



## **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.

---------

Co-authored-by: vinnyhoward <vincenguyenhoward@gmail.com>
Co-authored-by: David Walsh <davidwalsh83@gmail.com>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 62cc460 commit 05da3f7
Show file tree
Hide file tree
Showing 39 changed files with 284 additions and 48 deletions.
1 change: 1 addition & 0 deletions app/scripts/constants/sentry-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export const SENTRY_BACKGROUND_STATE = {
showNativeTokenAsMainBalance: true,
petnamesEnabled: true,
showConfirmationAdvancedDetails: true,
privacyMode: false,
},
useExternalServices: false,
selectedAddress: false,
Expand Down
2 changes: 2 additions & 0 deletions app/scripts/controllers/preferences-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ describe('preferences controller', () => {
expect(controller.state.preferences).toStrictEqual({
autoLockTimeLimit: undefined,
showExtensionInFullSizeView: false,
privacyMode: false,
showFiatInTestnets: false,
showTestNetworks: false,
smartTransactionsOptInStatus: null,
Expand Down Expand Up @@ -764,6 +765,7 @@ describe('preferences controller', () => {
useNativeCurrencyAsPrimaryCurrency: true,
hideZeroBalanceTokens: false,
petnamesEnabled: true,
privacyMode: false,
redesignedConfirmationsEnabled: true,
redesignedTransactionsEnabled: true,
shouldShowAggregatedBalancePopover: true,
Expand Down
2 changes: 2 additions & 0 deletions app/scripts/controllers/preferences-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export type Preferences = {
redesignedTransactionsEnabled: boolean;
featureNotificationsEnabled: boolean;
showMultiRpcModal: boolean;
privacyMode: boolean;
isRedesignedConfirmationsDeveloperEnabled: boolean;
showConfirmationAdvancedDetails: boolean;
tokenSortConfig: {
Expand Down Expand Up @@ -214,6 +215,7 @@ export const getDefaultPreferencesControllerState =
isRedesignedConfirmationsDeveloperEnabled: false,
showConfirmationAdvancedDetails: false,
showMultiRpcModal: false,
privacyMode: false,
shouldShowAggregatedBalancePopover: true, // by default user should see popover;
tokenSortConfig: {
key: 'tokenFiatAmount',
Expand Down
1 change: 1 addition & 0 deletions app/scripts/fixtures/with-preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const FIXTURES_PREFERENCES = {
showNftAutodetectModal: false,
isRedesignedConfirmationsDeveloperEnabled: false,
showConfirmationAdvancedDetails: false,
privacyMode: false,
},
featureFlags: {
sendHexData: true,
Expand Down
1 change: 1 addition & 0 deletions test/e2e/fixture-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function onboardingFixture() {
hideZeroBalanceTokens: false,
showExtensionInFullSizeView: false,
showFiatInTestnets: false,
privacyMode: false,
showTestNetworks: false,
smartTransactionsOptInStatus: false,
showNativeTokenAsMainBalance: true,
Expand Down
1 change: 1 addition & 0 deletions test/e2e/tests/metrics/errors.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,7 @@ describe('Sentry errors', function () {
preferences: {
autoLockTimeLimit: true, // Initialized as undefined
showConfirmationAdvancedDetails: true,
privacyMode: false,
},
smartTransactionsState: {
fees: {
Expand Down
106 changes: 106 additions & 0 deletions test/e2e/tests/privacy-mode/privacy-mode.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { strict: assert } = require('assert');
const {
withFixtures,
unlockWallet,
defaultGanacheOptions,
} = require('../../helpers');
const FixtureBuilder = require('../../fixture-builder');

describe('Privacy Mode', function () {
it('should activate privacy mode, then deactivate it', async function () {
await withFixtures(
{
dapp: true,
fixtures: new FixtureBuilder().withPreferencesController().build(),
ganacheOptions: defaultGanacheOptions,
title: this.test.fullTitle(),
},
async ({ driver }) => {
async function checkForHeaderValue(value) {
const balanceElement = await driver.findElement(
'[data-testid="eth-overview__primary-currency"] .currency-display-component__text',
);
const surveyText = await balanceElement.getText();
assert.equal(
surveyText,
value,
`Header balance should be "${value}"`,
);
}

async function checkForTokenValue(value) {
const balanceElement = await driver.findElement(
'[data-testid="multichain-token-list-item-secondary-value"]',
);
const surveyText = await balanceElement.getText();
assert.equal(surveyText, value, `Token balance should be "${value}"`);
}

async function checkForPrivacy() {
await checkForHeaderValue('••••••');
await checkForTokenValue('•••••••••');
}

async function checkForNoPrivacy() {
await checkForHeaderValue('25');
await checkForTokenValue('25 ETH');
}

async function togglePrivacy() {
const balanceElement = await driver.findElement(
'[data-testid="eth-overview__primary-currency"] .currency-display-component__text',
);
const initialText = await balanceElement.getText();

await driver.clickElement('[data-testid="sensitive-toggle"]');
await driver.wait(async () => {
const currentText = await balanceElement.getText();
return currentText !== initialText;
}, 2e3);
}

await unlockWallet(driver);
await checkForNoPrivacy();
await togglePrivacy();
await checkForPrivacy();
await togglePrivacy();
await checkForNoPrivacy();
},
);
});

it('should hide fiat balance and token balance when privacy mode is activated', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().withPreferencesController().build(),
ganacheOptions: defaultGanacheOptions,
title: this.test.fullTitle(),
},
async ({ driver }) => {
await unlockWallet(driver);

async function togglePrivacy() {
const balanceElement = await driver.findElement(
'[data-testid="eth-overview__primary-currency"] .currency-display-component__text',
);
const initialText = await balanceElement.getText();

await driver.clickElement('[data-testid="sensitive-toggle"]');
await driver.wait(async () => {
const currentText = await balanceElement.getText();
return currentText !== initialText;
}, 2e3);
}

await togglePrivacy();
await driver.clickElement('[data-testid="account-menu-icon"]');
const valueText = await driver.findElement(
'[data-testid="account-value-and-suffix"]',
);
const valueTextContent = await valueText.getText();

assert.equal(valueTextContent, '••••••');
},
);
});
});
3 changes: 2 additions & 1 deletion test/integration/data/onboarding-completion-route.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@
"hideZeroBalanceTokens": false,
"petnamesEnabled": true,
"redesignedConfirmationsEnabled": true,
"featureNotificationsEnabled": false
"featureNotificationsEnabled": false,
"privacyMode": false
},
"preventPollingOnNetworkRestart": false,
"previousAppVersion": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ import {
getMultichainIsMainnet,
getMultichainSelectedAccountCachedBalance,
} from '../../../../../selectors/multichain';
import { getPreferences } from '../../../../../selectors';
import { TokenListItem } from '../../../../multichain';
import { useIsOriginalNativeTokenSymbol } from '../../../../../hooks/useIsOriginalNativeTokenSymbol';
import { AssetListProps } from '../asset-list';
import { useNativeTokenBalance } from './use-native-token-balance';
// import { getPreferences } from '../../../../../selectors';

const NativeToken = ({ onClickAsset }: AssetListProps) => {
const nativeCurrency = useSelector(getMultichainNativeCurrency);
const isMainnet = useSelector(getMultichainIsMainnet);
const { chainId, ticker, type, rpcUrl } = useSelector(
getMultichainCurrentNetwork,
);
const { privacyMode } = useSelector(getPreferences);
const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol(
chainId,
ticker,
Expand Down Expand Up @@ -52,6 +53,7 @@ const NativeToken = ({ onClickAsset }: AssetListProps) => {
isNativeCurrency
isStakeable={isStakeable}
showPercentage
privacyMode={privacyMode}
/>
);
};
Expand Down
5 changes: 4 additions & 1 deletion ui/components/app/assets/token-cell/token-cell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fireEvent } from '@testing-library/react';
import { useSelector } from 'react-redux';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount';
import { getTokenList } from '../../../../selectors';
import { getTokenList, getPreferences } from '../../../../selectors';
import {
getMultichainCurrentChainId,
getMultichainIsEvm,
Expand Down Expand Up @@ -98,6 +98,9 @@ describe('Token Cell', () => {
};
const useSelectorMock = useSelector;
(useSelectorMock as jest.Mock).mockImplementation((selector) => {
if (selector === getPreferences) {
return { privacyMode: false };
}
if (selector === getTokenList) {
return MOCK_GET_TOKEN_LIST;
}
Expand Down
4 changes: 3 additions & 1 deletion ui/components/app/assets/token-cell/token-cell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getTokenList } from '../../../../selectors';
import { getTokenList, getPreferences } from '../../../../selectors';
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount';
import { TokenListItem } from '../../../multichain';
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
Expand All @@ -23,6 +23,7 @@ export default function TokenCell({
onClick,
}: TokenCellProps) {
const tokenList = useSelector(getTokenList);
const { privacyMode } = useSelector(getPreferences);
const tokenData = Object.values(tokenList).find(
(token) =>
isEqualCaseInsensitive(token.symbol, symbol) &&
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function TokenCell({
isOriginalTokenSymbol={isOriginalTokenSymbol}
address={address}
showPercentage
privacyMode={privacyMode}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`ConfirmInfoRowCurrency should display in currency passed 1`] = `
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
$82.65
</span>
Expand All @@ -37,6 +38,7 @@ exports[`ConfirmInfoRowCurrency should display value in user preferred currency
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
0.14861879
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ exports[`CurrencyInput Component rendering should disable unit input 1`] = `
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
$0.00
</span>
Expand Down Expand Up @@ -89,6 +90,7 @@ exports[`CurrencyInput Component rendering should render properly with a fiat va
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
0.004327880204275946
</span>
Expand Down Expand Up @@ -183,6 +185,7 @@ exports[`CurrencyInput Component rendering should render properly with an ETH va
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
$231.06
</span>
Expand Down Expand Up @@ -237,6 +240,7 @@ exports[`CurrencyInput Component rendering should render properly without a suff
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
$0.00
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ exports[`CancelTransactionGasFee Component should render 1`] = `
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
&lt;0.000001
</span>
Expand All @@ -26,6 +27,7 @@ exports[`CancelTransactionGasFee Component should render 1`] = `
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
&lt;0.000001
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ exports[`UserPreferencedCurrencyDisplay Component rendering should match snapsho
>
<span
class="mm-box mm-text currency-display-component__text mm-text--inherit mm-text--ellipsis mm-box--color-text-default"
data-testid="account-value-and-suffix"
>
0
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getSelectedAccount,
getShouldHideZeroBalanceTokens,
getTokensMarketData,
getPreferences,
} from '../../../selectors';
import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance';
import { AggregatedPercentageOverview } from './aggregated-percentage-overview';
Expand All @@ -22,6 +23,7 @@ jest.mock('../../../ducks/locale/locale', () => ({
jest.mock('../../../selectors', () => ({
getCurrentCurrency: jest.fn(),
getSelectedAccount: jest.fn(),
getPreferences: jest.fn(),
getShouldHideZeroBalanceTokens: jest.fn(),
getTokensMarketData: jest.fn(),
}));
Expand All @@ -32,6 +34,7 @@ jest.mock('../../../hooks/useAccountTotalFiatBalance', () => ({

const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock;
const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock;
const mockGetPreferences = getPreferences as jest.Mock;
const mockGetSelectedAccount = getSelectedAccount as unknown as jest.Mock;
const mockGetShouldHideZeroBalanceTokens =
getShouldHideZeroBalanceTokens as jest.Mock;
Expand Down Expand Up @@ -159,6 +162,7 @@ describe('AggregatedPercentageOverview', () => {
beforeEach(() => {
mockGetIntlLocale.mockReturnValue('en-US');
mockGetCurrentCurrency.mockReturnValue('USD');
mockGetPreferences.mockReturnValue({ privacyMode: false });
mockGetSelectedAccount.mockReturnValue(selectedAccountMock);
mockGetShouldHideZeroBalanceTokens.mockReturnValue(false);
mockGetTokensMarketData.mockReturnValue(marketDataMock);
Expand Down
Loading

0 comments on commit 05da3f7

Please sign in to comment.