diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 0d8fde99345..506d7e6e37d 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -16,6 +16,7 @@ import { } from '../../../util/test/accountsControllerTestUtils'; import { mockNetworkState } from '../../../util/test/network'; import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { AccountSelectorListProps } from './AccountSelectorList.types'; const BUSINESS_ACCOUNT = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; const PERSONAL_ACCOUNT = '0xd018538C87232FF95acbCe4870629b75640a78E7'; @@ -82,8 +83,9 @@ const initialState = { const onSelectAccount = jest.fn(); const onRemoveImportedAccount = jest.fn(); - -const AccountSelectorListUseAccounts = () => { +const AccountSelectorListUseAccounts: React.FC = ({ + privacyMode = false, +}) => { const { accounts, ensByAccountAddress } = useAccounts(); return ( { accounts={accounts} ensByAccountAddress={ensByAccountAddress} isRemoveAccountEnabled + privacyMode={privacyMode} /> ); }; @@ -118,7 +121,7 @@ const renderComponent = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any state: any = {}, AccountSelectorListTest = AccountSelectorListUseAccounts, -) => renderWithProvider(, { state }); +) => renderWithProvider(, { state }); describe('AccountSelectorList', () => { beforeEach(() => { @@ -238,4 +241,46 @@ describe('AccountSelectorList', () => { expect(snapTag).toBeDefined(); }); }); + it('Text is not hidden when privacy mode is off', async () => { + const state = { + ...initialState, + privacyMode: false, + }; + + const { queryByTestId } = renderComponent(state); + + await waitFor(() => { + const businessAccountItem = queryByTestId( + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + ); + + expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); + expect( + within(businessAccountItem).getByText(regex.usd(3200)), + ).toBeDefined(); + + expect(within(businessAccountItem).queryByText('••••••')).toBeNull(); + }); + }); + it('Text is hidden when privacy mode is on', async () => { + const state = { + ...initialState, + privacyMode: true, + }; + + const { queryByTestId } = renderComponent(state); + + await waitFor(() => { + const businessAccountItem = queryByTestId( + `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + ); + + expect(within(businessAccountItem).queryByText(regex.eth(1))).toBeNull(); + expect( + within(businessAccountItem).queryByText(regex.usd(3200)), + ).toBeNull(); + + expect(within(businessAccountItem).getByText('••••••')).toBeDefined(); + }); + }); }); diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index f2364e63402..30b8241836f 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -14,7 +14,6 @@ import Cell, { } from '../../../component-library/components/Cells/Cell'; import { InternalAccount } from '@metamask/keyring-api'; import { useStyles } from '../../../component-library/hooks'; -import { selectPrivacyMode } from '../../../selectors/preferencesController'; import { TextColor } from '../../../component-library/components/Texts/Text'; import SensitiveText, { SensitiveTextLength, @@ -52,6 +51,7 @@ const AccountSelectorList = ({ isSelectionDisabled, isRemoveAccountEnabled = false, isAutoScrollEnabled = true, + privacyMode = false, ...props }: AccountSelectorListProps) => { const { navigate } = useNavigation(); @@ -72,7 +72,6 @@ const AccountSelectorList = ({ ); const internalAccounts = useSelector(selectInternalAccounts); - const privacyMode = useSelector(selectPrivacyMode); const getKeyExtractor = ({ address }: Account) => address; const renderAccountBalances = useCallback( diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts index 4059c710cc9..a2f651c718e 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.types.ts @@ -56,4 +56,8 @@ export interface AccountSelectorListProps * Optional boolean to enable removing accounts. */ isRemoveAccountEnabled?: boolean; + /** + * Optional boolean to indicate if privacy mode is enabled. + */ + privacyMode?: boolean; } diff --git a/app/components/UI/WalletAccount/WalletAccount.test.tsx b/app/components/UI/WalletAccount/WalletAccount.test.tsx index 621c000a565..d9a1de2c7d4 100644 --- a/app/components/UI/WalletAccount/WalletAccount.test.tsx +++ b/app/components/UI/WalletAccount/WalletAccount.test.tsx @@ -57,6 +57,9 @@ const mockInitialState: DeepPartial = { engine: { backgroundState: { ...backgroundState, + PreferencesController: { + privacyMode: false, + }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, NetworkController: { ...mockNetworkState({ @@ -101,14 +104,21 @@ jest.mock('../../../util/ENSUtils', () => ({ }), })); +const mockSelector = jest + .fn() + .mockImplementation((callback) => callback(mockInitialState)); + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), - useSelector: jest - .fn() - .mockImplementation((callback) => callback(mockInitialState)), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useSelector: (selector: any) => mockSelector(selector), })); describe('WalletAccount', () => { + beforeEach(() => { + mockSelector.mockImplementation((callback) => callback(mockInitialState)); + }); + it('renders correctly', () => { const { toJSON } = renderWithProvider(, { state: mockInitialState, @@ -132,7 +142,9 @@ describe('WalletAccount', () => { fireEvent.press(getByTestId(WalletViewSelectorsIDs.ACCOUNT_ICON)); expect(mockNavigate).toHaveBeenCalledWith( - ...createAccountSelectorNavDetails({}), + ...createAccountSelectorNavDetails({ + privacyMode: false, + }), ); }); it('displays the correct account name', () => { @@ -164,4 +176,47 @@ describe('WalletAccount', () => { expect(getByText(customAccountName)).toBeDefined(); }); }); + + it('should navigate to account selector with privacy mode disabled', () => { + const { getByTestId } = renderWithProvider(, { + state: mockInitialState, + }); + + fireEvent.press(getByTestId(WalletViewSelectorsIDs.ACCOUNT_ICON)); + expect(mockNavigate).toHaveBeenCalledWith( + ...createAccountSelectorNavDetails({ + privacyMode: false, + }), + ); + }); + + it('should navigate to account selector with privacy mode enabled', () => { + const stateWithPrivacyMode = { + ...mockInitialState, + engine: { + ...mockInitialState.engine, + backgroundState: { + ...mockInitialState.engine?.backgroundState, + PreferencesController: { + privacyMode: true, + }, + }, + }, + }; + + mockSelector.mockImplementation((callback) => + callback(stateWithPrivacyMode), + ); + + const { getByTestId } = renderWithProvider(, { + state: stateWithPrivacyMode, + }); + + fireEvent.press(getByTestId(WalletViewSelectorsIDs.ACCOUNT_ICON)); + expect(mockNavigate).toHaveBeenCalledWith( + ...createAccountSelectorNavDetails({ + privacyMode: true, + }), + ); + }); }); diff --git a/app/components/UI/WalletAccount/WalletAccount.tsx b/app/components/UI/WalletAccount/WalletAccount.tsx index 5c123f6d45a..d42f757971b 100644 --- a/app/components/UI/WalletAccount/WalletAccount.tsx +++ b/app/components/UI/WalletAccount/WalletAccount.tsx @@ -5,6 +5,7 @@ import { useNavigation } from '@react-navigation/native'; import { View } from 'react-native'; // External dependencies +import { selectPrivacyMode } from '../../../selectors/preferencesController'; import { IconName } from '../../../component-library/components/Icons/Icon'; import PickerAccount from '../../../component-library/components/Pickers/PickerAccount'; import { AvatarAccountType } from '../../../component-library/components/Avatars/Avatar/variants/AvatarAccount'; @@ -34,6 +35,7 @@ const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref) => { const yourAccountRef = useRef(null); const accountActionsRef = useRef(null); const selectedAccount = useSelector(selectSelectedInternalAccount); + const privacyMode = useSelector(selectPrivacyMode); const { ensName } = useEnsNameByAddress(selectedAccount?.address); const defaultName = selectedAccount?.metadata?.name; const accountName = useMemo( @@ -78,7 +80,11 @@ const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref) => { accountName={accountName} accountAvatarType={accountAvatarType} onPress={() => { - navigate(...createAccountSelectorNavDetails({})); + navigate( + ...createAccountSelectorNavDetails({ + privacyMode, + }), + ); }} accountTypeLabel={ getLabelTextByAddress(selectedAccount?.address) || undefined diff --git a/app/components/Views/AccountSelector/AccountSelector.test.tsx b/app/components/Views/AccountSelector/AccountSelector.test.tsx new file mode 100644 index 00000000000..c0382a68803 --- /dev/null +++ b/app/components/Views/AccountSelector/AccountSelector.test.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import { screen } from '@testing-library/react-native'; +import AccountSelector from './AccountSelector'; +import { renderScreen } from '../../../util/test/renderWithProvider'; +import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; +import Routes from '../../../constants/navigation/Routes'; +import { + AccountSelectorParams, + AccountSelectorProps, +} from './AccountSelector.types'; +import { + MOCK_ACCOUNTS_CONTROLLER_STATE, + expectedUuid2, +} from '../../../util/test/accountsControllerTestUtils'; + +const mockAccounts = [ + { + address: '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756', + balance: '0x0', + name: 'Account 1', + }, + { + address: '0x2B5634C42055806a59e9107ED44D43c426E58258', + balance: '0x0', + name: 'Account 2', + }, +]; + +const mockEnsByAccountAddress = { + '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756': 'test.eth', +}; + +const mockInitialState = { + engine: { + backgroundState: { + KeyringController: { + keyrings: [ + { + type: 'HD Key Tree', + accounts: [ + '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756', + '0x2B5634C42055806a59e9107ED44D43c426E58258', + ], + }, + ], + }, + AccountsController: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE, + internalAccounts: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts, + accounts: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts, + [expectedUuid2]: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts[ + expectedUuid2 + ], + methods: [], + }, + }, + }, + }, + }, + }, + accounts: { + reloadAccounts: false, + }, + settings: { + useBlockieIcon: false, + }, +}; + +// Mock the Redux dispatch +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useSelector: (selector: any) => selector(mockInitialState), +})); + +jest.mock('../../../components/hooks/useAccounts', () => ({ + useAccounts: jest.fn().mockReturnValue({ + accounts: mockAccounts, + ensByAccountAddress: mockEnsByAccountAddress, + isLoading: false, + }), +})); + +jest.mock('../../../core/Engine', () => ({ + setSelectedAddress: jest.fn(), +})); + +const mockTrackEvent = jest.fn(); +jest.mock('../../../components/hooks/useMetrics', () => ({ + useMetrics: () => ({ + trackEvent: mockTrackEvent, + }), +})); + +const mockRoute: AccountSelectorProps['route'] = { + params: { + onSelectAccount: jest.fn((address: string) => address), + checkBalanceError: (balance: string) => balance, + privacyMode: false, + } as AccountSelectorParams, +}; + +const AccountSelectorWrapper = () => ; + +describe('AccountSelector', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render correctly', () => { + const wrapper = renderScreen( + AccountSelectorWrapper, + { + name: Routes.SHEET.ACCOUNT_SELECTOR, + options: {}, + }, + { + state: mockInitialState, + }, + mockRoute.params, + ); + expect(wrapper.toJSON()).toMatchSnapshot(); + }); + + it('should display accounts list', () => { + renderScreen( + AccountSelectorWrapper, + { + name: Routes.SHEET.ACCOUNT_SELECTOR, + }, + { + state: mockInitialState, + }, + mockRoute.params, + ); + + const accountsList = screen.getByTestId( + AccountListViewSelectorsIDs.ACCOUNT_LIST_ID, + ); + expect(accountsList).toBeDefined(); + }); + + it('should display add account button', () => { + renderScreen( + AccountSelectorWrapper, + { + name: Routes.SHEET.ACCOUNT_SELECTOR, + }, + { + state: mockInitialState, + }, + mockRoute.params, + ); + + const addButton = screen.getByTestId( + AccountListViewSelectorsIDs.ACCOUNT_LIST_ADD_BUTTON_ID, + ); + expect(addButton).toBeDefined(); + }); +}); diff --git a/app/components/Views/AccountSelector/AccountSelector.tsx b/app/components/Views/AccountSelector/AccountSelector.tsx index 7bb4d67a150..e5b12aea6ed 100644 --- a/app/components/Views/AccountSelector/AccountSelector.tsx +++ b/app/components/Views/AccountSelector/AccountSelector.tsx @@ -39,7 +39,8 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; const AccountSelector = ({ route }: AccountSelectorProps) => { const dispatch = useDispatch(); const { trackEvent } = useMetrics(); - const { onSelectAccount, checkBalanceError } = route.params || {}; + const { onSelectAccount, checkBalanceError, privacyMode } = + route.params || {}; const { reloadAccounts } = useSelector((state: RootState) => state.accounts); // TODO: Replace "any" with type @@ -92,6 +93,7 @@ const AccountSelector = ({ route }: AccountSelectorProps) => { accounts={accounts} ensByAccountAddress={ensByAccountAddress} isRemoveAccountEnabled + privacyMode={privacyMode} testID={AccountListViewSelectorsIDs.ACCOUNT_LIST_ID} /> @@ -106,7 +108,13 @@ const AccountSelector = ({ route }: AccountSelectorProps) => { ), - [accounts, _onSelectAccount, ensByAccountAddress, onRemoveImportedAccount], + [ + accounts, + _onSelectAccount, + ensByAccountAddress, + onRemoveImportedAccount, + privacyMode, + ], ); const renderAddAccountActions = useCallback( diff --git a/app/components/Views/AccountSelector/AccountSelector.types.ts b/app/components/Views/AccountSelector/AccountSelector.types.ts index 99f79c3bc40..628d72b288d 100644 --- a/app/components/Views/AccountSelector/AccountSelector.types.ts +++ b/app/components/Views/AccountSelector/AccountSelector.types.ts @@ -35,6 +35,10 @@ export interface AccountSelectorParams { * @param balance - The ticker balance of an account in wei and hex string format. */ checkBalanceError?: UseAccountsParams['checkBalanceError']; + /** + * Optional boolean to indicate if privacy mode is enabled. + */ + privacyMode?: boolean; } /** diff --git a/app/components/Views/AccountSelector/__snapshots__/AccountSelector.test.tsx.snap b/app/components/Views/AccountSelector/__snapshots__/AccountSelector.test.tsx.snap new file mode 100644 index 00000000000..9b93f6abe02 --- /dev/null +++ b/app/components/Views/AccountSelector/__snapshots__/AccountSelector.test.tsx.snap @@ -0,0 +1,548 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountSelector should render correctly 1`] = ` + + + + + + + + + + + + + AccountSelector + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Accounts + + + + + + + + + + Add account or hardware wallet + + + + + + + + + + + + + + + + +`;