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"