diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index baa7b66630a..7ed8e90c153 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -78,6 +78,7 @@ import WalletRestored from '../../Views/RestoreWallet/WalletRestored';
import WalletResetNeeded from '../../Views/RestoreWallet/WalletResetNeeded';
import SDKLoadingModal from '../../Views/SDKLoadingModal/SDKLoadingModal';
import SDKFeedbackModal from '../../Views/SDKFeedbackModal/SDKFeedbackModal';
+import AccountActions from '../../../components/Views/AccountActions';
import WalletActions from '../../Views/WalletActions';
const clearStackNavigatorOptions = {
@@ -511,6 +512,10 @@ const App = ({ userLoggedIn }) => {
component={EnableAutomaticSecurityChecksModal}
/>
+
);
diff --git a/app/components/Nav/Main/RootRPCMethodsUI.js b/app/components/Nav/Main/RootRPCMethodsUI.js
index 6569949689c..779e72c21c8 100644
--- a/app/components/Nav/Main/RootRPCMethodsUI.js
+++ b/app/components/Nav/Main/RootRPCMethodsUI.js
@@ -16,8 +16,6 @@ import {
setEtherTransaction,
setTransactionObject,
} from '../../../actions/transaction';
-import PersonalSign from '../../UI/PersonalSign';
-import TypedSign from '../../UI/TypedSign';
import Modal from 'react-native-modal';
import WalletConnect from '../../../core/WalletConnect';
import {
@@ -32,7 +30,6 @@ import {
} from '../../../util/transactions';
import { BN } from 'ethereumjs-util';
import Logger from '../../../util/Logger';
-import MessageSign from '../../UI/MessageSign';
import Approve from '../../Views/ApproveView/Approve';
import WatchAssetRequest from '../../UI/WatchAssetRequest';
import AccountApproval from '../../UI/AccountApproval';
@@ -57,6 +54,7 @@ import AnalyticsV2 from '../../../util/analyticsV2';
import { useTheme } from '../../../util/theme';
import withQRHardwareAwareness from '../../UI/QRHardware/withQRHardwareAwareness';
import QRSigningModal from '../../UI/QRHardware/QRSigningModal';
+import SignatureRequestRoot from '../../UI/SignatureRequest/Root';
import { networkSwitched } from '../../../actions/onboardNetwork';
import {
selectChainId,
@@ -75,11 +73,8 @@ const styles = StyleSheet.create({
const RootRPCMethodsUI = (props) => {
const { colors } = useTheme();
const [showPendingApproval, setShowPendingApproval] = useState(false);
- const [signMessageParams, setSignMessageParams] = useState({ data: '' });
- const [signType, setSignType] = useState(false);
const [walletConnectRequestInfo, setWalletConnectRequestInfo] =
useState(undefined);
- const [showExpandedMessage, setShowExpandedMessage] = useState(false);
const [currentPageMeta, setCurrentPageMeta] = useState({});
const tokenList = useSelector(getTokenList);
@@ -123,18 +118,6 @@ const RootRPCMethodsUI = (props) => {
});
};
- const onUnapprovedMessage = (messageParams, type, origin) => {
- setCurrentPageMeta(messageParams.meta);
- const signMessageParams = { ...messageParams };
- delete signMessageParams.meta;
- setSignMessageParams(signMessageParams);
- setSignType(type);
- showPendingApprovalModal({
- type: ApprovalTypes.SIGN_MESSAGE,
- origin: signMessageParams.origin,
- });
- };
-
const initializeWalletConnect = () => {
WalletConnect.init();
};
@@ -394,64 +377,6 @@ const RootRPCMethodsUI = (props) => {
],
);
- const onSignAction = () => setShowPendingApproval(false);
-
- const toggleExpandedMessage = () =>
- setShowExpandedMessage(!showExpandedMessage);
-
- const renderSigningModal = () => (
-
- {signType === 'personal' && (
-
- )}
- {signType === 'typed' && (
-
- )}
- {signType === 'eth' && (
-
- )}
-
- );
-
const renderQRSigningModal = () => {
const {
isSigningQRObject,
@@ -781,20 +706,6 @@ const RootRPCMethodsUI = (props) => {
useEffect(() => {
initializeWalletConnect();
- Engine.context.MessageManager.hub.on('unapprovedMessage', (messageParams) =>
- onUnapprovedMessage(messageParams, 'eth'),
- );
-
- Engine.context.PersonalMessageManager.hub.on(
- 'unapprovedMessage',
- (messageParams) => onUnapprovedMessage(messageParams, 'personal'),
- );
-
- Engine.context.TypedMessageManager.hub.on(
- 'unapprovedMessage',
- (messageParams) => onUnapprovedMessage(messageParams, 'typed'),
- );
-
Engine.controllerMessenger.subscribe(
'ApprovalController:stateChange',
handlePendingApprovals,
@@ -809,8 +720,6 @@ const RootRPCMethodsUI = (props) => {
);
return function cleanup() {
- Engine.context.PersonalMessageManager.hub.removeAllListeners();
- Engine.context.TypedMessageManager.hub.removeAllListeners();
Engine.context.TokensController.hub.removeAllListeners();
Engine.controllerMessenger.unsubscribe(
'ApprovalController:stateChange',
@@ -823,7 +732,7 @@ const RootRPCMethodsUI = (props) => {
return (
- {renderSigningModal()}
+
{renderWalletConnectSessionRequestModal()}
{renderDappTransactionModal()}
{renderApproveModal()}
diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx
index 0ce6b4e6a4a..ab6e678fbe0 100644
--- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx
+++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx
@@ -43,9 +43,21 @@ const initialState = {
provider: {
ticker: 'eth',
},
+ network: '1',
},
AddressBookController: {
- addressBook: {},
+ addressBook: {
+ '1': {
+ '0x0': {
+ address: '0x0',
+ name: 'Account 1',
+ },
+ '0x1': {
+ address: '0x1',
+ name: 'Account 2',
+ },
+ },
+ },
},
},
},
@@ -109,4 +121,30 @@ describe('AccountFromToInfoCard', () => {
);
expect(await findByText('0x1...0x1')).toBeDefined();
});
+
+ it('should render correct to address for NFT send', async () => {
+ const NFTTransaction = {
+ assetType: 'ERC721',
+ selectedAsset: {
+ address: '0x26D6C3e7aEFCE970fe3BE5d589DbAbFD30026924',
+ standard: 'ERC721',
+ tokenId: '13764',
+ },
+ transaction: {
+ data: '0x23b872dd00000000000000000000000007be9763a718c0539017e2ab6fc42853b4aeeb6b000000000000000000000000f4e8263979a89dc357d7f9f79533febc7f3e287b00000000000000000000000000000000000000000000000000000000000035c4',
+ from: '0x07Be9763a718C0539017E2Ab6fC42853b4aEeb6B',
+ gas: '00',
+ to: '0x26D6C3e7aEFCE970fe3BE5d589DbAbFD30026924',
+ value: '0x0',
+ },
+ transactionFromName: 'Account 3',
+ transactionTo: '0xF4e8263979A89Dc357d7f9F79533Febc7f3e287B',
+ transactionToName: '0xF4e8263979A89Dc357d7f9F79533Febc7f3e287B',
+ };
+ const { findByText } = renderWithProvider(
+ ,
+ { state: initialState },
+ );
+ expect(await findByText('0xF4e8...287B')).toBeDefined();
+ });
});
diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx
index ea940efc092..d27e012a2af 100644
--- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx
+++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx
@@ -4,6 +4,7 @@ import { Text, TouchableOpacity, View } from 'react-native';
import { strings } from '../../../../locales/i18n';
import Engine from '../../../core/Engine';
+import TransactionTypes from '../../../core/TransactionTypes';
import {
selectNetwork,
selectTicker,
@@ -75,8 +76,8 @@ const AccountFromToInfoCard = (props: AccountFromToInfoCardProps) => {
const existingContact =
toAddress && addressBook[network] && addressBook[network][toAddress];
setIsExistingContact(existingContact !== undefined);
- if (transactionToName && transactionToName !== toAddress) {
- setToAccountName(transactionToName);
+ if (existingContact) {
+ setToAccountName(existingContact.name);
return;
}
(async () => {
@@ -124,7 +125,12 @@ const AccountFromToInfoCard = (props: AccountFromToInfoCardProps) => {
let fromAccBalance;
let toAddr;
if (selectedAsset.isETH || selectedAsset.tokenId) {
- toAddr = to;
+ if (
+ selectedAsset.standard !== TransactionTypes.ASSET.ERC721 &&
+ selectedAsset.standard !== TransactionTypes.ASSET.ERC1155
+ ) {
+ toAddr = to;
+ }
if (!fromAddress) {
return;
}
diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx
index dffbc9d5bc8..0fc1ded4925 100644
--- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx
+++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx
@@ -19,6 +19,7 @@ interface SelectedAsset {
decimals: number;
image?: string;
name?: string;
+ standard?: string;
}
export interface Transaction {
@@ -32,7 +33,7 @@ export interface Transaction {
export interface AccountFromToInfoCardProps {
accounts: Accounts;
- addressBook: Record>;
+ addressBook: Record>;
contractBalances: Record;
identities: Identities;
network: string;
diff --git a/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap b/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap
index e985eea7c4e..3ca82b3423f 100644
--- a/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap
+++ b/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap
@@ -846,7 +846,20 @@ exports[`AccountFromToInfoCard should render correctly 1`] = `
},
}
}
- addressBook={Object {}}
+ addressBook={
+ Object {
+ "1": Object {
+ "0x0": Object {
+ "address": "0x0",
+ "name": "Account 1",
+ },
+ "0x1": Object {
+ "address": "0x1",
+ "name": "Account 2",
+ },
+ },
+ }
+ }
dispatch={[Function]}
identities={
Object {
@@ -860,6 +873,7 @@ exports[`AccountFromToInfoCard should render correctly 1`] = `
},
}
}
+ network="1"
transactionState={
Object {
"selectedAsset": Object {
diff --git a/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx b/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx
index 93636bc7a6e..6a49a513c2d 100644
--- a/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx
+++ b/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx
@@ -25,11 +25,7 @@ import {
ORIGIN_DEEPLINK,
ORIGIN_QR_CODE,
} from './ApproveTransactionHeader.constants';
-import {
- AccountInfoI,
- ApproveTransactionHeaderI,
- OriginsI,
-} from './ApproveTransactionHeader.types';
+import { ApproveTransactionHeaderI } from './ApproveTransactionHeader.types';
import { selectProviderConfig } from '../../../selectors/networkController';
import AppConstants from '../../../../app/core/AppConstants';
@@ -39,16 +35,13 @@ const ApproveTransactionHeader = ({
url,
currentEnsName,
}: ApproveTransactionHeaderI) => {
- const [accountInfo, setAccountInfo] = useState({
- balance: 0,
- currency: '',
- accountName: '',
- });
- const [origins, setOrigins] = useState({
- isOriginDeepLink: false,
- isOriginWalletConnect: false,
- isOriginMMSDKRemoteConn: false,
- });
+ const [accountBalance, setAccountBalance] = useState(0);
+ const [accountCurrency, setAccountCurrency] = useState('');
+ const [accountName, setAccountName] = useState('');
+
+ const [isOriginDeepLink, setIsOriginDeepLink] = useState(false);
+ const [isOriginWalletConnect, setIsOriginWalletConnect] = useState(false);
+ const [isOriginMMSDKRemoteConn, setIsOriginMMSDKRemoteConn] = useState(false);
const { styles } = useStyles(stylesheet, {});
@@ -84,29 +77,25 @@ const ApproveTransactionHeader = ({
const balance = Number(renderFromWei(weiBalance));
const currency = getTicker(ticker);
- const accountName = activeAddress
+ const accountNameVal = activeAddress
? renderAccountName(activeAddress, identities)
: '';
- const isOriginDeepLink =
+ const isOriginDeepLinkVal =
origin === ORIGIN_DEEPLINK || origin === ORIGIN_QR_CODE;
- const isOriginWalletConnect = origin?.startsWith(WALLET_CONNECT_ORIGIN);
+ const isOriginWalletConnectVal = origin?.startsWith(WALLET_CONNECT_ORIGIN);
- const isOriginMMSDKRemoteConn = origin?.startsWith(
+ const isOriginMMSDKRemoteConnVal = origin?.startsWith(
AppConstants.MM_SDK.SDK_REMOTE_ORIGIN,
);
- setAccountInfo({
- balance,
- currency,
- accountName,
- });
- setOrigins({
- isOriginDeepLink,
- isOriginWalletConnect,
- isOriginMMSDKRemoteConn,
- });
- }, [accounts, identities, origin, activeAddress, network]);
+ setAccountBalance(balance);
+ setAccountCurrency(currency);
+ setAccountName(accountNameVal);
+ setIsOriginDeepLink(isOriginDeepLinkVal);
+ setIsOriginWalletConnect(isOriginWalletConnectVal);
+ setIsOriginMMSDKRemoteConn(isOriginMMSDKRemoteConnVal);
+ }, [accounts, identities, activeAddress, network, origin]);
const networkImage = getNetworkImageSource({
networkType: networkProvider.type,
@@ -114,8 +103,6 @@ const ApproveTransactionHeader = ({
});
const domainTitle = useMemo(() => {
- const { isOriginDeepLink, isOriginWalletConnect, isOriginMMSDKRemoteConn } =
- origins;
let title = '';
if (isOriginDeepLink) {
title = renderShortAddress(from);
@@ -130,10 +117,17 @@ const ApproveTransactionHeader = ({
}
return title;
- }, [currentEnsName, origin, origins, from, url]);
+ }, [
+ currentEnsName,
+ origin,
+ isOriginDeepLink,
+ isOriginWalletConnect,
+ isOriginMMSDKRemoteConn,
+ from,
+ url,
+ ]);
const favIconUrl = useMemo(() => {
- const { isOriginWalletConnect, isOriginMMSDKRemoteConn } = origins;
let newUrl = url;
if (isOriginWalletConnect) {
newUrl = origin.split(WALLET_CONNECT_ORIGIN)[1];
@@ -141,7 +135,7 @@ const ApproveTransactionHeader = ({
newUrl = origin.split(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN)[1];
}
return FAV_ICON_URL(getHost(newUrl));
- }, [origin, origins, url]);
+ }, [origin, isOriginWalletConnect, isOriginMMSDKRemoteConn, url]);
return (
@@ -152,9 +146,9 @@ const ApproveTransactionHeader = ({
/>
navigation.pop()}
style={styles.backButton}
+ {...generateTestId(Platform, BACK_BUTTON_SIMPLE_WEBVIEW)}
>
{
+ const KeyboardAwareScrollView = jest.requireActual('react-native').ScrollView;
+ return { KeyboardAwareScrollView };
+});
+
+jest.mock('../../../../core/Engine', () => ({
+ init: () => ({}),
+ context: {
+ KeyringController: {
+ getAccountKeyringType: jest.fn(() => Promise.resolve({ data: {} })),
+ getQRKeyringState: jest.fn(() => Promise.resolve({ data: {} })),
+ },
+ MessageManager: {
+ hub: { on: () => undefined },
+ },
+ PersonalMessageManager: {
+ hub: {
+ on: (_: any, fn: any) =>
+ fn(
+ JSON.parse(
+ '{"data":"0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765","from":"0x935e73edb9ff52e23bac7f7e043a1ecd06d05477","meta":{"url":"https://metamask.github.io/test-dapp/","title":"E2E Test Dapp","icon":"https://api.faviconkit.com/metamask.github.io/50","analytics":{"request_source":"In-App-Browser"}},"origin":"metamask.github.io","metamaskId":"85b76fd0-d1e9-11ed-a2fd-8ff017956a45"}',
+ ),
+ ),
+ },
+ },
+ TypedMessageManager: {
+ hub: { on: () => undefined },
+ },
+ },
+}));
+
+jest.mock('@react-navigation/native', () => ({
+ useNavigation: () => ({
+ navigation: {},
+ }),
+ createNavigatorFactory: () => ({}),
+}));
+
+const mockStore = configureMockStore();
+const initialState = {
+ settings: {},
+ engine: {
+ backgroundState: {
+ AccountTrackerController: {
+ accounts: {
+ '0x0': {
+ balance: 200,
+ },
+ '0x1': {
+ balance: 200,
+ },
+ },
+ },
+ PreferencesController: {
+ selectedAddress: '0x0',
+ identities: {
+ '0x0': {
+ address: '0x0',
+ name: 'Account 1',
+ },
+ '0x1': {
+ address: '0x1',
+ name: 'Account 2',
+ },
+ },
+ },
+ CurrencyRateController: {
+ conversionRate: 10,
+ currentCurrency: 'usd',
+ },
+ },
+ },
+};
+const store = mockStore(initialState);
+
+describe('Root', () => {
+ it('should render correctly', () => {
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should match snapshot', async () => {
+ const container = render(
+
+
+
+
+ ,
+ );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/SignatureRequest/Root/Root.tsx b/app/components/UI/SignatureRequest/Root/Root.tsx
new file mode 100644
index 00000000000..4b2cbcb7e76
--- /dev/null
+++ b/app/components/UI/SignatureRequest/Root/Root.tsx
@@ -0,0 +1,149 @@
+import Modal from 'react-native-modal';
+import React, { useEffect, useState } from 'react';
+import { InteractionManager, StyleSheet } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import Engine from '../../../../core/Engine';
+import { ApprovalTypes } from '../../../../core/RPCMethods/RPCMethodMiddleware';
+import { useTheme } from '../../../../util/theme';
+
+import MessageSign from '../../../UI/MessageSign';
+import PersonalSign from '../../../UI/PersonalSign';
+import TypedSign from '../../../UI/TypedSign';
+
+import { MessageInfo, MessageParams, PageMeta } from '../types';
+
+enum MessageType {
+ ETH = 'eth',
+ Personal = 'personal',
+ Typed = 'typed',
+}
+
+const styles = StyleSheet.create({
+ bottomModal: {
+ justifyContent: 'flex-end',
+ margin: 0,
+ },
+});
+
+const Root = () => {
+ const navigation = useNavigation();
+ const { colors } = useTheme();
+
+ const [currentPageMeta, setCurrentPageMeta] = useState();
+ const [pendingApproval, setPendingApproval] = useState();
+ const [showExpandedMessage, setShowExpandedMessage] = useState(false);
+ const [signMessageParams, setSignMessageParams] = useState();
+ const [signType, setSignType] = useState();
+
+ const showPendingApprovalModal = ({ type, origin }: MessageInfo) => {
+ InteractionManager.runAfterInteractions(() => {
+ setPendingApproval({ type, origin });
+ });
+ };
+
+ const onSignAction = () => setPendingApproval(undefined);
+
+ const toggleExpandedMessage = () =>
+ setShowExpandedMessage(!showExpandedMessage);
+
+ const onUnapprovedMessage = (messageParams: MessageParams, type: string) => {
+ setCurrentPageMeta(messageParams.meta);
+ const signMsgParams = { ...messageParams };
+ delete signMsgParams.meta;
+ setSignMessageParams(signMsgParams);
+ setSignType(type);
+ showPendingApprovalModal({
+ type: ApprovalTypes.SIGN_MESSAGE,
+ origin: signMsgParams.origin,
+ });
+ };
+
+ useEffect(() => {
+ Engine.context.MessageManager.hub.on(
+ 'unapprovedMessage',
+ (messageParams: MessageParams) => {
+ onUnapprovedMessage(messageParams, MessageType.ETH);
+ },
+ );
+
+ Engine.context.PersonalMessageManager.hub.on(
+ 'unapprovedMessage',
+ (messageParams: MessageParams) => {
+ onUnapprovedMessage(messageParams, MessageType.Personal);
+ },
+ );
+
+ Engine.context.TypedMessageManager.hub.on(
+ 'unapprovedMessage',
+ (messageParams: MessageParams) => {
+ onUnapprovedMessage(messageParams, MessageType.Typed);
+ },
+ );
+
+ return function cleanup() {
+ Engine.context.PersonalMessageManager.hub.removeAllListeners();
+ Engine.context.TypedMessageManager.hub.removeAllListeners();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ if (!signMessageParams || !currentPageMeta) {
+ return null;
+ }
+
+ return (
+
+ {signType === MessageType.Personal && (
+
+ )}
+ {signType === MessageType.Typed && (
+
+ )}
+ {signType === MessageType.ETH && (
+
+ )}
+
+ );
+};
+
+export default Root;
diff --git a/app/components/UI/SignatureRequest/Root/__snapshots__/Root.test.tsx.snap b/app/components/UI/SignatureRequest/Root/__snapshots__/Root.test.tsx.snap
new file mode 100644
index 00000000000..db536266c47
--- /dev/null
+++ b/app/components/UI/SignatureRequest/Root/__snapshots__/Root.test.tsx.snap
@@ -0,0 +1,785 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Root should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ metamask.github.io
+
+
+
+
+
+
+
+
+
+ Sign this message?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0x935E...5477
+
+
+ (
+ 0x935E...5477
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Message
+ :
+
+
+
+
+ Example \`personal_sign\` message
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ Sign
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Root should render correctly 1`] = `""`;
diff --git a/app/components/UI/SignatureRequest/Root/index.ts b/app/components/UI/SignatureRequest/Root/index.ts
new file mode 100644
index 00000000000..7ee9fa90d7a
--- /dev/null
+++ b/app/components/UI/SignatureRequest/Root/index.ts
@@ -0,0 +1 @@
+export { default } from './Root';
diff --git a/app/components/UI/SignatureRequest/types.ts b/app/components/UI/SignatureRequest/types.ts
new file mode 100644
index 00000000000..a2d868ad1e5
--- /dev/null
+++ b/app/components/UI/SignatureRequest/types.ts
@@ -0,0 +1,22 @@
+export interface MessageInfo {
+ origin: string;
+ type: string;
+}
+
+export interface PageMeta {
+ analytics?: {
+ request_platform: string;
+ request_source: string;
+ };
+ icon?: string;
+ title: string;
+ url: string;
+}
+
+export interface MessageParams {
+ data: string;
+ from: string;
+ metamaskId: string;
+ meta?: PageMeta;
+ origin: string;
+}
diff --git a/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap b/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap
index 23376380944..195112ecf47 100644
--- a/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap
@@ -1,37 +1,3134 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`TransactionReview should render correctly 1`] = `
-
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sepolia
+
+
+ 0x0...0x0
+
+
+
+
+
+ Balance
+
+
+ NaN
+
+ ETH
+
+
+
+
+
+
+
+
+ Confirm
+
+
+
+
+
+
+
+
+
+ From:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account 1
+
+
+
+ Balance: < 0.00001 ETH
+
+
+
+
+
+
+
+ To:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0x1...0x1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Check the recipient address
+
+
+
+
+
+
+
+
+
+ We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Estimated gas fee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This gas fee suggestion is using legacy gas estimation which may be inaccurate.
+
+
+
+
+
+
+
+
+
+
+
+ What are gas fees?
+
+
+
+
+
+
+
+
+
+
+ Gas fees are paid to crypto miners who process transactions on the
+ network.
+
+
+ MetaMask does not profit from gas fees.
+
+
+
+ Gas fees are set by the network and fluctuate based on network traffic and transaction complexity.
+
+
+
+ Learn more about gas fees
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View Data
+
+
+
+
+
+
+
+
+
+
+
+ Reject
+
+
+
+
+ Confirm
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+
+
+ Data
+
+
+
+
+
+
+ Data associated with this transaction
+
+
+
+ Hex data
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+]
+`;
+
+exports[`TransactionReview should render correctly 1`] = `
+
+>
+
+
`;
diff --git a/app/components/UI/TransactionReview/index.js b/app/components/UI/TransactionReview/index.js
index fdd4bb84502..e78bf62bdf1 100644
--- a/app/components/UI/TransactionReview/index.js
+++ b/app/components/UI/TransactionReview/index.js
@@ -31,6 +31,7 @@ import {
renderFromWei,
fromTokenMinimalUnit,
isZeroValue,
+ hexToBN,
} from '../../../util/number';
import { safeToChecksumAddress } from '../../../util/address';
import Device from '../../../util/device';
@@ -109,6 +110,10 @@ const createStyles = (colors) =>
*/
class TransactionReview extends PureComponent {
static propTypes = {
+ /**
+ * Balance of all the accounts
+ */
+ accounts: PropTypes.object,
/**
* Callback triggered when this transaction is cancelled
*/
@@ -247,6 +252,7 @@ class TransactionReview extends PureComponent {
conversionRate: undefined,
fiatValue: undefined,
multiLayerL1FeeTotal: '0x0',
+ senderBalanceIsZero: true,
};
fetchEstimatedL1Fee = async () => {
@@ -273,9 +279,10 @@ class TransactionReview extends PureComponent {
componentDidMount = async () => {
const {
+ accounts,
validate,
transaction,
- transaction: { data, to, value },
+ transaction: { data, to, value, from },
tokens,
chainId,
tokenList,
@@ -302,6 +309,8 @@ class TransactionReview extends PureComponent {
} else {
[assetAmount, conversionRate, fiatValue] = this.getRenderValues()();
}
+ const senderBalance = accounts[safeToChecksumAddress(from)]?.balance;
+ const senderBalanceIsZero = hexToBN(senderBalance).isZero();
this.setState({
error,
actionKey,
@@ -310,6 +319,7 @@ class TransactionReview extends PureComponent {
conversionRate,
fiatValue,
approveTransaction,
+ senderBalanceIsZero,
});
InteractionManager.runAfterInteractions(() => {
Analytics.trackEvent(MetaMetricsEvents.TRANSACTIONS_CONFIRM_STARTED);
@@ -381,7 +391,7 @@ class TransactionReview extends PureComponent {
};
getStyles = () => {
- const colors = this.context.colors || mockTheme.colors;
+ const colors = this.context?.colors || mockTheme.colors;
return createStyles(colors);
};
@@ -459,6 +469,7 @@ class TransactionReview extends PureComponent {
fiatValue,
approveTransaction,
multiLayerL1FeeTotal,
+ senderBalanceIsZero,
} = this.state;
const url = this.getUrlFromBrowser();
const styles = this.getStyles();
@@ -504,7 +515,10 @@ class TransactionReview extends PureComponent {
onConfirmPress={this.props.onConfirm}
confirmed={transactionConfirmed}
confirmDisabled={
- transactionConfirmed || Boolean(error) || isAnimating
+ senderBalanceIsZero ||
+ transactionConfirmed ||
+ Boolean(error) ||
+ isAnimating
}
>
diff --git a/app/components/UI/TransactionReview/index.test.tsx b/app/components/UI/TransactionReview/index.test.tsx
index 3159f5e949f..6254f4d46c5 100644
--- a/app/components/UI/TransactionReview/index.test.tsx
+++ b/app/components/UI/TransactionReview/index.test.tsx
@@ -3,17 +3,35 @@ import TransactionReview from './';
import configureMockStore from 'redux-mock-store';
import { shallow } from 'enzyme';
import { Provider } from 'react-redux';
+// eslint-disable-next-line import/no-namespace
+import * as TransactionUtils from '../../../util/transactions';
+import renderWithProvider from '../../../util/test/renderWithProvider';
+import Engine from '../../../core/Engine';
-const generateTransform = jest.fn();
-const mockStore = configureMockStore();
-const initialState = {
+jest.mock('react-native-keyboard-aware-scroll-view', () => {
+ const KeyboardAwareScrollView = jest.requireActual('react-native').ScrollView;
+ return { KeyboardAwareScrollView };
+});
+
+jest.mock('../QRHardware/withQRHardwareAwareness', () => (obj: any) => obj);
+
+jest.mock('@react-navigation/compat', () => {
+ const actualNav = jest.requireActual('@react-navigation/compat');
+ return {
+ actualNav,
+ withNavigation: (obj: any) => obj,
+ };
+});
+
+const mockState = {
engine: {
backgroundState: {
- PreferencesController: {
- selectedAddress: '0x2',
- },
AccountTrackerController: {
- accounts: [],
+ accounts: {
+ '0x0': {
+ balance: 0x2,
+ },
+ },
},
TokensController: {
tokens: [],
@@ -21,6 +39,19 @@ const initialState = {
TokenListController: {
tokenList: {},
},
+ PreferencesController: {
+ selectedAddress: '0x2',
+ },
+ NetworkController: {
+ providerConfig: {
+ chainId: '0xaa36a7',
+ type: 'sepolia',
+ nickname: 'Sepolia',
+ },
+ provider: {
+ ticker: 'eth',
+ },
+ },
CurrencyRateController: {
currentCurrency: 'usd',
},
@@ -29,10 +60,9 @@ const initialState = {
'0x': '0.1',
},
},
- NetworkController: {
- providerConfig: {
- ticker: 'ETH',
- },
+ TokenBalancesController: {},
+ AddressBookController: {
+ addressBook: {},
},
},
},
@@ -41,28 +71,100 @@ const initialState = {
primaryCurrency: 'ETH',
},
transaction: {
- value: '',
- data: '',
- from: '0x1',
- gas: '',
- gasPrice: '',
- to: '0x2',
- selectedAsset: undefined,
- assetType: undefined,
+ transaction: { from: '0x0', to: '0x1' },
+ transactionTo: '0x1',
+ selectedAsset: { isETH: true, address: '0x0', symbol: 'ETH', decimals: 8 },
+ transactionToName: 'Account 2',
+ transactionFromName: 'Account 1',
},
- browser: {
- tabs: [],
+ fiatOrders: {
+ networks: [
+ {
+ chainId: '0xaa36a7',
+ type: 'sepolia',
+ nickname: 'Sepolia',
+ },
+ ],
},
+ alert: { isVisible: false },
};
-const store = mockStore(initialState);
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: (fn: any) => fn(mockState),
+}));
+
+Engine.init({});
+const generateTransform = jest.fn();
describe('TransactionReview', () => {
it('should render correctly', () => {
+ const mockStore = configureMockStore();
+ const store = mockStore(mockState);
const wrapper = shallow(
-
+
,
);
- expect(wrapper.dive()).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should match snapshot', () => {
+ const container = renderWithProvider(
+ ,
+ { state: mockState },
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should have enabled confirm button if from account has balance', async () => {
+ jest
+ .spyOn(TransactionUtils, 'getTransactionReviewActionKey')
+ .mockReturnValue(Promise.resolve(undefined));
+ const { queryByRole } = renderWithProvider(
+ ,
+ { state: mockState },
+ );
+ const confirmButton = await queryByRole('button', { name: 'Confirm' });
+ expect(confirmButton.props.disabled).not.toBe(true);
+ });
+
+ it('should have confirm button disabled if from account has no balance', () => {
+ const mockNewState = {
+ ...mockState,
+ engine: {
+ ...mockState.engine,
+ backgroundState: {
+ ...mockState.engine.backgroundState,
+ AccountTrackerController: {
+ ...mockState.engine.backgroundState.AccountTrackerController,
+ accounts: {
+ '0x0': {
+ balance: 0x0,
+ },
+ },
+ },
+ },
+ },
+ };
+ jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: (fn: any) => fn(mockNewState),
+ }));
+ const { getByRole } = renderWithProvider(
+ ,
+ { state: mockState },
+ );
+ const confirmButton = getByRole('button', { name: 'Confirm' });
+ expect(confirmButton.props.disabled).toBe(true);
});
});
diff --git a/app/components/UI/WalletAccount/WalletAccount.tsx b/app/components/UI/WalletAccount/WalletAccount.tsx
index e38ba126155..84cfa88d843 100644
--- a/app/components/UI/WalletAccount/WalletAccount.tsx
+++ b/app/components/UI/WalletAccount/WalletAccount.tsx
@@ -11,10 +11,7 @@ import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { Platform, View } from 'react-native';
// External dependencies
-import Icon, {
- IconName,
- IconSize,
-} from '../../../component-library/components/Icons/Icon';
+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';
import { createAccountSelectorNavDetails } from '../../../components/Views/AccountSelector';
@@ -26,11 +23,17 @@ import {
isDefaultAccountName,
} from '../../../util/ENSUtils';
import { selectChainId } from '../../../selectors/networkController';
+import ButtonIcon from '../../../component-library/components/Buttons/ButtonIcon/ButtonIcon';
+import { ButtonIconSizes } from '../../../component-library/components/Buttons/ButtonIcon';
+import Routes from '../../../constants/navigation/Routes';
// Internal dependencies
import styleSheet from './WalletAccount.styles';
import { WalletAccountProps } from './WalletAccount.types';
-import { WALLET_ACCOUNT_ICON } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
+import {
+ WALLET_ACCOUNT_ICON,
+ MAIN_WALLET_ACCOUNT_ACTIONS,
+} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref) => {
const { styles } = useStyles(styleSheet, { style });
@@ -86,6 +89,12 @@ const WalletAccount = ({ style }: WalletAccountProps, ref: React.Ref) => {
lookupEns();
}, [lookupEns]);
+ const onNavigateToAccountActions = () => {
+ navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ACCOUNT_ACTIONS,
+ });
+ };
+
return (
) => {
-
-
+
);
diff --git a/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap b/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap
index 4f7b8dcab7d..282f0fed2e2 100644
--- a/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap
+++ b/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap
@@ -287,18 +287,34 @@ exports[`WalletAccount renders correctly 1`] = `
-
+ testID="main-wallet-account-actions"
+ >
+
+
`;
diff --git a/app/components/Views/AccountAction/AccountAction.styles.ts b/app/components/Views/AccountAction/AccountAction.styles.ts
new file mode 100644
index 00000000000..b86cd5253a1
--- /dev/null
+++ b/app/components/Views/AccountAction/AccountAction.styles.ts
@@ -0,0 +1,45 @@
+// Third party dependencies.
+import { StyleSheet, ViewStyle } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../util/theme/models';
+
+// Internal dependencies.
+import { TouchableOpacityStyleSheetVars } from './AccountAction.types';
+
+/**
+ * Style sheet function for AccountAction component.
+ *
+ * @param params Style sheet params.
+ * @param params.theme App theme from ThemeContext.
+ * @param params.vars Inputs that the style sheet depends on.
+ * @returns StyleSheet object.
+ */
+const styleSheet = (params: {
+ theme: Theme;
+ vars: TouchableOpacityStyleSheetVars;
+}) => {
+ const { theme, vars } = params;
+ const { style } = vars;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ base: Object.assign(
+ {
+ width: '100%',
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 16,
+ },
+ style,
+ ) as ViewStyle,
+ descriptionLabel: {
+ color: colors.text.alternative,
+ },
+ icon: {
+ marginHorizontal: 16,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/components/Views/AccountAction/AccountAction.tsx b/app/components/Views/AccountAction/AccountAction.tsx
new file mode 100644
index 00000000000..86125de7638
--- /dev/null
+++ b/app/components/Views/AccountAction/AccountAction.tsx
@@ -0,0 +1,33 @@
+// Third party dependencies.
+import React from 'react';
+import { TouchableOpacity } from 'react-native';
+// External dependencies.
+import Text, {
+ TextVariant,
+} from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+// Internal dependencies.
+import { WalletActionProps } from './AccountAction.types';
+import styleSheet from './AccountAction.styles';
+import Icon, {
+ IconSize,
+} from '../../../component-library/components/Icons/Icon';
+
+const AccountAction = ({
+ actionTitle,
+ iconName,
+ iconSize = IconSize.Md,
+ style,
+ ...props
+}: WalletActionProps) => {
+ const { styles } = useStyles(styleSheet, { style });
+ return (
+
+
+
+ {actionTitle}
+
+ );
+};
+
+export default AccountAction;
diff --git a/app/components/Views/AccountAction/AccountAction.types.ts b/app/components/Views/AccountAction/AccountAction.types.ts
new file mode 100644
index 00000000000..2878d5e92de
--- /dev/null
+++ b/app/components/Views/AccountAction/AccountAction.types.ts
@@ -0,0 +1,19 @@
+import { TouchableOpacityProps } from 'react-native';
+import {
+ IconName,
+ IconSize,
+} from '../../../component-library/components/Icons/Icon';
+
+export interface WalletActionProps extends TouchableOpacityProps {
+ actionTitle: string;
+ iconName: IconName;
+ iconSize?: IconSize;
+}
+
+/**
+ * Style sheet input parameters.
+ */
+export type TouchableOpacityStyleSheetVars = Pick<
+ TouchableOpacityProps,
+ 'style'
+>;
diff --git a/app/components/Views/AccountAction/index.tsx b/app/components/Views/AccountAction/index.tsx
new file mode 100644
index 00000000000..671895ee22b
--- /dev/null
+++ b/app/components/Views/AccountAction/index.tsx
@@ -0,0 +1 @@
+export { default } from './AccountAction';
diff --git a/app/components/Views/AccountActions/AccountActions.constants.ts b/app/components/Views/AccountActions/AccountActions.constants.ts
new file mode 100644
index 00000000000..1041c9a6af1
--- /dev/null
+++ b/app/components/Views/AccountActions/AccountActions.constants.ts
@@ -0,0 +1,5 @@
+// Test IDs
+export const EDIT_ACCOUNT = 'edit-account-action';
+export const VIEW_ETHERSCAN = 'view-etherscan-action';
+export const SHARE_ADDRESS = 'share-address-action';
+export const SHOW_PRIVATE_KEY = 'show-private-key-action';
diff --git a/app/components/Views/AccountActions/AccountActions.styles.ts b/app/components/Views/AccountActions/AccountActions.styles.ts
new file mode 100644
index 00000000000..153fbd3857f
--- /dev/null
+++ b/app/components/Views/AccountActions/AccountActions.styles.ts
@@ -0,0 +1,18 @@
+// Third party dependencies.
+import { StyleSheet } from 'react-native';
+
+/**
+ * Style sheet function for AccountActions component.
+ *
+ * @returns StyleSheet object.
+ */
+const styleSheet = () =>
+ StyleSheet.create({
+ actionsContainer: {
+ alignItems: 'flex-start',
+ justifyContent: 'center',
+ paddingVertical: 16,
+ },
+ });
+
+export default styleSheet;
diff --git a/app/components/Views/AccountActions/AccountActions.test.tsx b/app/components/Views/AccountActions/AccountActions.test.tsx
new file mode 100644
index 00000000000..d6203fda9c5
--- /dev/null
+++ b/app/components/Views/AccountActions/AccountActions.test.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import Share from 'react-native-share';
+
+import { fireEvent } from '@testing-library/react-native';
+
+import renderWithProvider from '../../../util/test/renderWithProvider';
+
+import Engine from '../../../core/Engine';
+import Routes from '../../../constants/navigation/Routes';
+import AccountActions from './AccountActions';
+import {
+ EDIT_ACCOUNT,
+ SHARE_ADDRESS,
+ SHOW_PRIVATE_KEY,
+ VIEW_ETHERSCAN,
+} from './AccountActions.constants';
+
+const mockEngine = Engine;
+
+const initialState = {
+ swaps: { '1': { isLive: true }, hasOnboarded: false, isLive: true },
+ engine: {
+ backgroundState: {
+ PreferencesController: {
+ selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1',
+ identities: {
+ '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' },
+ },
+ frequentRpcList: [],
+ },
+ NetworkController: {
+ providerConfig: { type: 'mainnet', chainId: '1', ticker: 'ETH' },
+ },
+ },
+ },
+};
+
+jest.unmock('react-redux');
+jest.mock('../../../core/Engine', () => ({
+ init: () => mockEngine.init({}),
+}));
+
+const mockNavigate = jest.fn();
+const mockGoBack = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ goBack: mockGoBack,
+ }),
+ };
+});
+
+jest.mock('react-native-safe-area-context', () => ({
+ useSafeAreaInsets: () => ({ top: 0, left: 0, right: 0, bottom: 0 }),
+}));
+
+jest.mock('react-native-share', () => ({
+ open: jest.fn(() => Promise.resolve()),
+}));
+
+describe('AccountActions', () => {
+ afterEach(() => {
+ mockNavigate.mockClear();
+ });
+ it('renders all actions', () => {
+ const { getByTestId } = renderWithProvider(, {
+ state: initialState,
+ });
+
+ expect(getByTestId(EDIT_ACCOUNT)).toBeDefined();
+ expect(getByTestId(VIEW_ETHERSCAN)).toBeDefined();
+ expect(getByTestId(SHARE_ADDRESS)).toBeDefined();
+ expect(getByTestId(SHOW_PRIVATE_KEY)).toBeDefined();
+ });
+
+ it('navigates to webview when View on Etherscan is clicked', () => {
+ const { getByTestId } = renderWithProvider(, {
+ state: initialState,
+ });
+
+ fireEvent.press(getByTestId(VIEW_ETHERSCAN));
+
+ expect(mockNavigate).toHaveBeenCalledWith('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url: 'https://etherscan.io/address/0xe7E125654064EEa56229f273dA586F10DF96B0a1',
+ title: 'etherscan.io',
+ },
+ });
+ });
+
+ it('opens the Share sheet when Share my public address is clicked', () => {
+ const { getByTestId } = renderWithProvider(, {
+ state: initialState,
+ });
+
+ fireEvent.press(getByTestId(SHARE_ADDRESS));
+
+ expect(Share.open).toHaveBeenCalledWith({
+ message: '0xe7E125654064EEa56229f273dA586F10DF96B0a1',
+ });
+ });
+
+ it('navigates to the export private key screen when Show private key is clicked', () => {
+ const { getByTestId } = renderWithProvider(, {
+ state: initialState,
+ });
+
+ fireEvent.press(getByTestId(SHOW_PRIVATE_KEY));
+
+ expect(mockNavigate).toHaveBeenCalledWith(
+ Routes.SETTINGS.REVEAL_PRIVATE_CREDENTIAL,
+ {
+ credentialName: 'private_key',
+ shouldUpdateNav: true,
+ },
+ );
+ });
+});
diff --git a/app/components/Views/AccountActions/AccountActions.tsx b/app/components/Views/AccountActions/AccountActions.tsx
new file mode 100644
index 00000000000..70c7268933a
--- /dev/null
+++ b/app/components/Views/AccountActions/AccountActions.tsx
@@ -0,0 +1,157 @@
+// Third party dependencies.
+import React, { useRef } from 'react';
+import { Platform, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { useDispatch, useSelector } from 'react-redux';
+import Share from 'react-native-share';
+
+// External dependencies.
+import SheetBottom, {
+ SheetBottomRef,
+} from '../../../component-library/components/Sheet/SheetBottom';
+import { useStyles } from '../../../component-library/hooks';
+import AccountAction from '../AccountAction/AccountAction';
+import { IconName } from '../../../component-library/components/Icons/Icon';
+import { findBlockExplorerForRpc } from '../../../util/networks';
+import {
+ getEtherscanAddressUrl,
+ getEtherscanBaseUrl,
+} from '../../../util/etherscan';
+import { Analytics, MetaMetricsEvents } from '../../../core/Analytics';
+import { RPC } from '../../../constants/network';
+import { selectProviderConfig } from '../../../selectors/networkController';
+import { strings } from '../../../../locales/i18n';
+
+// Internal dependencies
+import styleSheet from './AccountActions.styles';
+import Logger from '../../../util/Logger';
+import { protectWalletModalVisible } from '../../../actions/user';
+import AnalyticsV2 from '../../../util/analyticsV2';
+import Routes from '../../../constants/navigation/Routes';
+import generateTestId from '../../../../wdio/utils/generateTestId';
+import {
+ EDIT_ACCOUNT,
+ SHARE_ADDRESS,
+ SHOW_PRIVATE_KEY,
+ VIEW_ETHERSCAN,
+} from './AccountActions.constants';
+
+const AccountActions = () => {
+ const { styles } = useStyles(styleSheet, {});
+ const sheetRef = useRef(null);
+ const { navigate } = useNavigation();
+ const dispatch = useDispatch();
+
+ const providerConfig = useSelector(selectProviderConfig);
+
+ const selectedAddress = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.PreferencesController.selectedAddress,
+ );
+ const frequentRpcList = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.PreferencesController.frequentRpcList,
+ );
+
+ const goToBrowserUrl = (url: string, title: string) => {
+ navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url,
+ title,
+ },
+ });
+ };
+
+ const viewInEtherscan = () => {
+ sheetRef.current?.hide(() => {
+ if (providerConfig?.rpcTarget && providerConfig.type === RPC) {
+ const blockExplorer = findBlockExplorerForRpc(
+ providerConfig.rpcTarget,
+ frequentRpcList,
+ );
+ const url = `${blockExplorer}/address/${selectedAddress}`;
+ const title = new URL(blockExplorer).hostname;
+ goToBrowserUrl(url, title);
+ } else {
+ const url = getEtherscanAddressUrl(
+ providerConfig.type,
+ selectedAddress,
+ );
+ const etherscan_url = getEtherscanBaseUrl(providerConfig.type).replace(
+ 'https://',
+ '',
+ );
+ goToBrowserUrl(url, etherscan_url);
+ }
+
+ Analytics.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_VIEW_ETHERSCAN);
+ });
+ };
+
+ const onShare = () => {
+ sheetRef.current?.hide(() => {
+ Share.open({
+ message: selectedAddress,
+ })
+ .then(() => {
+ dispatch(protectWalletModalVisible());
+ })
+ .catch((err) => {
+ Logger.log('Error while trying to share address', err);
+ });
+
+ Analytics.trackEvent(
+ MetaMetricsEvents.NAVIGATION_TAPS_SHARE_PUBLIC_ADDRESS,
+ );
+ });
+ };
+
+ const goToExportPrivateKey = () => {
+ sheetRef.current?.hide(() => {
+ AnalyticsV2.trackEvent(
+ MetaMetricsEvents.REVEAL_PRIVATE_KEY_INITIATED,
+ {},
+ );
+
+ navigate(Routes.SETTINGS.REVEAL_PRIVATE_CREDENTIAL, {
+ credentialName: 'private_key',
+ shouldUpdateNav: true,
+ });
+ });
+ };
+
+ return (
+
+
+ null}
+ {...generateTestId(Platform, EDIT_ACCOUNT)}
+ />
+
+
+
+
+
+ );
+};
+
+export default AccountActions;
diff --git a/app/components/Views/AccountActions/index.tsx b/app/components/Views/AccountActions/index.tsx
new file mode 100644
index 00000000000..b3b9af7422a
--- /dev/null
+++ b/app/components/Views/AccountActions/index.tsx
@@ -0,0 +1 @@
+export { default } from './AccountActions';
diff --git a/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx b/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx
index 756b6d223c5..9ab19156fcc 100644
--- a/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx
+++ b/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx
@@ -1,15 +1,15 @@
/* eslint-disable no-mixed-spaces-and-tabs */
-import React, { useState, useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import {
Dimensions,
- View,
+ Linking,
+ Platform,
Text,
TextInput,
TouchableOpacity,
- Linking,
- Platform,
+ View,
} from 'react-native';
-import { useSelector, useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import AsyncStorage from '@react-native-async-storage/async-storage';
import QRCode from 'react-native-qrcode-svg';
import ScrollableTabView, {
@@ -19,8 +19,8 @@ import Icon from 'react-native-vector-icons/FontAwesome5';
import ActionView from '../../UI/ActionView';
import ButtonReveal from '../../UI/ButtonReveal';
import Button, {
- ButtonVariants,
ButtonSize,
+ ButtonVariants,
} from '../../../component-library/components/Buttons/Button';
import InfoModal from '../../UI/Swaps/components/InfoModal';
import { ScreenshotDeterrent } from '../../UI/ScreenshotDeterrent';
@@ -28,9 +28,9 @@ import { showAlert } from '../../../actions/alert';
import { recordSRPRevealTimestamp } from '../../../actions/privacy';
import { WRONG_PASSWORD_ERROR } from '../../../constants/error';
import {
- SRP_GUIDE_URL,
- NON_CUSTODIAL_WALLET_URL,
KEEP_SRP_SAFE_URL,
+ NON_CUSTODIAL_WALLET_URL,
+ SRP_GUIDE_URL,
} from '../../../constants/urls';
import ClipboardManager from '../../../core/ClipboardManager';
import { useTheme } from '../../../util/theme';
@@ -46,6 +46,16 @@ import { isQRHardwareAccount } from '../../../util/address';
import AppConstants from '../../../core/AppConstants';
import { createStyles } from './styles';
import { getNavigationOptionsTitle } from '../../../components/UI/Navbar';
+import generateTestId from '../../../../wdio/utils/generateTestId';
+import {
+ PASSWORD_INPUT_BOX_ID,
+ REVEAL_SECRET_RECOVERY_PHRASE_TOUCHABLE_BOX_ID,
+ SECRET_RECOVERY_PHRASE_CANCEL_BUTTON_ID,
+ SECRET_RECOVERY_PHRASE_CONTAINER_ID,
+ SECRET_RECOVERY_PHRASE_LONG_PRESS_BUTTON_ID,
+ SECRET_RECOVERY_PHRASE_NEXT_BUTTON_ID,
+ SECRET_RECOVERY_PHRASE_TEXT,
+} from '../../../../wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds';
const PRIVATE_KEY = 'private_key';
@@ -329,7 +339,7 @@ const RevealPrivateCredential = ({
selectTextOnFocus
style={styles.seedPhrase}
editable={false}
- testID={'private-credential-text'}
+ {...generateTestId(Platform, SECRET_RECOVERY_PHRASE_TEXT)}
placeholderTextColor={colors.text.muted}
keyboardAppearance={themeAppearance}
/>
@@ -341,7 +351,10 @@ const RevealPrivateCredential = ({
onPress={() =>
copyPrivateCredentialToClipboard(privCredentialName)
}
- testID={'private-credential-touchable'}
+ {...generateTestId(
+ Platform,
+ REVEAL_SECRET_RECOVERY_PHRASE_TOUCHABLE_BOX_ID,
+ )}
style={styles.clipboardButton}
/>
) : null}
@@ -368,13 +381,13 @@ const RevealPrivateCredential = ({
{warningIncorrectPassword}
@@ -447,6 +460,10 @@ const RevealPrivateCredential = ({
: strings('reveal_credential.srp_abbreviation_text'),
})}
onLongPress={() => revealCredential(privCredentialName)}
+ {...generateTestId(
+ Platform,
+ SECRET_RECOVERY_PHRASE_LONG_PRESS_BUTTON_ID,
+ )}
/>
>
}
@@ -503,7 +520,10 @@ const RevealPrivateCredential = ({
);
return (
-
+
tryUnlock()}
showConfirmButton={!unlocked}
confirmDisabled={!enableNextButton()}
+ cancelTestID={SECRET_RECOVERY_PHRASE_CANCEL_BUTTON_ID}
+ confirmTestID={SECRET_RECOVERY_PHRASE_NEXT_BUTTON_ID}
>
<>
diff --git a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
index 2b2d3babd36..023ac17daf1 100644
--- a/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
@@ -1,49 +1,2743 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Confirm should render correctly 1`] = `
-
+
+ >
+
+
+
+ From:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Balance: undefined
+
+
+
+
+
+
+
+ To:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0x2...0x2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Check the recipient address
+
+
+
+
+
+
+
+
+
+ We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam.
+
+
+
+
+
+
+
+
+
+ Amount
+
+
+
+
+
+
+
+
+
+
+ Estimated gas fee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This gas fee suggestion is using legacy gas estimation which may be inaccurate.
+
+
+
+
+
+
+
+
+
+
+
+ What are gas fees?
+
+
+
+
+
+
+
+
+
+
+ Gas fees are paid to crypto miners who process transactions on the
+ network.
+
+
+ MetaMask does not profit from gas fees.
+
+
+
+ Gas fees are set by the network and fluctuate based on network traffic and transaction complexity.
+
+
+
+ Learn more about gas fees
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hex Data
+
+
+
+
+
+
+
+
+ Send
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hex Data
+
+
+
+ 0x
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js
index 1ff412a779e..13767c3e3e4 100644
--- a/app/components/Views/SendFlow/Confirm/index.js
+++ b/app/components/Views/SendFlow/Confirm/index.js
@@ -471,7 +471,7 @@ class Confirm extends PureComponent {
prepareTransaction,
transactionState: { transaction },
} = this.props;
- const estimation = await getGasLimit(transaction);
+ const estimation = await getGasLimit(transaction, true);
prepareTransaction({ ...transaction, ...estimation });
};
diff --git a/app/components/Views/SendFlow/Confirm/index.test.tsx b/app/components/Views/SendFlow/Confirm/index.test.tsx
index 40436017a24..b5fde06dc6a 100644
--- a/app/components/Views/SendFlow/Confirm/index.test.tsx
+++ b/app/components/Views/SendFlow/Confirm/index.test.tsx
@@ -1,10 +1,12 @@
import React from 'react';
-import { shallow } from 'enzyme';
-import Confirm from './';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
+import Confirm from '.';
+
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+
+import Engine from '../../../../core/Engine';
+
+Engine.init();
-const mockStore = configureMockStore();
const initialState = {
engine: {
backgroundState: {
@@ -15,6 +17,9 @@ const initialState = {
type: 'mainnet',
},
},
+ AddressBookController: {
+ addressBook: {},
+ },
AccountTrackerController: {
accounts: { '0x2': { balance: '0' } },
},
@@ -38,7 +43,7 @@ const initialState = {
keyrings: [{ accounts: ['0x'], type: 'HD Key Tree' }],
},
GasFeeController: {
- gasEstimates: {},
+ gasFeeEstimates: {},
},
},
},
@@ -63,15 +68,17 @@ const initialState = {
],
},
};
-const store = mockStore(initialState);
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest
+ .fn()
+ .mockImplementation((callback) => callback(initialState)),
+}));
describe('Confirm', () => {
it('should render correctly', () => {
- const wrapper = shallow(
-
-
- ,
- );
- expect(wrapper.dive()).toMatchSnapshot();
+ const wrapper = renderWithProvider(, { state: initialState });
+ expect(wrapper).toMatchSnapshot();
});
});
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 014621b30ff..0914aa9a15e 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -60,6 +60,7 @@ const Routes = {
SDK_FEEDBACK: 'SDKFeedback',
ACCOUNT_CONNECT: 'AccountConnect',
ACCOUNT_PERMISSIONS: 'AccountPermissions',
+ ACCOUNT_ACTIONS: 'AccountActions',
},
BROWSER: {
HOME: 'BrowserTabHome',
diff --git a/app/util/custom-gas/index.js b/app/util/custom-gas/index.js
index 62c779a3192..b675408e8bb 100644
--- a/app/util/custom-gas/index.js
+++ b/app/util/custom-gas/index.js
@@ -107,12 +107,16 @@ export function parseWaitTime(min) {
return parsed.trim();
}
-export async function getGasLimit(transaction) {
+export async function getGasLimit(transaction, resetGas = false) {
const { TransactionController } = Engine.context;
let estimation;
try {
- estimation = await TransactionController.estimateGas(transaction);
+ const newTransactionObj = resetGas
+ ? { ...transaction, gas: undefined }
+ : transaction;
+
+ estimation = await TransactionController.estimateGas(newTransactionObj);
} catch (error) {
estimation = {
gas: TransactionTypes.CUSTOM_GAS.DEFAULT_GAS_LIMIT,
diff --git a/app/util/custom-gas/index.test.ts b/app/util/custom-gas/index.test.ts
index ca6aa4f8a14..90f09de934f 100644
--- a/app/util/custom-gas/index.test.ts
+++ b/app/util/custom-gas/index.test.ts
@@ -1,4 +1,18 @@
-import { parseWaitTime } from '.';
+import { parseWaitTime, getGasLimit } from '.';
+import Engine from '../../core/Engine';
+
+jest.mock('../../core/Engine');
+
+const ENGINE_MOCK = Engine as jest.MockedClass;
+
+ENGINE_MOCK.context = {
+ TransactionController: {
+ estimateGas: jest.fn().mockImplementation(({ gas }) => {
+ if (gas === undefined) return Promise.resolve({ gas: '0x5208' });
+ return Promise.resolve({ gas });
+ }),
+ },
+};
describe('CustomGas utils :: parseWaitTime', () => {
it('parseWaitTime', () => {
@@ -26,3 +40,15 @@ describe('CustomGas utils :: parseWaitTime', () => {
expect(parseWaitTime(3360, 'hr', 'min', 'sec')).toEqual('2day 8hr');
});
});
+
+describe('CustomGas Util:: GetGasLimit', () => {
+ it('should return passed gas value', async () => {
+ const estimate = await getGasLimit({ gas: '0x9fd2', gasPrice: '12' });
+ expect(estimate.gas.toNumber()).toEqual(40914);
+ });
+
+ it('should fetch new estimated gas value', async () => {
+ const estimate = await getGasLimit({ gas: '0x9fd2', gasPrice: '12' }, true);
+ expect(estimate.gas.toNumber()).toEqual(21000);
+ });
+});
diff --git a/e2e/pages/Drawer/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js b/e2e/pages/Drawer/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js
index 253999db240..89dc888485e 100644
--- a/e2e/pages/Drawer/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js
+++ b/e2e/pages/Drawer/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.js
@@ -1,12 +1,12 @@
import TestHelpers from '../../../../helpers';
import messages from '../../../../../locales/languages/en.json';
-
-const SECRET_RECOVERY_PHRASE_CONTAINER_ID = 'reveal-private-credential-screen';
-const PASSWORD_INPUT_BOX_ID = 'private-credential-password-text-input';
-const PASSWORD_WARNING_ID = 'password-warning';
-const REVEAL_SECRET_RECOVERY_PHRASE_TOUCHABLE_BOX_ID =
- 'private-credential-touchable';
-const SECRET_RECOVERY_PHRASE_TEXT = 'private-credential-text';
+import { PASSWORD_INPUT_BOX_ID } from '../../../../../app/constants/test-ids';
+import {
+ PASSWORD_WARNING_ID,
+ REVEAL_SECRET_RECOVERY_PHRASE_TOUCHABLE_BOX_ID,
+ SECRET_RECOVERY_PHRASE_CONTAINER_ID,
+ SECRET_RECOVERY_PHRASE_TEXT,
+} from '../../../../../wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds';
const PASSWORD_WARNING = messages.reveal_credential.unknown_error;
@@ -26,14 +26,17 @@ export default class RevealSecretRecoveryPhrase {
static async passwordWarningIsVisible() {
await TestHelpers.checkIfHasText(PASSWORD_WARNING_ID, PASSWORD_WARNING);
}
+
static async passwordInputIsNotVisible() {
await TestHelpers.checkIfNotVisible(PASSWORD_INPUT_BOX_ID);
}
+
static async isSecretRecoveryPhraseTouchableBoxVisible() {
await TestHelpers.checkIfVisible(
REVEAL_SECRET_RECOVERY_PHRASE_TOUCHABLE_BOX_ID,
);
}
+
static async isSecretRecoveryPhraseTextCorrect(Correct_Seed_Words) {
await TestHelpers.checkIfHasText(
SECRET_RECOVERY_PHRASE_TEXT,
diff --git a/package.json b/package.json
index f2c968e3194..f0a1b1eef51 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
"**/elliptic": "^6.5.4",
"**/y18n": "^3.2.2",
"pubnub/**/netmask": "^2.0.1",
- "**/vm2": "3.9.16",
+ "**/vm2": ">=3.9.17",
"**/optimist/minimist": "^1.2.5",
"react-native-level-fs/**/bl": "^1.2.3",
"react-native-level-fs/**/semver": "^4.3.2",
diff --git a/wdio/config/android.config.browserstack.local.js b/wdio/config/android.config.browserstack.local.js
index 5261f9f0eea..0a87e774ebd 100644
--- a/wdio/config/android.config.browserstack.local.js
+++ b/wdio/config/android.config.browserstack.local.js
@@ -1,6 +1,7 @@
-import { removeSync } from 'fs-extra';
+import {removeSync} from 'fs-extra';
import generateTestReports from '../../wdio/utils/generateTestReports';
-import { config } from '../../wdio.conf';
+import {config} from '../../wdio.conf';
+
const browserstack = require('browserstack-local');
// Appium capabilities
@@ -30,7 +31,7 @@ config.onPrepare = function (config, capabilities) {
console.log('Connecting local');
return new Promise((resolve, reject) => {
exports.bs_local = new browserstack.Local();
- exports.bs_local.start({ key: config.key }, (error) => {
+ exports.bs_local.start({key: config.key}, (error) => {
if (error) return reject(error);
console.log('Connected. Now testing...');
@@ -57,4 +58,4 @@ delete config.services;
const _config = config;
// eslint-disable-next-line import/prefer-default-export
-export { _config as config };
+export {_config as config};
diff --git a/wdio/config/android.config.debug.js b/wdio/config/android.config.debug.js
index 0d8af9e7420..e3cb7236f0c 100644
--- a/wdio/config/android.config.debug.js
+++ b/wdio/config/android.config.debug.js
@@ -1,4 +1,4 @@
-import { config } from '../../wdio.conf';
+import {config} from '../../wdio.conf';
// Appium capabilities
// https://appium.io/docs/en/writing-running-appium/caps/
@@ -19,4 +19,4 @@ config.cucumberOpts.tagExpression = '@androidApp'; // pass tag to run tests spec
const _config = config;
// eslint-disable-next-line import/prefer-default-export
-export { _config as config };
+export {_config as config};
diff --git a/wdio/features/Accounts/AccountActions.feature b/wdio/features/Accounts/AccountActions.feature
new file mode 100644
index 00000000000..6127690e5d8
--- /dev/null
+++ b/wdio/features/Accounts/AccountActions.feature
@@ -0,0 +1,23 @@
+Feature: Displaying account actions
+
+ Scenario: Show private key screen
+ Given the app displayed the splash animation
+ And I have imported my wallet
+ And I tap No Thanks on the Enable security check screen
+ And I tap No thanks on the onboarding welcome tutorial
+ And I close the Whats New modal
+ And I open the account actions
+ And I press show private key
+ Then The Reveal Private key screen should be displayed
+ When I enter my password on the Reveal Private Credential screen
+ Then my Private Key is displayed
+
+ Scenario: View address on Etherscan
+ Given I am on the main wallet view
+ And I open the account actions
+ And I press view on etherscan
+
+ Scenario: Press share address
+ Given I am on the main wallet view
+ And I open the account actions
+ And I press share address
diff --git a/wdio/features/Accounts/ImportingAccount.feature b/wdio/features/Accounts/ImportingAccount.feature
index 6dab9121cb6..d4e0c7335fc 100644
--- a/wdio/features/Accounts/ImportingAccount.feature
+++ b/wdio/features/Accounts/ImportingAccount.feature
@@ -34,8 +34,6 @@ Feature: Importing account in wallet
When I type into the private key input field
And I tap on the private key import button
Then The account is imported
- When I dismiss the account list
- Then I am on the imported account
Examples:
| PRIVATEKEY |
| cbfd798afcfd1fd8ecc48cbecb6dc7e876543395640b758a90e11d986e758ad1 |
diff --git a/wdio/features/Wallet/SendToken.feature b/wdio/features/Wallet/SendToken.feature
index 739aaa7e834..07e9fffed1f 100644
--- a/wdio/features/Wallet/SendToken.feature
+++ b/wdio/features/Wallet/SendToken.feature
@@ -70,8 +70,7 @@ Feature: Sending Native and ERC Tokens
And the token being sent is visible
And the token amount to be sent is visible
When I tap button "Send" on Confirm Amount view
- Then the transaction is submitted with Transaction Complete! toast appearing
- And I am taken to the token overview screen
+ Then I am taken to the token overview screen
Examples:
diff --git a/wdio/screen-objects/ImportSuccessScreen.js b/wdio/screen-objects/ImportSuccessScreen.js
index f8e56a1a225..0b723619926 100644
--- a/wdio/screen-objects/ImportSuccessScreen.js
+++ b/wdio/screen-objects/ImportSuccessScreen.js
@@ -1,22 +1,28 @@
import Gestures from '../helpers/Gestures';
import Selectors from '../helpers/Selectors';
import {
- IMPORT_SUCESS_SCREEN_ID,
IMPORT_SUCESS_SCREEN_CLOSE_BUTTON_ID,
+ IMPORT_SUCESS_SCREEN_ID,
} from './testIDs/Screens/ImportSuccessScreen.testIds';
class ImportAccountScreen {
- get importSuccessScreenContainer() {
+ get container() {
return Selectors.getElementByPlatform(IMPORT_SUCESS_SCREEN_ID);
}
+
get closeButton() {
return Selectors.getElementByPlatform(IMPORT_SUCESS_SCREEN_CLOSE_BUTTON_ID);
}
+
async tapCloseButton() {
await Gestures.waitAndTap(this.closeButton);
+ const closeButton = await this.closeButton;
+ await closeButton.waitForDisplayed({ reverse: true });
}
+
async isVisible() {
- await expect(this.importSuccessScreenContainer).toBeDisplayed();
+ const importSuccessScreen = await this.container;
+ await importSuccessScreen.waitForDisplayed();
}
}
diff --git a/wdio/screen-objects/RevealSecretRecoveryPhraseScreen.js b/wdio/screen-objects/RevealSecretRecoveryPhraseScreen.js
new file mode 100644
index 00000000000..17697e09058
--- /dev/null
+++ b/wdio/screen-objects/RevealSecretRecoveryPhraseScreen.js
@@ -0,0 +1,3 @@
+class RevealSecretRecoveryPhraseScreen {}
+
+export default new RevealSecretRecoveryPhraseScreen();
diff --git a/wdio/screen-objects/WalletMainScreen.js b/wdio/screen-objects/WalletMainScreen.js
index b833f49d2b9..030bfc2e0b0 100644
--- a/wdio/screen-objects/WalletMainScreen.js
+++ b/wdio/screen-objects/WalletMainScreen.js
@@ -10,11 +10,15 @@ import {
HAMBURGER_MENU_BUTTON,
IMPORT_NFT_BUTTON_ID,
IMPORT_TOKEN_BUTTON_ID,
+ MAIN_WALLET_ACCOUNT_ACTIONS,
MAIN_WALLET_VIEW_VIA_TOKENS_ID,
NAVBAR_NETWORK_BUTTON,
NAVBAR_NETWORK_TEXT,
NOTIFICATION_REMIND_ME_LATER_BUTTON_ID,
SECURE_WALLET_BACKUP_ALERT_MODAL,
+ SHARE_ADDRESS,
+ SHOW_PRIVATE_KEY,
+ VIEW_ETHERSCAN,
WALLET_ACCOUNT_ICON,
WALLET_VIEW_BURGER_ICON_ID,
} from './testIDs/Screens/WalletView.testIds';
@@ -22,6 +26,8 @@ import {
import {DRAWER_VIEW_SETTINGS_TEXT_ID} from './testIDs/Screens/DrawerView.testIds';
import {NOTIFICATION_TITLE} from './testIDs/Components/Notification.testIds';
+import {TAB_BAR_WALLET_BUTTON} from './testIDs/Components/TabBar.testIds';
+import {BACK_BUTTON_SIMPLE_WEBVIEW} from './testIDs/Components/SimpleWebView.testIds';
class WalletMainScreen {
get wizardContainer() {
@@ -90,6 +96,30 @@ class WalletMainScreen {
return Selectors.getElementByPlatform(NAVBAR_NETWORK_TEXT);
}
+ get accountActionsButton() {
+ return Selectors.getElementByPlatform(MAIN_WALLET_ACCOUNT_ACTIONS);
+ }
+
+ get privateKeyActionButton() {
+ return Selectors.getElementByPlatform(SHOW_PRIVATE_KEY);
+ }
+
+ get shareAddressActionButton() {
+ return Selectors.getElementByPlatform(SHARE_ADDRESS);
+ }
+
+ get viewEtherscanActionButton() {
+ return Selectors.getElementByPlatform(VIEW_ETHERSCAN);
+ }
+
+ get walletButton() {
+ return Selectors.getElementByPlatform(TAB_BAR_WALLET_BUTTON);
+ }
+
+ get goBackSimpleWebViewButton() {
+ return Selectors.getElementByPlatform(BACK_BUTTON_SIMPLE_WEBVIEW);
+ }
+
async tapSettings() {
await Gestures.waitAndTap(this.drawerSettings);
}
@@ -190,6 +220,24 @@ class WalletMainScreen {
const element = await this.networkNavbarTitle;
await expect(await element.getText()).toContain(text);
}
+
+ async tapAccountActions() {
+ await Gestures.waitAndTap(this.accountActionsButton);
+ }
+
+ async tapShowPrivateKey() {
+ await Gestures.waitAndTap(this.privateKeyActionButton);
+ await Gestures.waitAndTap(this.walletButton);
+ }
+
+ async tapShareAddress() {
+ await Gestures.waitAndTap(this.shareAddressActionButton);
+ }
+
+ async tapViewOnEtherscan() {
+ await Gestures.waitAndTap(this.viewEtherscanActionButton);
+ await Gestures.waitAndTap(this.goBackSimpleWebViewButton);
+ }
}
export default new WalletMainScreen();
diff --git a/wdio/screen-objects/testIDs/Components/SimpleWebView.testIds.js b/wdio/screen-objects/testIDs/Components/SimpleWebView.testIds.js
new file mode 100644
index 00000000000..9ee96b48c78
--- /dev/null
+++ b/wdio/screen-objects/testIDs/Components/SimpleWebView.testIds.js
@@ -0,0 +1 @@
+export const BACK_BUTTON_SIMPLE_WEBVIEW = 'back_button_simple_webview';
diff --git a/wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds.js b/wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds.js
new file mode 100644
index 00000000000..f733e0d34ce
--- /dev/null
+++ b/wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds.js
@@ -0,0 +1,16 @@
+export const SECRET_RECOVERY_PHRASE_CONTAINER_ID =
+ 'reveal-private-credential-screen';
+export const PASSWORD_INPUT_BOX_ID = 'private-credential-password-text-input';
+export const PASSWORD_WARNING_ID = 'password-warning';
+export const REVEAL_SECRET_RECOVERY_PHRASE_TOUCHABLE_BOX_ID =
+ 'private-credential-touchable';
+export const SECRET_RECOVERY_PHRASE_TEXT = 'private-credential-text';
+
+export const SECRET_RECOVERY_PHRASE_CANCEL_BUTTON_ID =
+ 'reveal-private-credential-cancel-button';
+
+export const SECRET_RECOVERY_PHRASE_NEXT_BUTTON_ID =
+ 'reveal-private-credential-next-button';
+
+export const SECRET_RECOVERY_PHRASE_LONG_PRESS_BUTTON_ID =
+ 'reveal-private-long-press-button';
diff --git a/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js b/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js
index 48a6d71949a..d4f7e474d2a 100644
--- a/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js
+++ b/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js
@@ -25,3 +25,9 @@ export const NAVBAR_NETWORK_TEXT = 'open-networks-text';
export const getAssetTestId = (token) => {
return `asset-${token}`;
};
+
+export const MAIN_WALLET_ACCOUNT_ACTIONS = 'main-wallet-account-actions';
+export const EDIT_ACCOUNT = 'edit-account-action';
+export const VIEW_ETHERSCAN = 'view-etherscan-action';
+export const SHARE_ADDRESS = 'share-address-action';
+export const SHOW_PRIVATE_KEY = 'show-private-key-action';
diff --git a/wdio/step-definitions/import-wallet-via-private-key.steps.js b/wdio/step-definitions/import-wallet-via-private-key.steps.js
index 69a51aff57b..53510a40bf7 100644
--- a/wdio/step-definitions/import-wallet-via-private-key.steps.js
+++ b/wdio/step-definitions/import-wallet-via-private-key.steps.js
@@ -1,9 +1,8 @@
/* eslint-disable no-undef */
-import { When, Then } from '@wdio/cucumber-framework';
+import { Then, When } from '@wdio/cucumber-framework';
import AccountListComponent from '../screen-objects/AccountListComponent';
import ImportAccountScreen from '../screen-objects/ImportAccountScreen';
import ImportSuccessScreen from '../screen-objects/ImportSuccessScreen';
-import WalletAccountModal from '../screen-objects/Modals/WalletAccountModal.js';
When(/^I tap on Import an account/, async () => {
await driver.pause(setTimeout);
@@ -30,11 +29,6 @@ Then(/^The account is imported/, async () => {
await ImportSuccessScreen.tapCloseButton();
});
-Then(/^I am on the imported account/, async () => {
- await driver.pause(2500);
- await WalletAccountModal.isAccountNameLabelEqualTo('Account 2'); // this can be better
-});
-
Then(/^I should see an error (.*)/, async (errorMessage) => {
await ImportAccountScreen.isAlertTextVisible(errorMessage);
await driver.acceptAlert();
diff --git a/wdio/step-definitions/reveal-private-credential.steps.js b/wdio/step-definitions/reveal-private-credential.steps.js
new file mode 100644
index 00000000000..401fe2c25d4
--- /dev/null
+++ b/wdio/step-definitions/reveal-private-credential.steps.js
@@ -0,0 +1,14 @@
+import { Then, When } from '@wdio/cucumber-framework';
+
+Then(/^The Reveal Private key screen should be displayed$/, async () => {
+ //
+});
+When(
+ /^I enter my password on the Reveal Private Credential screen$/,
+ async () => {
+ //
+ },
+);
+Then(/^my Private Key is displayed$/, async () => {
+ //
+});
diff --git a/wdio/step-definitions/wallet-view.steps.js b/wdio/step-definitions/wallet-view.steps.js
index 8f4b8772e70..6490eec7021 100644
--- a/wdio/step-definitions/wallet-view.steps.js
+++ b/wdio/step-definitions/wallet-view.steps.js
@@ -1,4 +1,4 @@
-import {Given, Then, When} from '@wdio/cucumber-framework';
+import { Given, Then, When } from '@wdio/cucumber-framework';
import WalletMainScreen from '../screen-objects/WalletMainScreen.js';
import AccountListComponent from '../screen-objects/AccountListComponent';
import CommonScreen from '../screen-objects/CommonScreen';
@@ -80,3 +80,19 @@ Given(/^On the Main Wallet view I tap on the Send Action$/, async () => {
await TabBarModal.tapActionButton();
await WalletActionModal.tapSendButton();
});
+
+Then(/^I open the account actions$/, async () => {
+ await WalletMainScreen.tapAccountActions();
+});
+
+Then(/^I press show private key$/, async () => {
+ await WalletMainScreen.tapShowPrivateKey();
+});
+
+Then(/^I press share address$/, async () => {
+ await WalletMainScreen.tapShareAddress();
+});
+
+Then(/^I press view on etherscan$/, async () => {
+ await WalletMainScreen.tapViewOnEtherscan();
+});
diff --git a/yarn.lock b/yarn.lock
index 43c13e38867..3a0fe4e516e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -16173,9 +16173,9 @@ json5@^1.0.1:
minimist "^1.2.0"
json5@^2.1.2, json5@^2.2.0, json5@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
- integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonfile@^2.1.0:
version "2.4.0"
@@ -24260,10 +24260,10 @@ vm-browserify@1.1.2:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
-vm2@3.9.16, vm2@^3.9.3:
- version "3.9.16"
- resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.16.tgz#0fbc2a265f7bf8b837cea6f4a908f88a3f93b8e6"
- integrity sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==
+vm2@>=3.9.17, vm2@^3.9.3:
+ version "3.9.17"
+ resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.17.tgz#251b165ff8a0e034942b5181057305e39570aeab"
+ integrity sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==
dependencies:
acorn "^8.7.0"
acorn-walk "^8.2.0"