diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts b/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts
index 9bf03d0adbc..d4d99c40475 100644
--- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts
+++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.styles.ts
@@ -3,7 +3,6 @@ import { StyleSheet, ViewStyle } from 'react-native';
// External dependencies.
import { Theme } from '../../../../util/theme/models';
-import { fontStyles } from '../../../../styles/common';
// Internal dependencies.
import { PickerAccountStyleSheetVars } from './PickerAccount.types';
@@ -24,34 +23,39 @@ const styleSheet = (params: {
const { colors } = theme;
const { style, cellAccountContainerStyle } = vars;
return StyleSheet.create({
- base: Object.assign({} as ViewStyle, style) as ViewStyle,
+ base: {
+ ...(style as ViewStyle),
+ flexDirection: 'row',
+ padding: 0,
+ borderWidth: 0,
+ },
accountAvatar: {
- marginRight: 16,
+ marginRight: 8,
},
accountAddressLabel: {
color: colors.text.alternative,
+ textAlign: 'center',
},
cellAccount: {
- flex: 1,
flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
...cellAccountContainerStyle,
},
accountNameLabel: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ accountNameAvatar: {
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'flex-start',
},
- accountNameLabelText: {
- marginTop: 4,
- marginHorizontal: 5,
- paddingHorizontal: 5,
- ...fontStyles.bold,
- color: colors.text.alternative,
- borderWidth: 1,
- borderRadius: 10,
- borderColor: colors.border.default,
+ pickerAccountContainer: {
justifyContent: 'center',
- textAlign: 'center',
+ alignItems: 'center',
+ },
+ dropDownIcon: {
+ marginLeft: 8,
},
});
};
diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx
index 4093e7da1ae..0bec81483f0 100644
--- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx
+++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx
@@ -9,6 +9,7 @@ import Avatar, { AvatarSize, AvatarVariant } from '../../Avatars/Avatar';
import Text, { TextVariant } from '../../Texts/Text';
import { formatAddress } from '../../../../util/address';
import { useStyles } from '../../../hooks';
+import { IconSize } from '../../Icons/Icon';
// Internal dependencies.
import PickerBase from '../PickerBase';
@@ -25,7 +26,6 @@ const PickerAccount: React.ForwardRefRenderFunction<
accountAddress,
accountName,
accountAvatarType,
- accountTypeLabel,
showAddress = true,
cellAccountContainerStyle = {},
...props
@@ -40,33 +40,46 @@ const PickerAccount: React.ForwardRefRenderFunction<
const renderCellAccount = () => (
-
-
- {accountName}
-
- {showAddress && (
-
- {shortenedAddress}
+
+
+
+ {accountName}
- )}
+
);
return (
-
- {renderCellAccount()}
-
+
+
+ {renderCellAccount()}
+
+ {showAddress && (
+
+ {shortenedAddress}
+
+ )}
+
);
};
diff --git a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap
index 0afdb8affeb..a078072c4da 100644
--- a/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap
+++ b/app/component-library/components/Pickers/PickerAccount/__snapshots__/PickerAccount.test.tsx.snap
@@ -1,223 +1,242 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PickerAccount should render correctly 1`] = `
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+ }
+ testID="account-label"
+ >
+ Orangefox.eth
+
+
-
-
- Orangefox.eth
-
-
- 0x2990...a21a
-
-
-
-
+
+
-
+ >
+ 0x2990...a21a
+
+
`;
diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx
index 948721d6ac4..65666c2e125 100644
--- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx
+++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.test.tsx
@@ -45,6 +45,20 @@ describe('PickerNetwork', () => {
).toBeNull();
});
+ it('shows network name when hideNetworkName is false', () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(
+ queryByTestId(WalletViewSelectorsIDs.NAVBAR_NETWORK_TEXT),
+ ).not.toBeNull();
+ });
+
it('calls onPress when pressed', () => {
const onPress = jest.fn();
const { getByTestId } = render(
diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx
index 1b4642ba967..29c48def333 100644
--- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx
+++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx
@@ -34,6 +34,8 @@ const PickerNetwork = ({
size={AvatarSize.Xs}
name={label}
imageSource={imageSource}
+ testID={WalletViewSelectorsIDs.NAVBAR_NETWORK_PICKER}
+ accessibilityLabel={label}
/>
{hideNetworkName ? null : (
diff --git a/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap b/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap
index f59fab74933..c2965c38bb0 100644
--- a/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap
+++ b/app/component-library/components/Pickers/PickerNetwork/__snapshots__/PickerNetwork.test.tsx.snap
@@ -19,6 +19,7 @@ exports[`PickerNetwork renders correctly 1`] = `
style={null}
>
(
name={Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS}
component={AccountPermissionsConfirmRevokeAll}
/>
+
{
const sdkInit = useRef();
const [onboarded, setOnboarded] = useState(false);
+ trace({
+ name: TraceName.NavInit,
+ parentContext: getUIStartupSpan(),
+ op: TraceOperation.NavInit,
+ });
+
const triggerSetCurrentRoute = (route) => {
dispatch(setCurrentRoute(route));
if (route === 'Wallet' || route === 'BrowserView') {
@@ -594,9 +610,10 @@ const App = (props) => {
setOnboarded(!!existingUser);
try {
if (existingUser) {
+ // This should only be called if the auth type is not password, which is not the case so consider removing it
await trace(
{
- name: TraceName.BiometricAuthentication,
+ name: TraceName.AppStartBiometricAuthentication,
op: TraceOperation.BiometricAuthentication,
},
async () => {
@@ -619,6 +636,7 @@ const App = (props) => {
}),
);
}
+
await Authentication.lockApp({ reset: false });
trackErrorAsAnalytics(
'App: Max Attempts Reached',
@@ -627,9 +645,15 @@ const App = (props) => {
);
}
};
- appTriggeredAuth().catch((error) => {
- Logger.error(error, 'App: Error in appTriggeredAuth');
- });
+ appTriggeredAuth()
+ .catch((error) => {
+ Logger.error(error, 'App: Error in appTriggeredAuth');
+ })
+ .finally(() => {
+ endTrace({ name: TraceName.NavInit });
+
+ endTrace({ name: TraceName.UIStartup });
+ });
}, [navigator, queueOfHandleDeeplinkFunctions]);
const handleDeeplink = useCallback(({ error, params, uri }) => {
@@ -679,8 +703,6 @@ const App = (props) => {
});
if (!prevNavigator.current) {
- // Setup navigator with Sentry instrumentation
- routingInstrumentation.registerNavigationContainer(navigator);
// Subscribe to incoming deeplinks
// Branch.io documentation: https://help.branch.io/developers-hub/docs/react-native
branch.subscribe((opts) => {
@@ -965,7 +987,7 @@ const App = (props) => {
{
};
});
+const mockNavigate = jest.fn();
+
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ }),
+}));
+
const initialState = {
engine: {
backgroundState: {
@@ -73,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}
/>
);
};
@@ -109,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(() => {
@@ -229,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 27405b354f7..30b8241836f 100644
--- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx
+++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx
@@ -1,17 +1,19 @@
// Third party dependencies.
import React, { useCallback, useRef } from 'react';
-import { Alert, ListRenderItem, View } from 'react-native';
+import { Alert, ListRenderItem, View, ViewStyle } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { useSelector } from 'react-redux';
+import { useNavigation } from '@react-navigation/native';
import { KeyringTypes } from '@metamask/keyring-controller';
import type { Hex } from '@metamask/utils';
// External dependencies.
+import { selectInternalAccounts } from '../../../selectors/accountsController';
import Cell, {
CellVariant,
} 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,
@@ -29,11 +31,13 @@ import { AvatarVariant } from '../../../component-library/components/Avatars/Ava
import { Account, Assets } from '../../hooks/useAccounts';
import UntypedEngine from '../../../core/Engine';
import { removeAccountsFromPermissions } from '../../../core/Permissions';
+import Routes from '../../../constants/navigation/Routes';
// Internal dependencies.
import { AccountSelectorListProps } from './AccountSelectorList.types';
import styleSheet from './AccountSelectorList.styles';
import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors';
+import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
const AccountSelectorList = ({
onSelectAccount,
@@ -47,8 +51,10 @@ const AccountSelectorList = ({
isSelectionDisabled,
isRemoveAccountEnabled = false,
isAutoScrollEnabled = true,
+ privacyMode = false,
...props
}: AccountSelectorListProps) => {
+ const { navigate } = useNavigation();
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Engine = UntypedEngine as any;
@@ -64,7 +70,8 @@ const AccountSelectorList = ({
? AvatarAccountType.Blockies
: AvatarAccountType.JazzIcon,
);
- const privacyMode = useSelector(selectPrivacyMode);
+
+ const internalAccounts = useSelector(selectInternalAccounts);
const getKeyExtractor = ({ address }: Account) => address;
const renderAccountBalances = useCallback(
@@ -170,6 +177,23 @@ const AccountSelectorList = ({
],
);
+ const onNavigateToAccountActions = useCallback(
+ (selectedAccount: string) => {
+ const account = internalAccounts.find(
+ (accountData: InternalAccount) =>
+ accountData.address.toLowerCase() === selectedAccount.toLowerCase(),
+ );
+
+ if (!account) return;
+
+ navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_ACTIONS,
+ params: { selectedAccount: account },
+ });
+ },
+ [navigate, internalAccounts],
+ );
+
const renderAccountItem: ListRenderItem = useCallback(
({
item: { name, address, assets, type, isSelected, balanceError },
@@ -183,7 +207,7 @@ const AccountSelectorList = ({
const isDisabled = !!balanceError || isLoading || isSelectionDisabled;
const cellVariant = isMultiSelect
? CellVariant.MultiSelect
- : CellVariant.Select;
+ : CellVariant.SelectWithMenu;
let isSelectedAccount = isSelected;
if (selectedAddresses) {
const lowercasedSelectedAddresses = selectedAddresses.map(
@@ -194,12 +218,16 @@ const AccountSelectorList = ({
);
}
- const cellStyle = {
+ const cellStyle: ViewStyle = {
opacity: isLoading ? 0.5 : 1,
};
+ if (!isMultiSelect) {
+ cellStyle.alignItems = 'center';
+ }
return (
{
onLongPress({
address,
@@ -212,6 +240,7 @@ const AccountSelectorList = ({
isSelected={isSelectedAccount}
title={accountName}
secondaryText={shortAddress}
+ showSecondaryTextIcon={false}
tertiaryText={balanceError}
onPress={() => onSelectAccount?.(address, isSelectedAccount)}
avatarProps={{
@@ -222,6 +251,10 @@ const AccountSelectorList = ({
tagLabel={tagLabel}
disabled={isDisabled}
style={cellStyle}
+ buttonProps={{
+ onButtonClick: () => onNavigateToAccountActions(address),
+ buttonTestId: `${WalletViewSelectorsIDs.ACCOUNT_ACTIONS}-${index}`,
+ }}
>
{renderRightAccessory?.(address, accountName) ||
(assets && renderAccountBalances(assets, address))}
@@ -229,6 +262,7 @@ const AccountSelectorList = ({
);
},
[
+ onNavigateToAccountActions,
accountAvatarType,
onSelectAccount,
renderAccountBalances,
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/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
index 036a2be8d53..dd4954812d8 100644
--- a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
+++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
@@ -57,591 +57,772 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = `
onLayout={[Function]}
style={null}
>
-
-
-
-
-
-
+
-
-
-
+ propList={
+ [
+ "fill",
+ ]
+ }
+ width={32}
+ x={0}
+ y={0}
+ />
+
+
+
+
+
-
-
-
- Account 1
-
-
- 0xC495...D272
-
-
-
- $3200.00
+ Account 1
-
- 1 ETH
-
+
+ 0xC495...D272
+
+
+ |
+
+
+
+ $3200.00
+
+
+ 1 ETH
+
+
-
+
+
+
+
-
+ testID="main-wallet-account-actions-0"
+ >
+
+
-
+
-
-
-
-
-
-
+
-
-
-
+ propList={
+ [
+ "fill",
+ ]
+ }
+ width={32}
+ x={0}
+ y={0}
+ />
+
+
+
+
+
-
-
-
- Account 2
-
-
- 0xd018...78E7
-
-
-
- $6400.00
+ Account 2
-
- 2 ETH
-
+
+ 0xd018...78E7
+
+
+
+
+
+
+ $6400.00
+
+
+ 2 ETH
+
+
+
+
+
+
+
-
+
@@ -704,492 +885,672 @@ exports[`AccountSelectorList renders all accounts with right accessory 1`] = `
onLayout={[Function]}
style={null}
>
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
- Account 1
-
-
+ Account 1
+
+
+
+ 0xC495...D272
+
+
+
+
- 0xC495...D272
-
-
-
-
- 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1
+
+ 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1
+
+
+
+
+
+
-
+
-
-
-
-
-
-
+
-
-
-
+ propList={
+ [
+ "fill",
+ ]
+ }
+ width={32}
+ x={0}
+ y={0}
+ />
+
+
+
+
+
-
-
-
- Account 2
-
-
+ Account 2
+
+
+
+ 0xd018...78E7
+
+
+
+
- 0xd018...78E7
-
-
-
-
- 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2
+
+ 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2
+
+
+
+
+
+
-
+
@@ -1252,591 +1613,772 @@ exports[`AccountSelectorList renders correctly 1`] = `
onLayout={[Function]}
style={null}
>
-
-
-
-
-
-
+
-
-
-
+ propList={
+ [
+ "fill",
+ ]
+ }
+ width={32}
+ x={0}
+ y={0}
+ />
+
+
+
+
+
-
-
-
- Account 1
-
-
- 0xC495...D272
-
-
-
- $3200.00
+ Account 1
-
- 1 ETH
-
+
+ 0xC495...D272
+
+
+
+
+
+
+ $3200.00
+
+
+ 1 ETH
+
+
-
+
+
+
+
-
+ testID="main-wallet-account-actions-0"
+ >
+
+
-
+
-
-
-
-
-
-
+
-
-
-
+ propList={
+ [
+ "fill",
+ ]
+ }
+ width={32}
+ x={0}
+ y={0}
+ />
+
+
+
+
+
-
-
-
- Account 2
-
-
- 0xd018...78E7
-
-
-
- $6400.00
+ Account 2
-
- 2 ETH
-
+
+ 0xd018...78E7
+
+
+
+
+
+
+ $6400.00
+
+
+ 2 ETH
+
+
+
+
+
+
+
-
+
@@ -1896,309 +2438,490 @@ exports[`AccountSelectorList should render all accounts but only the balance for
onLayout={[Function]}
style={null}
>
-
-
-
-
-
-
- Account 1
-
-
- 0xC495...D272
-
-
-
+ style={
+ {
+ "flex": 1,
+ }
+ }
+ />
+
- $3200.00
+ Account 1
-
- 1 ETH
-
+
+ 0xC495...D272
+
+
+
+
+
+
+ $3200.00
+
+
+ 1 ETH
+
+
-
+
+
+
+
-
+ testID="main-wallet-account-actions-0"
+ >
+
+
-
+
-
-
-
-
-
-
- Account 2
-
-
+
+
- 0xd018...78E7
-
+
+ Account 2
+
+
+
+ 0xd018...78E7
+
+
+
+
+
+
+
+
-
+
diff --git a/app/components/UI/AddressCopy/AddressCopy.styles.ts b/app/components/UI/AddressCopy/AddressCopy.styles.ts
index 089c48d5136..46d5ba9d066 100644
--- a/app/components/UI/AddressCopy/AddressCopy.styles.ts
+++ b/app/components/UI/AddressCopy/AddressCopy.styles.ts
@@ -1,26 +1,14 @@
import { StyleSheet } from 'react-native';
-// External dependencies.
-import { Theme } from '../../../util/theme/models';
-const styleSheet = (params: { theme: Theme }) => {
- const { theme } = params;
- const { colors } = theme;
-
- return StyleSheet.create({
+const styleSheet = () =>
+ StyleSheet.create({
address: {
flexDirection: 'row',
alignItems: 'center',
},
copyButton: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: colors.primary.muted,
- borderRadius: 20,
- paddingHorizontal: 12,
padding: 4,
- marginLeft: 12,
},
- icon: { marginLeft: 4 },
});
-};
+
export default styleSheet;
diff --git a/app/components/UI/AddressCopy/AddressCopy.tsx b/app/components/UI/AddressCopy/AddressCopy.tsx
index d295c5f75ad..a99e46a4727 100644
--- a/app/components/UI/AddressCopy/AddressCopy.tsx
+++ b/app/components/UI/AddressCopy/AddressCopy.tsx
@@ -3,12 +3,7 @@ import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
// External dependencies
-import Text, {
- TextColor,
- TextVariant,
-} from '../../../component-library/components/Texts/Text';
import { TouchableOpacity } from 'react-native-gesture-handler';
-import { formatAddress } from '../../../util/address';
import Icon, {
IconColor,
IconName,
@@ -25,12 +20,11 @@ import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletV
// Internal dependencies
import styleSheet from './AddressCopy.styles';
-import { AddressCopyProps } from './AddressCopy.types';
import { selectSelectedInternalAccount } from '../../../selectors/accountsController';
import { useMetrics } from '../../../components/hooks/useMetrics';
import { toChecksumHexAddress } from '@metamask/controller-utils';
-const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => {
+const AddressCopy = () => {
const { styles } = useStyles(styleSheet, {});
const dispatch = useDispatch();
@@ -69,28 +63,15 @@ const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => {
};
return (
-
- {strings('asset_overview.address')}:
-
-
- {selectedInternalAccount
- ? formatAddress(selectedInternalAccount.address, formatAddressType)
- : null}
-
diff --git a/app/components/UI/AddressCopy/AddressCopy.types.ts b/app/components/UI/AddressCopy/AddressCopy.types.ts
deleted file mode 100644
index 6efa05bbea0..00000000000
--- a/app/components/UI/AddressCopy/AddressCopy.types.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export interface AddressCopyProps {
- formatAddressType?: 'short' | 'mid' | 'full';
-}
diff --git a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx
index ed1a2f5fdc3..99054057d7e 100644
--- a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx
+++ b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx
@@ -44,6 +44,7 @@ jest.mock('../../../util/navigation/navUtils', () => ({
ticker: 'ETH',
addTokenList: jest.fn(),
}),
+ createNavigationDetails: jest.fn(),
}));
const mockUseBalanceInitialValue: Partial> = {
@@ -101,14 +102,12 @@ describe('ConfirmAddAsset', () => {
expect(getByText('USDT')).toBeTruthy();
expect(getByText('$27.02')).toBeTruthy();
});
-
it('handles cancel button click', () => {
const { getByText } = renderWithProvider(, {
state: mockInitialState,
});
const cancelButton = getByText('Cancel');
fireEvent.press(cancelButton);
-
expect(getByText('Are you sure you want to exit?')).toBeTruthy();
expect(
getByText('Your search information will not be saved.'),
diff --git a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap
index 4d89a952349..3aef46b9799 100644
--- a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap
+++ b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap
@@ -87,6 +87,7 @@ exports[`ManageNetworks should render correctly 1`] = `
style={null}
>
{
@@ -95,7 +101,7 @@ const styles = StyleSheet.create({
disabled: {
opacity: 0.3,
},
- leftButtonContainer: {
+ rightElementContainer: {
marginRight: 12,
flexDirection: 'row',
alignItems: 'flex-end',
@@ -113,16 +119,11 @@ const styles = StyleSheet.create({
metamaskNameWrapper: {
marginLeft: Device.isAndroid() ? 20 : 0,
},
- fox: {
- width: 24,
- height: 24,
+ leftElementContainer: {
marginLeft: 16,
},
notificationsWrapper: {
- position: 'relative',
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
+ marginHorizontal: 4,
},
notificationsBadge: {
width: 8,
@@ -133,6 +134,9 @@ const styles = StyleSheet.create({
top: 2,
right: 10,
},
+ addressCopyWrapper: {
+ marginHorizontal: 4,
+ },
});
const metamask_name = require('../../../images/metamask-name.png'); // eslint-disable-line
@@ -903,12 +907,28 @@ export function getOfflineModalNavbar() {
}
/**
- * Function that returns the navigation options
- * for our wallet screen,
+ * Function that returns the navigation options for the wallet screen.
*
- * @returns {Object} - Corresponding navbar options containing headerTitle, headerTitle and headerTitle
+ * @param {Object} accountActionsRef - The ref object for the account actions
+ * @param {string} selectedAddress - The currently selected Ethereum address
+ * @param {string} accountName - The name of the currently selected account
+ * @param {string} accountAvatarType - The type of avatar for the currently selected account
+ * @param {string} networkName - The name of the current network
+ * @param {Object} networkImageSource - The image source for the network icon
+ * @param {Function} onPressTitle - Callback function when the title is pressed
+ * @param {Object} navigation - The navigation object
+ * @param {Object} themeColors - The theme colors object
+ * @param {boolean} isNotificationEnabled - Whether notifications are enabled
+ * @param {boolean | null} isProfileSyncingEnabled - Whether profile syncing is enabled
+ * @param {number} unreadNotificationCount - The number of unread notifications
+ * @param {number} readNotificationCount - The number of read notifications
+ * @returns {Object} An object containing the navbar options for the wallet screen
*/
export function getWalletNavbarOptions(
+ accountActionsRef,
+ selectedAddress,
+ accountName,
+ accountAvatarType,
networkName,
networkImageSource,
onPressTitle,
@@ -921,7 +941,7 @@ export function getWalletNavbarOptions(
) {
const innerStyles = StyleSheet.create({
headerStyle: {
- backgroundColor: themeColors.background.default,
+ backgroundColor: themeColors.background,
shadowColor: importedColors.transparent,
elevation: 0,
},
@@ -1005,24 +1025,40 @@ export function getWalletNavbarOptions(
return {
headerTitle: () => (
+ {
+ navigation.navigate(...createAccountSelectorNavDetails({}));
+ }}
+ accountTypeLabel={getLabelTextByAddress(selectedAddress) || undefined}
+ showAddress
+ cellAccountContainerStyle={styles.account}
+ testID={WalletViewSelectorsIDs.ACCOUNT_ICON}
+ />
+
+ ),
+ headerLeft: () => (
+
),
- headerLeft: () => (
-
- ),
headerRight: () => (
-
+
+
+
+
{isNotificationsFeatureEnabled() && (
diff --git a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap
index 8099909473b..fa6b7d2bf5b 100644
--- a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap
@@ -233,6 +233,7 @@ exports[`NetworkDetails renders correctly 1`] = `
style={null}
>
{
const url = new URLPARSE(rpcUrl);
const existingNetwork = networkConfigurationByChainId[chainId];
- CurrencyRateController.updateExchangeRate(ticker);
+ CurrencyRateController.updateExchangeRate([ticker]);
if (!isPrivateConnection(url.hostname)) {
url.set('protocol', 'https:');
diff --git a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap
index bbcf6abbafd..7b3fccd6bdf 100644
--- a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap
+++ b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap
@@ -94,6 +94,7 @@ exports[`NetworkVerificationInfo renders correctly 1`] = `
style={null}
>
{renderTopIcon()}
-
+
+ {!isRenderedAsBottomSheet && (
+ {
+ navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.CONNECTION_DETAILS,
+ params: {
+ hostInfo: {
+ metadata: {
+ origin:
+ currentPageInformation?.url &&
+ new URL(currentPageInformation?.url).hostname,
+ },
+ },
+ connectionDateTime: new Date().getTime(),
+ },
+ });
+ }}
+ testID={SDKSelectorsIDs.CONNECTION_DETAILS_BUTTON}
+ />
+ )}
+
);
}
@@ -150,20 +178,24 @@ const PermissionsSummary = ({
);
+ const onRevokeAllHandler = useCallback(async () => {
+ await Engine.context.PermissionController.revokeAllPermissions(hostname);
+ navigate('PermissionsManager');
+ }, [hostname, navigate]);
+
const toggleRevokeAllPermissionsModal = useCallback(() => {
navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS,
params: {
hostInfo: {
metadata: {
- origin:
- currentPageInformation?.url &&
- new URL(currentPageInformation?.url).hostname,
+ origin: hostname,
},
},
+ onRevokeAll: !isRenderedAsBottomSheet && onRevokeAllHandler,
},
});
- }, [navigate, currentPageInformation?.url]);
+ }, [navigate, isRenderedAsBottomSheet, onRevokeAllHandler, hostname]);
const getAccountLabel = useCallback(() => {
if (isAlreadyConnected) {
@@ -346,10 +378,10 @@ const PermissionsSummary = ({
{!isAlreadyConnected || isNetworkSwitch
? strings('permissions.title_dapp_url_wants_to', {
- dappUrl: new URL(currentPageInformation.url).hostname,
+ dappUrl: hostname,
})
: strings('permissions.title_dapp_url_has_approval_to', {
- dappUrl: new URL(currentPageInformation.url).hostname,
+ dappUrl: hostname,
})}
diff --git a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
index 5fb871e2b3d..dc178c8b497 100644
--- a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
+++ b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
@@ -99,6 +99,7 @@ exports[`PermissionsSummary should render correctly 1`] = `
{
).toMatchInlineSnapshot(`
[
[
- "POL",
+ [
+ "POL",
+ ],
],
]
`);
diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx
index d1235a4f8b4..5178c067e10 100644
--- a/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx
+++ b/app/components/UI/Ramp/Views/NetworkSwitcher/NetworkSwitcher.tsx
@@ -168,7 +168,7 @@ function NetworkSwitcher() {
const { networkClientId } =
rpcEndpoints?.[defaultRpcEndpointIndex] ?? {};
- CurrencyRateController.updateExchangeRate(ticker);
+ CurrencyRateController.updateExchangeRate([ticker]);
NetworkController.setActiveNetwork(networkClientId);
navigateToGetStarted();
}
diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap b/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap
index bdf21b1ce92..1459c889bad 100644
--- a/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap
+++ b/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap
@@ -1065,6 +1065,7 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing
style={null}
>
({
refresh: jest.fn(() => Promise.resolve()),
},
CurrencyRateController: {
- startPolling: jest.fn(() => Promise.resolve()),
+ updateExchangeRate: jest.fn(() => Promise.resolve()),
},
TokenRatesController: {
updateExchangeRates: jest.fn(() => Promise.resolve()),
@@ -356,7 +356,7 @@ describe('Tokens', () => {
Engine.context.AccountTrackerController.refresh,
).toHaveBeenCalled();
expect(
- Engine.context.CurrencyRateController.startPolling,
+ Engine.context.CurrencyRateController.updateExchangeRate,
).toHaveBeenCalled();
expect(
Engine.context.TokenRatesController.updateExchangeRates,
diff --git a/app/components/UI/Tokens/index.tsx b/app/components/UI/Tokens/index.tsx
index 35f7c393774..6f16616cc3f 100644
--- a/app/components/UI/Tokens/index.tsx
+++ b/app/components/UI/Tokens/index.tsx
@@ -11,7 +11,7 @@ import { MetaMetricsEvents } from '../../../core/Analytics';
import Logger from '../../../util/Logger';
import {
selectChainId,
- selectNetworkClientId,
+ selectNetworkConfigurations,
} from '../../../selectors/networkController';
import { getDecimalChainId } from '../../../util/networks';
import { isZero } from '../../../util/lodash';
@@ -75,7 +75,9 @@ const Tokens: React.FC = ({ tokens }) => {
const { data: tokenBalances } = useTokenBalancesController();
const tokenSortConfig = useSelector(selectTokenSortConfig);
const chainId = useSelector(selectChainId);
- const networkClientId = useSelector(selectNetworkClientId);
+ const networkConfigurationsByChainId = useSelector(
+ selectNetworkConfigurations,
+ );
const hideZeroBalanceTokens = useSelector(
(state: RootState) => state.settings.hideZeroBalanceTokens,
);
@@ -83,6 +85,13 @@ const Tokens: React.FC = ({ tokens }) => {
const tokenExchangeRates = useSelector(selectContractExchangeRates);
const currentCurrency = useSelector(selectCurrentCurrency);
const conversionRate = useSelector(selectConversionRate);
+ const nativeCurrencies = [
+ ...new Set(
+ Object.values(networkConfigurationsByChainId).map(
+ (n) => n.nativeCurrency,
+ ),
+ ),
+ ];
const actionSheet = useRef();
const [tokenToRemove, setTokenToRemove] = useState();
@@ -159,9 +168,7 @@ const Tokens: React.FC = ({ tokens }) => {
const actions = [
TokenDetectionController.detectTokens(),
AccountTrackerController.refresh(),
- CurrencyRateController.startPolling({
- networkClientId,
- }),
+ CurrencyRateController.updateExchangeRate(nativeCurrencies),
TokenRatesController.updateExchangeRates(),
];
await Promise.all(actions).catch((error) => {
diff --git a/app/components/UI/WalletAccount/WalletAccount.test.tsx b/app/components/UI/WalletAccount/WalletAccount.test.tsx
index 709b497bb02..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,
@@ -116,13 +126,6 @@ describe('WalletAccount', () => {
expect(toJSON()).toMatchSnapshot();
});
- it('shows the account address', () => {
- const { getByTestId } = renderWithProvider(, {
- state: mockInitialState,
- });
- expect(getByTestId(WalletViewSelectorsIDs.ACCOUNT_ADDRESS)).toBeDefined();
- });
-
it('copies the account address to the clipboard when the copy button is pressed', async () => {
const { getByTestId } = renderWithProvider(, {
state: mockInitialState,
@@ -139,7 +142,9 @@ describe('WalletAccount', () => {
fireEvent.press(getByTestId(WalletViewSelectorsIDs.ACCOUNT_ICON));
expect(mockNavigate).toHaveBeenCalledWith(
- ...createAccountSelectorNavDetails({}),
+ ...createAccountSelectorNavDetails({
+ privacyMode: false,
+ }),
);
});
it('displays the correct account name', () => {
@@ -171,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 e96ea0e1e29..63cf33fccdd 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';
@@ -24,6 +25,9 @@ import Logger from '../../../util/Logger';
// Internal dependencies
import styleSheet from './WalletAccount.styles';
import { WalletAccountProps } from './WalletAccount.types';
+import { TraceName, TraceOperation, trace } from '../../../util/trace';
+import { store } from '../../../store';
+import { getTraceTags } from '../../../util/sentry/tags';
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -34,6 +38,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 +83,16 @@ const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref) => {
accountName={accountName}
accountAvatarType={accountAvatarType}
onPress={() => {
- navigate(...createAccountSelectorNavDetails({}));
+ trace({
+ name: TraceName.AccountList,
+ tags: getTraceTags(store.getState()),
+ op: TraceOperation.AccountList,
+ });
+ navigate(
+ ...createAccountSelectorNavDetails({
+ privacyMode,
+ }),
+ );
}}
accountTypeLabel={
getLabelTextByAddress(selectedAccount?.address) || undefined
@@ -90,7 +104,7 @@ const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref) => {
/>
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+ }
+ testID="account-label"
+ >
+ Account 2
+
+
-
-
- Account 2
-
-
-
-
-
+ width={12}
+ />
+
+
-
- Address
- :
-
-
- 0xC496...a756
-
diff --git a/app/components/Views/AccountActions/AccountActions.test.tsx b/app/components/Views/AccountActions/AccountActions.test.tsx
index 02655b77080..730761ea373 100644
--- a/app/components/Views/AccountActions/AccountActions.test.tsx
+++ b/app/components/Views/AccountActions/AccountActions.test.tsx
@@ -66,6 +66,18 @@ jest.mock('@react-navigation/native', () => {
navigate: mockNavigate,
goBack: mockGoBack,
}),
+ useRoute: () => ({
+ params: {
+ selectedAccount: {
+ address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756',
+ metadata: {
+ keyring: {
+ type: 'HD Key Tree',
+ },
+ },
+ },
+ },
+ }),
};
});
@@ -130,7 +142,7 @@ describe('AccountActions', () => {
expect(mockNavigate).toHaveBeenCalledWith('Webview', {
screen: 'SimpleWebview',
params: {
- url: 'https://etherscan.io/address/0xc4966c0d659d99699bfd7eb54d8fafee40e4a756',
+ url: 'https://etherscan.io/address/0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756',
title: 'etherscan.io',
},
});
@@ -144,7 +156,7 @@ describe('AccountActions', () => {
fireEvent.press(getByTestId(AccountActionsModalSelectorsIDs.SHARE_ADDRESS));
expect(Share.open).toHaveBeenCalledWith({
- message: '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756',
+ message: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756',
});
});
@@ -162,6 +174,14 @@ describe('AccountActions', () => {
{
credentialName: 'private_key',
shouldUpdateNav: true,
+ selectedAccount: {
+ address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756',
+ metadata: {
+ keyring: {
+ type: 'HD Key Tree',
+ },
+ },
+ },
},
);
});
@@ -173,7 +193,16 @@ describe('AccountActions', () => {
fireEvent.press(getByTestId(AccountActionsModalSelectorsIDs.EDIT_ACCOUNT));
- expect(mockNavigate).toHaveBeenCalledWith('EditAccountName');
+ expect(mockNavigate).toHaveBeenCalledWith('EditAccountName', {
+ selectedAccount: {
+ address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756',
+ metadata: {
+ keyring: {
+ type: 'HD Key Tree',
+ },
+ },
+ },
+ });
});
describe('clicks remove account', () => {
diff --git a/app/components/Views/AccountActions/AccountActions.tsx b/app/components/Views/AccountActions/AccountActions.tsx
index 99547d6cfda..c44a1bc31a1 100644
--- a/app/components/Views/AccountActions/AccountActions.tsx
+++ b/app/components/Views/AccountActions/AccountActions.tsx
@@ -1,11 +1,17 @@
// Third party dependencies.
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Alert, View, Text } from 'react-native';
-import { useNavigation } from '@react-navigation/native';
+import {
+ useNavigation,
+ RouteProp,
+ ParamListBase,
+ useRoute,
+} from '@react-navigation/native';
import { useDispatch, useSelector } from 'react-redux';
import Share from 'react-native-share';
-// External dependencies.
+// External dependencies
+import { InternalAccount } from '@metamask/keyring-api';
import BottomSheet, {
BottomSheetRef,
} from '../../../component-library/components/BottomSheets/BottomSheet';
@@ -25,7 +31,6 @@ import {
selectNetworkConfigurations,
selectProviderConfig,
} from '../../../selectors/networkController';
-import { selectSelectedInternalAccount } from '../../../selectors/accountsController';
import { strings } from '../../../../locales/i18n';
// Internal dependencies
import styleSheet from './AccountActions.styles';
@@ -50,7 +55,13 @@ import BlockingActionModal from '../../UI/BlockingActionModal';
import { useTheme } from '../../../util/theme';
import { Hex } from '@metamask/utils';
+interface AccountActionsParams {
+ selectedAccount: InternalAccount;
+}
+
const AccountActions = () => {
+ const route = useRoute>();
+ const { selectedAccount } = route.params as AccountActionsParams;
const { colors } = useTheme();
const styles = styleSheet(colors);
const sheetRef = useRef(null);
@@ -67,7 +78,6 @@ const AccountActions = () => {
const providerConfig = useSelector(selectProviderConfig);
- const selectedAccount = useSelector(selectSelectedInternalAccount);
const selectedAddress = selectedAccount?.address;
const keyring = selectedAccount?.metadata.keyring;
@@ -140,6 +150,7 @@ const AccountActions = () => {
navigate(Routes.SETTINGS.REVEAL_PRIVATE_CREDENTIAL, {
credentialName: 'private_key',
shouldUpdateNav: true,
+ selectedAccount,
});
});
};
@@ -305,7 +316,7 @@ const AccountActions = () => {
]);
const goToEditAccountName = () => {
- navigate('EditAccountName');
+ navigate('EditAccountName', { selectedAccount });
};
const isExplorerVisible = Boolean(
diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx
index a9f976fe494..0819d6d647e 100644
--- a/app/components/Views/AccountConnect/AccountConnect.tsx
+++ b/app/components/Views/AccountConnect/AccountConnect.tsx
@@ -13,7 +13,6 @@ import Modal from 'react-native-modal';
import { useSelector } from 'react-redux';
// External dependencies.
import { strings } from '../../../../locales/i18n';
-import { AvatarAccountType } from '../../../component-library/components/Avatars/Avatar/variants/AvatarAccount';
import BottomSheet, {
BottomSheetRef,
} from '../../../component-library/components/BottomSheets/BottomSheet';
@@ -33,7 +32,6 @@ import {
} from '../../../selectors/accountsController';
import { isDefaultAccountName } from '../../../util/ENSUtils';
import Logger from '../../../util/Logger';
-import getAccountNameWithENS from '../../../util/accounts';
import {
getAddressAccountType,
safeToChecksumAddress,
@@ -133,11 +131,6 @@ const AccountConnect = (props: AccountConnectProps) => {
>([]);
const { toastRef } = useContext(ToastContext);
- const accountAvatarType = useSelector((state: RootState) =>
- state.settings.useBlockieIcon
- ? AvatarAccountType.Blockies
- : AvatarAccountType.JazzIcon,
- );
// origin is set to the last active tab url in the browser which can conflict with sdk
const inappBrowserOrigin: string = useSelector(getActiveTabUrl, isEqual);
@@ -439,11 +432,6 @@ const AccountConnect = (props: AccountConnectProps) => {
};
const connectedAccountLength = selectedAddresses.length;
const activeAddress = selectedAddresses[0];
- const activeAccountName = getAccountNameWithENS({
- accountAddress: activeAddress,
- accounts,
- ensByAccountAddress,
- });
try {
setIsLoading(true);
@@ -463,26 +451,15 @@ const AccountConnect = (props: AccountConnectProps) => {
source: eventSource,
});
let labelOptions: ToastOptions['labelOptions'] = [];
- if (connectedAccountLength > 1) {
- labelOptions = [
- { label: `${connectedAccountLength} `, isBold: true },
- {
- label: `${strings('toast.accounts_connected')}`,
- },
- { label: `\n${activeAccountName} `, isBold: true },
- { label: strings('toast.now_active') },
- ];
- } else {
- labelOptions = [
- { label: `${activeAccountName} `, isBold: true },
- { label: strings('toast.connected_and_active') },
- ];
+
+ if (connectedAccountLength >= 1) {
+ labelOptions = [{ label: `${strings('toast.permissions_updated')}` }];
}
+
toastRef?.current?.showToast({
- variant: ToastVariants.Account,
+ variant: ToastVariants.Network,
labelOptions,
- accountAddress: activeAddress,
- accountAvatarType,
+ networkImageSource: faviconSource,
hasNoTimeout: false,
});
} catch (e) {
@@ -496,14 +473,12 @@ const AccountConnect = (props: AccountConnectProps) => {
eventSource,
selectedAddresses,
hostInfo,
- accounts,
- ensByAccountAddress,
- accountAvatarType,
toastRef,
accountsLength,
channelIdOrHostname,
triggerDappViewedEvent,
trackEvent,
+ faviconSource,
]);
const handleCreateAccount = useCallback(
diff --git a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx
index 157534a5c53..3dc556a5f01 100644
--- a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx
+++ b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx
@@ -21,6 +21,7 @@ import AccountSelectorList from '../../../UI/AccountSelectorList';
import HelpText, {
HelpTextSeverity,
} from '../../../../component-library/components/Form/HelpText';
+import Engine from '../../../../core/Engine';
// Internal dependencies.
import { ConnectAccountBottomSheetSelectorsIDs } from '../../../../../e2e/selectors/Browser/ConnectAccountBottomSheet.selectors';
@@ -84,6 +85,11 @@ const AccountConnectMultiSelector = ({
[accounts, selectedAddresses, onSelectAddress],
);
+ const onRevokeAllHandler = useCallback(async () => {
+ await Engine.context.PermissionController.revokeAllPermissions(hostname);
+ navigate('PermissionsManager');
+ }, [hostname, navigate]);
+
const toggleRevokeAllAccountPermissionsModal = useCallback(() => {
navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS,
@@ -93,9 +99,10 @@ const AccountConnectMultiSelector = ({
origin: urlWithProtocol && new URL(urlWithProtocol).hostname,
},
},
+ onRevokeAll: !isRenderedAsBottomSheet && onRevokeAllHandler,
},
});
- }, [navigate, urlWithProtocol]);
+ }, [navigate, urlWithProtocol, isRenderedAsBottomSheet, onRevokeAllHandler]);
const renderSelectAllButton = useCallback(
() =>
diff --git a/app/components/Views/AccountPermissions/AccountPermissions.tsx b/app/components/Views/AccountPermissions/AccountPermissions.tsx
index ed6589becd9..42101ac25ef 100755
--- a/app/components/Views/AccountPermissions/AccountPermissions.tsx
+++ b/app/components/Views/AccountPermissions/AccountPermissions.tsx
@@ -196,8 +196,7 @@ const AccountPermissions = (props: AccountPermissionsProps) => {
useEffect(() => {
if (
previousPermittedAccounts.current === undefined &&
- permittedAccountsByHostname.length === 0 &&
- isRenderedAsBottomSheet
+ permittedAccountsByHostname.length === 0
) {
// TODO - Figure out better UX instead of auto dismissing. However, we cannot be in this state as long as accounts are not connected.
hideSheet();
diff --git a/app/components/Views/AccountPermissions/AccountPermissionsConfirmRevokeAll/AccountPermissionsConfirmRevokeAll.tsx b/app/components/Views/AccountPermissions/AccountPermissionsConfirmRevokeAll/AccountPermissionsConfirmRevokeAll.tsx
index fa06c49bbc6..8d3082a2ef7 100644
--- a/app/components/Views/AccountPermissions/AccountPermissionsConfirmRevokeAll/AccountPermissionsConfirmRevokeAll.tsx
+++ b/app/components/Views/AccountPermissions/AccountPermissionsConfirmRevokeAll/AccountPermissionsConfirmRevokeAll.tsx
@@ -26,6 +26,7 @@ interface AccountPermissionsConfirmRevokeAllProps {
hostInfo: {
metadata: { origin: string };
};
+ onRevokeAll?: () => void;
};
};
}
@@ -37,6 +38,7 @@ const AccountPermissionsConfirmRevokeAll = (
hostInfo: {
metadata: { origin: hostname },
},
+ onRevokeAll,
} = props.route.params;
const { styles } = useStyles(styleSheet, {});
@@ -48,12 +50,18 @@ const AccountPermissionsConfirmRevokeAll = (
const revokeAllAccounts = useCallback(async () => {
try {
- await Engine.context.PermissionController.revokeAllPermissions(hostname);
- sheetRef.current?.onCloseBottomSheet();
+ if (onRevokeAll) {
+ onRevokeAll();
+ } else {
+ await Engine.context.PermissionController.revokeAllPermissions(
+ hostname,
+ );
+ sheetRef.current?.onCloseBottomSheet();
+ }
} catch (e) {
Logger.log(`Failed to revoke all accounts for ${hostname}`, e);
}
- }, [hostname, Engine.context.PermissionController]);
+ }, [hostname, Engine.context.PermissionController, onRevokeAll]);
const onCancel = () => {
sheetRef.current?.onCloseBottomSheet();
diff --git a/app/components/Views/AccountPermissions/ConnectionDetails/ConnectionDetails.styles.ts b/app/components/Views/AccountPermissions/ConnectionDetails/ConnectionDetails.styles.ts
new file mode 100644
index 00000000000..4cd67a1f432
--- /dev/null
+++ b/app/components/Views/AccountPermissions/ConnectionDetails/ConnectionDetails.styles.ts
@@ -0,0 +1,26 @@
+// Third party dependencies.
+import { StyleSheet } from 'react-native';
+
+/**
+ * Style sheet function for AccountConnectMultiSelector screen.
+ * @returns StyleSheet object.
+ */
+const styleSheet = () =>
+ StyleSheet.create({
+ container: {
+ paddingHorizontal: 16,
+ alignItems: 'center',
+ },
+ descriptionContainer: {
+ marginBottom: 16,
+ },
+ buttonsContainer: {
+ flexDirection: 'row',
+ gap: 16,
+ },
+ button: {
+ flex: 1,
+ },
+ });
+
+export default styleSheet;
diff --git a/app/components/Views/AccountPermissions/ConnectionDetails/ConnectionDetails.tsx b/app/components/Views/AccountPermissions/ConnectionDetails/ConnectionDetails.tsx
new file mode 100644
index 00000000000..49463cd91de
--- /dev/null
+++ b/app/components/Views/AccountPermissions/ConnectionDetails/ConnectionDetails.tsx
@@ -0,0 +1,76 @@
+// Third party dependencies
+import React, { useRef } from 'react';
+
+// External dependencies
+import { View } from 'react-native';
+import BottomSheetHeader from '../../../../component-library/components/BottomSheets/BottomSheetHeader';
+import Button, {
+ ButtonVariants,
+ ButtonSize,
+} from '../../../../component-library/components/Buttons/Button';
+import Text, {
+ TextVariant,
+} from '../../../../component-library/components/Texts/Text';
+import { strings } from '../../../../../locales/i18n';
+import BottomSheet, {
+ BottomSheetRef,
+} from '../../../../component-library/components/BottomSheets/BottomSheet';
+import { useStyles } from '../../../../component-library/hooks';
+import styleSheet from './ConnectionDetails.styles';
+
+interface ConnectionDetailsProps {
+ route: {
+ params: {
+ connectionDateTime?: number;
+ };
+ };
+}
+
+const AccountPermissionsConfirmRevokeAll = (props: ConnectionDetailsProps) => {
+ const { connectionDateTime = 123456789 } = props.route.params;
+
+ const { styles } = useStyles(styleSheet, {});
+
+ const sheetRef = useRef(null);
+
+ const onDismiss = () => {
+ sheetRef.current?.onCloseBottomSheet();
+ };
+
+ const formatConnectionDate = (timestamp: number) =>
+ new Date(timestamp).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ });
+
+ return (
+
+
+
+
+ {strings('permissions.connection_details_title')}
+
+
+
+
+ {strings('permissions.connection_details_description', {
+ connectionDateTime: formatConnectionDate(connectionDateTime),
+ })}
+
+
+
+
+
+
+
+ );
+};
+
+export default AccountPermissionsConfirmRevokeAll;
diff --git a/app/components/Views/AccountPermissions/ConnectionDetails/index.ts b/app/components/Views/AccountPermissions/ConnectionDetails/index.ts
new file mode 100644
index 00000000000..508efb8efe9
--- /dev/null
+++ b/app/components/Views/AccountPermissions/ConnectionDetails/index.ts
@@ -0,0 +1 @@
+export { default } from './ConnectionDetails';
diff --git a/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap b/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap
index 226158d5611..8fbd535e84c 100644
--- a/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap
+++ b/app/components/Views/AccountPermissions/__snapshots__/AccountPermissions.test.tsx.snap
@@ -281,6 +281,7 @@ exports[`AccountPermissions renders correctly 1`] = `
style={null}
>
({
+ ...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..656c54a8673 100644
--- a/app/components/Views/AccountSelector/AccountSelector.tsx
+++ b/app/components/Views/AccountSelector/AccountSelector.tsx
@@ -35,11 +35,13 @@ import { useDispatch, useSelector } from 'react-redux';
import { setReloadAccounts } from '../../../actions/accounts';
import { RootState } from '../../../reducers';
import { useMetrics } from '../../../components/hooks/useMetrics';
+import { TraceName, endTrace } from '../../../util/trace';
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
@@ -53,7 +55,9 @@ const AccountSelector = ({ route }: AccountSelectorProps) => {
const [screen, setScreen] = useState(
AccountSelectorScreens.AccountSelector,
);
-
+ useEffect(() => {
+ endTrace({ name: TraceName.AccountList });
+ }, []);
useEffect(() => {
if (reloadAccounts) {
dispatch(setReloadAccounts(false));
@@ -92,6 +96,7 @@ const AccountSelector = ({ route }: AccountSelectorProps) => {
accounts={accounts}
ensByAccountAddress={ensByAccountAddress}
isRemoveAccountEnabled
+ privacyMode={privacyMode}
testID={AccountListViewSelectorsIDs.ACCOUNT_LIST_ID}
/>
@@ -106,7 +111,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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/Views/EditAccountName/EditAccountName.test.tsx b/app/components/Views/EditAccountName/EditAccountName.test.tsx
index edfd649140b..624e9c15ade 100644
--- a/app/components/Views/EditAccountName/EditAccountName.test.tsx
+++ b/app/components/Views/EditAccountName/EditAccountName.test.tsx
@@ -60,6 +60,17 @@ const mockNavigate = jest.fn();
const mockSetOptions = jest.fn();
const mockGoBack = jest.fn();
+const mockRoute = {
+ params: {
+ selectedAccount: {
+ address: MOCK_ADDRESS,
+ metadata: {
+ name: 'Test Account',
+ },
+ },
+ },
+};
+
jest.mock('@react-navigation/native', () => {
const actualReactNavigation = jest.requireActual('@react-navigation/native');
return {
@@ -69,6 +80,7 @@ jest.mock('@react-navigation/native', () => {
setOptions: mockSetOptions,
goBack: mockGoBack,
}),
+ useRoute: () => mockRoute,
};
});
diff --git a/app/components/Views/EditAccountName/EditAccountName.tsx b/app/components/Views/EditAccountName/EditAccountName.tsx
index d6b4a043e95..2d6c0bacc22 100644
--- a/app/components/Views/EditAccountName/EditAccountName.tsx
+++ b/app/components/Views/EditAccountName/EditAccountName.tsx
@@ -1,10 +1,16 @@
// Third party dependencies
import React, { useCallback, useEffect, useState } from 'react';
-import { useNavigation } from '@react-navigation/native';
+import {
+ useRoute,
+ useNavigation,
+ RouteProp,
+ ParamListBase,
+} from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { SafeAreaView } from 'react-native';
// External dependencies
+import { InternalAccount } from '@metamask/keyring-api';
import Text from '../../../component-library/components/Texts/Text/Text';
import { View } from 'react-native-animatable';
import { TextVariant } from '../../../component-library/components/Texts/Text';
@@ -24,7 +30,6 @@ import { getEditAccountNameNavBarOptions } from '../../../components/UI/Navbar';
import Engine from '../../../core/Engine';
import { MetaMetricsEvents } from '../../../core/Analytics';
import { selectChainId } from '../../../selectors/networkController';
-import { selectSelectedInternalAccount } from '../../../selectors/accountsController';
import {
doENSReverseLookup,
isDefaultAccountName,
@@ -37,7 +42,20 @@ import styleSheet from './EditAccountName.styles';
import { getDecimalChainId } from '../../../util/networks';
import { useMetrics } from '../../../components/hooks/useMetrics';
+interface RootNavigationParamList extends ParamListBase {
+ EditAccountName: {
+ selectedAccount: InternalAccount;
+ };
+}
+
+type EditAccountNameRouteProp = RouteProp<
+ RootNavigationParamList,
+ 'EditAccountName'
+>;
+
const EditAccountName = () => {
+ const route = useRoute();
+ const { selectedAccount } = route.params;
const { colors } = useTheme();
const { trackEvent } = useMetrics();
const { styles } = useStyles(styleSheet, {});
@@ -45,10 +63,8 @@ const EditAccountName = () => {
const [accountName, setAccountName] = useState();
const [ens, setEns] = useState();
- const selectedInternalAccount = useSelector(selectSelectedInternalAccount);
-
- const selectedChecksummedAddress = selectedInternalAccount?.address
- ? toChecksumHexAddress(selectedInternalAccount.address)
+ const selectedChecksummedAddress = selectedAccount?.address
+ ? toChecksumHexAddress(selectedAccount.address)
: undefined;
const chainId = useSelector(selectChainId);
@@ -80,28 +96,22 @@ const EditAccountName = () => {
}, [updateNavBar]);
useEffect(() => {
- const name = selectedInternalAccount?.metadata.name;
+ const name = selectedAccount?.metadata.name;
setAccountName(isDefaultAccountName(name) && ens ? ens : name);
- }, [ens, selectedInternalAccount?.metadata.name]);
+ }, [ens, selectedAccount?.metadata.name]);
const onChangeName = (name: string) => {
setAccountName(name);
};
const saveAccountName = async () => {
- if (
- accountName &&
- accountName.length > 0 &&
- selectedInternalAccount?.address
- ) {
- Engine.setAccountLabel(selectedInternalAccount?.address, accountName);
+ if (accountName && accountName.length > 0 && selectedAccount?.address) {
+ Engine.setAccountLabel(selectedAccount?.address, accountName);
navigate('WalletView');
try {
const analyticsProperties = async () => {
- const accountType = getAddressAccountType(
- selectedInternalAccount?.address,
- );
+ const accountType = getAddressAccountType(selectedAccount?.address);
const account_type = accountType === 'QR' ? 'hardware' : accountType;
return { account_type, chain_id: getDecimalChainId(chainId) };
};
@@ -131,13 +141,10 @@ const EditAccountName = () => {
{strings('address_book.address')}
- {selectedInternalAccount?.address ? (
+ {selectedAccount?.address ? (
) : null}
diff --git a/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap b/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap
index 5de9ca67544..9e6a8d2bc18 100644
--- a/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap
+++ b/app/components/Views/EditAccountName/__snapshots__/EditAccountName.test.tsx.snap
@@ -87,7 +87,7 @@ exports[`EditAccountName should render correctly 1`] = `
}
}
testID="account-name-input"
- value="Account 1"
+ value="Test Account"
/>
diff --git a/app/components/Views/ErrorBoundary/index.js b/app/components/Views/ErrorBoundary/index.js
index a0730857d36..f300d534ea3 100644
--- a/app/components/Views/ErrorBoundary/index.js
+++ b/app/components/Views/ErrorBoundary/index.js
@@ -1,12 +1,17 @@
-import React, { Component, useCallback } from 'react';
+import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
View,
StyleSheet,
- Image,
Linking,
Alert,
+ Platform,
+ Modal,
+ KeyboardAvoidingView,
+ DevSettings,
+ Image,
+ TextInput,
} from 'react-native';
import PropTypes from 'prop-types';
import { lastEventId as getLatestSentryId } from '@sentry/react-native';
@@ -16,39 +21,49 @@ import Logger from '../../../util/Logger';
import { fontStyles } from '../../../styles/common';
import { ScrollView } from 'react-native-gesture-handler';
import { strings } from '../../../../locales/i18n';
-import Icon from 'react-native-vector-icons/FontAwesome';
+import CLIcon, {
+ IconColor,
+ IconName,
+ IconSize,
+} from '../../../component-library/components/Icons/Icon';
import ClipboardManager from '../../../core/ClipboardManager';
import { mockTheme, ThemeContext, useTheme } from '../../../util/theme';
import { SafeAreaView } from 'react-native-safe-area-context';
+import BannerAlert from '../../../component-library/components/Banners/Banner/variants/BannerAlert';
+import { BannerAlertSeverity } from '../../../component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.types';
+import CLText, {
+ TextVariant,
+} from '../../../component-library/components/Texts/Text';
import {
MetaMetricsEvents,
withMetricsAwareness,
} from '../../../components/hooks/useMetrics';
+import AppConstants from '../../../core/AppConstants';
+import { useSelector } from 'react-redux';
// eslint-disable-next-line import/no-commonjs
-const metamaskErrorImage = require('../../../images/metamask-error.png');
+const WarningIcon = require('./warning-icon.png');
const createStyles = (colors) =>
StyleSheet.create({
container: {
flex: 1,
+ paddingHorizontal: 8,
backgroundColor: colors.background.default,
},
- content: {
- paddingHorizontal: 24,
- flex: 1,
- },
header: {
alignItems: 'center',
+ paddingTop: 20,
},
errorImage: {
- width: 50,
- height: 50,
- marginTop: 24,
+ width: 32,
+ height: 32,
},
title: {
color: colors.text.default,
fontSize: 24,
+ paddingTop: 10,
+ paddingBottom: 20,
lineHeight: 34,
...fontStyles.bold,
},
@@ -60,23 +75,36 @@ const createStyles = (colors) =>
textAlign: 'center',
...fontStyles.normal,
},
- errorContainer: {
+ errorMessageContainer: {
+ flexShrink: 1,
backgroundColor: colors.error.muted,
borderRadius: 8,
- marginTop: 24,
+ marginTop: 10,
+ padding: 10,
},
error: {
- color: colors.text.default,
+ color: colors.error.default,
padding: 8,
fontSize: 14,
lineHeight: 20,
...fontStyles.normal,
},
button: {
- marginTop: 24,
+ marginTop: 16,
borderColor: colors.primary.default,
borderWidth: 1,
- borderRadius: 50,
+ borderRadius: 48,
+ height: 48,
+ padding: 12,
+ paddingHorizontal: 34,
+ },
+ blueButton: {
+ marginTop: 16,
+ borderColor: colors.primary.default,
+ backgroundColor: colors.primary.default,
+ borderWidth: 1,
+ borderRadius: 48,
+ height: 48,
padding: 12,
paddingHorizontal: 34,
},
@@ -85,6 +113,55 @@ const createStyles = (colors) =>
textAlign: 'center',
...fontStyles.normal,
},
+ blueButtonText: {
+ color: colors.background.default,
+ textAlign: 'center',
+ ...fontStyles.normal,
+ },
+ submitButton: {
+ width: '45%',
+ backgroundColor: colors.primary.default,
+ marginTop: 24,
+ borderColor: colors.primary.default,
+ borderWidth: 1,
+ borderRadius: 48,
+ height: 48,
+ padding: 12,
+ paddingHorizontal: 34,
+ },
+ cancelButton: {
+ width: '45%',
+ marginTop: 24,
+ borderColor: colors.primary.default,
+ borderWidth: 1,
+ borderRadius: 48,
+ height: 48,
+ padding: 12,
+ paddingHorizontal: 34,
+ },
+ buttonsContainer: {
+ flexGrow: 1,
+ bottom: 10,
+ justifyContent: 'flex-end',
+ },
+ modalButtonsWrapper: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ alignItems: 'flex-end',
+ bottom: 24,
+ paddingHorizontal: 10,
+ },
+ feedbackInput: {
+ borderColor: colors.primary.default,
+ minHeight: 175,
+ minWidth: '100%',
+ paddingHorizontal: 16,
+ paddingTop: 10,
+ borderRadius: 10,
+ borderWidth: 1,
+ marginTop: 20,
+ },
textContainer: {
marginTop: 24,
},
@@ -105,120 +182,216 @@ const createStyles = (colors) =>
reportStep: {
marginTop: 14,
},
+ banner: {
+ width: '100%',
+ marginTop: 20,
+ paddingHorizontal: 16,
+ },
+ keyboardViewContainer: { flex: 1, justifyContent: 'flex-end' },
+ modalWrapper: { flex: 1, justifyContent: 'space-between' },
+ modalTopContainer: { flex: 1, paddingTop: '20%', paddingHorizontal: 16 },
+ closeIconWrapper: {
+ position: 'absolute',
+ right: 0,
+ top: 2,
+ bottom: 0,
+ },
+ modalTitleWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ modalTitleText: { paddingTop: 0 },
+ errorBoxTitle: { fontWeight: '600' },
+ contentContainer: {
+ justifyContent: 'space-between',
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ errorContentWrapper: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 20,
+ },
+ row: { flexDirection: 'row' },
+ copyText: {
+ color: colors.primary.default,
+ fontSize: 14,
+ paddingLeft: 5,
+ fontWeight: '500',
+ },
+ infoBanner: { marginBottom: 20 },
+ hitSlop: { top: 50, right: 50, bottom: 50, left: 50 },
});
-const UserFeedbackSection = ({ styles, sentryId }) => {
- /**
- * Prompt bug report form
- */
- const promptBugReport = useCallback(() => {
- Alert.prompt(
- strings('error_screen.bug_report_prompt_title'),
- strings('error_screen.bug_report_prompt_description'),
- [
- { text: strings('error_screen.cancel'), style: 'cancel' },
- {
- text: strings('error_screen.send'),
- onPress: (comments = '') => {
- // Send Sentry feedback
- captureSentryFeedback({ sentryId, comments });
- Alert.alert(strings('error_screen.bug_report_thanks'));
- },
- },
- ],
- );
- }, [sentryId]);
-
- return (
-
-
- {' '}
- {strings('error_screen.submit_ticket_8')}{' '}
-
- {strings('error_screen.submit_ticket_6')}
- {' '}
- {strings('error_screen.submit_ticket_9')}
-
+export const Fallback = (props) => {
+ const { colors } = useTheme();
+ const styles = createStyles(colors);
+ const [modalVisible, setModalVisible] = React.useState(false);
+ const [feedback, setFeedback] = React.useState('');
+ const dataCollectionForMarketing = useSelector(
+ (state) => state.security.dataCollectionForMarketing,
);
-};
-UserFeedbackSection.propTypes = {
- styles: PropTypes.object,
- sentryId: PropTypes.string,
-};
+ const toggleModal = () => {
+ setModalVisible((visible) => !visible);
+ setFeedback('');
+ };
+ const handleContactSupport = () =>
+ Linking.openURL(AppConstants.REVIEW_PROMPT.SUPPORT);
+ const handleTryAgain = () => DevSettings.reload();
-const Fallback = (props) => {
- const { colors } = useTheme();
- const styles = createStyles(colors);
+ const handleSubmit = () => {
+ toggleModal();
+ captureSentryFeedback({ sentryId: props.sentryId, comments: feedback });
+ Alert.alert(strings('error_screen.bug_report_thanks'));
+ };
return (
-
+
-
+
{strings('error_screen.title')}
- {strings('error_screen.subtitle')}
-
-
- {props.errorMessage}
-
-
-
-
- {' '}
- {strings('error_screen.try_again_button')}
+ {strings('error_screen.subtitle')}}
+ />
+
+ {strings('error_screen.save_seedphrase_1')}{' '}
+
+ {strings('error_screen.save_seedphrase_2')}
+ {' '}
+ {strings('error_screen.save_seedphrase_3')}
+ }
+ />
+
+
+ {strings('error_screen.error_message')}
+
+
+
+ {strings('error_screen.copy')}
-
-
- {strings('error_screen.submit_ticket_1')}
-
-
-
-
- {' '}
- {strings('error_screen.submit_ticket_2')}
-
-
-
-
- {' '}
-
- {strings('error_screen.submit_ticket_3')}
- {' '}
- {strings('error_screen.submit_ticket_4')}
+
+
+ {props.errorMessage}
+
+
+
+ {dataCollectionForMarketing && (
+
+
+ {strings('error_screen.describe')}
+
+
+ )}
+
+
+ {strings('error_screen.contact_support')}
-
-
-
- {' '}
- {strings('error_screen.submit_ticket_5')}{' '}
-
- {strings('error_screen.submit_ticket_6')}
- {' '}
- {strings('error_screen.submit_ticket_7')}
+
+
+
+ {strings('error_screen.try_again')}
-
-
-
- {strings('error_screen.save_seedphrase_1')}{' '}
-
- {strings('error_screen.save_seedphrase_2')}
- {' '}
- {strings('error_screen.save_seedphrase_3')}
-
+
-
+
+
+
+
+
+
+ {strings('error_screen.modal_title')}
+
+
+
+
+
+
+
+
+
+
+ {strings('error_screen.cancel')}
+
+
+
+
+ {strings('error_screen.submit')}
+
+
+
+
+
+
+
);
};
Fallback.propTypes = {
errorMessage: PropTypes.string,
- resetError: PropTypes.func,
showExportSeedphrase: PropTypes.func,
copyErrorToClipboard: PropTypes.func,
- openTicket: PropTypes.func,
sentryId: PropTypes.string,
};
diff --git a/app/components/Views/ErrorBoundary/warning-icon.png b/app/components/Views/ErrorBoundary/warning-icon.png
new file mode 100644
index 00000000000..91a9fdd9fd9
Binary files /dev/null and b/app/components/Views/ErrorBoundary/warning-icon.png differ
diff --git a/app/components/Views/LockScreen/index.js b/app/components/Views/LockScreen/index.js
index 030bc9ace1d..676300ee1db 100644
--- a/app/components/Views/LockScreen/index.js
+++ b/app/components/Views/LockScreen/index.js
@@ -22,7 +22,6 @@ import {
import Routes from '../../../constants/navigation/Routes';
import { CommonActions } from '@react-navigation/native';
import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics';
-import { trace, TraceName, TraceOperation } from '../../../util/trace';
const LOGO_SIZE = 175;
const createStyles = (colors) =>
@@ -135,18 +134,10 @@ class LockScreen extends PureComponent {
// Retrieve the credentials
Logger.log('Lockscreen::unlockKeychain - getting credentials');
- await trace(
- {
- name: TraceName.BiometricAuthentication,
- op: TraceOperation.BiometricAuthentication,
- },
- async () => {
- await Authentication.appTriggeredAuth({
- bioStateMachineId,
- disableAutoLogout: true,
- });
- },
- );
+ await Authentication.appTriggeredAuth({
+ bioStateMachineId,
+ disableAutoLogout: true,
+ });
this.setState({ ready: true });
Logger.log('Lockscreen::unlockKeychain - state: ready');
diff --git a/app/components/Views/Login/__snapshots__/index.test.tsx.snap b/app/components/Views/Login/__snapshots__/index.test.tsx.snap
index df38946a89c..4beae542b9e 100644
--- a/app/components/Views/Login/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Login/__snapshots__/index.test.tsx.snap
@@ -6,401 +6,344 @@ exports[`Login should render correctly 1`] = `
{
"backgroundColor": "#ffffff",
"flex": 1,
+ "paddingHorizontal": 8,
}
}
>
-
-
-
+
+
-
+
+
+
+
-
+
- An error occurred
-
+ }
+ >
Your information can't be shown. Don’t worry, your wallet and funds are safe.
+
+
-
- View: Login
-TypeError: (0 , _reactNativeDeviceInfo.getTotalMemorySync) is not a function
-
+ width={24}
+ />
-
+ If you keep getting this error,
+
-
-
-
-
- Try again
+ save your Secret Recovery Phrase
-
+
+ and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet.
+
-
+
+
-
+
+
-
- Please report this issue so we can fix it:
-
-
-
+
+ Copy
+
+
+
+
+
+
-
-
-
-
- Take a screenshot of this screen.
-
-
-
-
-
-
-
- Copy
-
-
- the error message to clipboard.
-
-
-
-
-
-
- Submit a ticket
-
-
- here.
-
-
- Please include the error message and the screenshot.
-
-
-
-
-
-
- Send us a bug report
-
-
- here.
-
-
- Please include details about what happened.
+ View: Login
+TypeError: (0 , _reactNativeDeviceInfo.getTotalMemorySync) is not a function
+
+
+
+
- If this error persists,
-
-
+
+
+
- save your Secret Recovery Phrase
-
-
- & re-install the app. Note: you can NOT restore your wallet without your Secret Recovery Phrase.
+ }
+ >
+ Try again
-
+
-
+
+
`;
diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js
index 49771339141..20664341975 100644
--- a/app/components/Views/Login/index.js
+++ b/app/components/Views/Login/index.js
@@ -72,6 +72,8 @@ import Label from '../../../component-library/components/Form/Label';
import HelpText, {
HelpTextSeverity,
} from '../../../component-library/components/Form/HelpText';
+import { getTraceTags } from '../../../util/sentry/tags';
+import { store } from '../../../store';
const deviceHeight = Device.getDeviceHeight();
const breakPoint = deviceHeight < 700;
@@ -250,11 +252,19 @@ class Login extends PureComponent {
fieldRef = React.createRef();
+ parentSpan = trace({
+ name: TraceName.Login,
+ op: TraceOperation.Login,
+ tags: getTraceTags(store.getState()),
+ });
+
async componentDidMount() {
trace({
- name: TraceName.LoginToPasswordEntry,
- op: TraceOperation.LoginToPasswordEntry,
+ name: TraceName.LoginUserInteraction,
+ op: TraceOperation.Login,
+ parentContext: this.parentSpan,
});
+
this.props.metrics.trackEvent(MetaMetricsEvents.LOGIN_SCREEN_VIEWED);
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
@@ -365,6 +375,7 @@ class Login extends PureComponent {
};
onLogin = async () => {
+ endTrace({ name: TraceName.LoginUserInteraction });
const { password } = this.state;
const { current: field } = this.fieldRef;
const locked = !passwordRequirementsMet(password);
@@ -381,13 +392,13 @@ class Login extends PureComponent {
await trace(
{
name: TraceName.AuthenticateUser,
- op: TraceOperation.AuthenticateUser,
+ op: TraceOperation.Login,
+ parentContext: this.parentSpan,
},
async () => {
await Authentication.userEntryAuth(password, authType);
},
);
-
Keyboard.dismiss();
// Get onboarding wizard state
@@ -447,17 +458,20 @@ class Login extends PureComponent {
}
Logger.error(e, 'Failed to unlock');
}
+ endTrace({ name: TraceName.Login });
};
tryBiometric = async (e) => {
if (e) e.preventDefault();
+ endTrace({ name: TraceName.LoginUserInteraction });
const { current: field } = this.fieldRef;
field?.blur();
try {
await trace(
{
- name: TraceName.BiometricAuthentication,
- op: TraceOperation.BiometricAuthentication,
+ name: TraceName.LoginBiometricAuthentication,
+ op: TraceOperation.Login,
+ parentContext: this.parentSpan,
},
async () => {
await Authentication.appTriggeredAuth();
@@ -480,11 +494,6 @@ class Login extends PureComponent {
field?.blur();
};
- triggerLogIn = () => {
- endTrace({ name: TraceName.LoginToPasswordEntry });
- this.onLogin();
- };
-
toggleWarningModal = () => {
const { navigation } = this.props;
navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
@@ -587,7 +596,7 @@ class Login extends PureComponent {
value={this.state.password}
baseColor={colors.border.default}
tintColor={colors.primary.default}
- onSubmitEditing={this.triggerLogIn}
+ onSubmitEditing={this.onLogin}
endAccessory={
{
.mockImplementation(() => undefined);
const { toJSON } = renderWithProvider();
expect(toJSON()).toMatchSnapshot();
- expect(spyFetch).toHaveBeenCalledTimes(1);
+ expect(spyFetch).toHaveBeenCalledTimes(2);
});
});
diff --git a/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx b/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx
index d44f7b3bea5..7f865484329 100644
--- a/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx
+++ b/app/components/Views/NetworkConnect/NetworkConnectMultiSelector/NetworkConnectMultiSelector.tsx
@@ -159,6 +159,11 @@ const NetworkConnectMultiSelector = ({
[networks, selectedChainIds],
);
+ const onRevokeAllHandler = useCallback(async () => {
+ await Engine.context.PermissionController.revokeAllPermissions(hostname);
+ navigate('PermissionsManager');
+ }, [hostname, navigate]);
+
const toggleRevokeAllNetworkPermissionsModal = useCallback(() => {
navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.REVOKE_ALL_ACCOUNT_PERMISSIONS,
@@ -168,13 +173,14 @@ const NetworkConnectMultiSelector = ({
origin: urlWithProtocol && new URL(urlWithProtocol).hostname,
},
},
+ onRevokeAll: !isRenderedAsBottomSheet && onRevokeAllHandler,
},
});
- }, [navigate, urlWithProtocol]);
+ }, [navigate, urlWithProtocol, isRenderedAsBottomSheet, onRevokeAllHandler]);
+
const areAllNetworksSelected = networks
.map(({ id }) => id)
.every((id) => selectedChainIds?.includes(id));
-
const areAnyNetworksSelected = selectedChainIds?.length !== 0;
const areNoNetworksSelected = selectedChainIds?.length === 0;
diff --git a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx
index 077bded6d8c..970634f0db7 100644
--- a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx
+++ b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx
@@ -86,6 +86,46 @@ const initialState = {
mainnet: { status: 'available', EIPS: { '1559': true } },
},
networkConfigurationsByChainId: {
+ '0x1': {
+ blockExplorerUrls: [],
+ chainId: '0x1',
+ defaultRpcEndpointIndex: 1,
+ name: 'Ethereum Mainnet',
+ nativeCurrency: 'ETH',
+ rpcEndpoints: [
+ {
+ networkClientId: 'mainnet',
+ type: 'infura',
+ url: 'https://mainnet.infura.io/v3/{infuraProjectId}',
+ },
+ {
+ name: 'public',
+ networkClientId: 'ea57f659-c004-4902-bfca-0c9688a43872',
+ type: 'custom',
+ url: 'https://mainnet-rpc.publicnode.com',
+ },
+ ],
+ },
+ '0xe708': {
+ blockExplorerUrls: [],
+ chainId: '0xe708',
+ defaultRpcEndpointIndex: 1,
+ name: 'Linea',
+ nativeCurrency: 'ETH',
+ rpcEndpoints: [
+ {
+ networkClientId: 'linea-mainnet',
+ type: 'infura',
+ url: 'https://linea-mainnet.infura.io/v3/{infuraProjectId}',
+ },
+ {
+ name: 'public',
+ networkClientId: 'ea57f659-c004-4902-bfca-0c9688a43877',
+ type: 'custom',
+ url: 'https://linea-rpc.publicnode.com',
+ },
+ ],
+ },
'0xa86a': {
blockExplorerUrls: ['https://snowtrace.io'],
chainId: '0xa86a',
@@ -459,4 +499,37 @@ describe('Network Selector', () => {
fireEvent(testNetworksSwitch, 'onValueChange', false);
expect(setShowTestNetworksSpy).toBeCalledWith(false);
});
+
+ describe('renderLineaMainnet', () => {
+ it('renders the linea mainnet cell correctly', () => {
+ (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true);
+ const { getByText } = renderComponent(initialState);
+ const lineaRpcUrl = getByText('https://linea-rpc.publicnode.com');
+ const lineaCell = getByText('Linea');
+ expect(lineaCell).toBeTruthy();
+ expect(lineaRpcUrl).toBeTruthy();
+ });
+ });
+
+ describe('renderRpcUrl', () => {
+ it('renders the RPC URL correctly for avalanche', () => {
+ (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true);
+ const { getByText } = renderComponent(initialState);
+ const avalancheRpcUrl = getByText('api.avax.network/ext/bc/C');
+ const avalancheCell = getByText('Avalanche Mainnet C-Chain');
+ expect(avalancheRpcUrl).toBeTruthy();
+ expect(avalancheCell).toBeTruthy();
+ });
+ });
+
+ describe('renderMainnet', () => {
+ it('renders the mainnet cell correctly', () => {
+ (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true);
+ const { getByText } = renderComponent(initialState);
+ const mainnetRpcUrl = getByText('https://mainnet-rpc.publicnode.com');
+ const mainnetCell = getByText('Ethereum Mainnet');
+ expect(mainnetCell).toBeTruthy();
+ expect(mainnetRpcUrl).toBeTruthy();
+ });
+ });
});
diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx
index 1ce26c69267..ea36deab166 100644
--- a/app/components/Views/NetworkSelector/NetworkSelector.tsx
+++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx
@@ -84,11 +84,18 @@ import { isNetworkUiRedesignEnabled } from '../../../util/networks/isNetworkUiRe
import { Hex } from '@metamask/utils';
import hideProtocolFromUrl from '../../../util/hideProtocolFromUrl';
import { CHAIN_IDS } from '@metamask/transaction-controller';
-import { LINEA_DEFAULT_RPC_URL } from '../../../constants/urls';
import { useNetworkInfo } from '../../../selectors/selectedNetworkController';
import { NetworkConfiguration } from '@metamask/network-controller';
import Logger from '../../../util/Logger';
import RpcSelectionModal from './RpcSelectionModal/RpcSelectionModal';
+import {
+ TraceName,
+ TraceOperation,
+ endTrace,
+ trace,
+} from '../../../util/trace';
+import { getTraceTags } from '../../../util/sentry/tags';
+import { store } from '../../../store';
interface infuraNetwork {
name: string;
@@ -130,7 +137,11 @@ const NetworkSelector = () => {
// origin is defined if network selector is opened from a dapp
const origin = route.params?.hostInfo?.metadata?.origin || '';
-
+ const parentSpan = trace({
+ name: TraceName.NetworkSwitch,
+ tags: getTraceTags(store.getState()),
+ op: TraceOperation.NetworkSwitch,
+ });
const {
chainId: selectedChainId,
rpcUrl: selectedRpcUrl,
@@ -234,7 +245,11 @@ const NetworkSelector = () => {
NetworkController,
SelectedNetworkController,
} = Engine.context;
-
+ trace({
+ name: TraceName.SwitchCustomNetwork,
+ parentContext: parentSpan,
+ op: TraceOperation.SwitchCustomNetwork,
+ });
if (networkConfiguration) {
const {
name: nickname,
@@ -253,7 +268,7 @@ const NetworkSelector = () => {
networkConfigurationId,
);
} else {
- CurrencyRateController.updateExchangeRate(ticker);
+ CurrencyRateController.updateExchangeRate([ticker]);
const { networkClientId } = rpcEndpoints[defaultRpcEndpointIndex];
@@ -261,6 +276,8 @@ const NetworkSelector = () => {
}
sheetRef.current?.onCloseBottomSheet();
+ endTrace({ name: TraceName.SwitchCustomNetwork });
+ endTrace({ name: TraceName.NetworkSwitch });
trackEvent(MetaMetricsEvents.NETWORK_SWITCHED, {
chain_id: getDecimalChainId(chainId),
from_network: selectedNetworkName,
@@ -346,6 +363,11 @@ const NetworkSelector = () => {
// The only possible value types are mainnet, linea-mainnet, sepolia and linea-sepolia
const onNetworkChange = (type: InfuraNetworkType) => {
+ trace({
+ name: TraceName.SwitchBuiltInNetwork,
+ parentContext: parentSpan,
+ op: TraceOperation.SwitchBuiltInNetwork,
+ });
const {
NetworkController,
CurrencyRateController,
@@ -372,7 +394,7 @@ const NetworkSelector = () => {
networkConfiguration.defaultRpcEndpointIndex
].networkClientId ?? type;
- CurrencyRateController.updateExchangeRate(ticker);
+ CurrencyRateController.updateExchangeRate([ticker]);
NetworkController.setActiveNetwork(clientId);
closeRpcModal();
AccountTrackerController.refresh();
@@ -383,7 +405,8 @@ const NetworkSelector = () => {
}
sheetRef.current?.onCloseBottomSheet();
-
+ endTrace({ name: TraceName.SwitchBuiltInNetwork });
+ endTrace({ name: TraceName.NetworkSwitch });
trackEvent(MetaMetricsEvents.NETWORK_SWITCHED, {
chain_id: getDecimalChainId(selectedChainId),
from_network: selectedNetworkName,
@@ -485,6 +508,10 @@ const NetworkSelector = () => {
const renderLineaMainnet = () => {
const { name: lineaMainnetName, chainId } = Networks['linea-mainnet'];
const name = networkConfigurations?.[chainId]?.name ?? lineaMainnetName;
+ const rpcUrl =
+ networkConfigurations?.[chainId]?.rpcEndpoints?.[
+ networkConfigurations?.[chainId]?.defaultRpcEndpointIndex
+ ].url;
if (isNetworkUiRedesignEnabled() && isNoSearchResults('linea-mainnet'))
return null;
@@ -505,7 +532,7 @@ const NetworkSelector = () => {
onPress={() => onNetworkChange(LINEA_MAINNET)}
style={styles.networkCell}
buttonIcon={IconName.MoreVertical}
- secondaryText={hideKeyFromUrl(LINEA_DEFAULT_RPC_URL)}
+ secondaryText={hideKeyFromUrl(rpcUrl)}
buttonProps={{
onButtonClick: () => {
openModal(chainId, false, LINEA_MAINNET, true);
diff --git a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap
index 959570801e2..3773996bbf7 100644
--- a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap
+++ b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap
@@ -560,7 +560,7 @@ exports[`Network Selector renders correctly 1`] = `
}
testID="cellbase-avatar-title"
>
- Ethereum Main Network
+ Ethereum Mainnet
@@ -686,7 +686,7 @@ exports[`Network Selector renders correctly 1`] = `
}
testID="cellbase-avatar-title"
>
- Linea Main Network
+ Linea
@@ -1890,8 +1890,68 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
}
testID="cellbase-avatar-title"
>
- Ethereum Main Network
+ Ethereum Mainnet
+
+
+ https://mainnet-rpc.publicnode.com
+
+
+
@@ -2080,7 +2140,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
}
testID="cellbase-avatar-title"
>
- Linea Main Network
+ Linea
- https://linea-mainnet.infura.io/v3
+ https://linea-rpc.publicnode.com
{
const action = () => {
- trace(
- {
- name: TraceName.CreateNewWalletToChoosePassword,
- op: TraceOperation.CreateNewWalletToChoosePassword,
- },
- () => {
- const { metrics } = this.props;
- if (metrics.isEnabled()) {
- this.props.navigation.navigate('ChoosePassword', {
+ const { metrics } = this.props;
+ if (metrics.isEnabled()) {
+ this.props.navigation.navigate('ChoosePassword', {
+ [PREVIOUS_SCREEN]: ONBOARDING,
+ });
+ this.track(MetaMetricsEvents.WALLET_SETUP_STARTED);
+ } else {
+ this.props.navigation.navigate('OptinMetrics', {
+ onContinue: () => {
+ this.props.navigation.replace('ChoosePassword', {
[PREVIOUS_SCREEN]: ONBOARDING,
});
this.track(MetaMetricsEvents.WALLET_SETUP_STARTED);
- } else {
- this.props.navigation.navigate('OptinMetrics', {
- onContinue: () => {
- this.props.navigation.replace('ChoosePassword', {
- [PREVIOUS_SCREEN]: ONBOARDING,
- });
- this.track(MetaMetricsEvents.WALLET_SETUP_STARTED);
- },
- });
- }
- },
- );
+ },
+ });
+ }
};
this.handleExistingUser(action);
diff --git a/app/components/Views/Onboarding/index.test.tsx b/app/components/Views/Onboarding/index.test.tsx
index 1945e0fc2ee..9bb3fe0e865 100644
--- a/app/components/Views/Onboarding/index.test.tsx
+++ b/app/components/Views/Onboarding/index.test.tsx
@@ -1,10 +1,6 @@
import { renderScreen } from '../../../util/test/renderWithProvider';
import Onboarding from './';
import { backgroundState } from '../../../util/test/initial-root-state';
-import { OnboardingSelectorIDs } from '../../../../e2e/selectors/Onboarding/Onboarding.selectors';
-import { fireEvent } from '@testing-library/react-native';
-// eslint-disable-next-line import/no-namespace
-import * as traceObj from '../../../util/trace';
const mockInitialState = {
engine: {
@@ -25,20 +21,4 @@ describe('Onboarding', () => {
);
expect(toJSON()).toMatchSnapshot();
});
- it('must call trace when press start', () => {
- const spyFetch = jest
- .spyOn(traceObj, 'trace')
- .mockImplementation(() => undefined);
- const { getByTestId } = renderScreen(
- Onboarding,
- { name: 'Onboarding' },
- {
- state: mockInitialState,
- },
- );
-
- const startButton = getByTestId(OnboardingSelectorIDs.NEW_WALLET_BUTTON);
- fireEvent.press(startButton);
- expect(spyFetch).toHaveBeenCalledTimes(1);
- });
});
diff --git a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap
index 04b33ca2b84..c2d68ea9cf5 100644
--- a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap
@@ -185,6 +185,7 @@ exports[`OnboardingGeneralSettings should render correctly 1`] = `
style={null}
>
;
+
interface IRevealPrivateCredentialProps {
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navigation: any;
credentialName: string;
cancel: () => void;
- // TODO: Replace "any" with type
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- route: any;
+ route: RevealPrivateCredentialRouteProp;
}
const RevealPrivateCredential = ({
@@ -91,9 +104,10 @@ const RevealPrivateCredential = ({
const [clipboardEnabled, setClipboardEnabled] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
- const selectedAddress = useSelector(
+ const checkSummedAddress = useSelector(
selectSelectedInternalAccountChecksummedAddress,
);
+
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const passwordSet = useSelector((state: any) => state.user.passwordSet);
@@ -106,6 +120,8 @@ const RevealPrivateCredential = ({
const styles = createStyles(theme);
const credentialSlug = credentialName || route?.params.credentialName;
+ const selectedAddress =
+ route?.params?.selectedAccount?.address || checkSummedAddress;
const isPrivateKey = credentialSlug === PRIVATE_KEY;
const updateNavBar = () => {
diff --git a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap
index 3ae0062fa7f..4a8e888f4a9 100644
--- a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap
@@ -1266,3 +1266,376 @@ exports[`RevealPrivateCredential renders reveal private key correctly 1`] = `
`;
+
+exports[`RevealPrivateCredential renders with a custom selectedAddress 1`] = `
+
+
+
+
+
+
+ Save it somewhere safe and secret.
+
+
+
+
+
+
+ Never disclose this key. Anyone with your private key can fully control your account, including transferring away any of your funds.
+
+
+
+
+
+ Enter password to continue
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ Next
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/Views/RevealPrivateCredential/index.test.tsx b/app/components/Views/RevealPrivateCredential/index.test.tsx
index 9057bf751d9..6e9d90b7916 100644
--- a/app/components/Views/RevealPrivateCredential/index.test.tsx
+++ b/app/components/Views/RevealPrivateCredential/index.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
+import { InternalAccount } from '@metamask/keyring-api';
import { backgroundState } from '../../../util/test/initial-root-state';
import { RevealPrivateCredential } from './';
import { ThemeContext, mockTheme } from '../../../util/theme';
@@ -43,8 +44,10 @@ describe('RevealPrivateCredential', () => {
const { toJSON } = renderWithProviders(
{
const { toJSON } = renderWithProviders(
{
const { toJSON } = renderWithProviders(
{
const { getByPlaceholderText, getByTestId } = renderWithProviders(
{
const { getByPlaceholderText, getByTestId } = renderWithProviders(
{
).toBeTruthy();
});
});
+
+ it('renders with a custom selectedAddress', async () => {
+ const mockInternalAccount: InternalAccount = {
+ type: 'eip155:eoa',
+ id: 'unique-account-id-1',
+ address: '0x1234567890123456789012345678901234567890',
+ options: {
+ someOption: 'optionValue',
+ anotherOption: 42,
+ },
+ methods: [
+ 'personal_sign',
+ 'eth_sign',
+ 'eth_signTransaction',
+ 'eth_sendTransaction',
+ ],
+ metadata: {
+ name: 'Test Account',
+ importTime: Date.now(),
+ keyring: {
+ type: 'HD Key Tree',
+ },
+ nameLastUpdatedAt: Date.now(),
+ snap: {
+ id: 'npm:@metamask/test-snap',
+ name: 'Test Snap',
+ enabled: true,
+ },
+ lastSelected: Date.now(),
+ },
+ };
+
+ const { toJSON } = renderWithProviders(
+ null}
+ credentialName={PRIV_KEY_CREDENTIAL}
+ />,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
});
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
index 67815bae912..44873cd9344 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
@@ -829,7 +829,7 @@ export class NetworkSettings extends PureComponent {
url.set('protocol', 'https:');
}
- CurrencyRateController.updateExchangeRate(ticker);
+ CurrencyRateController.updateExchangeRate([ticker]);
const existingNetwork = this.props.networkConfigurations[chainId];
const indexRpc = rpcUrls.findIndex(({ url }) => url === rpcUrl);
diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx b/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx
index d0eb1bc2dfc..5705ce2141a 100644
--- a/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx
+++ b/app/components/Views/Settings/PermissionsSettings/PermissionItem/PermissionItem.tsx
@@ -11,13 +11,10 @@ import Icon, {
IconSize,
} from '../../../../../component-library/components/Icons/Icon';
import styleSheet from './PermissionItem.style';
-import {
- PermissionListItemViewModel,
- PermissionSource,
-} from './PermissionItem.types';
+import { PermissionListItemViewModel } from './PermissionItem.types';
import WebsiteIcon from '../../../../../components/UI/WebsiteIcon';
-import Tag from '../../../../../component-library/components/Tags/Tag';
import { strings } from '../../../../../../locales/i18n';
+import { useFavicon } from '../../../../hooks/useFavicon';
interface PermissionListItemProps {
item: PermissionListItemViewModel;
@@ -29,10 +26,11 @@ const PermissionItem: React.FC = ({
onPress,
}) => {
const { styles } = useStyles(styleSheet, {});
+ const faviconUrl = useFavicon(item.dappHostName);
return (
-
+
@@ -40,32 +38,19 @@ const PermissionItem: React.FC = ({
- {item.numberOfAccountPermissions}
+ {item.numberOfAccountPermissions}{' '}
{item.numberOfAccountPermissions > 1
? strings('app_settings.accounts')
: strings('app_settings.account')}
•
- {item.numberOfNetworkPermissions}
+ {item.numberOfNetworkPermissions}{' '}
{item.numberOfNetworkPermissions > 1
? strings('app_settings.networks')
: strings('app_settings.network')}
-
-
-
-
-
diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap b/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap
index 26274e0fa98..0f349286650 100644
--- a/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap
+++ b/app/components/Views/Settings/PermissionsSettings/PermissionItem/__snapshots__/PermissionItem.test.tsx.snap
@@ -133,6 +133,7 @@ exports[`PermissionItem renders correctly 1`] = `
}
>
5
+
accounts
2
+
networks
-
-
-
-
- WalletConnect
-
-
-
-
;
@@ -53,6 +64,52 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => {
const { colors, typography } = useTheme();
const styles = createStyles(colors, typography, safeAreaInsets);
const { navigation } = props;
+ const [inAppBrowserPermissions, setInAppBrowserPermissions] = useState<
+ PermissionListItemViewModel[]
+ >([]);
+ const subjects = useSelector(
+ (state: RootState) =>
+ (
+ state.engine.backgroundState
+ .PermissionController as PermissionControllerState
+ ).subjects,
+ );
+
+ useEffect(() => {
+ const uuidRegex =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ const walletConnectRegex = /^https?:\/\//;
+ const inAppBrowserSubjects: any[] = [];
+
+ Object.entries(subjects || {}).forEach(([key, value]) => {
+ if (key === 'npm:@metamask/message-signing-snap') return;
+
+ if (
+ !uuidRegex.test(key) &&
+ !walletConnectRegex.test((value as { origin: string }).origin)
+ ) {
+ inAppBrowserSubjects.push(value);
+ }
+ });
+
+ const mappedInAppBrowserPermissions: PermissionListItemViewModel[] =
+ inAppBrowserSubjects.map((subject) => ({
+ dappLogoUrl: '',
+ dappHostName: subject.origin,
+ numberOfAccountPermissions:
+ subject.permissions?.eth_accounts?.caveats?.[0]?.value?.length ?? 0,
+ numberOfNetworkPermissions:
+ subject.permissions?.['endowment:permitted-chains']?.caveats?.[0]
+ ?.value?.length ?? 0,
+ permissionSource: PermissionSource.MetaMaskBrowser,
+ }));
+
+ const mappedPermissions: PermissionListItemViewModel[] = [
+ ...mappedInAppBrowserPermissions,
+ ];
+
+ setInAppBrowserPermissions(mappedPermissions);
+ }, [subjects]);
useEffect(() => {
navigation.setOptions(
@@ -65,36 +122,38 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => {
);
}, [navigation, colors]);
- const goToPermissionsDetails = useCallback(() => {
- navigation.navigate('AccountPermissionsAsFullScreen', {
- hostInfo: {
- metadata: {
- origin: 'https://app.uniswap.org/',
+ const goToPermissionsDetails = useCallback(
+ (permissionItem: PermissionListItemViewModel) => {
+ navigation.navigate('AccountPermissionsAsFullScreen', {
+ hostInfo: {
+ metadata: {
+ origin: permissionItem.dappHostName,
+ },
},
- },
- isRenderedAsBottomSheet: false,
- });
- }, [navigation]);
+ isRenderedAsBottomSheet: false,
+ });
+ },
+ [navigation],
+ );
const renderPermissions = useCallback(
() => (
<>
- {
- /* TODO: replace mock data with real data once available */
- isMultichainVersion1Enabled &&
- mockPermissionItems.map((mockPermissionItem, _index) => (
-
- ))
- }
+ {isMultichainVersion1Enabled &&
+ inAppBrowserPermissions.map((permissionItem, index) => (
+ {
+ goToPermissionsDetails(permissionItem);
+ }}
+ />
+ ))}
>
),
- [goToPermissionsDetails],
+ [goToPermissionsDetails, inAppBrowserPermissions],
);
const renderEmptyResult = () => (
@@ -114,7 +173,7 @@ const PermissionsManager = (props: SDKSessionsManagerProps) => {
style={styles.perissionsWrapper}
testID={SDKSelectorsIDs.SESSION_MANAGER_CONTAINER}
>
- {isMultichainVersion1Enabled && mockPermissionItems.length
+ {isMultichainVersion1Enabled && inAppBrowserPermissions.length
? renderPermissions()
: renderEmptyResult()}
diff --git a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap
index 7822a45d4f8..9514928b1a6 100644
--- a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap
@@ -54,7 +54,16 @@ exports[`Wallet should render correctly 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "#ffffff",
+ "backgroundColor": {
+ "alternative": "#f2f4f6",
+ "alternativeHover": "#e7ebee",
+ "alternativePressed": "#dbe0e6",
+ "default": "#ffffff",
+ "defaultHover": "#f5f5f5",
+ "defaultPressed": "#ebebeb",
+ "hover": "#0000000a",
+ "pressed": "#00000014",
+ },
"borderBottomColor": "rgb(216, 216, 216)",
"elevation": 0,
"flex": 1,
@@ -115,42 +124,11 @@ exports[`Wallet should render correctly 1`] = `
"top": 0,
}
}
- >
-
-
-
@@ -170,9 +148,14 @@ exports[`Wallet should render correctly 1`] = `
testID="open-networks-button"
>
-
- Ethereum Mainnet
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account 2
+
+
+
+
+
+
+
+ 0xC496...a756
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -294,9 +567,9 @@ exports[`Wallet should render correctly 1`] = `
testID="wallet-scan-button"
>
-
-
-
-
-
-
- An error occurred
-
-
- Your information can't be shown. Don’t worry, your wallet and funds are safe.
-
-
+ }
+ >
-
- View: Wallet
-TypeError: Cannot read properties of undefined (reading 'internalAccounts')
-
-
-
-
-
-
-
-
-
- Try again
-
-
-
-
-
-
- Please report this issue so we can fix it:
-
-
+ width={24}
+ />
+
-
-
-
-
- Take a screenshot of this screen.
-
-
-
-
-
-
-
- Copy
-
-
- the error message to clipboard.
+ Basic functionality is off
-
-
-
-
- Submit a ticket
-
-
- here.
-
-
- Please include the error message and the screenshot.
-
-
-
-
-
-
- Send us a bug report
-
-
- here.
-
-
- Please include details about what happened.
-
-
-
- If this error persists,
-
-
- save your Secret Recovery Phrase
+ Turn on basic functionality
-
- & re-install the app. Note: you can NOT restore your wallet without your Secret Recovery Phrase.
-
+
-
-
+
+
diff --git a/app/components/Views/Wallet/index.test.tsx b/app/components/Views/Wallet/index.test.tsx
index 87ef6fe0502..a7baa2653b3 100644
--- a/app/components/Views/Wallet/index.test.tsx
+++ b/app/components/Views/Wallet/index.test.tsx
@@ -5,59 +5,67 @@ import { act, screen } from '@testing-library/react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import Routes from '../../../constants/navigation/Routes';
import { backgroundState } from '../../../util/test/initial-root-state';
-import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils';
+import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils';
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
-import { CommonSelectorsIDs } from '../../../../e2e/selectors/Common.selectors';
import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing';
import { AppState } from 'react-native';
const MOCK_ADDRESS = '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272';
-const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([
- MOCK_ADDRESS,
-]);
-
-jest.mock('../../../core/Engine', () => ({
- getTotalFiatAccountBalance: jest.fn(),
- context: {
- NftController: {
- allNfts: {
- [MOCK_ADDRESS]: {
- [MOCK_ADDRESS]: [],
+jest.mock('../../../util/address', () => {
+ const actual = jest.requireActual('../../../util/address');
+ return {
+ ...actual,
+ getLabelTextByAddress: jest.fn(),
+ };
+});
+
+jest.mock('../../../core/Engine', () => {
+ const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } =
+ jest.requireActual('../../../util/test/accountsControllerTestUtils');
+ return {
+ getTotalFiatAccountBalance: jest.fn(),
+ context: {
+ NftController: {
+ allNfts: {
+ [MOCK_ADDRESS]: {
+ [MOCK_ADDRESS]: [],
+ },
},
+ allNftContracts: {
+ [MOCK_ADDRESS]: {
+ [MOCK_ADDRESS]: [],
+ },
+ },
+ },
+ TokenRatesController: {
+ poll: jest.fn(),
+ },
+ TokenDetectionController: {
+ detectTokens: jest.fn(),
},
- allNftContracts: {
- [MOCK_ADDRESS]: {
- [MOCK_ADDRESS]: [],
+ NftDetectionController: {
+ detectNfts: jest.fn(),
+ },
+ AccountTrackerController: {
+ refresh: jest.fn(),
+ },
+ KeyringController: {
+ state: {
+ keyrings: [
+ {
+ accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'],
+ },
+ ],
},
},
- },
- TokenRatesController: {
- poll: jest.fn(),
- },
- TokenDetectionController: {
- detectTokens: jest.fn(),
- },
- NftDetectionController: {
- detectNfts: jest.fn(),
- },
- AccountTrackerController: {
- refresh: jest.fn(),
- },
- KeyringController: {
- state: {
- keyrings: [
- {
- accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'],
- },
- ],
+ AccountsController: {
+ ...mockAccountsControllerState,
+ state: mockAccountsControllerState,
},
},
- AccountsController: {
- ...MOCK_ACCOUNTS_CONTROLLER_STATE,
- },
- },
-}));
+ };
+});
const mockInitialState = {
networkOnboarded: {
@@ -117,6 +125,21 @@ jest.mock('../../../util/notifications/hooks/useAccountSyncing', () => ({
}),
}));
+jest.mock('../../../util/address', () => ({
+ ...jest.requireActual('../../../util/address'),
+ getInternalAccountByAddress: jest.fn().mockReturnValue({
+ address: MOCK_ADDRESS,
+ balance: '0x0',
+ name: 'Account 1',
+ type: 'default',
+ metadata: {
+ keyring: {
+ type: 'HD Key Tree',
+ },
+ },
+ }),
+}));
+
const render = (Component: React.ComponentType) =>
renderScreen(
Component,
@@ -147,11 +170,21 @@ describe('Wallet', () => {
render(Wallet);
expect(ScrollableTabView).toHaveBeenCalled();
});
- it('should render fox icon', () => {
+ it('should render the address copy button', () => {
+ //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar
+ render(Wallet);
+ const addressCopyButton = screen.getByTestId(
+ WalletViewSelectorsIDs.NAVBAR_ADDRESS_COPY_BUTTON,
+ );
+ expect(addressCopyButton).toBeDefined();
+ });
+ it('should render the account picker', () => {
//@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar
render(Wallet);
- const foxIcon = screen.getByTestId(CommonSelectorsIDs.FOX_ICON);
- expect(foxIcon).toBeDefined();
+ const accountPicker = screen.getByTestId(
+ WalletViewSelectorsIDs.ACCOUNT_ICON,
+ );
+ expect(accountPicker).toBeDefined();
});
it('dispatches account syncing on mount', () => {
jest.clearAllMocks();
diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx
index 5f04c86d930..864beb3876d 100644
--- a/app/components/Views/Wallet/index.tsx
+++ b/app/components/Views/Wallet/index.tsx
@@ -35,6 +35,7 @@ import {
ToastContext,
ToastVariants,
} from '../../../component-library/components/Toast';
+import { AvatarAccountType } from '../../../component-library/components/Avatars/Avatar/variants/AvatarAccount';
import NotificationsService from '../../../util/notifications/services/NotificationService';
import Engine from '../../../core/Engine';
import CollectibleContracts from '../../UI/CollectibleContracts';
@@ -66,7 +67,6 @@ import {
ParamListBase,
useNavigation,
} from '@react-navigation/native';
-import { WalletAccount } from '../../../components/UI/WalletAccount';
import {
selectConversionRate,
selectCurrentCurrency,
@@ -94,6 +94,7 @@ import {
} from '../../../selectors/notifications';
import { ButtonVariants } from '../../../component-library/components/Buttons/Button';
import { useListNotifications } from '../../../util/notifications/hooks/useNotifications';
+import { useAccountName } from '../../hooks/useAccountName';
import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing';
import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance';
@@ -230,6 +231,14 @@ const Wallet = ({
const currentToast = toastRef?.current;
+ const accountName = useAccountName();
+
+ const accountAvatarType = useSelector((state: RootState) =>
+ state.settings.useBlockieIcon
+ ? AvatarAccountType.Blockies
+ : AvatarAccountType.JazzIcon,
+ );
+
useEffect(() => {
if (
isDataCollectionForMarketingEnabled === null &&
@@ -438,6 +447,10 @@ const Wallet = ({
useEffect(() => {
navigation.setOptions(
getWalletNavbarOptions(
+ walletRef,
+ selectedAddress || '',
+ accountName,
+ accountAvatarType,
networkName,
networkImageSource,
onTitlePress,
@@ -451,6 +464,9 @@ const Wallet = ({
);
/* eslint-disable-next-line */
}, [
+ selectedAddress,
+ accountName,
+ accountAvatarType,
navigation,
colors,
networkName,
@@ -560,9 +576,6 @@ const Wallet = ({
/>
) : null}
- {selectedAddress ? (
-
- ) : null}
<>
{accountBalanceByChainId && }
({
+ useSelector: jest.fn(),
+}));
+jest.mock('../../components/hooks/useEnsNameByAddress', () => jest.fn());
+jest.mock('../../util/ENSUtils', () => ({
+ isDefaultAccountName: jest.fn(),
+}));
+
+describe('useAccountName', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return the ENS name when default name is a default account name', () => {
+ (useSelector as jest.Mock).mockReturnValue({
+ metadata: { name: 'Account 1' },
+ address: '0x1234567890123456789012345678901234567890',
+ });
+ (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' });
+ (isDefaultAccountName as jest.Mock).mockReturnValue(true);
+
+ const { result } = renderHook(() => useAccountName());
+ expect(result.current).toBe('test.eth');
+ });
+
+ it('should return the default name when it is not a default account name', () => {
+ (useSelector as jest.Mock).mockReturnValue({
+ metadata: { name: 'My Custom Account' },
+ address: '0x1234567890123456789012345678901234567890',
+ });
+ (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' });
+ (isDefaultAccountName as jest.Mock).mockReturnValue(false);
+
+ const { result } = renderHook(() => useAccountName());
+ expect(result.current).toBe('My Custom Account');
+ });
+
+ it('should return an empty string when both default name and ENS name are undefined', () => {
+ (useSelector as jest.Mock).mockReturnValue({
+ metadata: { name: undefined },
+ address: '0x1234567890123456789012345678901234567890',
+ });
+ (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: undefined });
+ (isDefaultAccountName as jest.Mock).mockReturnValue(false);
+
+ const { result } = renderHook(() => useAccountName());
+ expect(result.current).toBe('');
+ });
+
+ it('should return an empty string when default name is undefined and ENS name is available', () => {
+ (useSelector as jest.Mock).mockReturnValue({
+ metadata: { name: undefined },
+ address: '0x1234567890123456789012345678901234567890',
+ });
+ (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' });
+ (isDefaultAccountName as jest.Mock).mockReturnValue(false);
+
+ const { result } = renderHook(() => useAccountName());
+ expect(result.current).toBe('');
+ });
+
+ it('should return the ENS name when default name is a default account name and ENS name is available', () => {
+ (useSelector as jest.Mock).mockReturnValue({
+ metadata: { name: 'Account 1' },
+ address: '0x1234567890123456789012345678901234567890',
+ });
+ (useEnsNameByAddress as jest.Mock).mockReturnValue({ ensName: 'test.eth' });
+ (isDefaultAccountName as jest.Mock).mockReturnValue(true);
+
+ const { result } = renderHook(() => useAccountName());
+ expect(result.current).toBe('test.eth');
+ });
+});
diff --git a/app/components/hooks/useAccountName.ts b/app/components/hooks/useAccountName.ts
new file mode 100644
index 00000000000..72ec9e8dd53
--- /dev/null
+++ b/app/components/hooks/useAccountName.ts
@@ -0,0 +1,18 @@
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { selectSelectedInternalAccount } from '../../selectors/accountsController';
+import useEnsNameByAddress from '../../components/hooks/useEnsNameByAddress';
+import { isDefaultAccountName } from '../../util/ENSUtils';
+
+export const useAccountName = () => {
+ const selectedAccount = useSelector(selectSelectedInternalAccount);
+ const { ensName } = useEnsNameByAddress(selectedAccount?.address);
+ const defaultName = selectedAccount?.metadata?.name;
+
+ return useMemo(
+ () =>
+ (isDefaultAccountName(defaultName) && ensName ? ensName : defaultName) ||
+ '',
+ [defaultName, ensName],
+ );
+};
diff --git a/app/components/hooks/usePolling.test.ts b/app/components/hooks/usePolling.test.ts
new file mode 100644
index 00000000000..6accee1e7b3
--- /dev/null
+++ b/app/components/hooks/usePolling.test.ts
@@ -0,0 +1,38 @@
+import { renderHook } from '@testing-library/react-hooks';
+
+import usePolling from './usePolling';
+
+describe('usePolling', () => {
+
+ it('Should start/stop polling when inputs are added/removed, and stop on dismount', async () => {
+
+ const inputs = ['foo', 'bar'];
+ const mockStartPolling = jest.fn().mockImplementation((input) => `${input}_token`);
+ const mockStopPollingByPollingToken = jest.fn();
+
+ const { unmount, rerender } = renderHook(() =>
+ usePolling({
+ startPolling: mockStartPolling,
+ stopPollingByPollingToken: mockStopPollingByPollingToken,
+ input: inputs,
+ })
+ );
+
+ // All inputs should start polling
+ for (const input of inputs) {
+ expect(mockStartPolling).toHaveBeenCalledWith(input);
+ }
+
+ // Remove one input, and add another
+ inputs[0] = 'baz';
+ rerender({ input: inputs });
+ expect(mockStopPollingByPollingToken).toHaveBeenCalledWith('foo_token');
+ expect(mockStartPolling).toHaveBeenCalledWith('baz');
+
+ // All inputs should stop polling on dismount
+ unmount();
+ for (const input of inputs) {
+ expect(mockStopPollingByPollingToken).toHaveBeenCalledWith(`${input}_token`);
+ }
+ });
+});
diff --git a/app/components/hooks/usePolling.ts b/app/components/hooks/usePolling.ts
new file mode 100644
index 00000000000..a7772399c6e
--- /dev/null
+++ b/app/components/hooks/usePolling.ts
@@ -0,0 +1,55 @@
+import { useEffect, useRef } from 'react';
+
+interface UsePollingOptions {
+ startPolling: (input: PollingInput) => string;
+ stopPollingByPollingToken: (pollingToken: string) => void;
+ input: PollingInput[];
+}
+
+// A hook that manages multiple polling loops of a polling controller.
+// Callers provide an array of inputs, and the hook manages starting
+// and stopping polling loops for each input.
+const usePolling = (
+ usePollingOptions: UsePollingOptions,
+) => {
+
+ const pollingTokens = useRef