diff --git a/.circleci/scripts/chrome-install.sh b/.circleci/scripts/chrome-install.sh index e2cbd7bdc..8be51ad30 100755 --- a/.circleci/scripts/chrome-install.sh +++ b/.circleci/scripts/chrome-install.sh @@ -5,12 +5,12 @@ set -u set -o pipefail # To get the latest version, see -CHROME_VERSION='99.0.4844.51-1' +CHROME_VERSION='100.0.4896.60-1' CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb" CHROME_BINARY_URL="https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_BINARY}" # To retrieve this checksum, run the `wget` and `shasum` commands below -CHROME_BINARY_SHA512SUM='f0fa5c6c23d9e8373aafa14622ed4362cbf3a101691ce309864e4aa0030dc3bf7b8e7c8ce294c06106b26eb6b8dc0f2b80376bf2a49d703fc9f6597961b9432e' +CHROME_BINARY_SHA512SUM='d7a98777650e8218fef4acc8466d4ddf5e234b97fbc16c33f38f69f9ebfe7b6bb6827a90aad15ea729d7c1bacfa78488c5d5194b531f00f454302dd9c0957e4e' wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}" diff --git a/.github/workflows/crowdin_action.yml b/.github/workflows/crowdin_action.yml index b07ac651a..9ddc21705 100644 --- a/.github/workflows/crowdin_action.yml +++ b/.github/workflows/crowdin_action.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v2 - name: crowdin action - uses: crowdin/github-action@d0622816ed4f4744db27d04374b2cef6867f7bed + uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415 with: upload_translations: true download_translations: true diff --git a/.storybook/metametrics.js b/.storybook/metametrics.js index 387b0d467..aafd1b182 100644 --- a/.storybook/metametrics.js +++ b/.storybook/metametrics.js @@ -1,24 +1,16 @@ import React from 'react'; import { MetaMetricsProvider, - LegacyMetaMetricsProvider, + LegacyMetaMetricsProvider } from '../ui/contexts/metametrics'; -import { - MetaMetricsProvider as NewMetaMetricsProvider, - LegacyMetaMetricsProvider as NewLegacyMetaMetricsProvider, -} from '../ui/contexts/metametrics.new'; const MetaMetricsProviderStorybook = (props) => ( - - - - + + {props.children} - - - - + + ); export default MetaMetricsProviderStorybook \ No newline at end of file diff --git a/.storybook/test-data.js b/.storybook/test-data.js index ba5ef0b53..6188a6122 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -13,8 +13,128 @@ const state = { protocol: 'https:', url: 'https://metamask.github.io/test-dapp/', }, + networkList: [ + { + blockExplorerUrl: "https://etherscan.io", + chainId: "0x1", + iconColor: 'var(--mainnet)', + isATestNetwork: false, + labelKey: "mainnet", + providerType: "mainnet", + rpcUrl: "https://mainnet.infura.io/v3/", + ticker: "ETH", + viewOnly: true, + }, + { + blockExplorerUrl: "https://ropsten.etherscan.io", + chainId: "0x3", + iconColor: 'var(--ropsten)', + isATestNetwork: true, + labelKey: "ropsten", + providerType: "ropsten", + rpcUrl: "https://ropsten.infura.io/v3/", + ticker: "ETH", + viewOnly: true, + }, + { + blockExplorerUrl: "https://rinkeby.etherscan.io", + chainId: "0x4", + iconColor: 'var(--rinkeby)', + isATestNetwork: true, + labelKey: "rinkeby", + providerType: "rinkeby", + rpcUrl: "https://rinkeby.infura.io/v3/", + ticker: "ETH", + viewOnly: true, + }, + { + blockExplorerUrl: "https://goerli.etherscan.io", + chainId: "0x5", + iconColor: 'var(--goerli)', + isATestNetwork: true, + labelKey: "goerli", + providerType: "goerli", + rpcUrl: "https://goerli.infura.io/v3/", + ticker: "ETH", + viewOnly: true, + }, + { + blockExplorerUrl: "https://kovan.etherscan.io", + chainId: "0x2a", + iconColor: 'var(--kovan)', + isATestNetwork: true, + labelKey: "kovan", + providerType: "kovan", + rpcUrl: "https://kovan.infura.io/v3/", + ticker: "ETH", + viewOnly: true, + }, + { + blockExplorerUrl: "", + chainId: "0x539", + iconColor: 'var(--localhost)', + isATestNetwork: true, + label: "Localhost 8545", + providerType: "rpc", + rpcUrl: "http://localhost:8545", + ticker: "ETH", + }, + { + blockExplorerUrl: "https://bscscan.com", + chainId: "0x38", + iconColor: 'var(--localhost)', + isATestNetwork: false, + label: "Binance Smart Chain", + providerType: "rpc", + rpcUrl: "https://bsc-dataseed.binance.org/", + ticker: "BNB", + }, + { + blockExplorerUrl: "https://cchain.explorer.avax.network/", + chainId: "0xa86a", + iconColor: 'var(--localhost)', + isATestNetwork: false, + label: "Avalanche", + providerType: "rpc", + rpcUrl: "https://api.avax.network/ext/bc/C/rpc", + ticker: "AVAX", + }, + { + blockExplorerUrl: "https://polygonscan.com", + chainId: "0x89", + iconColor: 'var(--localhost)', + isATestNetwork: false, + label: "Polygon", + providerType: "rpc", + rpcUrl: "https://polygon-rpc.com", + ticker: "MATIC", + }, + ], metamask: { tokenList: { + '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f': { + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + symbol: 'SNX', + decimals: 18, + name: 'Synthetix Network Token', + iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png', + aggregators: [ + 'Aave', + 'Bancor', + 'CMC', + 'Crypto.com', + 'CoinGecko', + '1inch', + 'Paraswap', + 'PMM', + 'Synthetix', + 'Zapper', + 'Zerion', + '0x', + ], + occurrences: 12, + unlisted: false + }, '0x6b175474e89094c44da98b954eedeac495271d0f': { address: '0x6b175474e89094c44da98b954eedeac495271d0f', symbol: 'META', @@ -989,6 +1109,7 @@ const state = { suggestedAssets: [], useNonceField: false, usePhishDetect: true, + useTokenDetection: true, lostIdentities: {}, forgottenPassword: false, ipfsGateway: 'dweb.link', diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c35c1ab..c30bf6edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - This version was used to rollback from v10.14.0 to v10.13.0. +## [10.14.0] +### Added +- **[FLASK]** Add snap version to details page ([#14110](https://github.com/MetaMask/metamask-extension/pull/14110)) +- **[FLASK]** Add support for searching installed snaps in Settings ([#14419](https://github.com/MetaMask/metamask-extension/pull/14419)) + +### Changed +- Disable Swaps on Rinkeby ([#14372](https://github.com/MetaMask/metamask-extension/pull/14372)) +- Swaps: Asset sorting improvements ([#14436](https://github.com/MetaMask/metamask-extension/pull/14436)) + - In 'Swap from' field: tokens are sorted by user ownership and fiat value + - In 'Swap to' field: tokens are sorted by top assets +- Redesign Networks view in Settings ([#13560](https://github.com/MetaMask/metamask-extension/pull/13560)) + - Adding network search functionality +- Show Smart Transaction switch when wrapping/unwrapping ([#14225](https://github.com/MetaMask/metamask-extension/pull/14225)) + +### Fixed +- Improving identicon settings accessibility ([#13760](https://github.com/MetaMask/metamask-extension/pull/13760)) +- Enhanced Gas Fee UI: Fix gas values overlapping with labels ([#14392](https://github.com/MetaMask/metamask-extension/pull/14392)) +- Settings search improvements ([#14350](https://github.com/MetaMask/metamask-extension/pull/14350)) + - Allow ampersands in search input + - Fix duplicate entry issue in results +- Fix text wrapping issue in Settings search tabs ([#14368](https://github.com/MetaMask/metamask-extension/pull/14368)) +- Dark Mode: Fix button styles in dialog actions ([#14361](https://github.com/MetaMask/metamask-extension/pull/14361)) + ## [10.13.0] ### Added - Add a new fiat onboarding option via MoonPay ([#13934](https://github.com/MetaMask/metamask-extension/pull/13934)) @@ -2925,7 +2948,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [10.14.4]: https://github.com/MetaMask/metamask-extension/compare/v10.14.3...v10.14.4 [10.14.3]: https://github.com/MetaMask/metamask-extension/compare/v10.14.2...v10.14.3 [10.14.2]: https://github.com/MetaMask/metamask-extension/compare/v10.14.1...v10.14.2 -[10.14.1]: https://github.com/MetaMask/metamask-extension/compare/v10.13.0...v10.14.1 +[10.14.1]: https://github.com/MetaMask/metamask-extension/compare/v10.14.0...v10.14.1 +[10.14.0]: https://github.com/MetaMask/metamask-extension/compare/v10.13.0...v10.14.0 [10.13.0]: https://github.com/MetaMask/metamask-extension/compare/v10.12.4...v10.13.0 [10.12.4]: https://github.com/MetaMask/metamask-extension/compare/v10.12.3...v10.12.4 [10.12.3]: https://github.com/MetaMask/metamask-extension/compare/v10.12.2...v10.12.3 diff --git a/README.md b/README.md index 289b93534..d609aafd2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ To learn how to contribute to the MetaMask project itself, visit our [Internal D - Install dependencies: `yarn setup` (not the usual install command) - Copy the `.metamaskrc.dist` file to `.metamaskrc` - Replace the `INFURA_PROJECT_ID` value with your own personal [Infura Project ID](https://infura.io/docs). - - If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/). + - If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/), see [Developing on MetaMask](./development/README.md). - Build the project to the `./dist/` folder with `yarn dist`. Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built. @@ -97,6 +97,7 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack - [How to add a new translation to MetaMask](./docs/translating-guide.md) - [Publishing Guide](./docs/publishing.md) - [How to use the TREZOR emulator](./docs/trezor-emulator.md) +- [Developing on MetaMask](./development/README.md) - [How to generate a visualization of this repository's development](./development/gource-viz.sh) [1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BbackgroundConnection%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index bbfbc552e..e6d28003d 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1276,13 +1276,6 @@ "importAccountSeedPhrase": { "message": "Ein Konto mit einem Seed-Schlüssel importieren" }, - "importExistingWalletDescription": { - "message": "Geben Sie die Geheime Wiederherstellungsphrase (alias Seed Phrase) ein, die Sie beim Erstellen Ihrer Wallet erhalten haben. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Eine bestehende Wallet mit einer Geheime Wiederherstellungsphrase importieren" - }, "importMyWallet": { "message": "Meine Wallet importieren" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Neu! NFT-Erkennung" }, - "newNFTsDetectedInfo": { - "message": "Erlaube der MetaMaske, NFTs automatisch von Opensea zu erkennen und in deiner MetaMask Wallet anzuzeigen." - }, "newNetworkAdded": { "message": "“$1” wurde erfolgreich hinzugefügt!" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index c25a1549c..0ba089f74 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "Εισαγωγή λογαριασμού με Μυστική Φράση Ανάκτησης" }, - "importExistingWalletDescription": { - "message": "Εισάγετε τη Μυστική Φράση Ανάκτησης (δλδ Seed Phrase) που σας δόθηκε όταν δημιουργήσατε το πορτοφόλι σας. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Εισαγωγή υπάρχοντος πορτοφολιού με Μυστική Φράση Ανάκτησης" - }, "importMyWallet": { "message": "Εισαγωγή Πορτοφολιού μου" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Νέο! Εντοπισμός NFT" }, - "newNFTsDetectedInfo": { - "message": "Επιτρέψτε στο MetaMask να ανιχνεύει αυτόματα NFTs από το Opensea και να εμφανίζεται στο πορτοφόλι σας MetaMask." - }, "newNetworkAdded": { "message": "Το “$1” προστέθηκε με επιτυχία!" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 9713782fb..27b711cd9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -62,6 +62,13 @@ "message": "$1 may access and spend this asset", "description": "$1 is the url of the site requesting ability to spend" }, + "accessYourWalletWithSRP": { + "message": "Access your wallet with your Secret Recovery Phrase" + }, + "accessYourWalletWithSRPDescription": { + "message": "MetaMask cannot recover your password. We will use your Secret Recovery Phrase to validate your ownership, restore your wallet and set up a new password. First, enter the Secret Recovery Phrase that you were given when you created your wallet. $1", + "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" + }, "accessingYourCamera": { "message": "Accessing your camera..." }, @@ -714,6 +721,9 @@ "custom": { "message": "Advanced" }, + "customContentSearch": { + "message": "Search for a previously added network" + }, "customGas": { "message": "Customize Gas" }, @@ -733,6 +743,12 @@ "customToken": { "message": "Custom Token" }, + "customTokenWarningInNonTokenDetectionNetwork": { + "message": "Token detection is not available on this network yet. Please import token manually and make sure you trust it. Learn about $1" + }, + "customTokenWarningInTokenDetectionNetwork": { + "message": "Before manually importing a token, make sure you trust it. Learn about $1." + }, "customerSupport": { "message": "customer support" }, @@ -1258,6 +1274,9 @@ "message": "From: $1", "description": "$1 is the address to include in the From label. It is typically shortened first using shortenAddress" }, + "fromTokenLists": { + "message": "From token lists: $1" + }, "functionApprove": { "message": "Function: Approve" }, @@ -1468,13 +1487,6 @@ "importAccountSeedPhrase": { "message": "Import a wallet with Secret Recovery Phrase" }, - "importExistingWalletDescription": { - "message": "Enter your Secret Recovery Phrase (aka Seed Phrase) that you were given when you created your wallet. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Import existing wallet with Secret Recovery Phrase" - }, "importMyWallet": { "message": "Import My Wallet" }, @@ -2000,12 +2012,12 @@ "newContract": { "message": "New Contract" }, + "newNFTDetectedMessage": { + "message": "Allow MetaMask to automatically detect NFTs from Opensea and display in your wallet." + }, "newNFTsDetected": { "message": "New! NFT detection" }, - "newNFTsDetectedInfo": { - "message": "Allow MetaMask to automatically detect NFTs from Opensea and display in your MetaMask wallet." - }, "newNetworkAdded": { "message": "“$1” was successfully added!" }, @@ -2097,6 +2109,28 @@ "notEnoughGas": { "message": "Not Enough Gas" }, + "notifications10ActionText": { + "message": "Visit in settings", + "description": "The 'call to action' on the button, or link, of the 'Visit in settings' notification. Upon clicking, users will be taken to settings page." + }, + "notifications10DescriptionOne": { + "message": "Improved token detection is currently available on Ethereum Mainnet, Polygon, BSC, and Avalanche networks. More to come!" + }, + "notifications10DescriptionThree": { + "message": "Token detection feature is ON by default. But you can disable it from Settings." + }, + "notifications10DescriptionTwo": { + "message": "We source tokens from third party tokens lists. Tokens listed on more than two token lists will be automatically detected." + }, + "notifications10Title": { + "message": "Improved token detection is here" + }, + "notifications11Description": { + "message": "Tokens can be created by anyone and can have duplicate names. If you see a token appear that you don’t trust or haven’t interacted with - it’s safer to not trust it." + }, + "notifications11Title": { + "message": "Scam and security risks" + }, "notifications1Description": { "message": "MetaMask Mobile users can now swap tokens inside their mobile wallet. Scan the QR code to get the mobile app and start swapping.", "description": "Description of a notification in the 'See What's New' popup. Describes the swapping on mobile feature." @@ -2186,6 +2220,10 @@ "notifications9Title": { "message": "👓 We are making transactions easier to read." }, + "numberOfNewTokensDetected": { + "message": "$1 new tokens found in this account", + "description": "$1 is the number of new tokens detected" + }, "ofTextNofM": { "message": "of" }, @@ -2283,6 +2321,9 @@ "origin": { "message": "Origin" }, + "padlock": { + "message": "Padlock" + }, "parameters": { "message": "Parameters" }, @@ -2767,7 +2808,7 @@ "message": "Settings" }, "settingsSearchMatchingNotFound": { - "message": "No matching results found" + "message": "No matching results found." }, "shorthandVersion": { "message": "v$1", @@ -2858,6 +2899,10 @@ "message": "$1 snap has access to:", "description": "$1 represents the name of the snap" }, + "snapAdded": { + "message": "Added on $1 from $2", + "description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap." + }, "snapError": { "message": "Snap Error: '$1'. Error Code: '$2'", "description": "This is shown when a snap encounters an error. $1 is the error message from the snap, and $2 is the error code." @@ -3505,6 +3550,9 @@ "testFaucet": { "message": "Test Faucet" }, + "testNetworks": { + "message": "Test networks" + }, "theme": { "message": "Theme" }, @@ -3555,6 +3603,9 @@ "tokenDetection": { "message": "Token detection" }, + "tokenDetectionAlertMessage": { + "message": "Token detection is currently available on $1. $2" + }, "tokenDetectionAnnouncement": { "message": "New! Improved token detection is available on Ethereum Mainnet as an experimental feature. $1" }, @@ -3713,6 +3764,9 @@ "typePassword": { "message": "Type your MetaMask password" }, + "typeYourSRP": { + "message": "Type your Secret Recovery Phrase" + }, "u2f": { "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index d43eb4f92..3ac62593e 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1319,13 +1319,6 @@ "importAccountSeedPhrase": { "message": "Importar una cartera con la frase secreta de recuperación" }, - "importExistingWalletDescription": { - "message": "Ingrese su frase secreta de recuperación (también conocida como Frase Semilla) que recibió al crear su cartera. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Importar la cartera existente con la frase secreta de recuperación" - }, "importMyWallet": { "message": "Importar Mi cartera" }, @@ -1787,9 +1780,6 @@ "newNFTsDetected": { "message": "¡Nuevo! Detección NFT" }, - "newNFTsDetectedInfo": { - "message": "Permitir que MetaMask detecte automáticamente NFT de Opensea y los muestre en su cartera MetaMask." - }, "newNetworkAdded": { "message": "¡\"$1\" se añadió con éxito!" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index af76f6953..0f74f00c7 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "Importez un compte avec une phrase mnémotechnique" }, - "importExistingWalletDescription": { - "message": "Saisissez la phrase secrète de récupération (aussi appelée « phrase mnémonique » ou « seed ») qui vous a été attribuée lors de la création de votre portefeuille. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Importer un portefeuille existant avec la phrase secrète de récupération" - }, "importMyWallet": { "message": "Importer mon portefeuille" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Nouveau ! Détection de NFT" }, - "newNFTsDetectedInfo": { - "message": "Cela permet à MetaMask de détecter automatiquement les NFT d’Opensea et de les afficher dans votre portefeuille MetaMask." - }, "newNetworkAdded": { "message": "« $1 » a été ajouté avec succès !" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 65459170a..d7e126397 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "गुप्त रिकवरी फ्रेज के साथ एक खाता आयात करें" }, - "importExistingWalletDescription": { - "message": "अपना सीक्रेट रिकवरी फ्रेज (उर्फ सीड फ्रेज) दर्ज करें जो आपको अपना वॉलेट बनाने पर दिया गया था। $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "सीक्रेट रिकवरी फ्रेज के साथ मौजूदा वॉलेट आयात करें" - }, "importMyWallet": { "message": "मेरा वॉलेट आयात करें" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "नया! NFT डिटेक्शन" }, - "newNFTsDetectedInfo": { - "message": "MetaMask को Opensea से NFT का स्वचालित रूप से पता लगाने और अपने MetaMask वॉलेट में प्रदर्शित करने की अनुमति दें।" - }, "newNetworkAdded": { "message": "\"$1\" सफलतापूर्वक जोड़ा गया था!" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 20876b81d..02c602bd2 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "Impor dompet dengan Frasa Pemulihan Rahasia" }, - "importExistingWalletDescription": { - "message": "Masukkan Frasa Pemulihan Rahasia Anda (alias Frasa Benih) yang diberikan saat Anda membuat dompet. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Impor dompet yang ada dengan Frasa Pemulihan Rahasia" - }, "importMyWallet": { "message": "Impor Dompet Saya" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Baru! Deteksi NFT" }, - "newNFTsDetectedInfo": { - "message": "Izinkan MetaMask untuk mendeteksi NFT dari Opensea secara otomatis dan menampilkannya di dompet MetaMask Anda." - }, "newNetworkAdded": { "message": "“$1” berhasil ditambahkan!" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index c1a4350bd..50b314ddc 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "シークレットリカバリーフレーズを使用してウォレットをインポート" }, - "importExistingWalletDescription": { - "message": "ウォレットの作成時に提供されたシークレットリカバリーフレーズ (シードフレーズ) を入力してください。$1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "シークレットリカバリーフレーズで既存のウォレットをインポート" - }, "importMyWallet": { "message": "ウォレットをインポート" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "新機能! NFT検出" }, - "newNFTsDetectedInfo": { - "message": "MetaMaskがOpenseからNFTを自動的に検出し、MetaMaskウォレットに表示できるようにします。" - }, "newNetworkAdded": { "message": "「$1」が追加されました!" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index e115da9d9..c8d3ca1d8 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "비밀 복구 구문으로 계정 가져오기" }, - "importExistingWalletDescription": { - "message": "지갑을 만들 때 받은 비밀 복구 구문(시드 구문)을 입력하세요. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "비밀 복구 구문을 사용하여 기존 지갑 가져오기" - }, "importMyWallet": { "message": "내 지갑 가져오기" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "신규! NFT 감지" }, - "newNFTsDetectedInfo": { - "message": "MetaMask가 Opensea에서 자동으로 NFT를 감지하고 MetaMask 지갑에 표시하도록 허용합니다." - }, "newNetworkAdded": { "message": "“$1”가 성공적으로 추가되었습니다!" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index cda080e4c..c59a092a2 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1303,13 +1303,6 @@ "importAccountSeedPhrase": { "message": "Importe uma carteira com a Frase de Recuperação Secreta" }, - "importExistingWalletDescription": { - "message": "Digite sua Frase de Recuperação Secreta (ou seja, a frase seed) que lhe foi dada quando você criou a sua carteira. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Importar carteira existente com Frase de Recuperação Secreta" - }, "importMyWallet": { "message": "Importar minha carteira" }, @@ -1771,9 +1764,6 @@ "newNFTsDetected": { "message": "Novidade! Detecção de NFT" }, - "newNFTsDetectedInfo": { - "message": "Autorize que a MetaMask detecte NFTs automaticamente do Opensea e os exiba na sua carteira MetaMask." - }, "newNetworkAdded": { "message": "“$1” foi adicionado com sucesso!" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 78573e8ff..ec80758b6 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "Импорт кошелька с помощью секретной фразы для восстановления" }, - "importExistingWalletDescription": { - "message": "Введите секретную фразу для восстановления (также известную как «сид-фраза»), которую вы получили при создании кошелька. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Импортируйте существующий кошелек с помощью секретной фразы для восстановления" - }, "importMyWallet": { "message": "Импорт моего кошелька" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Новинка! Обнаружение NFT" }, - "newNFTsDetectedInfo": { - "message": "Разрешите MetaMask автоматически обнаруживать NFT из Opensea и отображать их в вашем кошельке MetaMask." - }, "newNetworkAdded": { "message": "«$1» успешно добавлен!" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index e6f793c02..0ff209e95 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "Mag-import ng account gamit ang Secret Recovery Phrase" }, - "importExistingWalletDescription": { - "message": "Ilagay ang iyong Secret Recovery Phrase (kilala rin bilang Seed Phrase) na ibinigay sa iyo noong gumawa ka ng iyong wallet. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Mag-import ng umiiral na wallet gamit ang Secret Recovery Phrase" - }, "importMyWallet": { "message": "I-import ang Wallet Ko" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Bago! Pag-detect ng NFT" }, - "newNFTsDetectedInfo": { - "message": "Payagan ang MetaMask na awtomatikong i-detect ang mga NFT mula sa Opensea at ipakita sa iyong MetaMask wallet." - }, "newNetworkAdded": { "message": "Ang “$1” matagumpay na naidagdag!" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 0ff8c2d27..87a748e52 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "Gizli Kurtarma İfadesi ile bir cüzdanı içe aktarın" }, - "importExistingWalletDescription": { - "message": "Cüzdanınızı oluşturduğunuzda size verilen Gizli Kurtarma İfadenizi (başka bir deyişle Tohum İfadesi) girin. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Gizli Kurtarma İfadesi ile mevcut cüzdanı içe aktarın" - }, "importMyWallet": { "message": "Cüzdanımı İçe Aktar" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Yeni! NFT algılama" }, - "newNFTsDetectedInfo": { - "message": "MetaMask'in otomatik olarak Opensea'den NFT'leri algılamasına ve MetaMask cüzdanınızda görüntülemesine izin verin." - }, "newNetworkAdded": { "message": "\"$1\" başarılı bir şekilde eklendi!" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index c94d6d291..f0f5d65cb 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "Nhập một ví bằng Cụm mật khẩu khôi phục bí mật" }, - "importExistingWalletDescription": { - "message": "Nhập Cụm Mật Khẩu Khôi Phục Bí Mật (còn được gọi là Cụm Mật Khẩu Gốc) mà bạn được cấp khi tạo ví. $1", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "Nhập ví hiện tại bằng Cụm Mật Khẩu Khôi Phục Bí Mật" - }, "importMyWallet": { "message": "Nhập Ví Của Tôi" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "Mới! Phát hiện NFT" }, - "newNFTsDetectedInfo": { - "message": "Cho phép MetaMask tự động phát hiện NFT từ Opensea và hiển thị trong ví MetaMask của bạn." - }, "newNetworkAdded": { "message": "“$1” đã được thêm thành công!" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index c144dd3f1..ad530bbd8 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1270,13 +1270,6 @@ "importAccountSeedPhrase": { "message": "使用账户助记词导入账户" }, - "importExistingWalletDescription": { - "message": "输入您创建$1钱包时提供的保密恢复短语(或Seed Phrase)。", - "description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link" - }, - "importExistingWalletTitle": { - "message": "使用账户助记词导入现有钱包" - }, "importMyWallet": { "message": "导入我的钱包" }, @@ -1738,9 +1731,6 @@ "newNFTsDetected": { "message": "新功能!NFT 检测" }, - "newNFTsDetectedInfo": { - "message": "允许 MetaMask自动检测Opensea 的 NFT,并在您的 MetaMask钱包中显示。" - }, "newNetworkAdded": { "message": "成功添加了“$1”!" }, diff --git a/app/images/lock-icon.svg b/app/images/lock-icon.svg new file mode 100644 index 000000000..824974a09 --- /dev/null +++ b/app/images/lock-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/images/token-detection.svg b/app/images/token-detection.svg new file mode 100644 index 000000000..c7246e8f7 --- /dev/null +++ b/app/images/token-detection.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/unlock-icon.svg b/app/images/unlock-icon.svg new file mode 100644 index 000000000..2ad1eadeb --- /dev/null +++ b/app/images/unlock-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index c98c1683c..40ccfb5f0 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -4,6 +4,7 @@ import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'; import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts'; import { MINUTE } from '../../../shared/constants/time'; import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; +import { isTokenDetectionEnabledForNetwork } from '../../../shared/modules/network.utils'; import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; // By default, poll every 3 minutes @@ -24,6 +25,7 @@ export default class DetectTokensController { * @param config.keyringMemStore * @param config.tokenList * @param config.tokensController + * @param config.assetsContractController */ constructor({ interval = DEFAULT_INTERVAL, @@ -32,7 +34,9 @@ export default class DetectTokensController { keyringMemStore, tokenList, tokensController, + assetsContractController = null, } = {}) { + this.assetsContractController = assetsContractController; this.tokensController = tokensController; this.preferences = preferences; this.interval = interval; @@ -44,6 +48,9 @@ export default class DetectTokensController { return token.address; }); this.hiddenTokens = this.tokensController?.state.ignoredTokens; + this.detectedTokens = process.env.TOKEN_DETECTION_V2 + ? this.tokensController?.state.detectedTokens + : []; preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => { if ( @@ -55,14 +62,24 @@ export default class DetectTokensController { this.restartTokenDetection(); } }); - tokensController?.subscribe(({ tokens = [], ignoredTokens = [] }) => { - this.tokenAddresses = tokens.map((token) => { - return token.address; - }); - this.hiddenTokens = ignoredTokens; - }); + tokensController?.subscribe( + ({ tokens = [], ignoredTokens = [], detectedTokens = [] }) => { + this.tokenAddresses = tokens.map((token) => { + return token.address; + }); + this.hiddenTokens = ignoredTokens; + this.detectedTokens = process.env.TOKEN_DETECTION_V2 + ? detectedTokens + : []; + }, + ); } + /** + * TODO: Remove during TOKEN_DETECTION_V2 feature flag clean up + * + * @param tokens + */ async _getTokenBalances(tokens) { const ethContract = this.web3.eth .contract(SINGLE_CALL_BALANCES_ABI) @@ -84,14 +101,23 @@ export default class DetectTokensController { if (!this.isActive) { return; } - + if ( + process.env.TOKEN_DETECTION_V2 && + (!this.useTokenDetection || + !isTokenDetectionEnabledForNetwork( + this._network.store.getState().provider.chainId, + )) + ) { + return; + } const { tokenList } = this._tokenList.state; // since the token detection is currently enabled only on Mainnet // we can use the chainId check to ensure token detection is not triggered for any other network // but once the balance check contract for other networks are deploayed and ready to use, we need to update this check. if ( - this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID || - Object.keys(tokenList).length === 0 + !process.env.TOKEN_DETECTION_V2 && + (this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID || + Object.keys(tokenList).length === 0) ) { return; } @@ -105,6 +131,9 @@ export default class DetectTokensController { ) && !this.hiddenTokens.find((address) => isEqualCaseInsensitive(address, tokenAddress), + ) && + !this.detectedTokens.find(({ address }) => + isEqualCaseInsensitive(address, tokenAddress), ) ) { tokensToDetect.push(tokenAddress); @@ -117,7 +146,12 @@ export default class DetectTokensController { for (const tokensSlice of sliceOfTokensToDetect) { let result; try { - result = await this._getTokenBalances(tokensSlice); + result = process.env.TOKEN_DETECTION_V2 + ? await this.assetsContractController.getBalancesInSingleCall( + this.selectedAddress, + tokensSlice, + ) + : await this._getTokenBalances(tokensSlice); } catch (error) { warn( `MetaMask - DetectTokensController single call balance fetch failed`, @@ -126,20 +160,45 @@ export default class DetectTokensController { return; } - const tokensWithBalance = tokensSlice.filter((_, index) => { - const balance = result[index]; - return balance && !balance.isZero(); - }); - - await Promise.all( - tokensWithBalance.map((tokenAddress) => { - return this.tokensController.addToken( - tokenAddress, - tokenList[tokenAddress].symbol, - tokenList[tokenAddress].decimals, - ); - }), - ); + let tokensWithBalance = []; + if (process.env.TOKEN_DETECTION_V2) { + if (result) { + const nonZeroTokenAddresses = Object.keys(result); + for (const nonZeroTokenAddress of nonZeroTokenAddresses) { + const { + address, + symbol, + decimals, + iconUrl, + aggregators, + } = tokenList[nonZeroTokenAddress]; + tokensWithBalance.push({ + address, + symbol, + decimals, + image: iconUrl, + aggregators, + }); + } + if (tokensWithBalance.length > 0) { + await this.tokensController.addDetectedTokens(tokensWithBalance); + } + } + } else { + tokensWithBalance = tokensSlice.filter((_, index) => { + const balance = result[index]; + return balance && !balance.isZero(); + }); + await Promise.all( + tokensWithBalance.map((tokenAddress) => { + return this.tokensController.addToken( + tokenAddress, + tokenList[tokenAddress].symbol, + tokenList[tokenAddress].decimals, + ); + }), + ); + } } } diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 830a980d0..555576d00 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -1,4 +1,4 @@ -import { isEqual, merge, omit, omitBy, pickBy } from 'lodash'; +import { isEqual, merge, omit, omitBy, pickBy, size, sum } from 'lodash'; import { ObservableStore } from '@metamask/obs-store'; import { bufferToHex, keccak } from 'ethereumjs-util'; import { generateUUID } from 'pubnub'; @@ -31,6 +31,7 @@ const exceptionsToFilter = { * @typedef {import('../../../shared/constants/metametrics').MetaMetricsPagePayload} MetaMetricsPagePayload * @typedef {import('../../../shared/constants/metametrics').MetaMetricsPageOptions} MetaMetricsPageOptions * @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventFragment} MetaMetricsEventFragment + * @typedef {import('../../../shared/constants/metametrics').MetaMetricsTraits} MetaMetricsTraits */ /** @@ -281,6 +282,30 @@ export default class MetaMetricsController { this.store.updateState({ fragments }); } + /** + * Calls this._identify with validated metaMetricsId and user traits if user is participating + * in the MetaMetrics analytics program + * + * @param {Object} userTraits + */ + identify(userTraits) { + const { metaMetricsId, participateInMetaMetrics } = this.state; + + if (!participateInMetaMetrics || !metaMetricsId || !userTraits) { + return; + } + if (typeof userTraits !== 'object') { + console.warn( + `MetaMetricsController#identify: userTraits parameter must be an object. Received type: ${typeof userTraits}`, + ); + return; + } + + const allValidTraits = this._buildValidTraits(userTraits); + + this._identify(allValidTraits); + } + /** * Setter for the `participateInMetaMetrics` property * @@ -434,7 +459,7 @@ export default class MetaMetricsController { handleMetaMaskStateUpdate(newState) { const userTraits = this._buildUserTraitsObject(newState); if (userTraits) { - // this.identify(userTraits); + this.identify(userTraits); } } @@ -507,14 +532,33 @@ export default class MetaMetricsController { }; } + /** + * This method generates the MetaMetrics user traits object, omitting any + * traits that have not changed since the last invocation of this method. + * + * @param {object} metamaskState - Full metamask state object. + * @returns {MetaMetricsTraits | null} traits that have changed since last update + */ _buildUserTraitsObject(metamaskState) { + /** + * @type {MetaMetricsTraits} + */ const currentTraits = { + [TRAITS.ADDRESS_BOOK_ENTRIES]: sum( + Object.values(metamaskState.addressBook).map(size), + ), [TRAITS.LEDGER_CONNECTION_TYPE]: metamaskState.ledgerTransportType, - [TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities) - .length, [TRAITS.NETWORKS_ADDED]: metamaskState.frequentRpcListDetail.map( (rpc) => rpc.chainId, ), + [TRAITS.NFT_AUTODETECTION_ENABLED]: metamaskState.useCollectibleDetection, + [TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities) + .length, + [TRAITS.NUMBER_OF_NFT_COLLECTIONS]: this._getNumberOfNFtCollection( + metamaskState, + ), + [TRAITS.NUMBER_OF_TOKENS]: this._getNumberOfTokens(metamaskState), + [TRAITS.OPENSEA_API_ENABLED]: metamaskState.openSeaEnabled, [TRAITS.THREE_BOX_ENABLED]: metamaskState.threeBoxSyncingAllowed, [TRAITS.THEME]: metamaskState.theme || 'default', }; @@ -536,6 +580,135 @@ export default class MetaMetricsController { return null; } + /** + * Returns a new object of all valid user traits. For dates, we transform them into ISO-8601 timestamp strings. + * + * @see {@link https://segment.com/docs/connections/spec/common/#timestamps} + * @param {Object} userTraits + * @returns {Object} + */ + _buildValidTraits(userTraits) { + return Object.entries(userTraits).reduce((validTraits, [key, value]) => { + if (this._isValidTraitDate(value)) { + validTraits[key] = value.toISOString(); + } else if (this._isValidTrait(value)) { + validTraits[key] = value; + } else { + console.warn( + `MetaMetricsController: "${key}" value is not a valid trait type`, + ); + } + return validTraits; + }, {}); + } + + /** + * + * @param {object} metamaskState + * @returns number of unique collectible addresses + */ + _getNumberOfNFtCollection(metamaskState) { + const { allCollectibles } = metamaskState; + if (!allCollectibles) { + return 0; + } + + const allAddresses = Object.values(allCollectibles) + .flatMap((chainCollectibles) => Object.values(chainCollectibles)) + .flat() + .map((collectible) => collectible.address); + const unique = [...new Set(allAddresses)]; + return unique.length; + } + + /** + * @param {object} metamaskState + * @returns number of unique token addresses + */ + _getNumberOfTokens(metamaskState) { + return Object.values(metamaskState.allTokens).reduce( + (result, accountsByChain) => { + return result + sum(Object.values(accountsByChain).map(size)); + }, + 0, + ); + } + + /** + * Calls segment.identify with given user traits + * + * @see {@link https://segment.com/docs/connections/sources/catalog/libraries/server/node/#identify} + * @private + * @param {Object} userTraits + */ + _identify(userTraits) { + const { metaMetricsId } = this.state; + + if (!userTraits || Object.keys(userTraits).length === 0) { + console.warn('MetaMetricsController#_identify: No userTraits found'); + return; + } + + try { + this.segment.identify({ + userId: metaMetricsId, + traits: userTraits, + }); + } catch (err) { + this._captureException(err); + } + } + + /** + * Validates the trait value. Segment accepts any data type. We are adding validation here to + * support data types for our Segment destination(s) e.g. MixPanel + * + * @param {*} value + * @returns {boolean} + */ + _isValidTrait(value) { + const type = typeof value; + + return ( + type === 'string' || + type === 'boolean' || + type === 'number' || + this._isValidTraitArray(value) || + this._isValidTraitDate(value) + ); + } + + /** + * Segment accepts any data type value. We have special logic to validate arrays. + * + * @param {*} value + * @returns {boolean} + */ + _isValidTraitArray = (value) => { + return ( + Array.isArray(value) && + (value.every((element) => { + return typeof element === 'string'; + }) || + value.every((element) => { + return typeof element === 'boolean'; + }) || + value.every((element) => { + return typeof element === 'number'; + })) + ); + }; + + /** + * Returns true if the value is an accepted date type + * + * @param {*} value + * @returns {boolean} + */ + _isValidTraitDate = (value) => { + return Object.prototype.toString.call(value) === '[object Date]'; + }; + /** * Perform validation on the payload and update the id type to use before * sending to Segment. Also examines the options to route and handle the diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 355d00f97..37269d6fc 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -23,6 +23,20 @@ const FAKE_CHAIN_ID = '0x1338'; const LOCALE = 'en_US'; const TEST_META_METRICS_ID = '0xabc'; +const MOCK_TRAITS = { + test_boolean: true, + test_string: 'abc', + test_number: 123, + test_bool_array: [true, true, false], + test_string_array: ['test', 'test', 'test'], + test_boolean_array: [1, 2, 3], +}; + +const MOCK_INVALID_TRAITS = { + test_null: null, + test_array_multi_types: [true, 'a', 1], +}; + const DEFAULT_TEST_CONTEXT = { app: { name: 'MetaMask Extension', version: VERSION }, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, @@ -216,6 +230,78 @@ describe('MetaMetricsController', function () { }); }); + describe('identify', function () { + it('should call segment.identify for valid traits if user is participating in metametrics', async function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + metaMetricsId: TEST_META_METRICS_ID, + }); + const mock = sinon.mock(segment); + + mock + .expects('identify') + .once() + .withArgs({ userId: TEST_META_METRICS_ID, traits: MOCK_TRAITS }); + + metaMetricsController.identify({ + ...MOCK_TRAITS, + ...MOCK_INVALID_TRAITS, + }); + mock.verify(); + }); + + it('should transform date type traits into ISO-8601 timestamp strings', async function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + metaMetricsId: TEST_META_METRICS_ID, + }); + const mock = sinon.mock(segment); + + const mockDate = new Date(); + const mockDateISOString = mockDate.toISOString(); + + mock + .expects('identify') + .once() + .withArgs({ + userId: TEST_META_METRICS_ID, + traits: { + test_date: mockDateISOString, + }, + }); + + metaMetricsController.identify({ + test_date: mockDate, + }); + mock.verify(); + }); + + it('should not call segment.identify if user is not participating in metametrics', function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: false, + }); + const mock = sinon.mock(segment); + + mock.expects('identify').never(); + + metaMetricsController.identify(MOCK_TRAITS); + mock.verify(); + }); + + it('should not call segment.identify if there are no valid traits to identify', async function () { + const metaMetricsController = getMetaMetricsController({ + participateInMetaMetrics: true, + metaMetricsId: TEST_META_METRICS_ID, + }); + const mock = sinon.mock(segment); + + mock.expects('identify').never(); + + metaMetricsController.identify(MOCK_INVALID_TRAITS); + mock.verify(); + }); + }); + describe('setParticipateInMetaMetrics', function () { it('should update the value of participateInMetaMetrics', function () { const metaMetricsController = getMetaMetricsController({ @@ -525,23 +611,88 @@ describe('MetaMetricsController', function () { describe('_buildUserTraitsObject', function () { it('should return full user traits object on first call', function () { + const MOCK_ALL_TOKENS = { + '0x1': { + '0x1235ce91d74254f29d4609f25932fe6d97bf4842': [ + { + address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91', + }, + { + address: '0xabc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', + }, + ], + '0xe364b0f9d1879e53e8183055c9d7dd2b7375d86b': [ + { + address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91', + }, + ], + }, + '0x4': { + '0x1235ce91d74254f29d4609f25932fe6d97bf4842': [ + { + address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91', + }, + { + address: '0x12317F958D2ee523a2206206994597C13D831ec7', + }, + ], + }, + }; + const metaMetricsController = getMetaMetricsController(); const traits = metaMetricsController._buildUserTraitsObject({ + addressBook: { + [MAINNET_CHAIN_ID]: [{ address: '0x' }], + [ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }], + }, + allCollectibles: { + '0xac706cE8A9BF27Afecf080fB298d0ee13cfb978A': { + 56: [ + { + address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91', + tokenId: '100', + }, + { + address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91', + tokenId: '101', + }, + { + address: '0x7488d2ce5deb26db021285b50b661d655eb3d3d9', + tokenId: '99', + }, + ], + }, + '0xe04AB39684A24D8D4124b114F3bd6FBEB779cacA': { + 69: [ + { + address: '0x63d646bc7380562376d5de205123a57b1718184d', + tokenId: '14', + }, + ], + }, + }, + allTokens: MOCK_ALL_TOKENS, frequentRpcListDetail: [ { chainId: MAINNET_CHAIN_ID }, { chainId: ROPSTEN_CHAIN_ID }, ], - ledgerTransportType: 'web-hid', identities: [{}, {}], + ledgerTransportType: 'web-hid', + openSeaEnabled: true, threeBoxSyncingAllowed: false, + useCollectibleDetection: false, theme: 'default', }); assert.deepEqual(traits, { - [TRAITS.THREE_BOX_ENABLED]: false, + [TRAITS.ADDRESS_BOOK_ENTRIES]: 3, [TRAITS.LEDGER_CONNECTION_TYPE]: 'web-hid', - [TRAITS.NUMBER_OF_ACCOUNTS]: 2, [TRAITS.NETWORKS_ADDED]: [MAINNET_CHAIN_ID, ROPSTEN_CHAIN_ID], + [TRAITS.NFT_AUTODETECTION_ENABLED]: false, + [TRAITS.NUMBER_OF_ACCOUNTS]: 2, + [TRAITS.NUMBER_OF_NFT_COLLECTIONS]: 3, + [TRAITS.NUMBER_OF_TOKENS]: 5, + [TRAITS.OPENSEA_API_ENABLED]: true, [TRAITS.THREE_BOX_ENABLED]: false, [TRAITS.THEME]: 'default', }); @@ -550,53 +701,86 @@ describe('MetaMetricsController', function () { it('should return only changed traits object on subsequent calls', function () { const metaMetricsController = getMetaMetricsController(); metaMetricsController._buildUserTraitsObject({ + addressBook: { + [MAINNET_CHAIN_ID]: [{ address: '0x' }], + [ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }], + }, + allTokens: {}, frequentRpcListDetail: [ { chainId: MAINNET_CHAIN_ID }, { chainId: ROPSTEN_CHAIN_ID }, ], ledgerTransportType: 'web-hid', + openSeaEnabled: true, identities: [{}, {}], threeBoxSyncingAllowed: false, + useCollectibleDetection: false, theme: 'default', }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ + addressBook: { + [MAINNET_CHAIN_ID]: [{ address: '0x' }, { address: '0x1' }], + [ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }], + }, + allTokens: { + '0x1': { '0xabcde': [{ '0x12345': { address: '0xtestAddress' } }] }, + }, frequentRpcListDetail: [ { chainId: MAINNET_CHAIN_ID }, { chainId: ROPSTEN_CHAIN_ID }, ], ledgerTransportType: 'web-hid', + openSeaEnabled: false, identities: [{}, {}, {}], threeBoxSyncingAllowed: false, + useCollectibleDetection: false, theme: 'default', }); assert.deepEqual(updatedTraits, { + [TRAITS.ADDRESS_BOOK_ENTRIES]: 4, [TRAITS.NUMBER_OF_ACCOUNTS]: 3, + [TRAITS.NUMBER_OF_TOKENS]: 1, + [TRAITS.OPENSEA_API_ENABLED]: false, }); }); it('should return null if no traits changed', function () { const metaMetricsController = getMetaMetricsController(); metaMetricsController._buildUserTraitsObject({ + addressBook: { + [MAINNET_CHAIN_ID]: [{ address: '0x' }], + [ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }], + }, + allTokens: {}, frequentRpcListDetail: [ { chainId: MAINNET_CHAIN_ID }, { chainId: ROPSTEN_CHAIN_ID }, ], ledgerTransportType: 'web-hid', + openSeaEnabled: true, identities: [{}, {}], threeBoxSyncingAllowed: false, + useCollectibleDetection: true, theme: 'default', }); const updatedTraits = metaMetricsController._buildUserTraitsObject({ + addressBook: { + [MAINNET_CHAIN_ID]: [{ address: '0x' }], + [ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }], + }, + allTokens: {}, frequentRpcListDetail: [ { chainId: MAINNET_CHAIN_ID }, { chainId: ROPSTEN_CHAIN_ID }, ], ledgerTransportType: 'web-hid', + openSeaEnabled: true, identities: [{}, {}], threeBoxSyncingAllowed: false, + useCollectibleDetection: true, theme: 'default', }); diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 73e3b7963..2b0fdbb41 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -38,7 +38,7 @@ export default class PreferencesController { // set to true means the dynamic list from the API is being used // set to false will be using the static list from contract-metadata - useTokenDetection: false, + useTokenDetection: Boolean(process.env.TOKEN_DETECTION_V2), useCollectibleDetection: false, openSeaEnabled: false, advancedGasFee: null, diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 964be3bdb..27cf59277 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -361,13 +361,30 @@ export default class TransactionController extends EventEmitter { return transactions[txId]; } - _checkIfTxStatusIsUnapproved(txId) { + /** + * @param {number} txId + * @returns {boolean} + */ + _isUnapprovedTransaction(txId) { return ( this.txStateManager.getTransaction(txId).status === TRANSACTION_STATUSES.UNAPPROVED ); } + /** + * @param {number} txId + * @param {string} fnName + */ + _throwErrorIfNotUnapprovedTx(txId, fnName) { + if (!this._isUnapprovedTransaction(txId)) { + throw new Error( + `TransactionsController: Can only call ${fnName} on an unapproved transaction. + Current tx status: ${this.txStateManager.getTransaction(txId).status}`, + ); + } + } + _updateTransaction(txId, proposedUpdate, note) { const txMeta = this.txStateManager.getTransaction(txId); const updated = merge(txMeta, proposedUpdate); @@ -416,11 +433,7 @@ export default class TransactionController extends EventEmitter { * @returns {TransactionMeta} the txMeta of the updated transaction */ updateEditableParams(txId, { data, from, to, value, gas, gasPrice }) { - if (!this._checkIfTxStatusIsUnapproved(txId)) { - throw new Error( - 'Cannot call updateEditableParams on a transaction that is not in an unapproved state', - ); - } + this._throwErrorIfNotUnapprovedTx(txId, 'updateEditableParams'); const editableParams = { txParams: { @@ -474,11 +487,7 @@ export default class TransactionController extends EventEmitter { userFeeLevel, }, ) { - if (!this._checkIfTxStatusIsUnapproved(txId)) { - throw new Error( - 'Cannot call updateTransactionGasFees on a transaction that is not in an unapproved state', - ); - } + this._throwErrorIfNotUnapprovedTx(txId, 'updateTransactionGasFees'); let txGasFees = { txParams: { @@ -517,11 +526,10 @@ export default class TransactionController extends EventEmitter { txId, { estimatedBaseFee, decEstimatedBaseFee }, ) { - if (!this._checkIfTxStatusIsUnapproved(txId)) { - throw new Error( - 'Cannot call updateTransactionEstimatedBaseFee on a transaction that is not in an unapproved state', - ); - } + this._throwErrorIfNotUnapprovedTx( + txId, + 'updateTransactionEstimatedBaseFee', + ); let txEstimateBaseFees = { estimatedBaseFee, decEstimatedBaseFee }; // only update what is defined @@ -543,11 +551,7 @@ export default class TransactionController extends EventEmitter { * @returns {TransactionMeta} the txMeta of the updated transaction */ updateSwapApprovalTransaction(txId, { type, sourceTokenSymbol }) { - if (!this._checkIfTxStatusIsUnapproved(txId)) { - throw new Error( - 'Cannot call updateSwapApprovalTransaction on a transaction that is not in an unapproved state', - ); - } + this._throwErrorIfNotUnapprovedTx(txId, 'updateSwapApprovalTransaction'); let swapApprovalTransaction = { type, sourceTokenSymbol }; // only update what is defined @@ -589,11 +593,7 @@ export default class TransactionController extends EventEmitter { approvalTxId, }, ) { - if (!this._checkIfTxStatusIsUnapproved(txId)) { - throw new Error( - 'Cannot call updateSwapTransaction on a transaction that is not in an unapproved state', - ); - } + this._throwErrorIfNotUnapprovedTx(txId, 'updateSwapTransaction'); let swapTransaction = { sourceTokenSymbol, @@ -625,11 +625,7 @@ export default class TransactionController extends EventEmitter { * @returns {TransactionMeta} the txMeta of the updated transaction */ updateTransactionUserSettings(txId, { userEditedGasLimit, userFeeLevel }) { - if (!this._checkIfTxStatusIsUnapproved(txId)) { - throw new Error( - 'Cannot call updateTransactionUserSettings on a transaction that is not in an unapproved state', - ); - } + this._throwErrorIfNotUnapprovedTx(txId, 'updateTransactionUserSettings'); let userSettings = { userEditedGasLimit, userFeeLevel }; // only update what is defined diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 0d99759bf..ce0a3339f 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -2186,10 +2186,10 @@ describe('Transaction Controller', function () { assert.equal(result.userFeeLevel, 'high'); }); - it('throws error if status is not unapproved', function () { + it('should not update and should throw error if status is not type "unapproved"', function () { txStateManager.addTransaction({ id: '4', - status: TRANSACTION_STATUSES.APPROVED, + status: TRANSACTION_STATUSES.DROPPED, metamaskNetworkId: currentNetworkId, txParams: { maxPriorityFeePerGas: '0x007', @@ -2200,14 +2200,18 @@ describe('Transaction Controller', function () { estimateUsed: '0x009', }); - try { - txController.updateTransactionGasFees('4', { maxFeePerGas: '0x0088' }); - } catch (e) { - assert.equal( - e.message, - 'Cannot call updateTransactionGasFees on a transaction that is not in an unapproved state', - ); - } + assert.throws( + () => + txController.updateTransactionGasFees('4', { + maxFeePerGas: '0x0088', + }), + Error, + `TransactionsController: Can only call updateTransactionGasFees on an unapproved transaction. + Current tx status: ${TRANSACTION_STATUSES.DROPPED}`, + ); + + const transaction = txStateManager.getTransaction('4'); + assert.equal(transaction.txParams.maxFeePerGas, '0x008'); }); it('does not update unknown parameters in update method', function () { diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js new file mode 100644 index 000000000..0b47c3462 --- /dev/null +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -0,0 +1,86 @@ +import { EVENT_NAMES } from '../../../shared/constants/metametrics'; +import { SECOND } from '../../../shared/constants/time'; + +const USER_PROMPTED_EVENT_NAME_MAP = { + eth_signTypedData_v4: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_signTypedData_v3: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_signTypedData: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_personal_sign: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_sign: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_getEncryptionPublicKey: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED, + eth_decrypt: EVENT_NAMES.DECRYPTION_REQUESTED, + wallet_requestPermissions: EVENT_NAMES.PERMISSIONS_REQUESTED, + eth_requestAccounts: EVENT_NAMES.PERMISSIONS_REQUESTED, +}; + +const samplingTimeouts = {}; + +/** + * Returns a middleware that tracks inpage_provider usage using sampling for + * each type of event except those that require user interaction, such as + * signature requests + * + * @param {object} opts - options for the rpc method tracking middleware + * @param {Function} opts.trackEvent - trackEvent method from MetaMetricsController + * @param {Function} opts.getMetricsState - get the state of MetaMetricsController + * @returns {Function} + */ +export default function createRPCMethodTrackingMiddleware({ + trackEvent, + getMetricsState, +}) { + return function rpcMethodTrackingMiddleware( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {Function} */ next, + ) { + const startTime = Date.now(); + const { origin } = req; + + next((callback) => { + const endTime = Date.now(); + if (!getMetricsState().participateInMetaMetrics) { + return callback(); + } + if (USER_PROMPTED_EVENT_NAME_MAP[req.method]) { + const userRejected = res.error?.code === 4001; + trackEvent({ + event: USER_PROMPTED_EVENT_NAME_MAP[req.method], + category: 'inpage_provider', + referrer: { + url: origin, + }, + properties: { + method: req.method, + status: userRejected ? 'rejected' : 'approved', + error_code: res.error?.code, + error_message: res.error?.message, + has_result: typeof res.result !== 'undefined', + duration: endTime - startTime, + }, + }); + } else if (typeof samplingTimeouts[req.method] === 'undefined') { + trackEvent({ + event: 'Provider Method Called', + category: 'inpage_provider', + referrer: { + url: origin, + }, + properties: { + method: req.method, + error_code: res.error?.code, + error_message: res.error?.message, + has_result: typeof res.result !== 'undefined', + duration: endTime - startTime, + }, + }); + // Only record one call to this method every ten seconds to avoid + // overloading network requests. + samplingTimeouts[req.method] = setTimeout(() => { + delete samplingTimeouts[req.method]; + }, SECOND * 10); + } + return callback(); + }); + }; +} diff --git a/app/scripts/lib/segment.js b/app/scripts/lib/segment.js index bb52c42d8..1b054333b 100644 --- a/app/scripts/lib/segment.js +++ b/app/scripts/lib/segment.js @@ -1,9 +1,6 @@ import Analytics from 'analytics-node'; import { SECOND } from '../../../shared/constants/time'; -const isDevEnvironment = Boolean( - process.env.METAMASK_DEBUG && !process.env.IN_TEST, -); const SEGMENT_WRITE_KEY = process.env.SEGMENT_WRITE_KEY ?? null; const SEGMENT_HOST = process.env.SEGMENT_HOST ?? null; @@ -81,11 +78,10 @@ export const createSegmentMock = (flushAt = SEGMENT_FLUSH_AT) => { return segmentMock; }; -export const segment = - !SEGMENT_WRITE_KEY || (isDevEnvironment && !SEGMENT_HOST) - ? createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL) - : new Analytics(SEGMENT_WRITE_KEY, { - host: SEGMENT_HOST, - flushAt: SEGMENT_FLUSH_AT, - flushInterval: SEGMENT_FLUSH_INTERVAL, - }); +export const segment = SEGMENT_WRITE_KEY + ? new Analytics(SEGMENT_WRITE_KEY, { + host: SEGMENT_HOST, + flushAt: SEGMENT_FLUSH_AT, + flushInterval: SEGMENT_FLUSH_INTERVAL, + }) + : createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c307068d2..61b35e7f7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -136,6 +136,7 @@ import { buildSnapRestrictedMethodSpecifications, ///: END:ONLY_INCLUDE_IN } from './controllers/permissions'; +import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -235,16 +236,35 @@ export default class MetamaskController extends EventEmitter { config: { provider: this.provider }, state: initState.TokensController, }); - - this.assetsContractController = new AssetsContractController( - { - onPreferencesStateChange: (listener) => - this.preferencesController.store.subscribe(listener), - }, - { - provider: this.provider, - }, - ); + process.env.TOKEN_DETECTION_V2 + ? (this.assetsContractController = new AssetsContractController({ + onPreferencesStateChange: (listener) => + this.preferencesController.store.subscribe(listener), + onNetworkStateChange: (cb) => + this.networkController.store.subscribe((networkState) => { + const modifiedNetworkState = { + ...networkState, + provider: { + ...networkState.provider, + chainId: hexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }), + config: { + provider: this.provider, + }, + state: initState.AssetsContractController, + })) + : (this.assetsContractController = new AssetsContractController( + { + onPreferencesStateChange: (listener) => + this.preferencesController.store.subscribe(listener), + }, + { + provider: this.provider, + }, + )); this.collectiblesController = new CollectiblesController( { @@ -385,33 +405,50 @@ export default class MetamaskController extends EventEmitter { const tokenListMessenger = this.controllerMessenger.getRestricted({ name: 'TokenListController', }); - this.tokenListController = new TokenListController({ - chainId: hexToDecimal(this.networkController.getCurrentChainId()), - useStaticTokenList: !this.preferencesController.store.getState() - .useTokenDetection, - onNetworkStateChange: (cb) => - this.networkController.store.subscribe((networkState) => { - const modifiedNetworkState = { - ...networkState, - provider: { - ...networkState.provider, - chainId: hexToDecimal(networkState.provider.chainId), - }, - }; - return cb(modifiedNetworkState); - }), - onPreferencesStateChange: (cb) => - this.preferencesController.store.subscribe((preferencesState) => { - const modifiedPreferencesState = { - ...preferencesState, - useStaticTokenList: !this.preferencesController.store.getState() - .useTokenDetection, - }; - return cb(modifiedPreferencesState); - }), - messenger: tokenListMessenger, - state: initState.TokenListController, - }); + process.env.TOKEN_DETECTION_V2 + ? (this.tokenListController = new TokenListController({ + chainId: hexToDecimal(this.networkController.getCurrentChainId()), + onNetworkStateChange: (cb) => + this.networkController.store.subscribe((networkState) => { + const modifiedNetworkState = { + ...networkState, + provider: { + ...networkState.provider, + chainId: hexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }), + messenger: tokenListMessenger, + state: initState.TokenListController, + })) + : (this.tokenListController = new TokenListController({ + chainId: hexToDecimal(this.networkController.getCurrentChainId()), + useStaticTokenList: !this.preferencesController.store.getState() + .useTokenDetection, + onNetworkStateChange: (cb) => + this.networkController.store.subscribe((networkState) => { + const modifiedNetworkState = { + ...networkState, + provider: { + ...networkState.provider, + chainId: hexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }), + onPreferencesStateChange: (cb) => + this.preferencesController.store.subscribe((preferencesState) => { + const modifiedPreferencesState = { + ...preferencesState, + useStaticTokenList: !this.preferencesController.store.getState() + .useTokenDetection, + }; + return cb(modifiedPreferencesState); + }), + messenger: tokenListMessenger, + state: initState.TokenListController, + })); this.phishingController = new PhishingController(); @@ -594,7 +631,7 @@ export default class MetamaskController extends EventEmitter { this.workerController = new IframeExecutionService({ onError: this.onExecutionEnvironmentError.bind(this), iframeUrl: new URL( - 'https://metamask.github.io/iframe-execution-environment/0.4.2', + 'https://metamask.github.io/iframe-execution-environment/0.4.3', ), messenger: this.controllerMessenger.getRestricted({ name: 'ExecutionService', @@ -659,13 +696,22 @@ export default class MetamaskController extends EventEmitter { }); ///: END:ONLY_INCLUDE_IN - this.detectTokensController = new DetectTokensController({ - preferences: this.preferencesController, - tokensController: this.tokensController, - network: this.networkController, - keyringMemStore: this.keyringController.memStore, - tokenList: this.tokenListController, - }); + process.env.TOKEN_DETECTION_V2 + ? (this.detectTokensController = new DetectTokensController({ + preferences: this.preferencesController, + tokensController: this.tokensController, + assetsContractController: this.assetsContractController, + network: this.networkController, + keyringMemStore: this.keyringController.memStore, + tokenList: this.tokenListController, + })) + : (this.detectTokensController = new DetectTokensController({ + preferences: this.preferencesController, + tokensController: this.tokensController, + network: this.networkController, + keyringMemStore: this.keyringController.memStore, + tokenList: this.tokenListController, + })); this.addressBookController = new AddressBookController( undefined, @@ -1029,10 +1075,10 @@ export default class MetamaskController extends EventEmitter { 'SnapController:add', ), clearSnapState: (fromSubject) => - this.controllerMessenger( - 'SnapController:updateSnap', + this.controllerMessenger.call( + 'SnapController:updateSnapState', fromSubject, - {}, + null, ), getMnemonic: this.getPrimaryKeyringMnemonic.bind(this), getSnap: this.controllerMessenger.call.bind( @@ -1043,16 +1089,10 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getRpcMessageHandler', ), - getSnapState: async (...args) => { - // TODO:flask Just return the action result directly in the next - // @metamask/snap-controllers update. - return ( - (await this.controllerMessenger.call( - 'SnapController:getSnapState', - ...args, - )) ?? null - ); - }, + getSnapState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:getSnapState', + ), showConfirmation: (origin, confirmationData) => this.approvalController.addAndShowApprovalRequest({ origin, @@ -1337,6 +1377,7 @@ export default class MetamaskController extends EventEmitter { tokensController, smartTransactionsController, txController, + assetsContractController, } = this; return { @@ -1816,6 +1857,22 @@ export default class MetamaskController extends EventEmitter { collectibleDetectionController, ) : null, + + /** Token Detection V2 */ + addDetectedTokens: process.env.TOKEN_DETECTION_V2 + ? tokensController.addDetectedTokens.bind(tokensController) + : null, + importTokens: process.env.TOKEN_DETECTION_V2 + ? tokensController.importTokens.bind(tokensController) + : null, + ignoreTokens: process.env.TOKEN_DETECTION_V2 + ? tokensController.ignoreTokens.bind(tokensController) + : null, + getBalancesInSingleCall: process.env.TOKEN_DETECTION_V2 + ? assetsContractController.getBalancesInSingleCall.bind( + assetsContractController, + ) + : null, }; } @@ -3357,6 +3414,17 @@ export default class MetamaskController extends EventEmitter { engine.push(createLoggerMiddleware({ origin })); engine.push(this.permissionLogController.createMiddleware()); + engine.push( + createRPCMethodTrackingMiddleware({ + trackEvent: this.metaMetricsController.trackEvent.bind( + this.metaMetricsController, + ), + getMetricsState: this.metaMetricsController.store.getState.bind( + this.metaMetricsController.store, + ), + }), + ); + // onboarding if (subjectType === SUBJECT_TYPES.WEBSITE) { engine.push( diff --git a/crowdin.yml b/crowdin.yml index 60bc9c2fc..25f294025 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,7 +1,6 @@ "project_id_env": CROWDIN_PROJECT_ID "api_token_env": CROWDIN_PERSONAL_TOKEN "base_path" : "." -"base_url" : "https://metamask.crowdin.com" "preserve_hierarchy": true diff --git a/development/README.md b/development/README.md index a8e4eb6fc..16df8fb89 100644 --- a/development/README.md +++ b/development/README.md @@ -3,3 +3,54 @@ Several files which are needed for developing on MetaMask. Usually each file or directory contains information about its scope / usage. + +## Segment + +### Debugging with the Mock Segment API + +To start the [Mock Segment API](./mock-segment.js): + +- Add/replace the `SEGMENT_HOST` and `SEGMENT_WRITE_KEY` variables in `.metamaskrc` + ``` + SEGMENT_HOST='http://localhost:9090' + SEGMENT_WRITE_KEY='FAKE' + ``` +- Build the project to the `./dist/` folder with `yarn dist` +- Run the Mock Segment API from the command line + ``` + node development/mock-segment.js + ``` + +Events triggered whilst using the extension will be logged to the console of the Mock Segment API. + +More information on the API and its usage can be found [here](./mock-segment.js#L28). + +### Debugging in Segment + +To debug in a production Segment environment: + +- Create a free account on [Segment](https://segment.com/) +- Create a New Workspace +- Add a Source (Node.js) +- Copy the `Write Key` from the API Keys section under Settings +- Add/replace the `SEGMENT_HOST` and `SEGMENT_WRITE_KEY` variables in `.metamaskrc` + ``` + SEGMENT_HOST='https://api.segment.io' + SEGMENT_WRITE_KEY='COPIED_WRITE_KEY' + ``` +- Build the project to the `./dist/` folder with `yarn dist` + +Events triggered whilst using the extension will be displayed in Segment's Debugger. + +### Debugging Segment requests in MetaMask + +To opt in to MetaMetrics; +- Unlock the extension +- Open the Account menu +- Click the `Settings` menu item +- Click the `Security & Privacy` menu item +- Toggle the `Participate in MetaMetrics` menu option to the `ON` position + +You can inspect the requests in the `Network` tab of your browser's Developer Tools (background.html) +by filtering for `POST` requests to `/v1/batch`. The full url will be `http://localhost:9090/v1/batch` +or `https://api.segment.io/v1/batch` respectively. diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index 8f1a0bece..9310a92d8 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -19,6 +19,8 @@ async function start() { console.log('CIRCLE_SHA1', CIRCLE_SHA1); const { CIRCLE_BUILD_NUM } = process.env; console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM); + const { CIRCLE_WORKFLOW_JOB_ID } = process.env; + console.log('CIRCLE_WORKFLOW_JOB_ID', CIRCLE_WORKFLOW_JOB_ID); if (!CIRCLE_PULL_REQUEST) { console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`); @@ -27,7 +29,7 @@ async function start() { const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop(); const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7); - const BUILD_LINK_BASE = `https://${CIRCLE_BUILD_NUM}-42009758-gh.circle-artifacts.com/0`; + const BUILD_LINK_BASE = `https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0`; // build the github comment content diff --git a/docs/README.md b/docs/README.md index 6c54c98cf..4e87d90c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,8 +13,5 @@ To learn how to develop MetaMask-compatible applications, visit our [Developer D - [How to add custom build to Chrome](./add-to-chrome.md) - [How to add custom build to Firefox](./add-to-firefox.md) - [Publishing Guide](./publishing.md) -- [How to live reload on local dependency changes](./developing-on-deps.md) -- [How to add new networks to the Provider Menu](./adding-new-networks.md) -- [How to port MetaMask to a new platform](./porting_to_new_environment.md) -- [How to generate a visualization of this repository's development](./development-visualization.md) - [How to add a feature behind a secret feature flag](./secret-preferences.md) +- [Developing on MetaMask](../development/README.md) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 7b93e1ecd..c343f5241 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -585,8 +585,7 @@ "console.log": true, "document.createElement": true, "document.head.appendChild": true, - "fetch": true, - "removeEventListener": true + "fetch": true }, "packages": { "@ethereumjs/tx": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index de99bb329..5346933d0 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -585,8 +585,7 @@ "console.log": true, "document.createElement": true, "document.head.appendChild": true, - "fetch": true, - "removeEventListener": true + "fetch": true }, "packages": { "@ethereumjs/tx": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 7b93e1ecd..c343f5241 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -585,8 +585,7 @@ "console.log": true, "document.createElement": true, "document.head.appendChild": true, - "fetch": true, - "removeEventListener": true + "fetch": true }, "packages": { "@ethereumjs/tx": true, diff --git a/package.json b/package.json index ce56919f0..4d7846653 100644 --- a/package.json +++ b/package.json @@ -115,21 +115,21 @@ "@material-ui/core": "^4.11.0", "@metamask/contract-metadata": "^1.31.0", "@metamask/controllers": "^27.0.0", - "@metamask/design-tokens": "^1.4.2", - "@metamask/eth-ledger-bridge-keyring": "^0.10.0", + "@metamask/design-tokens": "^1.5.1", + "@metamask/eth-ledger-bridge-keyring": "^0.11.0", "@metamask/eth-token-tracker": "^4.0.0", "@metamask/etherscan-link": "^2.1.0", - "@metamask/iframe-execution-environment-service": "^0.10.6", + "@metamask/iframe-execution-environment-service": "^0.10.7", "@metamask/jazzicon": "^2.0.0", "@metamask/logo": "^3.1.1", "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/obs-store": "^5.0.0", "@metamask/post-message-stream": "^4.0.0", "@metamask/providers": "^8.1.1", - "@metamask/rpc-methods": "^0.10.6", + "@metamask/rpc-methods": "^0.10.7", "@metamask/slip44": "^2.0.0", "@metamask/smart-transactions-controller": "^1.10.0", - "@metamask/snap-controllers": "^0.10.6", + "@metamask/snap-controllers": "^0.10.7", "@ngraveio/bc-ur": "^1.1.6", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.6.2", @@ -283,7 +283,7 @@ "browser-util-inspect": "^0.2.0", "browserify": "^16.5.1", "chalk": "^3.0.0", - "chromedriver": "^99.0.0", + "chromedriver": "^100.0.0", "concurrently": "^5.2.0", "copy-webpack-plugin": "^6.0.3", "cross-spawn": "^7.0.3", @@ -310,7 +310,7 @@ "fancy-log": "^1.3.3", "fast-glob": "^3.2.2", "fs-extra": "^8.1.0", - "ganache": "^v7.0.0-rc.0", + "ganache": "^v7.0.4", "geckodriver": "^1.21.0", "globby": "^11.0.4", "gulp": "^4.0.2", diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js index ab81641ac..5636b5d67 100644 --- a/shared/constants/metametrics.js +++ b/shared/constants/metametrics.js @@ -156,14 +156,26 @@ /** * @typedef {Object} Traits - * @property {string} [LEDGER_CONNECTION_TYPE] - when ledger live connnection - * type is changed we identify the ledger_connection_type trait - * @property {string} [NETWORKS_ADDED] - when user modifies networks we - * identify the networks_added trait - * @property {string} [NUMBER_OF_ACCOUNTS] - when identities change, we - * identify the new number_of_accounts trait - * @property {string} [THREE_BOX_ENABLED] - when 3box feature is toggled we - * identify the 3box_enabled trait + * @property {'address_book_entries'} ADDRESS_BOOK_ENTRIES - When the user + * adds or modifies addresses in address book the address_book_entries trait + * is identified. + * @property {'ledger_connection_type'} LEDGER_CONNECTION_TYPE - when ledger + * live connnection type is changed we identify the ledger_connection_type + * trait + * @property {'networks_added'} NETWORKS_ADDED - when user modifies networks + * we identify the networks_added trait + * @property {'nft_autodetection_enabled'} NFT_AUTODETECTION_ENABLED - when Autodetect NFTs + * feature is toggled we identify the nft_autodetection_enabled trait + * @property {'number_of_accounts'} NUMBER_OF_ACCOUNTS - when identities + * change, we identify the new number_of_accounts trait + * @property {'number_of_nft_collections'} NUMBER_OF_NFT_COLLECTIONS - user + * trait for number of unique NFT addresses + * @property {'number_of_tokens'} NUMBER_OF_TOKENS - when the number of tokens change, we + * identify the new number_of_tokens trait + * @property {'opensea_api_enabled'} OPENSEA_API_ENABLED - when the OpenSea API is enabled + * we identify the opensea_api_enabled trait + * @property {'three_box_enabled'} THREE_BOX_ENABLED - when 3box feature is + * toggled we identify the 3box_enabled trait * @property {'theme'} THEME - when the user's theme changes we identify the theme trait */ @@ -173,10 +185,15 @@ */ export const TRAITS = { + ADDRESS_BOOK_ENTRIES: 'address_book_entries', LEDGER_CONNECTION_TYPE: 'ledger_connection_type', - THREE_BOX_ENABLED: 'three_box_enabled', - NUMBER_OF_ACCOUNTS: 'number_of_accounts', NETWORKS_ADDED: 'networks_added', + NFT_AUTODETECTION_ENABLED: 'nft_autodetection_enabled', + NUMBER_OF_ACCOUNTS: 'number_of_accounts', + NUMBER_OF_NFT_COLLECTIONS: 'number_of_nft_collections', + NUMBER_OF_TOKENS: 'number_of_tokens', + OPENSEA_API_ENABLED: 'opensea_api_enabled', + THREE_BOX_ENABLED: 'three_box_enabled', THEME: 'theme', }; @@ -238,3 +255,10 @@ export const METAMETRICS_BACKGROUND_PAGE_OBJECT = { export const REJECT_NOTFICIATION_CLOSE = 'Cancel Via Notification Close'; export const REJECT_NOTFICIATION_CLOSE_SIG = 'Cancel Sig Request Via Notification Close'; + +export const EVENT_NAMES = { + SIGNATURE_REQUESTED: 'Signature Requested', + ENCRYPTION_PUBLIC_KEY_REQUESTED: 'Encryption Public Key Requested', + DECRYPTION_REQUESTED: 'Decryption Requested', + PERMISSIONS_REQUESTED: 'Permissions Requested', +}; diff --git a/shared/constants/network.js b/shared/constants/network.js index 844533a0c..dd3d78684 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -39,6 +39,9 @@ export const KOVAN_DISPLAY_NAME = 'Kovan'; export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'; export const GOERLI_DISPLAY_NAME = 'Goerli'; export const LOCALHOST_DISPLAY_NAME = 'Localhost 8545'; +export const BSC_DISPLAY_NAME = 'Binance Smart Chain'; +export const POLYGON_DISPLAY_NAME = 'Polygon'; +export const AVALANCHE_DISPLAY_NAME = 'Avalanche'; const infuraProjectId = process.env.INFURA_PROJECT_ID; export const getRpcUrl = ({ network, excludeProjectId = false }) => @@ -127,6 +130,13 @@ export const CHAIN_ID_TO_RPC_URL_MAP = { [LOCALHOST_CHAIN_ID]: LOCALHOST_RPC_URL, }; +export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { + [MAINNET_CHAIN_ID]: ETH_TOKEN_IMAGE_URL, + [AVALANCHE_CHAIN_ID]: AVAX_TOKEN_IMAGE_URL, + [BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL, + [POLYGON_CHAIN_ID]: MATIC_TOKEN_IMAGE_URL, +}; + export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values( NETWORK_TYPE_TO_ID_MAP, ).reduce((chainIdToNetworkIdMap, { chainId, networkId }) => { diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js index c9b183f7a..10db41a2e 100644 --- a/shared/constants/swaps.js +++ b/shared/constants/swaps.js @@ -114,14 +114,18 @@ const RINKEBY_DEFAULT_BLOCK_EXPLORER_URL = 'https://rinkeby.etherscan.io/'; const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/'; const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/'; -export const ALLOWED_SWAPS_CHAIN_IDS = { - [MAINNET_CHAIN_ID]: true, - [SWAPS_TESTNET_CHAIN_ID]: true, - [BSC_CHAIN_ID]: true, - [POLYGON_CHAIN_ID]: true, - [RINKEBY_CHAIN_ID]: true, - [AVALANCHE_CHAIN_ID]: true, -}; +export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [ + MAINNET_CHAIN_ID, + SWAPS_TESTNET_CHAIN_ID, + BSC_CHAIN_ID, + POLYGON_CHAIN_ID, + AVALANCHE_CHAIN_ID, +]; + +export const ALLOWED_DEV_SWAPS_CHAIN_IDS = [ + ...ALLOWED_PROD_SWAPS_CHAIN_IDS, + RINKEBY_CHAIN_ID, +]; export const ALLOWED_SMART_TRANSACTIONS_CHAIN_IDS = [ MAINNET_CHAIN_ID, @@ -197,3 +201,8 @@ export const RINKEBY = 'rinkeby'; export const AVALANCHE = 'avalanche'; export const SWAPS_CLIENT_ID = 'extension'; + +export const TOKEN_BUCKET_PRIORITY = { + OWNED: 'owned', + TOP: 'top', +}; diff --git a/shared/modules/network.utils.js b/shared/modules/network.utils.js index 7d72d18f7..eaa7148a3 100644 --- a/shared/modules/network.utils.js +++ b/shared/modules/network.utils.js @@ -1,4 +1,10 @@ -import { MAX_SAFE_CHAIN_ID } from '../constants/network'; +import { + MAX_SAFE_CHAIN_ID, + BSC_CHAIN_ID, + POLYGON_CHAIN_ID, + AVALANCHE_CHAIN_ID, + MAINNET_CHAIN_ID, +} from '../constants/network'; /** * Checks whether the given number primitive chain ID is safe. @@ -28,3 +34,21 @@ export function isPrefixedFormattedHexString(value) { } return /^0x[1-9a-f]+[0-9a-f]*$/iu.test(value); } + +/** + * Check if token detection is enabled for certain networks + * + * @param chainId - ChainID of network + * @returns Whether the current network supports token detection + */ +export function isTokenDetectionEnabledForNetwork(chainId) { + switch (chainId) { + case MAINNET_CHAIN_ID: + case BSC_CHAIN_ID: + case POLYGON_CHAIN_ID: + case AVALANCHE_CHAIN_ID: + return true; + default: + return false; + } +} diff --git a/shared/notifications/index.js b/shared/notifications/index.js index 38ae68c0a..52e0662a4 100644 --- a/shared/notifications/index.js +++ b/shared/notifications/index.js @@ -46,6 +46,18 @@ export const UI_NOTIFICATIONS = { width: '80%', }, }, + 10: { + id: 10, + date: '2022-04-18', + image: { + src: 'images/token-detection.svg', + width: '100%', + }, + }, + 11: { + id: 11, + date: '2022-04-18', + }, }; export const getTranslatedUINoficiations = (t, locale) => { @@ -132,5 +144,26 @@ export const getTranslatedUINoficiations = (t, locale) => { new Date(UI_NOTIFICATIONS[9].date), ), }, + 10: { + ...UI_NOTIFICATIONS[10], + title: t('notifications10Title'), + description: [ + t('notifications10DescriptionOne'), + t('notifications10DescriptionTwo'), + t('notifications10DescriptionThree'), + ], + actionText: t('notifications10ActionText'), + date: new Intl.DateTimeFormat(formattedLocale).format( + new Date(UI_NOTIFICATIONS[10].date), + ), + }, + 11: { + ...UI_NOTIFICATIONS[11], + title: t('notifications11Title'), + description: t('notifications11Description'), + date: new Intl.DateTimeFormat(formattedLocale).format( + new Date(UI_NOTIFICATIONS[11].date), + ), + }, }; }; diff --git a/test/data/fetch-mocks.json b/test/data/fetch-mocks.json deleted file mode 100644 index f9b2885f2..000000000 --- a/test/data/fetch-mocks.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "gasPricesBasic": { - "average": 85, - "fast": 200, - "safeLow": 80 - }, - "metametrics": { - "mockMetaMetricsResponse": true - }, - "swaps": { - "featureFlags": { - "bsc": { - "mobile_active": false, - "extension_active": true, - "fallback_to_v1": true - }, - "ethereum": { - "mobile_active": false, - "extension_active": true, - "fallback_to_v1": true - }, - "polygon": { - "mobile_active": false, - "extension_active": true, - "fallback_to_v1": false - } - } - }, - "tokenList": { - "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": { - "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", - "symbol": "MATIC", - "decimals": 18, - "name": "Polygon", - "iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0x0d8775f648430679a709e98d2b0cb6250d2887ef": { - "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", - "symbol": "BAT", - "decimals": 18, - "name": "Basic Attention Tok", - "iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - } - } -} diff --git a/test/e2e/fixtures/custom-token/state.json b/test/e2e/fixtures/custom-token/state.json index 158ab303b..da6098fcf 100644 --- a/test/e2e/fixtures/custom-token/state.json +++ b/test/e2e/fixtures/custom-token/state.json @@ -123,10 +123,6 @@ "usePhishDetect": true, "useStaticTokenList": false }, - "TokenListController": { - "tokenList": {}, - "tokensChainsCache": {} - }, "TokensController": { "allTokens": { "0x539": { diff --git a/test/e2e/fixtures/import-utc-json/test-json-import-account-file.json b/test/e2e/fixtures/import-utc-json/test-json-import-account-file.json new file mode 100644 index 000000000..41c79d89e --- /dev/null +++ b/test/e2e/fixtures/import-utc-json/test-json-import-account-file.json @@ -0,0 +1,21 @@ +{ + "address": "0961ca10d49b9b8e371aa0bcf77fe5730b18f2e4", + "crypto": { + "ciphertext": "eb10547515f1bf3bb6eefed3cb39303c64a30560f378b789592b1ac632bbc14c", + "cipherparams": { + "iv": "25ebfc7358ce77462ef7b970e91309d6" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "42417a49fb6cbb10167d7bc4ed8ae71d02cb4ee6309a42417e0051e7472d45c5", + "n": 8192, + "r": 8, + "p": 1 + }, + "mac": "0491462fbca0c7a71d249de141736304d699749c2faf5ab575d6a502e26099d7" + }, + "id": "03754e23-770b-43d3-a35e-bae1196e6f86", + "version": 3 +} diff --git a/test/e2e/fixtures/imported-account/state.json b/test/e2e/fixtures/imported-account/state.json index 449f6018b..c6e2f4a8e 100644 --- a/test/e2e/fixtures/imported-account/state.json +++ b/test/e2e/fixtures/imported-account/state.json @@ -91,361 +91,6 @@ } ] }, - "TokenListController": { - "tokenList": { - "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": { - "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", - "symbol": "LRC", - "decimals": 18, - "name": "Loopring", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 12 - }, - "0x04fa0d235c4abf4bcf4787af4cf447de572ef828": { - "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", - "symbol": "UMA", - "decimals": 18, - "name": "UMA", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": { - "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", - "symbol": "SUSHI", - "decimals": 18, - "name": "SushiSwap", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0xd533a949740bb3306d119cc777fa900ba034cd52": { - "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", - "symbol": "CRV", - "decimals": 18, - "name": "Curve DAO Token", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0xc00e94cb662c3520282e6f5717214004a7f26888": { - "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", - "symbol": "COMP", - "decimals": 18, - "name": "Compound", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0xba100000625a3754423978a60c9317c58a424e3d": { - "address": "0xba100000625a3754423978a60c9317c58a424e3d", - "symbol": "BAL", - "decimals": 18, - "name": "Balancer", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": { - "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", - "symbol": "MATIC", - "decimals": 18, - "name": "Polygon", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0x0d8775f648430679a709e98d2b0cb6250d2887ef": { - "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", - "symbol": "BAT", - "decimals": 18, - "name": "Basic Attention Tok", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - } - }, - "tokensChainsCache": { - "1": { - "timestamp": 1628769574961, - "data": [ - { - "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", - "symbol": "LRC", - "decimals": 18, - "name": "Loopring", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 12 - }, - { - "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", - "symbol": "UMA", - "decimals": 18, - "name": "UMA", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", - "symbol": "SUSHI", - "decimals": 18, - "name": "SushiSwap", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", - "symbol": "CRV", - "decimals": 18, - "name": "Curve DAO Token", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", - "symbol": "COMP", - "decimals": 18, - "name": "Compound", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0xba100000625a3754423978a60c9317c58a424e3d", - "symbol": "BAL", - "decimals": 18, - "name": "Balancer", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", - "symbol": "MATIC", - "decimals": 18, - "name": "Polygon", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", - "symbol": "BAT", - "decimals": 18, - "name": "Basic Attention Tok", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - } - ] - }, - "3": { - "timestamp": 1628769543620 - }, - "1337": { - "timestamp": 1628769513476 - } - } - }, "PreferencesController": { "accountTokens": { "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { diff --git a/test/e2e/fixtures/navigate-transactions/state.json b/test/e2e/fixtures/navigate-transactions/state.json index 16aae0325..6760e0d4f 100644 --- a/test/e2e/fixtures/navigate-transactions/state.json +++ b/test/e2e/fixtures/navigate-transactions/state.json @@ -91,361 +91,6 @@ } ] }, - "TokenListController": { - "tokenList": { - "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": { - "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", - "symbol": "LRC", - "decimals": 18, - "name": "Loopring", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 12 - }, - "0x04fa0d235c4abf4bcf4787af4cf447de572ef828": { - "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", - "symbol": "UMA", - "decimals": 18, - "name": "UMA", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": { - "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", - "symbol": "SUSHI", - "decimals": 18, - "name": "SushiSwap", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0xd533a949740bb3306d119cc777fa900ba034cd52": { - "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", - "symbol": "CRV", - "decimals": 18, - "name": "Curve DAO Token", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0xc00e94cb662c3520282e6f5717214004a7f26888": { - "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", - "symbol": "COMP", - "decimals": 18, - "name": "Compound", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0xba100000625a3754423978a60c9317c58a424e3d": { - "address": "0xba100000625a3754423978a60c9317c58a424e3d", - "symbol": "BAL", - "decimals": 18, - "name": "Balancer", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": { - "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", - "symbol": "MATIC", - "decimals": 18, - "name": "Polygon", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - "0x0d8775f648430679a709e98d2b0cb6250d2887ef": { - "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", - "symbol": "BAT", - "decimals": 18, - "name": "Basic Attention Tok", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - } - }, - "tokensChainsCache": { - "1": { - "timestamp": 1628769574961, - "data": [ - { - "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", - "symbol": "LRC", - "decimals": 18, - "name": "Loopring", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 12 - }, - { - "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", - "symbol": "UMA", - "decimals": 18, - "name": "UMA", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", - "symbol": "SUSHI", - "decimals": 18, - "name": "SushiSwap", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", - "symbol": "CRV", - "decimals": 18, - "name": "Curve DAO Token", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", - "symbol": "COMP", - "decimals": 18, - "name": "Compound", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0xba100000625a3754423978a60c9317c58a424e3d", - "symbol": "BAL", - "decimals": 18, - "name": "Balancer", - "iconUrl": "", - "aggregators": [ - "bancor", - "cmc", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", - "symbol": "MATIC", - "decimals": 18, - "name": "Polygon", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - }, - { - "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", - "symbol": "BAT", - "decimals": 18, - "name": "Basic Attention Tok", - "iconUrl": "", - "aggregators": [ - "airswapLight", - "bancor", - "coinGecko", - "kleros", - "oneInch", - "paraswap", - "pmm", - "totle", - "zapper", - "zerion", - "zeroEx" - ], - "occurrences": 11 - } - ] - }, - "3": { - "timestamp": 1628769543620 - }, - "1337": { - "timestamp": 1628769513476 - } - } - }, "PreferencesController": { "accountTokens": { "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index fa0d0b30f..a6d5d6518 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -1,5 +1,29 @@ +const blacklistedHosts = [ + 'goerli.infura.io', + 'kovan.infura.io', + 'mainnet.infura.io', + 'rinkeby.infura.io', + 'ropsten.infura.io', +]; + async function setupMocking(server, testSpecificMock) { - await server.forAnyRequest().thenPassThrough(); + await server.forAnyRequest().thenPassThrough({ + beforeRequest: (req) => { + const { host } = req.headers; + if (blacklistedHosts.includes(host)) { + return { + url: 'http://localhost:8545', + }; + } + return {}; + }, + }); + + await server.forPost('https://api.segment.io/v1/batch').thenCallback(() => { + return { + statusCode: 200, + }; + }); await server .forGet('https://gas-api.metaswap.codefi.network/networks/1/gasPrices') @@ -14,12 +38,6 @@ async function setupMocking(server, testSpecificMock) { }; }); - await server.forPost('https://api.segment.io/v1/batch').thenCallback(() => { - return { - statusCode: 200, - }; - }); - await server .forGet( 'https://gas-api.metaswap.codefi.network/networks/1/suggestedGasFees', @@ -57,6 +75,85 @@ async function setupMocking(server, testSpecificMock) { }; }); + await server + .forGet('https://swap.metaswap.codefi.network/featureFlags') + .thenCallback(() => { + return { + statusCode: 200, + json: [ + { + ethereum: { + mobile_active: true, + extension_active: true, + fallback_to_v1: false, + mobileActive: true, + extensionActive: true, + }, + bsc: { + mobile_active: true, + extension_active: true, + fallback_to_v1: false, + mobileActive: true, + extensionActive: true, + }, + polygon: { + mobile_active: true, + extension_active: true, + fallback_to_v1: false, + mobileActive: true, + extensionActive: true, + }, + avalanche: { + mobile_active: true, + extension_active: true, + fallback_to_v1: false, + mobileActive: true, + extensionActive: true, + }, + smart_transactions: { + mobile_active: false, + extension_active: false, + }, + smartTransactions: { + mobileActive: false, + extensionActive: false, + }, + updated_at: '2022-03-17T15:54:00.360Z', + }, + ], + }; + }); + + await server + .forGet('https://token-api.metaswap.codefi.network/tokens/1337') + .thenCallback(() => { + return { + statusCode: 200, + json: [ + { + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + symbol: 'BAT', + decimals: 18, + name: 'Basic Attention Token', + iconUrl: + 'https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427', + aggregators: [ + 'aave', + 'bancor', + 'coinGecko', + 'oneInch', + 'paraswap', + 'pmm', + 'zapper', + 'zerion', + 'zeroEx', + ], + occurrences: 9, + }, + ], + }; + }); + testSpecificMock(server); } diff --git a/test/e2e/tests/chain-interactions.spec.js b/test/e2e/tests/chain-interactions.spec.js new file mode 100644 index 000000000..c1879ccef --- /dev/null +++ b/test/e2e/tests/chain-interactions.spec.js @@ -0,0 +1,114 @@ +const { strict: assert } = require('assert'); +const { convertToHexValue, withFixtures } = require('../helpers'); + +describe('Chain Interactions', function () { + it('should add the XDAI chain and not switch the network', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: convertToHexValue(25000000000000000000), + }, + ], + }; + await withFixtures( + { + dapp: true, + fixtures: 'connected-state', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // trigger add chain confirmation + await driver.openNewPage('http://127.0.0.1:8080/'); + await driver.clickElement('#addEthereumChain'); + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const extension = windowHandles[0]; + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); + + // verify chain details + const [networkName, networkUrl, chainId] = await driver.findElements( + '.definition-list dd', + ); + assert.equal(await networkName.getText(), 'xDAI Chain'); + assert.equal(await networkUrl.getText(), 'https://dai.poa.network'); + assert.equal(await chainId.getText(), '100'); + + // approve add chain, cancel switch chain + await driver.clickElement({ text: 'Approve', tag: 'button' }); + await driver.clickElement({ text: 'Cancel', tag: 'button' }); + + // switch to extension + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extension); + + // verify networks + const networkDisplay = await driver.findElement('.network-display'); + await networkDisplay.click(); + assert.equal(await networkDisplay.getText(), 'Localhost 8545'); + const xDaiChain = await driver.findElements({ + text: 'xDAI Chain', + tag: 'span', + }); + assert.ok(xDaiChain.length, 1); + }, + ); + }); + + it('should add the XDAI chain and switch the network', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: convertToHexValue(25000000000000000000), + }, + ], + }; + await withFixtures( + { + dapp: true, + fixtures: 'connected-state', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // trigger add chain confirmation + await driver.openNewPage('http://127.0.0.1:8080/'); + await driver.clickElement('#addEthereumChain'); + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const extension = windowHandles[0]; + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); + + // approve and switch chain + await driver.clickElement({ text: 'Approve', tag: 'button' }); + await driver.clickElement({ text: 'Switch network', tag: 'button' }); + + // switch to extension + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extension); + + // verify current network + const networkDisplay = await driver.findElement('.network-display'); + assert.equal(await networkDisplay.getText(), 'xDAI Chain'); + }, + ); + }); +}); diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js index 3157e6a43..dc31d1551 100644 --- a/test/e2e/tests/from-import-ui.spec.js +++ b/test/e2e/tests/from-import-ui.spec.js @@ -1,4 +1,5 @@ const { strict: assert } = require('assert'); +const path = require('path'); const { convertToHexValue, withFixtures, @@ -232,6 +233,78 @@ describe('Metamask Import UI', function () { ); }); + it('Import Account using json file', async function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', + balance: convertToHexValue(25000000000000000000), + }, + ], + }; + + await withFixtures( + { + fixtures: 'import-ui', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // Imports an account with JSON file + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Import Account', tag: 'div' }); + + await driver.clickElement('.new-account-import-form__select'); + await driver.clickElement({ text: 'JSON File', tag: 'option' }); + + const fileInput = await driver.findElement('input[type="file"]'); + const importJsonFile = path.join( + __dirname, + '..', + 'fixtures', + 'import-utc-json', + 'test-json-import-account-file.json', + ); + + fileInput.sendKeys(importJsonFile); + + await driver.fill('#json-password-box', 'foobarbazqux'); + + await driver.clickElement({ text: 'Import', tag: 'button' }); + + // should show the correct account name + const importedAccountName = await driver.findElement( + '.selected-account__name', + ); + assert.equal(await importedAccountName.getText(), 'Account 4'); + + // should show the imported label + await driver.clickElement('.account-menu__icon'); + // confirm 4th account is account 4, as expected + const accountMenuItemSelector = '.account-menu__account:nth-child(4)'; + const fourthAccountName = await driver.findElement( + `${accountMenuItemSelector} .account-menu__name`, + ); + assert.equal(await fourthAccountName.getText(), 'Account 4'); + // confirm label is present on the same menu item + const importedLabel = await driver.findElement( + `${accountMenuItemSelector} .keyring-label`, + ); + assert.equal(await importedLabel.getText(), 'IMPORTED'); + + const accountListItems = await driver.findElements( + '.account-menu__account', + ); + assert.equal(accountListItems.length, 4); + }, + ); + }); + it('Import Account using private key of an already active account should result in an error', async function () { const testPrivateKey = '0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9'; diff --git a/test/e2e/tests/settings-search.spec.js b/test/e2e/tests/settings-search.spec.js new file mode 100644 index 000000000..d817acb1c --- /dev/null +++ b/test/e2e/tests/settings-search.spec.js @@ -0,0 +1,273 @@ +const { strict: assert } = require('assert'); +const { convertToHexValue, withFixtures } = require('../helpers'); + +describe('Settings Search', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: convertToHexValue(25000000000000000000), + }, + ], + }; + const settingsSearch = { + general: 'Primary Currency', + advanced: 'State Logs', + contacts: 'Contacts', + security: 'Reveal Secret', + alerts: 'Browsing a website', + networks: 'Ethereum Mainnet', + experimental: 'Token Detection', + about: 'Terms of Use', + }; + + it('should find element inside the General tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.general); + + const page = 'General'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.general} item does not redirect to ${page} view`, + ); + }, + ); + }); + it('should find element inside the Advanced tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.advanced); + + // Check if element redirects to the correct page + const page = 'Advanced'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.advanced} item does not redirect to ${page} view`, + ); + }, + ); + }); + it('should find element inside the Contacts tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.contacts); + + // Check if element redirects to the correct page + const page = 'Contacts'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.contacts} item does not redirect to ${page} view`, + ); + }, + ); + }); + it('should find element inside the Security tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.security); + + // Check if element redirects to the correct page + const page = 'Security'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.security} item does not redirect to ${page} view`, + ); + }, + ); + }); + it('should find element inside the Alerts tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.alerts); + + // Check if element redirects to the correct page + const page = 'Alerts'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.alerts} item does not redirect to ${page} view`, + ); + }, + ); + }); + it('should find element inside the Networks tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.networks); + + // Check if element redirects to the correct page + const page = 'Networks'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.networks} item does not redirect to ${page} view`, + ); + }, + ); + }); + it('should find element inside the Experimental tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.experimental); + + // Check if element redirects to the correct page + const page = 'Experimental'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.experimental} item not redirect to ${page} view`, + ); + }, + ); + }); + it('should find element inside the About tab', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', settingsSearch.about); + + // Check if element redirects to the correct page + const page = 'About'; + await driver.clickElement({ text: page, tag: 'span' }); + assert.equal( + await driver.isElementPresent({ text: page, tag: 'div' }), + true, + `${settingsSearch.about} item does not redirect to ${page} view`, + ); + }, + ); + }); + it('should display "Element not found" for a non-existing element', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement('.account-menu__icon'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.fill('#search-settings', 'Lorem ipsum'); + + const found = await driver.isElementPresent({ + text: 'No matching results found', + tag: 'span', + }); + assert.equal(found, true, 'Non existent element was found'); + }, + ); + }); +}); diff --git a/test/e2e/tests/swap-eth.spec.js b/test/e2e/tests/swap-eth.spec.js new file mode 100644 index 000000000..5e26b00e0 --- /dev/null +++ b/test/e2e/tests/swap-eth.spec.js @@ -0,0 +1,94 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Swap Eth for another Token', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('Completes a Swap between Eth and Matic', async function () { + await withFixtures( + { + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + await driver.clickElement( + '.wallet-overview__buttons .icon-button:nth-child(3)', + ); + await driver.clickElement( + '[class*="dropdown-search-list"] + div[class*="MuiFormControl-root MuiTextField-root"]', + ); + await driver.fill('input[placeholder*="0"]', '2'); + await driver.clickElement( + '[class*="dropdown-search-list"] + div[class*="MuiFormControl-root MuiTextField-root"]', + ); + await driver.clickElement( + '[class="dropdown-search-list__closed-primary-label dropdown-search-list__select-default"]', + ); + await driver.clickElement('[placeholder="Search for a token"]'); + await driver.clickElement('[placeholder="Search for a token"]'); + await driver.fill('[placeholder="Search for a token"]', 'DAI'); + await driver.waitForSelector( + '[class="searchable-item-list__primary-label"]', + ); + await driver.clickElement( + '[class="searchable-item-list__primary-label"]', + ); + await driver.clickElement({ text: 'Review Swap', tag: 'button' }); + await driver.waitForSelector('[class*="box--align-items-center"]'); + const estimatedEth = await driver.waitForSelector({ + css: '[class*="box--align-items-center"]', + text: 'Estimated gas fee', + }); + assert.equal(await estimatedEth.getText(), 'Estimated gas fee'); + await driver.waitForSelector( + '[class="exchange-rate-display main-quote-summary__exchange-rate-display"]', + ); + await driver.waitForSelector( + '[class="fee-card__info-tooltip-container"]', + ); + await driver.waitForSelector({ + css: '[class="countdown-timer__time"]', + text: '0:24', + }); + await driver.clickElement({ text: 'Swap', tag: 'button' }); + const sucessfulTransactionMessage = await driver.waitForSelector({ + css: '[class="awaiting-swap__header"]', + text: 'Transaction complete', + }); + assert.equal( + await sucessfulTransactionMessage.getText(), + 'Transaction complete', + ); + const sucessfulTransactionToken = await driver.waitForSelector({ + css: '[class="awaiting-swap__amount-and-symbol"]', + text: 'DAI', + }); + assert.equal(await sucessfulTransactionToken.getText(), 'DAI'); + await driver.clickElement({ text: 'Close', tag: 'button' }); + await driver.clickElement('[data-testid="home__activity-tab"]'); + const swaptotal = await driver.waitForSelector({ + css: '[class="transaction-list-item__primary-currency"]', + text: '-2 TESTETH', + }); + assert.equal(await swaptotal.getText(), '-2 TESTETH'); + const swaptotaltext = await driver.waitForSelector({ + css: '[class="list-item__title"]', + text: 'Swap TESTETH to DAI', + }); + assert.equal(await swaptotaltext.getText(), 'Swap TESTETH to DAI'); + }, + ); + }); +}); diff --git a/test/e2e/webdriver/index.js b/test/e2e/webdriver/index.js index 9753c03fe..13fcbb29f 100644 --- a/test/e2e/webdriver/index.js +++ b/test/e2e/webdriver/index.js @@ -1,5 +1,4 @@ const { Browser } = require('selenium-webdriver'); -const fetchMockResponses = require('../../data/fetch-mocks.json'); const Driver = require('./driver'); const ChromeDriver = require('./chrome'); const FirefoxDriver = require('./firefox'); @@ -12,7 +11,6 @@ async function buildWebDriver({ responsive, port, type } = {}) { extensionId, extensionUrl, } = await buildBrowserWebDriver(browser, { responsive, port, type }); - await setupFetchMocking(seleniumDriver); const driver = new Driver(seleniumDriver, browser, extensionUrl); return { @@ -35,53 +33,6 @@ async function buildBrowserWebDriver(browser, webDriverOptions) { } } -async function setupFetchMocking(driver) { - // define fetchMocking script, to be evaluated in the browser - function fetchMocking(mockResponses) { - window.origFetch = window.fetch.bind(window); - window.fetch = async (...args) => { - const url = args[0]; - // api.metaswap.codefi.network/gasPrices - if ( - url.match(/^http(s)?:\/\/api\.metaswap\.codefi\.network\/gasPrices/u) - ) { - return { json: async () => clone(mockResponses.gasPricesBasic) }; - } else if (url.match(/chromeextensionmm/u)) { - return { json: async () => clone(mockResponses.metametrics) }; - } else if (url.match(/^https:\/\/(swap\.metaswap\.codefi\.network)/u)) { - if (url.match(/featureFlags$/u)) { - return { json: async () => clone(mockResponses.swaps.featureFlags) }; - } - } else if ( - url.match(/^https:\/\/(token-api\.airswap-prod\.codefi\.network)/u) - ) { - if (url.match(/tokens\/1337$/u)) { - return { json: async () => clone(mockResponses.tokenList) }; - } - } - return window.origFetch(...args); - }; - if (window.chrome && window.chrome.webRequest) { - window.chrome.webRequest.onBeforeRequest.addListener( - cancelInfuraRequest, - { urls: ['https://*.infura.io/*'] }, - ['blocking'], - ); - } - function cancelInfuraRequest(requestDetails) { - console.log(`fetchMocking - Canceling request: "${requestDetails.url}"`); - return { cancel: true }; - } - function clone(obj) { - return JSON.parse(JSON.stringify(obj)); - } - } - // fetchMockResponses are parsed last minute to ensure that objects are uniquely instantiated - const fetchMockResponsesJson = JSON.stringify(fetchMockResponses); - // eval the fetchMocking script in the browser - await driver.executeScript(`(${fetchMocking})(${fetchMockResponsesJson})`); -} - module.exports = { buildWebDriver, }; diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 4a49ad110..62be2129b 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -40,6 +40,7 @@ @import 'gas-details-item/index'; @import 'gas-details-item/gas-details-item-title/index'; @import 'gas-timing/index'; +@import 'hold-to-reveal-button/index'; @import 'home-notification/index'; @import 'info-box/index'; @import 'menu-bar/index'; @@ -84,4 +85,8 @@ @import 'advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index'; @import 'advanced-gas-fee-popover/advanced-gas-fee-defaults/index'; @import 'currency-input/index'; +@import 'asset-list/detetcted-tokens-link/index'; @import 'detected-token/detected-token-address/index'; +@import 'detected-token/detected-token-aggregators/index'; +@import 'detected-token/detected-token-values/index'; +@import 'detected-token/detected-token-details/index' diff --git a/ui/components/app/app-header/app-header.stories.js b/ui/components/app/app-header/app-header.stories.js index f783fd3e2..989e2997a 100644 --- a/ui/components/app/app-header/app-header.stories.js +++ b/ui/components/app/app-header/app-header.stories.js @@ -5,27 +5,6 @@ export default { title: 'Components/App/AppHeader', id: __filename, argTypes: { - history: { - control: 'object', - }, - networkDropdownOpen: { - control: 'boolean', - }, - showNetworkDropdown: { - action: 'showNetworkDropdown', - }, - hideNetworkDropdown: { - action: 'hideNetworkDropdown', - }, - toggleAccountMenu: { - action: 'toggleAccountMenu', - }, - selectedAddress: { - control: 'text', - }, - isUnlocked: { - control: 'boolean', - }, hideNetworkIndicator: { control: 'boolean', }, @@ -35,9 +14,6 @@ export default { disableNetworkIndicator: { control: 'boolean', }, - isAccountMenuOpen: { - control: 'boolean', - }, onClick: { action: 'onClick', }, diff --git a/ui/components/app/asset-list-item/asset-list-item.js b/ui/components/app/asset-list-item/asset-list-item.js index 8b743ac7a..2b54fce6c 100644 --- a/ui/components/app/asset-list-item/asset-list-item.js +++ b/ui/components/app/asset-list-item/asset-list-item.js @@ -14,7 +14,7 @@ import { SEND_ROUTE } from '../../../helpers/constants/routes'; import { SEVERITIES } from '../../../helpers/constants/design-system'; import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys'; import { ASSET_TYPES } from '../../../../shared/constants/transaction'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; const AssetListItem = ({ className, diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 75be4008d..adc404738 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -25,7 +25,7 @@ import { JUSTIFY_CONTENT, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; const AssetList = ({ onClickAsset }) => { const t = useI18nContext(); diff --git a/ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.js b/ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.js new file mode 100644 index 000000000..a1aa3fb41 --- /dev/null +++ b/ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import Box from '../../../ui/box/box'; +import Button from '../../../ui/button'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { getDetectedTokensInCurrentNetwork } from '../../../../selectors'; + +const DetectedTokensLink = ({ className = '', onClick }) => { + const t = useI18nContext(); + const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); + + return ( + + + + ); +}; + +DetectedTokensLink.propTypes = { + onClick: PropTypes.func.isRequired, + className: PropTypes.string, +}; + +export default DetectedTokensLink; diff --git a/ui/components/app/asset-list/detetcted-tokens-link/index.scss b/ui/components/app/asset-list/detetcted-tokens-link/index.scss new file mode 100644 index 000000000..01b5da1ff --- /dev/null +++ b/ui/components/app/asset-list/detetcted-tokens-link/index.scss @@ -0,0 +1,5 @@ +.detected-tokens-link { + & &__link { + @include H6; + } +} diff --git a/ui/components/app/collectibles-detection-notice/collectibles-detection-notice.js b/ui/components/app/collectibles-detection-notice/collectibles-detection-notice.js index d2fd732e0..3cf95e716 100644 --- a/ui/components/app/collectibles-detection-notice/collectibles-detection-notice.js +++ b/ui/components/app/collectibles-detection-notice/collectibles-detection-notice.js @@ -20,22 +20,28 @@ export default function CollectiblesDetectionNotice() { const history = useHistory(); return ( - + + + {t('noNFTs')} + + + - + )} - + ); } diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index f011405b9..82d6d2c41 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -70,6 +70,7 @@ export default class ConfirmPageContainer extends Component { unapprovedTxCount: PropTypes.number, origin: PropTypes.string.isRequired, ethGasPriceWarning: PropTypes.string, + networkIdentifier: PropTypes.string, // Navigation totalTx: PropTypes.number, positionOfCurrentTx: PropTypes.number, @@ -151,6 +152,7 @@ export default class ConfirmPageContainer extends Component { nativeCurrency, showBuyModal, isBuyableChain, + networkIdentifier, } = this.props; const showAddToAddressDialog = @@ -164,7 +166,8 @@ export default class ConfirmPageContainer extends Component { currentTransaction.type === TRANSACTION_TYPES.DEPLOY_CONTRACT) && currentTransaction.txParams?.value === '0x0'; - const networkName = NETWORK_TO_NAME_MAP[currentTransaction.chainId]; + const networkName = + NETWORK_TO_NAME_MAP[currentTransaction.chainId] || networkIdentifier; const { t } = this.context; diff --git a/ui/components/app/confirm-page-container/confirm-page-container.container.js b/ui/components/app/confirm-page-container/confirm-page-container.container.js index 2ad758908..31b7d5cd9 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.container.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.container.js @@ -3,6 +3,7 @@ import { getAccountsWithLabels, getAddressBookEntry, getIsBuyableChain, + getNetworkIdentifier, } from '../../../selectors'; import { showModal } from '../../../store/actions'; import ConfirmPageContainer from './confirm-page-container.component'; @@ -11,6 +12,7 @@ function mapStateToProps(state, ownProps) { const to = ownProps.toAddress; const isBuyableChain = getIsBuyableChain(state); const contact = getAddressBookEntry(state, to); + const networkIdentifier = getNetworkIdentifier(state); return { isBuyableChain, contact, @@ -19,6 +21,7 @@ function mapStateToProps(state, ownProps) { .map((accountWithLabel) => accountWithLabel.address) .includes(to), to, + networkIdentifier, }; } diff --git a/ui/components/app/create-new-vault/create-new-vault.js b/ui/components/app/create-new-vault/create-new-vault.js index 7d442ecd2..c14373837 100644 --- a/ui/components/app/create-new-vault/create-new-vault.js +++ b/ui/components/app/create-new-vault/create-new-vault.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useState } from 'react'; import PropTypes from 'prop-types'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; import TextField from '../../ui/text-field'; import Button from '../../ui/button'; import CheckBox from '../../ui/check-box'; @@ -108,7 +108,7 @@ export default function CreateNewVault({ return (
- +
{ + const t = useContext(I18nContext); + const numOfHiddenAggregators = parseInt(aggregatorsList.length, 10) - 2; + const [displayMore, setDisplayMore] = useState(false); + + return ( + + + {t('fromTokenLists', [ + numOfHiddenAggregators > 0 && !displayMore ? ( + + {`${aggregatorsList.slice(0, 2).join(', ')}`} + + + ) : ( + + {`${aggregatorsList.join(', ')}.`} + + ), + ])} + + + ); +}; + +DetectedTokenAggregators.propTypes = { + aggregatorsList: PropTypes.array.isRequired, +}; + +export default DetectedTokenAggregators; diff --git a/ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.stories.js b/ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.stories.js new file mode 100644 index 000000000..c4c96b684 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.stories.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { DISPLAY } from '../../../../helpers/constants/design-system'; + +import Box from '../../../ui/box'; +import DetectedTokenAggregators from './detected-token-aggregators'; + +export default { + title: 'Components/App/DetectedToken/DetectedTokenAggregators', + id: __filename, + argTypes: { + aggregatorsList: { control: 'array' }, + }, + args: { + aggregatorsList1: [ + 'Aave', + 'Bancor', + 'CMC', + 'Crypto.com', + 'CoinGecko', + '1inch', + 'Paraswap', + 'PMM', + 'Synthetix', + 'Zapper', + 'Zerion', + '0x', + ], + aggregatorsList2: ['Aave', 'Bancor'], + }, +}; + +const Template = (args) => { + return ( + + + + + ); +}; + +export const DefaultStory = Template.bind({}); + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.test.js b/ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.test.js new file mode 100644 index 000000000..27757fb32 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.test.js @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { + renderWithProvider, + screen, + fireEvent, +} from '../../../../../test/jest'; +import configureStore from '../../../../store/store'; + +import DetectedTokenAggregators from './detected-token-aggregators'; + +describe('DetectedTokenAggregators', () => { + const args = { + aggregatorsList: [ + 'Aave', + 'Bancor', + 'CMC', + 'Crypto.com', + 'CoinGecko', + '1inch', + 'Paraswap', + 'PMM', + 'Synthetix', + 'Zapper', + 'Zerion', + '0x', + ], + }; + + it('should render the detected token aggregators', async () => { + const store = configureStore({}); + renderWithProvider(, store); + + expect(screen.getByText('From token lists:')).toBeInTheDocument(); + expect(screen.getByText('Aave, Bancor')).toBeInTheDocument(); + expect(screen.getByText('+ 10 more')).toBeInTheDocument(); + fireEvent.click(screen.getByText('+ 10 more')); + expect( + screen.getByText( + 'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.', + ), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/detected-token/detected-token-aggregators/index.scss b/ui/components/app/detected-token/detected-token-aggregators/index.scss new file mode 100644 index 000000000..37767a3b7 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-aggregators/index.scss @@ -0,0 +1,13 @@ +.detected-token-aggregators { + .typography { + display: inline; + } + + & &__link { + @include H7; + + padding: 0; + display: inline; + margin-left: 4px; + } +} diff --git a/ui/components/app/detected-token/detected-token-details/detected-token-details.js b/ui/components/app/detected-token/detected-token-details/detected-token-details.js new file mode 100644 index 000000000..0b6d4d59a --- /dev/null +++ b/ui/components/app/detected-token/detected-token-details/detected-token-details.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; + +import Box from '../../../ui/box'; +import Identicon from '../../../ui/identicon'; +import DetectedTokenValues from '../detected-token-values/detected-token-values'; +import DetectedTokenAddress from '../detected-token-address/detected-token-address'; +import DetectedTokenAggregators from '../detected-token-aggregators/detected-token-aggregators'; +import { DISPLAY } from '../../../../helpers/constants/design-system'; +import { getTokenList } from '../../../../selectors'; + +const DetectedTokenDetails = ({ tokenAddress }) => { + const tokenList = useSelector(getTokenList); + const token = tokenList[tokenAddress]; + + return ( + + + + + + + + + ); +}; + +DetectedTokenDetails.propTypes = { + tokenAddress: PropTypes.string, +}; + +export default DetectedTokenDetails; diff --git a/ui/components/app/detected-token/detected-token-details/detected-token-details.stories.js b/ui/components/app/detected-token/detected-token-details/detected-token-details.stories.js new file mode 100644 index 000000000..fe95977f6 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-details/detected-token-details.stories.js @@ -0,0 +1,26 @@ +import React from 'react'; + +import DetectedTokenDetails from './detected-token-details'; + +export default { + title: 'Components/App/DetectedToken/DetectedTokenDetails', + id: __filename, + argTypes: { + address: { control: 'text' }, + }, + args: { + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + }, +}; + +const Template = (args) => { + return ( +
+ +
+ ); +}; + +export const DefaultStory = Template.bind({}); + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/detected-token/detected-token-details/detected-token-details.test.js b/ui/components/app/detected-token/detected-token-details/detected-token-details.test.js new file mode 100644 index 000000000..4bebcb8eb --- /dev/null +++ b/ui/components/app/detected-token/detected-token-details/detected-token-details.test.js @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { + renderWithProvider, + screen, + fireEvent, +} from '../../../../../test/jest'; +import configureStore from '../../../../store/store'; +import testData from '../../../../../.storybook/test-data'; + +import DetectedTokenDetails from './detected-token-details'; + +describe('DetectedTokenDetails', () => { + const args = { + tokenAddress: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + }; + + it('should render the detected token details', async () => { + const store = configureStore(testData); + renderWithProvider(, store); + + expect(screen.getByText('0 SNX')).toBeInTheDocument(); + expect(screen.getByText('$0')).toBeInTheDocument(); + expect(screen.getByText('Token address:')).toBeInTheDocument(); + expect(screen.getByText('0xc01...2a6f')).toBeInTheDocument(); + expect(screen.getByText('From token lists:')).toBeInTheDocument(); + expect(screen.getByText('Aave, Bancor')).toBeInTheDocument(); + expect(screen.getByText('+ 10 more')).toBeInTheDocument(); + fireEvent.click(screen.getByText('+ 10 more')); + expect( + screen.getByText( + 'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.', + ), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/detected-token/detected-token-details/index.scss b/ui/components/app/detected-token/detected-token-details/index.scss new file mode 100644 index 000000000..1722f3517 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-details/index.scss @@ -0,0 +1,9 @@ +.detected-token-details { + &__identicon { + margin-top: 4px; + } + + &__data { + flex-grow: 1; + } +} diff --git a/ui/components/app/detected-token/detected-token-values/detected-token-values.js b/ui/components/app/detected-token/detected-token-values/detected-token-values.js new file mode 100644 index 000000000..d234b67c2 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-values/detected-token-values.js @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import Box from '../../../ui/box'; +import Typography from '../../../ui/typography'; +import CheckBox from '../../../ui/check-box'; + +import { + COLORS, + DISPLAY, + TYPOGRAPHY, +} from '../../../../helpers/constants/design-system'; +import { useTokenTracker } from '../../../../hooks/useTokenTracker'; +import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; + +const DetectedTokenValues = ({ token }) => { + const [selectedTokens, setSelectedTokens] = useState(false); + const { tokensWithBalances } = useTokenTracker([token]); + const balanceToRender = tokensWithBalances[0]?.string; + const balance = tokensWithBalances[0]?.balance; + const formattedFiatBalance = useTokenFiatAmount( + token.address, + balanceToRender, + token.symbol, + ); + + return ( + + + + {`${balance || '0'} ${token.symbol}`} + + + {formattedFiatBalance || '$0'} + + + + setSelectedTokens((checked) => !checked)} + /> + + + ); +}; + +DetectedTokenValues.propTypes = { + token: PropTypes.shape({ + address: PropTypes.string.isRequired, + decimals: PropTypes.number, + symbol: PropTypes.string, + iconUrl: PropTypes.string, + aggregators: PropTypes.array, + }), +}; + +export default DetectedTokenValues; diff --git a/ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js b/ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js new file mode 100644 index 000000000..6a3d12c39 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js @@ -0,0 +1,43 @@ +import React from 'react'; + +import DetectedTokenValues from './detected-token-values'; + +export default { + title: 'Components/App/DetectedToken/DetectedTokenValues', + id: __filename, + argTypes: { + address: { control: 'text' }, + symbol: { control: 'text' }, + decimals: { control: 'text' }, + iconUrl: { control: 'text' }, + aggregators: { control: 'array' }, + }, + args: { + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + symbol: 'SNX', + decimals: 18, + iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png', + aggregators: [ + 'aave', + 'bancor', + 'cmc', + 'cryptocom', + 'coinGecko', + 'oneInch', + 'paraswap', + 'pmm', + 'synthetix', + 'zapper', + 'zerion', + 'zeroEx', + ], + }, +}; + +const Template = (args) => { + return ; +}; + +export const DefaultStory = Template.bind({}); + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/detected-token/detected-token-values/detected-token-values.test.js b/ui/components/app/detected-token/detected-token-values/detected-token-values.test.js new file mode 100644 index 000000000..18a5aae86 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-values/detected-token-values.test.js @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { renderWithProvider, screen } from '../../../../../test/jest'; +import configureStore from '../../../../store/store'; +import testData from '../../../../../.storybook/test-data'; + +import DetectedTokenValues from './detected-token-values'; + +describe('DetectedTokenValues', () => { + const args = { + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + symbol: 'SNX', + decimals: 18, + iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png', + aggregators: [ + 'aave', + 'bancor', + 'cmc', + 'cryptocom', + 'coinGecko', + 'oneInch', + 'paraswap', + 'pmm', + 'synthetix', + 'zapper', + 'zerion', + 'zeroEx', + ], + }; + + it('should render the detected token address', async () => { + const store = configureStore(testData); + renderWithProvider(, store); + + expect(screen.getByText('0 SNX')).toBeInTheDocument(); + expect(screen.getByText('$0')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/detected-token/detected-token-values/index.scss b/ui/components/app/detected-token/detected-token-values/index.scss new file mode 100644 index 000000000..4024c7fd8 --- /dev/null +++ b/ui/components/app/detected-token/detected-token-values/index.scss @@ -0,0 +1,5 @@ +.detected-token-values { + &__checkbox { + margin-left: auto; + } +} diff --git a/ui/components/app/edit-gas-display/edit-gas-display.component.js b/ui/components/app/edit-gas-display/edit-gas-display.component.js index dd1af320a..59518f605 100644 --- a/ui/components/app/edit-gas-display/edit-gas-display.component.js +++ b/ui/components/app/edit-gas-display/edit-gas-display.component.js @@ -35,7 +35,7 @@ import ActionableMessage from '../../ui/actionable-message/actionable-message'; import { I18nContext } from '../../../contexts/i18n'; import GasTiming from '../gas-timing'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; export default function EditGasDisplay({ mode = EDIT_GAS_MODES.MODIFY_IN_PLACE, diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/index.scss b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/index.scss index 63c1b7e98..92d067dec 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/index.scss +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/index.scss @@ -26,11 +26,11 @@ &__label { white-space: nowrap; - width: 50%; + min-width: 50%; } &__value { - width: 50%; + min-width: 50%; } p { @@ -44,6 +44,7 @@ div { display: flex; flex-direction: row; + flex-wrap: wrap; text-align: left; } } diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js new file mode 100644 index 000000000..076b02f30 --- /dev/null +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js @@ -0,0 +1,192 @@ +import React, { useCallback, useContext, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import Button from '../../ui/button'; +import { I18nContext } from '../../../contexts/i18n'; +import Box from '../../ui/box/box'; +import { + ALIGN_ITEMS, + DISPLAY, + JUSTIFY_CONTENT, +} from '../../../helpers/constants/design-system'; + +const radius = 14; +const strokeWidth = 2; +const radiusWithStroke = radius - strokeWidth / 2; + +export default function HoldToRevealButton({ buttonText, onLongPressed }) { + const t = useContext(I18nContext); + const isLongPressing = useRef(false); + const [isUnlocking, setIsUnlocking] = useState(false); + const [hasTriggeredUnlock, setHasTriggeredUnlock] = useState(false); + + /** + * Prevent animation events from propogating up + * + * @param e - Native animation event - React.AnimationEvent + */ + const preventPropogation = (e) => { + e.stopPropagation(); + }; + + /** + * Event for mouse click down + */ + const onMouseDown = () => { + isLongPressing.current = true; + }; + + /** + * Event for mouse click up + */ + const onMouseUp = () => { + isLongPressing.current = false; + }; + + /** + * 1. Progress cirle completed. Begin next animation phase (Shrink halo and show unlocked padlock) + */ + const onProgressComplete = () => { + isLongPressing.current && setIsUnlocking(true); + }; + + /** + * 2. Trigger onLongPressed callback. Begin next animation phase (Shrink unlocked padlock and fade in original content) + * + * @param e - Native animation event - React.AnimationEvent + */ + const triggerOnLongPressed = (e) => { + onLongPressed(); + setHasTriggeredUnlock(true); + preventPropogation(e); + }; + + /** + * 3. Reset animation states + */ + const resetAnimationStates = () => { + setIsUnlocking(false); + setHasTriggeredUnlock(false); + }; + + const renderPreCompleteContent = useCallback(() => { + return ( + + + + + + + + + + + + + {t('padlock')} + + + ); + }, [isUnlocking, hasTriggeredUnlock, t]); + + const renderPostCompleteContent = useCallback(() => { + return isUnlocking ? ( +
+
+ + + +
+
+ + + +
+
+ {t('padlock')} +
+
+ ) : null; + }, [isUnlocking, hasTriggeredUnlock, t]); + + return ( + + ); +} + +HoldToRevealButton.propTypes = { + /** + * Text to be displayed on the button + */ + buttonText: PropTypes.string.isRequired, + /** + * Function to be called after the animation is finished + */ + onLongPressed: PropTypes.func.isRequired, +}; diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.stories.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.stories.js new file mode 100644 index 000000000..3f46c1f0d --- /dev/null +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.stories.js @@ -0,0 +1,22 @@ +import React from 'react'; +import HoldToRevealButton from './hold-to-reveal-button'; + +export default { + title: 'Components/App/HoldToRevealButton', + id: __filename, + argTypes: { + buttonText: { control: 'text' }, + onLongPressed: { action: 'Revealing the SRP' }, + }, +}; + +export const DefaultStory = (args) => { + return ; +}; + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + buttonText: 'Hold to reveal SRP', + onLongPressed: () => console.log('Revealed'), +}; diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js new file mode 100644 index 000000000..f3f7c4d97 --- /dev/null +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import HoldToRevealButton from './hold-to-reveal-button'; + +describe('HoldToRevealButton', () => { + let props = {}; + + beforeEach(() => { + const mockOnLongPressed = jest.fn(); + + props = { + onLongPressed: mockOnLongPressed, + buttonText: 'Hold to reveal SRP', + }; + }); + + it('should render a button with label', () => { + const { getByText } = render(); + + expect(getByText('Hold to reveal SRP')).toBeInTheDocument(); + }); + + it('should render a button when mouse is down and up', () => { + const { getByText } = render(); + + const button = getByText('Hold to reveal SRP'); + + fireEvent.mouseDown(button); + + expect(button).toBeDefined(); + + fireEvent.mouseUp(button); + + expect(button).toBeDefined(); + }); + + it('should not show the locked padlock when a button is long pressed and then should show it after it was lifted off before the animation concludes', () => { + const { getByText } = render(); + + const button = getByText('Hold to reveal SRP'); + + fireEvent.mouseDown(button); + + waitFor(() => { + expect(button.firstChild).toHaveClass( + 'hold-to-reveal-button__lock-icon-container', + ); + }); + + fireEvent.mouseUp(button); + + waitFor(() => { + expect(button.firstChild).not.toHaveClass( + 'hold-to-reveal-button__lock-icon-container', + ); + }); + }); + + it('should show the unlocked padlock when a button is long pressed for the duration of the animation', () => { + const { getByText } = render(); + + const button = getByText('Hold to reveal SRP'); + + fireEvent.mouseDown(button); + + waitFor(() => { + expect(button.firstChild).toHaveClass( + 'hold-to-reveal-button__unlock-icon-container', + ); + }); + }); +}); diff --git a/ui/components/app/hold-to-reveal-button/index.js b/ui/components/app/hold-to-reveal-button/index.js new file mode 100644 index 000000000..d180f5e69 --- /dev/null +++ b/ui/components/app/hold-to-reveal-button/index.js @@ -0,0 +1 @@ +export { default } from './hold-to-reveal-button'; diff --git a/ui/components/app/hold-to-reveal-button/index.scss b/ui/components/app/hold-to-reveal-button/index.scss new file mode 100644 index 000000000..f9765ec60 --- /dev/null +++ b/ui/components/app/hold-to-reveal-button/index.scss @@ -0,0 +1,164 @@ +// Variables +$circle-radius: 14px; +$circle-diameter: $circle-radius * 2; +// Circumference ~ (2*PI*R). We reduced the number a little to create a snappier interaction +$circle-circumference: 82; +$circle-stroke-width: 2px; + +// Keyframes +@keyframes collapse { + from { + transform: scale(1); + } + + to { + transform: scale(0); + } +} + +@keyframes expand { + from { + transform: scale(0); + } + + to { + transform: scale(1); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.hold-to-reveal-button { + // Shared styles + &__absolute-fill { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } + + &__icon { + height: $circle-diameter; + width: $circle-diameter; + } + + &__circle-shared { + fill: transparent; + stroke-width: $circle-stroke-width; + } + + // Class styles + &__button-hold { + padding: 6px 13px 6px 9px !important; + transform: scale(1) !important; + transition: 0.5s transform !important; + + &:active { + background-color: var(--primary-1) !important; + transform: scale(1.05) !important; + + .hold-to-reveal-button__circle-foreground { + stroke-dashoffset: 0 !important; + } + + .hold-to-reveal-button__lock-icon-container { + opacity: 0 !important; + } + } + } + + &__absolute-fill { + @extend .hold-to-reveal-button__absolute-fill; + } + + &__icon-container { + @extend .hold-to-reveal-button__icon; + + position: relative; + } + + &__main-icon-show { + animation: 0.4s fadeIn 1.2s forwards; + } + + &__invisible { + opacity: 0; + } + + &__circle-svg { + @extend .hold-to-reveal-button__icon; + + transform: rotate(-90deg); + } + + &__circle-background { + @extend .hold-to-reveal-button__circle-shared; + + stroke: var(--primary-3); + } + + &__circle-foreground { + @extend .hold-to-reveal-button__circle-shared; + + stroke: var(--ui-white); + stroke-dasharray: $circle-circumference; + stroke-dashoffset: $circle-circumference; + transition: 1s stroke-dashoffset; + } + + &__lock-icon-container { + @extend .hold-to-reveal-button__absolute-fill; + + transition: 0.3s opacity; + opacity: 1; + } + + &__lock-icon { + width: 7.88px; + height: 9px; + } + + &__unlock-icon-hide { + animation: 0.3s collapse 1s forwards; + } + + &__circle-static-outer-container { + animation: 0.25s collapse forwards; + } + + &__circle-static-outer { + fill: var(--ui-white); + } + + &__circle-static-inner-container { + animation: 0.125s collapse forwards; + } + + &__circle-static-inner { + fill: var(--primary-1); + } + + &__unlock-icon-container { + @extend .hold-to-reveal-button__absolute-fill; + + display: flex; + align-items: center; + justify-content: center; + transform: scale(0); + animation: 0.175s expand 0.2s forwards; + } + + &__unlock-icon { + width: 14px; + height: 11px; + } +} diff --git a/ui/components/app/import-token-link/import-token-link.component.js b/ui/components/app/import-token-link/import-token-link.component.js index 4147a9256..d89323d46 100644 --- a/ui/components/app/import-token-link/import-token-link.component.js +++ b/ui/components/app/import-token-link/import-token-link.component.js @@ -7,7 +7,7 @@ import Button from '../../ui/button'; import Box from '../../ui/box/box'; import { TEXT_ALIGN } from '../../../helpers/constants/design-system'; import { detectNewTokens } from '../../../store/actions'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; export default function ImportTokenLink({ isMainnet }) { const trackEvent = useContext(MetaMetricsContext); diff --git a/ui/components/app/menu-bar/account-options-menu.js b/ui/components/app/menu-bar/account-options-menu.js index 9e83f3788..5aa821a22 100644 --- a/ui/components/app/menu-bar/account-options-menu.js +++ b/ui/components/app/menu-bar/account-options-menu.js @@ -17,7 +17,7 @@ import { import { useI18nContext } from '../../../hooks/useI18nContext'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; export default function AccountOptionsMenu({ anchorElement, onClose }) { const t = useI18nContext(); diff --git a/ui/components/app/menu-bar/menu-bar.js b/ui/components/app/menu-bar/menu-bar.js index 9d8af1066..b2e59d62b 100644 --- a/ui/components/app/menu-bar/menu-bar.js +++ b/ui/components/app/menu-bar/menu-bar.js @@ -9,7 +9,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { CONNECTED_ACCOUNTS_ROUTE } from '../../../helpers/constants/routes'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getOriginOfCurrentTab } from '../../../selectors'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; import AccountOptionsMenu from './account-options-menu'; export default function MenuBar() { diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js index 7bd6779aa..aee9bfc3e 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js @@ -6,30 +6,14 @@ export default { id: __filename, component: ConfirmRemoveAccount, argTypes: { - hideModal: { - action: 'hideModal', - }, - removeAccount: { - action: 'removeAccount', - }, identity: { control: 'object', }, - chainId: { - control: 'text', - }, - rpcPrefs: { - control: 'object', - }, }, args: { identity: { control: 'object', }, - chainId: 'chainId', - rpcPrefs: { - control: 'object', - }, }, }; diff --git a/ui/components/app/modals/export-private-key-modal/export-private-key-modal.stories.js b/ui/components/app/modals/export-private-key-modal/export-private-key-modal.stories.js index 74901533b..60a947f41 100644 --- a/ui/components/app/modals/export-private-key-modal/export-private-key-modal.stories.js +++ b/ui/components/app/modals/export-private-key-modal/export-private-key-modal.stories.js @@ -4,34 +4,8 @@ import ExportPrivateKeyModal from '.'; export default { title: 'Components/App/Modals/ExportPrivateKeyModal', id: __filename, - argTypes: { - exportAccount: { - action: 'exportAccount', - }, - selectedIdentity: { - control: 'object', - }, - warning: { - control: 'node', - }, - showAccountDetailModal: { - action: 'showAccountDetailModal', - }, - hideModal: { - action: 'hideModal', - }, - hideWarning: { - action: 'hideWarning', - }, - clearAccountDetails: { - action: 'clearAccountDetails', - }, - previousModalState: { - control: 'text', - }, - }, }; -export const DefaultStory = (args) => ; +export const DefaultStory = () => ; DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/srp-input/srp-input.js b/ui/components/app/srp-input/srp-input.js index f6ac3b261..0b114d63c 100644 --- a/ui/components/app/srp-input/srp-input.js +++ b/ui/components/app/srp-input/srp-input.js @@ -8,14 +8,17 @@ import ActionableMessage from '../../ui/actionable-message'; import Dropdown from '../../ui/dropdown'; import Typography from '../../ui/typography'; import ShowHideToggle from '../../ui/show-hide-toggle'; -import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; +import { + FONT_WEIGHT, + TYPOGRAPHY, +} from '../../../helpers/constants/design-system'; import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase'; const { isValidMnemonic } = ethers.utils; const defaultNumberOfWords = 12; -export default function SrpInput({ onChange }) { +export default function SrpInput({ onChange, srpText }) { const [srpError, setSrpError] = useState(''); const [pasteFailed, setPasteFailed] = useState(false); const [draftSrp, setDraftSrp] = useState( @@ -121,8 +124,8 @@ export default function SrpInput({ onChange }) { return (
{ const onChange = jest.fn(); const { getByText } = renderWithLocalization( - , + , ); await waitFor(() => getByText(enLocale.secretRecoveryPhrase.message)); @@ -56,7 +59,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.keyboard('test'); @@ -68,7 +74,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); for (const index of new Array(11).keys()) { @@ -83,7 +92,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); for (const index of new Array(11).keys()) { @@ -100,7 +112,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); for (const index of new Array(10).keys()) { @@ -117,7 +132,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = invalidChecksum.split(' '); @@ -133,7 +151,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = invalidWordCorrectChecksum.split(' '); @@ -149,7 +170,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); @@ -166,7 +190,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); @@ -185,7 +212,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); @@ -205,7 +235,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste('test'); @@ -217,7 +250,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); for (const index of new Array(11).keys()) { @@ -232,7 +268,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = invalidChecksum.split(' '); @@ -248,7 +287,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = invalidWordCorrectChecksum.split(' '); @@ -264,7 +306,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); @@ -281,7 +326,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); @@ -300,7 +348,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); @@ -320,7 +371,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste('test'); @@ -332,7 +386,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooManyWords); @@ -345,7 +402,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(invalidInput); @@ -357,7 +417,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-1').focus(); await userEvent.paste(invalidInput); @@ -369,7 +432,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-1').focus(); await userEvent.paste(correct); @@ -383,7 +449,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-1').focus(); await userEvent.paste(correct); @@ -398,7 +467,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -414,7 +486,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -433,7 +508,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -452,7 +530,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -472,7 +553,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -487,7 +571,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -504,7 +591,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -523,7 +613,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -542,7 +635,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -560,7 +656,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(correct); @@ -572,7 +671,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(invalidChecksum); @@ -586,7 +688,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(poorlyFormattedInput); @@ -598,7 +703,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-1').focus(); await userEvent.paste(poorlyFormattedInput); @@ -614,7 +722,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByText, queryByText } = renderWithLocalization( - , + , ); await waitFor(() => getByText(enLocale.secretRecoveryPhrase.message)); @@ -631,7 +742,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = tooFewWords.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -649,7 +763,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByRole, getByTestId, queryByText } = renderWithLocalization( - , + , ); await userEvent.selectOptions(getByRole('combobox'), '15'); const srpParts = invalidWordCount.split(' '); @@ -668,7 +785,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = invalidChecksum.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -688,7 +808,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = invalidWordCorrectChecksum.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -708,7 +831,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -729,7 +855,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -753,7 +882,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -777,7 +909,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -802,7 +937,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = tooFewWords.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -820,7 +958,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, getByRole, queryByText } = renderWithLocalization( - , + , ); await userEvent.selectOptions(getByRole('combobox'), '15'); const srpParts = invalidWordCount.split(' '); @@ -839,7 +980,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = invalidChecksum.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -859,7 +1003,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = invalidWordCorrectChecksum.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -879,7 +1026,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -900,7 +1050,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -924,7 +1077,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -948,7 +1104,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -971,7 +1130,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooFewWords); @@ -986,7 +1148,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(invalidWordCount); @@ -1001,7 +1166,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(invalidChecksum); @@ -1018,7 +1186,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(invalidWordCorrectChecksum); @@ -1035,7 +1206,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(correct); @@ -1053,7 +1227,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(poorlyFormattedInput); @@ -1074,7 +1251,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); for (const index of new Array(12).keys()) { @@ -1093,7 +1273,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryAllByRole } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1108,7 +1291,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryAllByRole } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1123,7 +1309,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryAllByRole } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(correct); @@ -1137,7 +1326,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1161,7 +1353,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1187,7 +1382,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1217,7 +1415,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1247,7 +1448,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(correct); @@ -1277,7 +1481,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1304,7 +1511,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1333,7 +1543,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryAllByRole } = renderWithLocalization( - , + , ); await userEvent.click(getByTestId('import-srp__srp-word-0-checkbox')); getByTestId('import-srp__srp-word-0').focus(); @@ -1353,7 +1566,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryAllByRole } = renderWithLocalization( - , + , ); await userEvent.click(getByTestId('import-srp__srp-word-0-checkbox')); getByTestId('import-srp__srp-word-0').focus(); @@ -1373,7 +1589,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryAllByRole } = renderWithLocalization( - , + , ); await userEvent.click(getByTestId('import-srp__srp-word-0-checkbox')); getByTestId('import-srp__srp-word-0').focus(); @@ -1390,7 +1609,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryAllByRole } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(correct); @@ -1410,7 +1632,10 @@ describe('srp-input', () => { const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText'); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1426,7 +1651,10 @@ describe('srp-input', () => { const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText'); const { getByTestId } = renderWithLocalization( - , + , ); const srpParts = correct.split(' '); for (const index of new Array(srpParts.length).keys()) { @@ -1442,7 +1670,10 @@ describe('srp-input', () => { const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText'); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooManyWords); @@ -1455,7 +1686,10 @@ describe('srp-input', () => { const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText'); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooFewWords); @@ -1468,7 +1702,10 @@ describe('srp-input', () => { const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText'); const { getByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(correct); @@ -1482,7 +1719,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { queryByTestId, queryByRole } = renderWithLocalization( - , + , ); expect( @@ -1502,7 +1742,12 @@ describe('srp-input', () => { getByTestId, queryByTestId, queryByRole, - } = renderWithLocalization(); + } = renderWithLocalization( + , + ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(new Array(15).fill('test').join(' ')); @@ -1524,7 +1769,12 @@ describe('srp-input', () => { getByTestId, queryByTestId, queryByRole, - } = renderWithLocalization(); + } = renderWithLocalization( + , + ); await userEvent.selectOptions(getByRole('combobox'), '15'); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(correct); @@ -1546,7 +1796,12 @@ describe('srp-input', () => { getByTestId, queryByTestId, queryByRole, - } = renderWithLocalization(); + } = renderWithLocalization( + , + ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(invalidWordCount); @@ -1564,7 +1819,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByRole, queryByTestId, queryByRole } = renderWithLocalization( - , + , ); await userEvent.selectOptions(getByRole('combobox'), '24'); @@ -1582,7 +1840,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByRole, getByTestId, queryByTestId } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(new Array(15).fill('test').join(' ')); @@ -1603,7 +1864,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooManyWords); @@ -1617,7 +1881,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, getByText, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooManyWords); @@ -1632,7 +1899,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooManyWords); @@ -1647,7 +1917,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooManyWords); @@ -1662,7 +1935,10 @@ describe('srp-input', () => { const onChange = jest.fn(); const { getByTestId, queryByText } = renderWithLocalization( - , + , ); getByTestId('import-srp__srp-word-0').focus(); await userEvent.paste(tooManyWords); diff --git a/ui/components/app/tab-bar/index.scss b/ui/components/app/tab-bar/index.scss index 36b575e31..4329b5b5c 100644 --- a/ui/components/app/tab-bar/index.scss +++ b/ui/components/app/tab-bar/index.scss @@ -38,7 +38,7 @@ &__content { padding: 12px 18px; display: flex; - flex-flow: row wrap; + flex: 1 1 auto; align-items: center; position: relative; @@ -64,8 +64,14 @@ } &__icon { - margin-inline-end: 16px; display: flex; + justify-content: center; + margin-inline-end: 16px; + flex: 0 0 18px; + + @media screen and (min-width: $break-large) { + flex: 0 0 14px; + } } } diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.js b/ui/components/app/transaction-list-item/transaction-list-item.component.js index f911dcff8..bdc58988c 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.js @@ -37,7 +37,7 @@ import CancelButton from '../cancel-button'; import CancelSpeedupPopover from '../cancel-speedup-popover'; import EditGasFeePopover from '../edit-gas-fee-popover'; import EditGasPopover from '../edit-gas-popover'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; function TransactionListItemInner({ transactionGroup, diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.stories.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.stories.js index 0fcdf1f9f..663ec82cf 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.stories.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.stories.js @@ -47,6 +47,9 @@ export default { fiatNumberOfDecimals: { control: 'number', }, + showFiat: { + control: 'boolean', + }, }, args: { type: ETH, diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 95ef5aec6..cf5073bf1 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -30,7 +30,7 @@ import SendIcon from '../../ui/icon/overview-send-icon.component'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import IconButton from '../../ui/icon-button'; import { isHardwareKeyring } from '../../../helpers/utils/hardware'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; import WalletOverview from './wallet-overview'; const EthOverview = ({ className }) => { diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index c2860e727..fc2caa1b7 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -27,7 +27,7 @@ import SendIcon from '../../ui/icon/overview-send-icon.component'; import IconButton from '../../ui/icon-button'; import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys'; import { showModal } from '../../../store/actions'; -import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; import { ASSET_TYPES } from '../../../../shared/constants/transaction'; import WalletOverview from './wallet-overview'; diff --git a/ui/components/app/whats-new-popup/whats-new-popup.js b/ui/components/app/whats-new-popup/whats-new-popup.js index 51b425b4c..bba492040 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -45,6 +45,10 @@ function getActionFunctionById(id, history) { updateViewedNotifications({ 8: true }); history.push(ADVANCED_ROUTE); }, + 10: () => { + updateViewedNotifications({ 10: true }); + history.push(`${ADVANCED_ROUTE}#token-description`); + }, }; return actionFunctions[id]; diff --git a/ui/components/ui/actionable-message/index.scss b/ui/components/ui/actionable-message/index.scss index e9f3e7f1f..3ab7cb146 100644 --- a/ui/components/ui/actionable-message/index.scss +++ b/ui/components/ui/actionable-message/index.scss @@ -108,7 +108,7 @@ text-decoration: underline; } - button { + .actionable-message__actions button { background: var(--color-warning-default); color: var(--color-warning-inverse); } @@ -126,7 +126,7 @@ text-align: left; } - button { + .actionable-message__actions button { background: var(--color-error-default); color: var(--color-error-inverse); } @@ -139,7 +139,7 @@ background: var(--color-success-muted); } - button { + .actionable-message__actions button { background: var(--color-success-default); color: var(--color-success-inverse); } diff --git a/ui/components/ui/identicon/identicon.component.js b/ui/components/ui/identicon/identicon.component.js index 8174ab38e..b7aa747ab 100644 --- a/ui/components/ui/identicon/identicon.component.js +++ b/ui/components/ui/identicon/identicon.component.js @@ -151,14 +151,22 @@ export default class Identicon extends PureComponent { } if (address) { - // token from dynamic api list is fetched when useTokenDetection is true - // And since the token.address from allTokens is checksumaddress - // tokenAddress have to be changed to lowercase when we are using dynamic list - const tokenAddress = useTokenDetection ? address.toLowerCase() : address; - if (tokenAddress && tokenList[tokenAddress]?.iconUrl) { - return this.renderJazzicon(); + if (process.env.TOKEN_DETECTION_V2) { + if (tokenList[address.toLowerCase()]?.iconUrl) { + return this.renderJazzicon(); + } + } else { + /** TODO: Remove during TOKEN_DETECTION_V2 feature flag clean up */ + // token from dynamic api list is fetched when useTokenDetection is true + // And since the token.address from allTokens is checksumaddress + // tokenAddress have to be changed to lowercase when we are using dynamic list + const tokenAddress = useTokenDetection + ? address.toLowerCase() + : address; + if (tokenAddress && tokenList[tokenAddress]?.iconUrl) { + return this.renderJazzicon(); + } } - return (
( +export const SendComponent = (args) => ( } titleIcon={} - title={text('title', 'Send DAI')} - className="list-item" - subtitle={text('subtitle', 'Sept 20 · To: 00X4...3058')} + title={args.title} + subtitle={args.subtitle} + className={args.className} rightContent={ } >
- +
); -export const PendingComponent = () => ( +SendComponent.argTypes = { + secondaryButtonText: { + control: 'text', + defaultValue: 'Speed Up', + }, + cancelButtonText: { + control: 'text', + defaultValue: 'Cancel', + }, +}; + +export const PendingComponent = (args) => ( } - title={text('title', 'Hatch Turtles')} - className="list-item" + className={args.className} subtitleStatus={ @@ -67,41 +101,39 @@ export const PendingComponent = () => ( ·{' '} } - subtitle={text('subtitle', 'Turtlefarm.com')} rightContent={ } /> ); -export const ApproveComponent = () => ( +export const ApproveComponent = (args) => ( } - title={text('title', 'Approve spend limit')} - className="list-item" - subtitle={text('subtitle', 'Sept 20 · oxuniverse.com')} + className={args.className} rightContent={ } /> ); -export const ReceiveComponent = () => ( +export const ReceiveComponent = (args) => ( } - title={text('title', 'Hatch Turtles')} - className="list-item" - subtitle={text('subtitle', 'Sept 20 · From: 00X4...3058')} + className={args.className} rightContent={ } /> diff --git a/ui/components/ui/nickname-popover/nickname-popover.component.js b/ui/components/ui/nickname-popover/nickname-popover.component.js index 87c1f9ef7..d0f8a194f 100644 --- a/ui/components/ui/nickname-popover/nickname-popover.component.js +++ b/ui/components/ui/nickname-popover/nickname-popover.component.js @@ -1,4 +1,5 @@ import React, { useCallback, useContext } from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { I18nContext } from '../../../contexts/i18n'; import Tooltip from '../tooltip'; @@ -8,6 +9,7 @@ import Identicon from '../identicon/identicon.component'; import { shortenAddress } from '../../../helpers/utils/util'; import CopyIcon from '../icon/copy-icon.component'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; +import { getUseTokenDetection, getTokenList } from '../../../selectors'; const NicknamePopover = ({ address, @@ -23,6 +25,8 @@ const NicknamePopover = ({ }, [onAdd]); const [copied, handleCopy] = useCopyToClipboard(); + const useTokenDetection = useSelector(getUseTokenDetection); + const tokenList = useSelector(getTokenList); return (
@@ -31,6 +35,8 @@ const NicknamePopover = ({ address={address} diameter={36} className="nickname-popover__identicon" + useTokenDetection={useTokenDetection} + tokenList={tokenList} />
{nickname || shortenAddress(address)} diff --git a/ui/components/ui/update-nickname-popover/update-nickname-popover.js b/ui/components/ui/update-nickname-popover/update-nickname-popover.js index 2713d7b70..3396d4fdc 100644 --- a/ui/components/ui/update-nickname-popover/update-nickname-popover.js +++ b/ui/components/ui/update-nickname-popover/update-nickname-popover.js @@ -1,4 +1,5 @@ import React, { useCallback, useContext, useState } from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import Popover from '../popover'; @@ -8,6 +9,7 @@ import TextField from '../text-field'; import { I18nContext } from '../../../contexts/i18n'; import Identicon from '../identicon/identicon.component'; +import { getUseTokenDetection, getTokenList } from '../../../selectors'; export default function UpdateNicknamePopover({ nickname, @@ -42,6 +44,9 @@ export default function UpdateNicknamePopover({ onClose(); }; + const useTokenDetection = useSelector(getUseTokenDetection); + const tokenList = useSelector(getTokenList); + return (