From 4f36a3121c1667f042a63dac0cb0300772952643 Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Fri, 17 Mar 2023 10:36:20 -0700 Subject: [PATCH 01/20] Sign in with Ethereum: re-enable warning UI for mismatched domains / disable domain binding (#18200) * siwe: re-enable warning UI for mismatched domains - unblocks mismatched domain support - we may re-add error handling here #18184 - reverts logic from #16616 * siwe: fix mismatch domain warning msg UI * lint: rm whitespace EOL * siwe: rm unit test * lint: fix whitespace * Revert "siwe: rm unit test" This reverts commit c80a4a2e661609c46c76d1e43e05909b6db3f0f5. --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- app/scripts/lib/personal-message-manager.js | 9 --------- app/scripts/lib/personal-message-manager.test.js | 10 ---------- .../app/signature-request-siwe/index.scss | 15 +++++++++++---- .../signature-request-siwe.js | 1 - 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 8440fa4ce0ac..bb0a2b0c1e04 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -153,15 +153,6 @@ export default class PersonalMessageManager extends EventEmitter { const siwe = detectSIWE(msgParams); msgParams.siwe = siwe; - if (siwe.isSIWEMessage && req.origin) { - const { host } = new URL(req.origin); - if (siwe.parsedMessage.domain !== host) { - throw new Error( - `SIWE domain is not valid: "${host}" !== "${siwe.parsedMessage.domain}"`, - ); - } - } - // create txData obj with parameters and meta data const time = new Date().getTime(); const msgId = createId(); diff --git a/app/scripts/lib/personal-message-manager.test.js b/app/scripts/lib/personal-message-manager.test.js index a4f8f4613f6d..e565c0229b69 100644 --- a/app/scripts/lib/personal-message-manager.test.js +++ b/app/scripts/lib/personal-message-manager.test.js @@ -178,15 +178,5 @@ describe('Personal Message Manager', () => { const result2 = messageManager.getMsg(msgId2); expect(result2.msgParams.siwe.isSIWEMessage).toStrictEqual(false); }); - - it("should throw an error if the SIWE message's domain doesn't match", async () => { - const request = { origin: 'https://mismatched-domain.com' }; - const { host: siweDomain } = new URL(origin); - const { host: browserDomain } = new URL(request.origin); - const expectedError = `SIWE domain is not valid: "${browserDomain}" !== "${siweDomain}"`; - await expect(async () => { - await messageManager.addUnapprovedMessage(msgParams, request); - }).rejects.toThrow(expectedError); - }); }); }); diff --git a/ui/components/app/signature-request-siwe/index.scss b/ui/components/app/signature-request-siwe/index.scss index a6de5df10bdf..2dedc438ace4 100644 --- a/ui/components/app/signature-request-siwe/index.scss +++ b/ui/components/app/signature-request-siwe/index.scss @@ -21,18 +21,25 @@ box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08); } + /** @todo replace ActionableMessage or remove overwritten code. */ .signature-request-siwe__actionable-message { - margin: 0 16px 16px; + margin: 0 16px; + flex-direction: row; + align-items: initial; .icon { position: absolute; left: 17px; top: 13px; } - } - .actionable-message--with-icon.actionable-message--with-right-button { - padding-left: 48px; + .actionable-message__message { + padding-left: 16px; + } + + &.actionable-message--with-icon { + padding-left: 16px; + } } } diff --git a/ui/components/app/signature-request-siwe/signature-request-siwe.js b/ui/components/app/signature-request-siwe/signature-request-siwe.js index b801be970938..e7d7e178a6a7 100644 --- a/ui/components/app/signature-request-siwe/signature-request-siwe.js +++ b/ui/components/app/signature-request-siwe/signature-request-siwe.js @@ -120,7 +120,6 @@ export default function SignatureRequestSIWE({ } iconFillColor="var(--color-error-default)" useIcon - withRightButton icon={} /> )} From f730c6c8b4345a198c56c49c9aef1c13e6519c4f Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Fri, 17 Mar 2023 18:19:39 +0000 Subject: [PATCH 02/20] Version v10.26.2 --- CHANGELOG.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f30379c39cc..3caea2e39b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.26.2] +### Uncategorized +- Sign in with Ethereum: re-enable warning UI for mismatched domains / disable domain binding ([#18200](https://github.com/MetaMask/metamask-extension/pull/18200)) + ## [10.26.1] ### Fixed - Fix main build by modifying desktop build steps ([#18112](https://github.com/MetaMask/metamask-extension/pull/18112)) @@ -3532,7 +3536,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.26.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.26.2...HEAD +[10.26.2]: https://github.com/MetaMask/metamask-extension/compare/v10.26.1...v10.26.2 [10.26.1]: https://github.com/MetaMask/metamask-extension/compare/v10.26.0...v10.26.1 [10.26.0]: https://github.com/MetaMask/metamask-extension/compare/v10.25.0...v10.26.0 [10.25.0]: https://github.com/MetaMask/metamask-extension/compare/v10.24.2...v10.25.0 diff --git a/package.json b/package.json index c0e532056db3..7b100c4a3538 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask-crx", - "version": "10.26.1", + "version": "10.26.2", "private": true, "repository": { "type": "git", From dacdaf031cd1b35a45e03041e55709d6302de4be Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Fri, 17 Mar 2023 23:29:39 +0900 Subject: [PATCH 03/20] security: patch request for CVE-2023-28155 (#18208) * security: patch request for CVE-2023-28155 GHSA-p8p7-x288-28g6 Ported from https://github.com/request/request/pull/3444 * add iyarc exclusion --- .iyarc | 4 +++ .../request-npm-2.88.2-f4a57c72c4.patch | 31 +++++++++++++++++++ package.json | 5 ++- yarn.lock | 30 +++++++++++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch diff --git a/.iyarc b/.iyarc index 3fa8de8b354d..79536d3837f1 100644 --- a/.iyarc +++ b/.iyarc @@ -15,3 +15,7 @@ GHSA-6fc8-4gx4-v693 # patched version of 3.3.1. We can remove this once the # smart-transaction-controller updates its dependency. GHSA-8gh8-hqwg-xf34 + +# request library is subject to SSRF. +# addressed by temporary patch in .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch +GHSA-p8p7-x288-28g6 diff --git a/.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch b/.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch new file mode 100644 index 000000000000..c879c340c938 --- /dev/null +++ b/.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch @@ -0,0 +1,31 @@ +diff --git a/lib/redirect.js b/lib/redirect.js +index b9150e77c73d63367845c0aec15b5684d900943f..2864f9f2abc481ecf2b2dd96b1293f5b93393efd 100644 +--- a/lib/redirect.js ++++ b/lib/redirect.js +@@ -14,6 +14,7 @@ function Redirect (request) { + this.redirects = [] + this.redirectsFollowed = 0 + this.removeRefererHeader = false ++ this.allowInsecureRedirect = false + } + + Redirect.prototype.onRequest = function (options) { +@@ -40,6 +41,9 @@ Redirect.prototype.onRequest = function (options) { + if (options.followOriginalHttpMethod !== undefined) { + self.followOriginalHttpMethod = options.followOriginalHttpMethod + } ++ if (options.allowInsecureRedirect !== undefined) { ++ self.allowInsecureRedirect = options.allowInsecureRedirect ++ } + } + + Redirect.prototype.redirectTo = function (response) { +@@ -108,7 +112,7 @@ Redirect.prototype.onResponse = function (response) { + request.uri = url.parse(redirectTo) + + // handle the case where we change protocol from https to http or vice versa +- if (request.uri.protocol !== uriPrev.protocol) { ++ if (request.uri.protocol !== uriPrev.protocol && self.allowInsecureRedirect) { + delete request.agent + } + diff --git a/package.json b/package.json index 7b100c4a3538..a298877942ba 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,10 @@ "lavamoat-core@^14.0.0": "patch:lavamoat-core@npm%3A14.0.0#./.yarn/patches/lavamoat-core-npm-14.0.0-0f5bdac846.patch", "lavamoat-core@^12.3.0": "patch:lavamoat-core@npm%3A12.4.0#./.yarn/patches/lavamoat-core-npm-12.4.0-cecca1a9b5.patch", "lavamoat-core@^12.4.0": "patch:lavamoat-core@npm%3A12.4.0#./.yarn/patches/lavamoat-core-npm-12.4.0-cecca1a9b5.patch", - "@lavamoat/snow@^1.4.1": "patch:@lavamoat/snow@npm%3A1.4.1#./.yarn/patches/@lavamoat-snow-npm-1.4.1-405a48e593.patch" + "@lavamoat/snow@^1.4.1": "patch:@lavamoat/snow@npm%3A1.4.1#./.yarn/patches/@lavamoat-snow-npm-1.4.1-405a48e593.patch", + "request@^2.83.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", + "request@^2.88.2": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", + "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch" }, "dependencies": { "@babel/runtime": "^7.5.5", diff --git a/yarn.lock b/yarn.lock index 1e38386698e4..e023cfe05f63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29774,7 +29774,7 @@ __metadata: languageName: node linkType: hard -"request@npm:^2.83.0, request@npm:^2.85.0, request@npm:^2.88.2": +"request@npm:2.88.2": version: 2.88.2 resolution: "request@npm:2.88.2" dependencies: @@ -29802,6 +29802,34 @@ __metadata: languageName: node linkType: hard +"request@patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch::locator=metamask-crx%40workspace%3A.": + version: 2.88.2 + resolution: "request@patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch::version=2.88.2&hash=2aadd7&locator=metamask-crx%40workspace%3A." + dependencies: + aws-sign2: ~0.7.0 + aws4: ^1.8.0 + caseless: ~0.12.0 + combined-stream: ~1.0.6 + extend: ~3.0.2 + forever-agent: ~0.6.1 + form-data: ~2.3.2 + har-validator: ~5.1.3 + http-signature: ~1.2.0 + is-typedarray: ~1.0.0 + isstream: ~0.1.2 + json-stringify-safe: ~5.0.1 + mime-types: ~2.1.19 + oauth-sign: ~0.9.0 + performance-now: ^2.1.0 + qs: ~6.5.2 + safe-buffer: ^5.1.2 + tough-cookie: ~2.5.0 + tunnel-agent: ^0.6.0 + uuid: ^3.3.2 + checksum: 1a64d706b36b2bdd5803c3a0fd3fee5e76e8c17d01c34f84972460fbfa5914302c300821a1fafce804d236e637f3745f3bdfbbb4219c139e112076790fc279af + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" From ad9181c374d2602524da2b19a9e67f8251f901b9 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Fri, 17 Mar 2023 16:29:11 -0230 Subject: [PATCH 04/20] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3caea2e39b89..aca89dc0deeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [10.26.2] -### Uncategorized +### Changed - Sign in with Ethereum: re-enable warning UI for mismatched domains / disable domain binding ([#18200](https://github.com/MetaMask/metamask-extension/pull/18200)) ## [10.26.1] From 3fc2adac872ccb61ade2f3e42216b3de1f311631 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:59:59 +0900 Subject: [PATCH 05/20] devdeps: resolve-url-loader@3.1.2->3.1.5 (#18183) * devdeps: resolve-url-loader@3.1.2->3.1.5 - bump/dedupe subdependency loader-utils - closes GHSA-76p3-8hx3-jpfq / CVE-2022-37601 - closes GHSA-3rfm-jhwj-7488 / CVE-2022-37603 - closes GHSA-hhq3-ff78-jv3g / CVE-2022-37599 - bump/dedupe subdependency emojis-list * devdeps: loader-utils@2.0.0->2.0.4 - closes GHSA-76p3-8hx3-jpfq / CVE-2022-37601 - closes GHSA-3rfm-jhwj-7488 / CVE-2022-37603 - closes GHSA-hhq3-ff78-jv3g / CVE-2022-37599 --- package.json | 2 +- yarn.lock | 46 ++++++++++++++-------------------------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 62e4af2ab549..75299690ade9 100644 --- a/package.json +++ b/package.json @@ -510,7 +510,7 @@ "redux-mock-store": "^1.5.4", "remote-redux-devtools": "^0.5.16", "require-from-string": "^2.0.2", - "resolve-url-loader": "^3.1.2", + "resolve-url-loader": "^3.1.5", "sass": "^1.32.4", "sass-loader": "^10.1.1", "selenium-webdriver": "^4.3.1", diff --git a/yarn.lock b/yarn.lock index 3f1224f76bcc..88cc3b9cd812 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14648,13 +14648,6 @@ __metadata: languageName: node linkType: hard -"emojis-list@npm:^2.0.0": - version: 2.1.0 - resolution: "emojis-list@npm:2.1.0" - checksum: fb61fa6356dfcc9fbe6db8e334c29da365a34d3d82a915cb59621883d3023d804fd5edad5acd42b8eec016936e81d3b38e2faf921b32e073758374253afe1272 - languageName: node - linkType: hard - "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -23288,17 +23281,6 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:1.2.3": - version: 1.2.3 - resolution: "loader-utils@npm:1.2.3" - dependencies: - big.js: ^5.2.2 - emojis-list: ^2.0.0 - json5: ^1.0.1 - checksum: 385407fc2683b6d664276fd41df962296de4a15030bb24389de77b175570c3b56bd896869376ba14cf8b33a9e257e17a91d395739ba7e23b5b68a8749a41df7e - languageName: node - linkType: hard - "loader-utils@npm:^1.1.0, loader-utils@npm:^1.2.3": version: 1.4.0 resolution: "loader-utils@npm:1.4.0" @@ -23311,13 +23293,13 @@ __metadata: linkType: hard "loader-utils@npm:^2.0.0": - version: 2.0.0 - resolution: "loader-utils@npm:2.0.0" + version: 2.0.4 + resolution: "loader-utils@npm:2.0.4" dependencies: big.js: ^5.2.2 emojis-list: ^3.0.0 json5: ^2.1.2 - checksum: 6856423131b50b6f5f259da36f498cfd7fc3c3f8bb17777cf87fdd9159e797d4ba4288d9a96415fd8da62c2906960e88f74711dee72d03a9003bddcd0d364a51 + checksum: a5281f5fff1eaa310ad5e1164095689443630f3411e927f95031ab4fb83b4a98f388185bb1fe949e8ab8d4247004336a625e9255c22122b815bb9a4c5d8fc3b7 languageName: node linkType: hard @@ -24536,7 +24518,7 @@ __metadata: remove-trailing-slash: ^0.1.1 require-from-string: ^2.0.2 reselect: ^3.0.1 - resolve-url-loader: ^3.1.2 + resolve-url-loader: ^3.1.5 safe-event-emitter: ^1.0.1 sass: ^1.32.4 sass-loader: ^10.1.1 @@ -27730,14 +27712,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:7.0.21": - version: 7.0.21 - resolution: "postcss@npm:7.0.21" +"postcss@npm:7.0.36": + version: 7.0.36 + resolution: "postcss@npm:7.0.36" dependencies: chalk: ^2.4.2 source-map: ^0.6.1 supports-color: ^6.1.0 - checksum: 5c11d58a4ffd54ddaf2f2f18ef7be10fc44405559ee56b52e41db8305d1b184d162138994dcce506ab77eef7283887a72d1b81cd1036c7fee106f50af0ef86d3 + checksum: 4cfc0989b9ad5d0e8971af80d87f9c5beac5c84cb89ff22ad69852edf73c0a2fa348e7e0a135b5897bf893edad0fe86c428769050431ad9b532f072ff530828d languageName: node linkType: hard @@ -30027,21 +30009,21 @@ __metadata: languageName: node linkType: hard -"resolve-url-loader@npm:^3.1.2": - version: 3.1.2 - resolution: "resolve-url-loader@npm:3.1.2" +"resolve-url-loader@npm:^3.1.5": + version: 3.1.5 + resolution: "resolve-url-loader@npm:3.1.5" dependencies: adjust-sourcemap-loader: 3.0.0 camelcase: 5.3.1 compose-function: 3.0.3 convert-source-map: 1.7.0 es6-iterator: 2.0.3 - loader-utils: 1.2.3 - postcss: 7.0.21 + loader-utils: ^1.2.3 + postcss: 7.0.36 rework: 1.0.1 rework-visit: 1.0.0 source-map: 0.6.1 - checksum: 02e559af8d10a8fda8d2cb1c61290b932787309309839288820438b4f25339a8c8cbd52598af89c1c1d277133d74914407e7a760e49acd966425a038798a6e70 + checksum: eb52911eff20723f07409cc12138d254fa0dd4a4f3b1ba11ee1b29912afb03f1272aaddb523658be1e3a946e0d1bf6f603d0e107753ab83d48ad2116cf04b7f6 languageName: node linkType: hard From d3026e7338fd376e0bf07a785846988d9421ef93 Mon Sep 17 00:00:00 2001 From: legobeat <109787230+legobeat@users.noreply.github.com> Date: Wed, 22 Mar 2023 10:00:44 +0900 Subject: [PATCH 06/20] devdeps: webpack@5.75.0->5.76.2 (#18182) CVE-2023-28154 / GHSA-hc6q-2mpp-qw7j --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 88cc3b9cd812..ddf886249df2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35046,8 +35046,8 @@ __metadata: linkType: hard "webpack@npm:>=4.43.0 <6.0.0, webpack@npm:^5.75.0, webpack@npm:^5.9.0": - version: 5.75.0 - resolution: "webpack@npm:5.75.0" + version: 5.76.2 + resolution: "webpack@npm:5.76.2" dependencies: "@types/eslint-scope": ^3.7.3 "@types/estree": ^0.0.51 @@ -35078,7 +35078,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 2bcc5f3c195f375944e8af2f00bf2feea39cb9fda5f763b0d1b00077f1c51783db25c94d3fae96a07dead9fa085e6ae7474417e5ab31719c9776ea5969ceb83a + checksum: 86db98299a175c371031449c26077e87b33acd8f45de7f7945ed4b9b37c8ca11bc5169af9c44743efccd4d55e08042a3aa3a3bc42aff831309a0821ffbcd395e languageName: node linkType: hard From de4cf0a7e57497f5638641f41edb2bb8bd50857b Mon Sep 17 00:00:00 2001 From: Garrett Bear Date: Tue, 21 Mar 2023 19:19:49 -0700 Subject: [PATCH 07/20] Fix/button base ellipsis support (#18205) * ButtonBase ellipsis update Update ui/components/multichain/account-picker/index.js Co-authored-by: Garrett Bear Update ui/components/multichain/account-picker/index.js Co-authored-by: Garrett Bear Update ui/components/multichain/account-picker/index.js Co-authored-by: Garrett Bear Update ui/components/multichain/account-picker/index.js Co-authored-by: Garrett Bear * buttonbase updates to fix ellipsis * multichain support * remove multichain * code cleanup * clean up * component clean up * span update * fix snapshots * fix snapshot * Updating ButtonBase to reduce html to a minimum but ensure all functionality still works (#18210) * fix color and disable * remove unused css * Update ui/components/component-library/button-base/README.mdx Co-authored-by: George Marshall * fix e2e test from button update * update e2e test from button base update --------- Co-authored-by: David Walsh Co-authored-by: George Marshall --- test/e2e/tests/add-account.spec.js | 2 +- test/e2e/tests/failing-contract.spec.js | 4 +- test/e2e/tests/from-import-ui.spec.js | 8 +-- .../component-library/button-base/README.mdx | 27 +++++-- .../__snapshots__/button-base.test.js.snap | 16 ++--- .../button-base/button-base.js | 70 +++++++++++++------ .../button-base/button-base.scss | 13 +--- .../button-base/button-base.stories.js | 14 +++- .../component-library/button-link/README.mdx | 4 +- .../__snapshots__/button-link.test.js.snap | 8 +-- .../button-link/button-link.js | 22 ++---- .../button-link/button-link.scss | 3 - .../button-link/button-link.stories.js | 6 +- .../__snapshots__/button-primary.test.js.snap | 8 +-- .../button-secondary.test.js.snap | 8 +-- .../button/__snapshots__/button.test.js.snap | 32 +++------ ui/helpers/constants/design-system.ts | 1 + .../__snapshots__/reveal-seed.test.js.snap | 32 +++------ 18 files changed, 131 insertions(+), 147 deletions(-) diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index dc10beb5653a..226c8bda950b 100644 --- a/test/e2e/tests/add-account.spec.js +++ b/test/e2e/tests/add-account.spec.js @@ -242,7 +242,7 @@ describe('Add account', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( diff --git a/test/e2e/tests/failing-contract.spec.js b/test/e2e/tests/failing-contract.spec.js index b3f9bb5c0085..3391d878ee65 100644 --- a/test/e2e/tests/failing-contract.spec.js +++ b/test/e2e/tests/failing-contract.spec.js @@ -63,7 +63,7 @@ describe('Failing contract interaction ', function () { // dismiss warning and confirm the transaction await driver.clickElement({ text: 'I want to proceed anyway', - tag: 'span', + tag: 'button', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); @@ -149,7 +149,7 @@ describe('Failing contract interaction on non-EIP1559 network', function () { // dismiss warning and confirm the transaction await driver.clickElement({ text: 'I want to proceed anyway', - tag: 'span', + tag: 'button', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); await driver.waitUntilXWindowHandles(2); diff --git a/test/e2e/tests/from-import-ui.spec.js b/test/e2e/tests/from-import-ui.spec.js index 6a931835152b..e56882c05728 100644 --- a/test/e2e/tests/from-import-ui.spec.js +++ b/test/e2e/tests/from-import-ui.spec.js @@ -212,7 +212,7 @@ describe('MetaMask Import UI', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey1); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( @@ -239,7 +239,7 @@ describe('MetaMask Import UI', function () { await driver.clickElement({ text: 'Import account', tag: 'div' }); // enter private key await driver.fill('#private-key-box', testPrivateKey2); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should see new account in account menu const importedAccount2Name = await driver.findElement( @@ -330,7 +330,7 @@ describe('MetaMask Import UI', function () { await driver.fill('#json-password-box', 'foobarbazqux'); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // should show the correct account name const importedAccountName = await driver.findElement( @@ -392,7 +392,7 @@ describe('MetaMask Import UI', function () { // enter private key', await driver.fill('#private-key-box', testPrivateKey); - await driver.clickElement({ text: 'Import', tag: 'span' }); + await driver.clickElement({ text: 'Import', tag: 'button' }); // error should occur await driver.waitForSelector({ diff --git a/ui/components/component-library/button-base/README.mdx b/ui/components/component-library/button-base/README.mdx index 2390323792a3..f18d98636efa 100644 --- a/ui/components/component-library/button-base/README.mdx +++ b/ui/components/component-library/button-base/README.mdx @@ -102,7 +102,9 @@ import { ButtonBase } from '../../component-library'; When an `externalLink` prop is passed it will change the element to an anchor(`a`) tag and add the `target="_blank"` and `rel="noopener noreferrer"` attributes. + + ```jsx import { ButtonBase } from '../../component-library'; @@ -168,7 +170,7 @@ import { ICON_NAMES } from '../icon'; ### RTL -For RTL language support use the `textProps` prop to pass a `textDirection` prop. +For RTL language support use the `textDirection` prop. @@ -187,11 +189,28 @@ import { ButtonBase, ICON_NAMES } from '../../component-library'; Button Demo ; ``` + +### Ellipsis + +Use the boolean `ellipsis` prop to change the if the `ButtonBase` component to have an ellipsis. + +Note: this should only be used for dynamic/user generated content or addresses. Generally, button text should be succinct and only contain one or two words. + + + + + +```jsx +import { ButtonBase } from '../../component-library'; + + + This is long text example without ellipsis + This is long text example with ellipsis +; +``` diff --git a/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap b/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap index 6dc5bfbcf11d..9e3221f01a85 100644 --- a/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap +++ b/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap @@ -3,17 +3,13 @@ exports[`ButtonBase should render anchor element correctly by href and externalLink, href target and rel exist 1`] = ` `; @@ -21,14 +17,10 @@ exports[`ButtonBase should render anchor element correctly by href and externalL exports[`ButtonBase should render button element correctly and match snapshot 1`] = `
`; diff --git a/ui/components/component-library/button-base/button-base.js b/ui/components/component-library/button-base/button-base.js index 1a870284ad29..c79a757e91b5 100644 --- a/ui/components/component-library/button-base/button-base.js +++ b/ui/components/component-library/button-base/button-base.js @@ -15,6 +15,7 @@ import { Size, BorderRadius, BackgroundColor, + IconColor, } from '../../../helpers/constants/design-system'; import { BUTTON_BASE_SIZES } from './button-base.constants'; @@ -24,6 +25,7 @@ export const ButtonBase = ({ children, className, href, + ellipsis = false, externalLink, size = BUTTON_BASE_SIZES.MD, startIconName, @@ -34,6 +36,7 @@ export const ButtonBase = ({ disabled, iconLoadingProps, textProps, + color = TextColor.textDefault, ...props }) => { const Tag = href ? 'a' : as; @@ -42,13 +45,14 @@ export const ButtonBase = ({ props.rel = 'noopener noreferrer'; } return ( - - - {startIconName && ( - - )} - {children} - {endIconName && ( - - )} - + {startIconName && ( + + )} + {/* + * If children is a string and doesn't need truncation or loading + * prevent html bloat by rendering just the string + * otherwise render with wrapper to allow truncation or loading + */} + {typeof children === 'string' && !ellipsis && !loading ? ( + children + ) : ( + + {children} + + )} + {endIconName && ( + + )} {loading && ( )} - + ); }; @@ -126,6 +150,10 @@ ButtonBase.propTypes = { * When an `href` prop is passed, ButtonBase will automatically change the root element to be an `a` (anchor) tag */ href: PropTypes.string, + /** + * Used for long strings that can be cut off... + */ + ellipsis: PropTypes.bool, /** * Boolean indicating if the link targets external content, it will cause the link to open in a new tab */ diff --git a/ui/components/component-library/button-base/button-base.scss b/ui/components/component-library/button-base/button-base.scss index 42e6d24a9db7..6b4e249c2542 100644 --- a/ui/components/component-library/button-base/button-base.scss +++ b/ui/components/component-library/button-base/button-base.scss @@ -6,18 +6,13 @@ vertical-align: middle; user-select: none; - &:active, - &:hover { - color: var(--color-text-default); - } - &--block { display: block; width: 100%; } - &__content { - height: 100%; + &--ellipsis { + max-width: 100%; } &--size-sm { @@ -36,10 +31,6 @@ cursor: not-allowed; } - &--loading &__content { - color: transparent; - } - &--disabled, &:disabled { opacity: 0.3; diff --git a/ui/components/component-library/button-base/button-base.stories.js b/ui/components/component-library/button-base/button-base.stories.js index ffb512783106..0a4afed2c247 100644 --- a/ui/components/component-library/button-base/button-base.stories.js +++ b/ui/components/component-library/button-base/button-base.stories.js @@ -1,6 +1,7 @@ import React from 'react'; import { AlignItems, + Color, DISPLAY, FLEX_DIRECTION, Size, @@ -199,11 +200,18 @@ export const Rtl = (args) => ( {...args} startIconName={ICON_NAMES.ADD_SQUARE} endIconName={ICON_NAMES.ARROW_2_RIGHT} - textProps={{ - textDirection: TEXT_DIRECTIONS.RIGHT_TO_LEFT, - }} + textDirection={TEXT_DIRECTIONS.RIGHT_TO_LEFT} > Button Demo ); + +export const Ellipsis = (args) => ( + + Example without ellipsis + + Example with ellipsis + + +); diff --git a/ui/components/component-library/button-link/README.mdx b/ui/components/component-library/button-link/README.mdx index 292190b07731..4850161adc80 100644 --- a/ui/components/component-library/button-link/README.mdx +++ b/ui/components/component-library/button-link/README.mdx @@ -67,7 +67,7 @@ import { ButtonLink, Text, TextVariant } from '../../component-library'; - Inherits the font-size of the parent element and example with textProps override for a success color. + Inherits the font-size of the parent element and example with override for a success color. Learn more ``` @@ -103,7 +103,7 @@ import { ButtonLink } from '../../component-library'; ### External Link -When an `externalLink` prop is passed it adds the `target="_blank"` and `rel="noopener noreferrer"` attributes. +When an `externalLink` prop is passed it adds the `target="_blank"` and `rel="noopener noreferrer"` attributes. `rel="noreferrer noopener"` is used in links to prevent security vulnerabilities that can be exploited by malicious websites. It disables the window.opener property and prevents the new page from sending the referrer information, providing an additional layer of security. diff --git a/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap b/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap index f62717f6cd73..1a5c0af51d6b 100644 --- a/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap +++ b/ui/components/component-library/button-link/__snapshots__/button-link.test.js.snap @@ -3,14 +3,10 @@ exports[`ButtonLink should render button element correctly 1`] = `
`; diff --git a/ui/components/component-library/button-link/button-link.js b/ui/components/component-library/button-link/button-link.js index aa77a788ad7b..c7d7dd14fe47 100644 --- a/ui/components/component-library/button-link/button-link.js +++ b/ui/components/component-library/button-link/button-link.js @@ -3,26 +3,25 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ButtonBase } from '../button-base'; -import { Text } from '../text'; import { BackgroundColor, Color, Size, - TextVariant, } from '../../../helpers/constants/design-system'; import { BUTTON_LINK_SIZES } from './button-link.constants'; export const ButtonLink = ({ className, danger, + disabled, size = Size.auto, - textProps, ...props }) => { return ( ); }; @@ -62,15 +54,15 @@ ButtonLink.propTypes = { * Boolean to change button type to Danger when true */ danger: PropTypes.bool, + /** + * Boolean to disable button + */ + disabled: PropTypes.bool, /** * Possible size values: 'SIZES.AUTO'(auto), 'SIZES.SM'(32px), 'SIZES.MD'(40px), 'SIZES.LG'(48px), 'SIZES.INHERIT'(inherits parents font-size) * Default value is 'SIZES.AUTO'. */ size: PropTypes.oneOf(Object.values(BUTTON_LINK_SIZES)), - /** - * textProps accepts all the props from Text component - */ - textProps: PropTypes.shape(Text.PropTypes), /** * ButtonLink accepts all the props from ButtonBase */ diff --git a/ui/components/component-library/button-link/button-link.scss b/ui/components/component-library/button-link/button-link.scss index 5c8a4c10b89f..30e40042c672 100644 --- a/ui/components/component-library/button-link/button-link.scss +++ b/ui/components/component-library/button-link/button-link.scss @@ -1,17 +1,14 @@ .mm-button-link { &:hover { - color: var(--color-primary-default); opacity: 0.5; } &:active { - color: var(--color-primary-default); opacity: 0.5; } &--disabled { &:hover { - color: var(--color-primary-default); opacity: 0.3; } diff --git a/ui/components/component-library/button-link/button-link.stories.js b/ui/components/component-library/button-link/button-link.stories.js index b97d8de36735..bda5a27f4083 100644 --- a/ui/components/component-library/button-link/button-link.stories.js +++ b/ui/components/component-library/button-link/button-link.stories.js @@ -162,12 +162,12 @@ export const SizeStory = (args) => ( - Inherits the font-size of the parent element and example with textProps - override for a success color.{' '} + Inherits the font-size of the parent element and example with override for + a success color.{' '} Learn more diff --git a/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap b/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap index 8b0c03a89fca..8768e1c7c56d 100644 --- a/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap +++ b/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap @@ -3,14 +3,10 @@ exports[`ButtonPrimary should render button element correctly 1`] = `
`; diff --git a/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap b/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap index c6f9a31779c5..fcc63172f02d 100644 --- a/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap +++ b/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap @@ -3,14 +3,10 @@ exports[`ButtonSecondary should render button element correctly 1`] = `
`; diff --git a/ui/components/component-library/button/__snapshots__/button.test.js.snap b/ui/components/component-library/button/__snapshots__/button.test.js.snap index a0685c51a84e..7d795a179e20 100644 --- a/ui/components/component-library/button/__snapshots__/button.test.js.snap +++ b/ui/components/component-library/button/__snapshots__/button.test.js.snap @@ -3,14 +3,10 @@ exports[`Button should render button element correctly 1`] = `
`; @@ -18,34 +14,22 @@ exports[`Button should render button element correctly 1`] = ` exports[`Button should render with different button types 1`] = `
`; diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts index c39e95f51378..a80999cbf56a 100644 --- a/ui/helpers/constants/design-system.ts +++ b/ui/helpers/constants/design-system.ts @@ -130,6 +130,7 @@ export enum TextColor { lineaTestnetInverse = 'lineatestnet-inverse', goerliInverse = 'goerli-inverse', sepoliaInverse = 'sepolia-inverse', + transparent = 'transparent', } export enum IconColor { diff --git a/ui/pages/keychains/__snapshots__/reveal-seed.test.js.snap b/ui/pages/keychains/__snapshots__/reveal-seed.test.js.snap index 0d97c028a221..5bf39129d3db 100644 --- a/ui/pages/keychains/__snapshots__/reveal-seed.test.js.snap +++ b/ui/pages/keychains/__snapshots__/reveal-seed.test.js.snap @@ -17,16 +17,12 @@ exports[`Reveal Seed Page should match snapshot 1`] = ` The - - Secret Recovery Phrase (SRP) - + Secret Recovery Phrase (SRP) provides - - non-custodial wallet - + non-custodial wallet . That means you're the owner of your SRP. @@ -112,23 +104,15 @@ exports[`Reveal Seed Page should match snapshot 1`] = ` class="box box--margin-top-auto box--display-flex box--gap-4 box--flex-direction-row" > From 7dab4b53a49e436c78597c7c5269690c12c9446d Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 22 Mar 2023 02:52:30 -0500 Subject: [PATCH 08/20] Fix TypographyVariant typescript issues (#18272) --- .../secure-your-wallet/skip-srp-backup-popover.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js b/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js index 4e9c69e895ab..bd64f93474f7 100644 --- a/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js +++ b/ui/pages/onboarding-flow/secure-your-wallet/skip-srp-backup-popover.js @@ -87,7 +87,7 @@ export default function SkipSRPBackup({ handleClose }) { color={IconColor.errorDefault} /> {t('skipAccountSecurity')} @@ -102,7 +102,7 @@ export default function SkipSRPBackup({ handleClose }) { /> {t('skipAccountSecurityDetails')} From c079c4320e1c17a13954cfa9ad88b1422c34d10c Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 22 Mar 2023 05:00:08 -0500 Subject: [PATCH 09/20] UX: Multichain: Account Menu List (#17947) * UX: Multichain: Account Menu List * Move to using stylesheet * Add hover state * Implement George's suggestions * Add connected site avatar * Add hardware tag * Create story for selected hardware item * Progress on the AccountListItemMenu * Add story for AccountListItemMenu * Better position the account menu * Fix AvatarFavicon missing name prop * Update menu options label to be account specific * Update text of 'View on Explorer' * Add AccountListMenu component * Move all items to multichain directory * Fix paths * Fix linting, use AvatarIcon * Add title and close button to account menu * Center the popover title * Add search functionality * Implementation WIP * Add MULTICHAIN feature flag * Add MULTICHAIN feature flag, add actions for menu items * Properly dispatch events * Fix search box padding * Fix sizing of menu item text * Fix isRequired * Fix alignment of the popover * Update label for hardware wallet items, add text for no search results * Update keyring retreival to remove account and add label * Fix storybook * Fix double link click issue, prevent wrapping of values * Use labelProps for tag variant * Restructure item menu story * Empower storybooks for all new components * Allow only 3 decimals for currencies * Avoid inline styles * Prefix classes with multichain, fix account-list-menu storybook * Close the accounts menu when account details is clicked * Restore tag.js * Create global file for multichain css * Add index file for multichain js * Update file paths * Ensure the block domain is present in menu * Add AccountListItem test * Add AccountListItemMenu tests * Show account connect to current dapp * Improve tests * Make avatar smaller * Add tooltip for account menu * Align icon better * Update snapshot * Rename files to DS standard * Add index files for export * Export all multichain components * Update snapshot * Remove embedded style in popover * Add comments for props, cleanup storybook * Improve test coverage * Improve test code quality * Remove border form avatar * Switch to using the ButtonLink iconName prop * Only show tooltip if character limit is reached * Restore prior search settings * Add test for tooltip --- .metamaskrc.dist | 1 + .storybook/test-data.js | 7 +- app/_locales/en/messages.json | 9 + development/build/config.js | 1 + development/build/scripts.js | 1 + .../account-list-item-menu.js | 147 ++++++++++ .../account-list-item-menu.stories.js | 41 +++ .../account-list-item-menu.test.js | 54 ++++ .../account-list-item-menu/index.js | 1 + .../account-list-item.test.js.snap | 140 ++++++++++ .../account-list-item/account-list-item.js | 255 ++++++++++++++++++ .../account-list-item.stories.js | 129 +++++++++ .../account-list-item.test.js | 103 +++++++ .../multichain/account-list-item/index.js | 1 + .../multichain/account-list-item/index.scss | 32 +++ .../account-list-menu/account-list-menu.js | 196 ++++++++++++++ .../account-list-menu.stories.js | 14 + .../account-list-menu.test.js | 103 +++++++ .../multichain/account-list-menu/index.js | 1 + .../multichain/account-list-menu/index.scss | 6 + ui/components/multichain/index.js | 3 + ui/components/multichain/index.scss | 2 + ui/components/ui/menu/menu-item.js | 5 +- ui/components/ui/popover/index.scss | 4 + ui/components/ui/popover/popover.component.js | 17 +- ui/css/index.scss | 1 + ui/helpers/constants/design-system.ts | 1 + ui/pages/routes/routes.component.js | 10 +- ui/pages/routes/routes.container.js | 3 + ui/selectors/permissions.js | 18 ++ 30 files changed, 1298 insertions(+), 8 deletions(-) create mode 100644 ui/components/multichain/account-list-item-menu/account-list-item-menu.js create mode 100644 ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js create mode 100644 ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js create mode 100644 ui/components/multichain/account-list-item-menu/index.js create mode 100644 ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap create mode 100644 ui/components/multichain/account-list-item/account-list-item.js create mode 100644 ui/components/multichain/account-list-item/account-list-item.stories.js create mode 100644 ui/components/multichain/account-list-item/account-list-item.test.js create mode 100644 ui/components/multichain/account-list-item/index.js create mode 100644 ui/components/multichain/account-list-item/index.scss create mode 100644 ui/components/multichain/account-list-menu/account-list-menu.js create mode 100644 ui/components/multichain/account-list-menu/account-list-menu.stories.js create mode 100644 ui/components/multichain/account-list-menu/account-list-menu.test.js create mode 100644 ui/components/multichain/account-list-menu/index.js create mode 100644 ui/components/multichain/account-list-menu/index.scss create mode 100644 ui/components/multichain/index.js create mode 100644 ui/components/multichain/index.scss diff --git a/.metamaskrc.dist b/.metamaskrc.dist index d5b8114a8f16..17350a99aaf5 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -7,6 +7,7 @@ PUBNUB_PUB_KEY= PUBNUB_SUB_KEY= PORTFOLIO_URL= TRANSACTION_SECURITY_PROVIDER= +MULTICHAIN= ; Set this to test changes to the phishing warning page. PHISHING_WARNING_PAGE_URL= diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 65bb7fb4c39a..a7a893354e12 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1179,9 +1179,14 @@ const state = { accounts: [ '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', - '0x9d0ba4ddac06032527b140912ec808ab9451b788', ], }, + { + type: HardwareKeyringTypes.ledger, + accounts: [ + '0x9d0ba4ddac06032527b140912ec808ab9451b788' + ], + } ], networkConfigurations: { 'test-networkConfigurationId-1': { diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1dfc9ec31738..ccaa85c05ca6 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -171,6 +171,9 @@ "addANickname": { "message": "Add a nickname" }, + "addAccount": { + "message": "Add account" + }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, @@ -1573,6 +1576,9 @@ "hardware": { "message": "Hardware" }, + "hardwareWallet": { + "message": "Hardware wallet" + }, "hardwareWalletConnected": { "message": "Hardware wallet connected" }, @@ -4611,6 +4617,9 @@ "message": "View $1 on Etherscan", "description": "$1 is the action type. e.g (Account, Transaction, Swap)" }, + "viewOnExplorer": { + "message": "View on explorer" + }, "viewOnOpensea": { "message": "View on Opensea" }, diff --git a/development/build/config.js b/development/build/config.js index 292a5cc0b146..e07707caa05c 100644 --- a/development/build/config.js +++ b/development/build/config.js @@ -7,6 +7,7 @@ const commonConfigurationPropertyNames = ['PUBNUB_PUB_KEY', 'PUBNUB_SUB_KEY']; const configurationPropertyNames = [ ...commonConfigurationPropertyNames, + 'MULTICHAIN', 'INFURA_PROJECT_ID', 'PHISHING_WARNING_PAGE_URL', 'PORTFOLIO_URL', diff --git a/development/build/scripts.js b/development/build/scripts.js index d28ef4db4e25..7d66260504c2 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -1109,6 +1109,7 @@ async function getEnvironmentVariables({ buildTarget, buildType, version }) { const iconNames = await generateIconNames(); return { ICON_NAMES: iconNames, + MULTICHAIN: config.MULTICHAIN === '1', CONF: devMode ? config : {}, IN_TEST: testing, INFURA_PROJECT_ID: getInfuraProjectId({ diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js new file mode 100644 index 000000000000..2d02363b6c99 --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -0,0 +1,147 @@ +import React, { useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { getAccountLink } from '@metamask/etherscan-link'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + getRpcPrefsForCurrentProvider, + getBlockExplorerLinkText, + getCurrentChainId, +} from '../../../selectors'; +import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; +import { Menu, MenuItem } from '../../ui/menu'; +import { ICON_NAMES, Text } from '../../component-library'; +import { EVENT_NAMES, EVENT } from '../../../../shared/constants/metametrics'; +import { getURLHostName } from '../../../helpers/utils/util'; +import { showModal } from '../../../store/actions'; +import { TextVariant } from '../../../helpers/constants/design-system'; + +export const AccountListItemMenu = ({ + anchorElement, + blockExplorerUrlSubTitle, + onClose, + closeMenu, + isRemovable, + identity, +}) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const dispatch = useDispatch(); + const history = useHistory(); + + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const addressLink = getAccountLink(identity.address, chainId, rpcPrefs); + + const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); + const openBlockExplorer = () => { + trackEvent({ + event: EVENT_NAMES.EXTERNAL_LINK_CLICKED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + link_type: EVENT.EXTERNAL_LINK_TYPES.ACCOUNT_TRACKER, + location: 'Account Options', + url_domain: getURLHostName(addressLink), + }, + }); + global.platform.openTab({ + url: addressLink, + }); + onClose(); + }; + + const routeToAddBlockExplorerUrl = () => { + history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); + }; + + return ( + + + {t('viewOnExplorer')} + + { + dispatch(showModal({ name: 'ACCOUNT_DETAILS' })); + trackEvent({ + event: EVENT_NAMES.NAV_ACCOUNT_DETAILS_OPENED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + location: 'Account Options', + }, + }); + onClose(); + closeMenu?.(); + }} + iconName={ICON_NAMES.SCAN_BARCODE} + > + {t('accountDetails')} + + {isRemovable ? ( + { + dispatch( + showModal({ + name: 'CONFIRM_REMOVE_ACCOUNT', + identity, + }), + ); + onClose(); + }} + iconName={ICON_NAMES.TRASH} + > + {t('removeAccount')} + + ) : null} + + ); +}; + +AccountListItemMenu.propTypes = { + /** + * Element that the menu should display next to + */ + anchorElement: PropTypes.instanceOf(window.Element), + /** + * Function that executes when the menu is closed + */ + onClose: PropTypes.func.isRequired, + /** + * Function that closes the menu + */ + closeMenu: PropTypes.func, + /** + * Domain of the block explorer + */ + blockExplorerUrlSubTitle: PropTypes.string, + /** + * Represents if the account should be removable + */ + isRemovable: PropTypes.bool.isRequired, + /** + * Identity of the account + */ + /** + * Identity of the account + */ + identity: PropTypes.shape({ + name: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + }).isRequired, +}; diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js new file mode 100644 index 000000000000..5ae50a1778ad --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.stories.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { AccountListItemMenu } from '.'; + +export default { + title: 'Components/Multichain/AccountListItemMenu', + component: AccountListItemMenu, + argTypes: { + anchorElement: { + control: 'window.Element', + }, + onClose: { + action: 'onClose', + }, + closeMenu: { + action: 'closeMenu', + }, + blockExplorerUrlSubTitle: { + control: 'text', + }, + isRemovable: { + control: 'boolean', + }, + identity: { + control: 'object', + }, + }, + args: { + anchorElement: null, + identity: { + address: '"0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e"', + name: 'Account 1', + balance: '0x152387ad22c3f0', + tokenBalance: '32.09 ETH', + }, + isRemovable: true, + blockExplorerUrlSubTitle: 'etherscan.io', + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js new file mode 100644 index 000000000000..f5b91104bd37 --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.test.js @@ -0,0 +1,54 @@ +/* eslint-disable jest/require-top-level-describe */ +import React from 'react'; +import { renderWithProvider, fireEvent } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { AccountListItemMenu } from '.'; + +const identity = { + ...mockState.metamask.identities[ + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' + ], + balance: '0x152387ad22c3f0', +}; + +const DEFAULT_PROPS = { + identity, + onClose: jest.fn(), + onHide: jest.fn(), + isRemovable: false, +}; + +const render = (props = {}) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + const allProps = { ...DEFAULT_PROPS, ...props }; + return renderWithProvider(, store); +}; + +describe('AccountListItem', () => { + it('renders the URL for explorer', () => { + const blockExplorerDomain = 'etherscan.io'; + const { getByText, getByTestId } = render({ + blockExplorerUrlSubTitle: blockExplorerDomain, + }); + expect(getByText(blockExplorerDomain)).toBeInTheDocument(); + + Object.defineProperty(global, 'platform', { + value: { + openTab: jest.fn(), + }, + }); + const openExplorerTabSpy = jest.spyOn(global.platform, 'openTab'); + fireEvent.click(getByTestId('account-list-menu-open-explorer')); + expect(openExplorerTabSpy).toHaveBeenCalled(); + }); + + it('renders remove icon with isRemovable', () => { + const { getByTestId } = render({ isRemovable: true }); + expect(getByTestId('account-list-menu-remove')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-list-item-menu/index.js b/ui/components/multichain/account-list-item-menu/index.js new file mode 100644 index 000000000000..6c0f7c65a38f --- /dev/null +++ b/ui/components/multichain/account-list-item-menu/index.js @@ -0,0 +1 @@ +export { AccountListItemMenu } from './account-list-item-menu'; diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap new file mode 100644 index 000000000000..c44eb702947e --- /dev/null +++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountListItem renders AccountListItem component and shows account name, address, and balance 1`] = ` +
+ +
+`; diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js new file mode 100644 index 000000000000..f0a2aea8c820 --- /dev/null +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -0,0 +1,255 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { useSelector } from 'react-redux'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { getRpcPrefsForCurrentProvider } from '../../../selectors'; +import { getURLHostName, shortenAddress } from '../../../helpers/utils/util'; + +import { AccountListItemMenu } from '..'; +import Box from '../../ui/box/box'; +import { + AvatarAccount, + ButtonIcon, + Text, + ICON_NAMES, + ICON_SIZES, + AvatarFavicon, + Tag, +} from '../../component-library'; +import { + Color, + TEXT_ALIGN, + AlignItems, + DISPLAY, + TextVariant, + FLEX_DIRECTION, + BorderRadius, + JustifyContent, + Size, + BorderColor, +} from '../../../helpers/constants/design-system'; +import { + HardwareKeyringTypes, + HardwareKeyringNames, +} from '../../../../shared/constants/hardware-wallets'; +import UserPreferencedCurrencyDisplay from '../../app/user-preferenced-currency-display/user-preferenced-currency-display.component'; +import { SECONDARY, PRIMARY } from '../../../helpers/constants/common'; +import { findKeyringForAddress } from '../../../ducks/metamask/metamask'; +import Tooltip from '../../ui/tooltip/tooltip'; + +const MAXIMUM_CURRENCY_DECIMALS = 3; +const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 17; + +function getLabel(keyring = {}, t) { + const { type } = keyring; + switch (type) { + case HardwareKeyringTypes.qr: + return HardwareKeyringNames.qr; + case HardwareKeyringTypes.imported: + return t('imported'); + case HardwareKeyringTypes.trezor: + return HardwareKeyringNames.trezor; + case HardwareKeyringTypes.ledger: + return HardwareKeyringNames.ledger; + case HardwareKeyringTypes.lattice: + return HardwareKeyringNames.lattice; + default: + return null; + } +} + +export const AccountListItem = ({ + identity, + selected = false, + onClick, + closeMenu, + connectedAvatar, + connectedAvatarName, +}) => { + const t = useI18nContext(); + const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); + const ref = useRef(false); + const keyring = useSelector((state) => + findKeyringForAddress(state, identity.address), + ); + const label = getLabel(keyring, t); + + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const { blockExplorerUrl } = rpcPrefs; + const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); + + return ( + { + e.preventDefault(); + // Without this check, the account will be selected after + // the account options menu closes + if (!accountOptionsMenuOpen) { + onClick(); + } + }} + > + {selected && ( + + )} + + + + + + {identity.name.length > MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP ? ( + + {identity.name} + + ) : ( + identity.name + )} + + + {connectedAvatar ? ( + + ) : null} + + + + + + + + + {shortenAddress(identity.address)} + + + + + + {label ? ( + + ) : null} + +
+ { + e.stopPropagation(); + setAccountOptionsMenuOpen(true); + }} + as="div" + tabIndex={0} + onKeyPress={(e) => { + if (e.key === 'Enter') { + setAccountOptionsMenuOpen(true); + } + }} + data-testid="account-list-item-menu-button" + /> + {accountOptionsMenuOpen ? ( + setAccountOptionsMenuOpen(false)} + isRemovable={keyring?.type !== HardwareKeyringTypes.hdKeyTree} + closeMenu={closeMenu} + /> + ) : null} +
+
+ ); +}; + +AccountListItem.propTypes = { + /** + * Identity of the account + */ + identity: PropTypes.shape({ + name: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, + balance: PropTypes.string.isRequired, + }).isRequired, + /** + * Represents if this account is currently selected + */ + selected: PropTypes.bool, + /** + * Function to execute when the item is clicked + */ + onClick: PropTypes.func.isRequired, + /** + * Function that closes the menu + */ + closeMenu: PropTypes.func, + /** + * File location of the avatar icon + */ + connectedAvatar: PropTypes.string, + /** + * Text used as the avatar alt text + */ + connectedAvatarName: PropTypes.string, +}; + +AccountListItem.displayName = 'AccountListItem'; diff --git a/ui/components/multichain/account-list-item/account-list-item.stories.js b/ui/components/multichain/account-list-item/account-list-item.stories.js new file mode 100644 index 000000000000..627f80bc9127 --- /dev/null +++ b/ui/components/multichain/account-list-item/account-list-item.stories.js @@ -0,0 +1,129 @@ +import React from 'react'; + +import { Provider } from 'react-redux'; + +import testData from '../../../../.storybook/test-data'; +import configureStore from '../../../store/store'; +import { AccountListItem } from '.'; + +const store = configureStore(testData); + +const [chaosAddress, simpleAddress, hardwareAddress] = Object.keys( + testData.metamask.identities, +); + +const SIMPLE_IDENTITY = { + ...testData.metamask.identities[simpleAddress], + balance: '0x152387ad22c3f0', +}; + +const HARDWARE_IDENTITY = { + ...testData.metamask.identities[hardwareAddress], + balance: '0x152387ad22c3f0', +}; + +const CHAOS_IDENTITY = { + ...testData.metamask.identities[chaosAddress], + balance: '0x152387ad22c3f0', +}; + +const CONTAINER_STYLES = { + style: { + width: '328px', + border: '1px solid var(--color-border-muted)', + }, +}; + +const onClick = () => console.log('Clicked account!'); + +export default { + title: 'Components/Multichain/AccountListItem', + component: AccountListItem, + argTypes: { + identity: { + control: 'object', + }, + selected: { + control: 'boolean', + }, + onClick: { + action: 'onClick', + }, + closeMenu: { + action: 'closeMenu', + }, + connectedAvatar: { + control: 'text', + }, + connectedAvatarName: { + control: 'text', + }, + }, + args: { + identity: SIMPLE_IDENTITY, + onClick, + }, +}; + +export const DefaultStory = (args) => ( +
+ +
+); + +export const SelectedItem = (args) => ( +
+ +
+); +SelectedItem.args = { selected: true }; + +export const HardwareItem = (args) => ( +
+ +
+); +HardwareItem.args = { identity: HARDWARE_IDENTITY }; +HardwareItem.decorators = [ + (story) => {story()}, +]; + +export const SelectedHardwareItem = (args) => ( +
+ +
+); +SelectedHardwareItem.args = { identity: HARDWARE_IDENTITY, selected: true }; +SelectedHardwareItem.decorators = [ + (story) => {story()}, +]; + +export const ChaosDataItem = (args) => ( +
+ +
+); +ChaosDataItem.args = { identity: CHAOS_IDENTITY }; + +export const ConnectedSiteItem = (args) => ( +
+ +
+); +ConnectedSiteItem.args = { + connectedAvatar: 'https://uniswap.org/favicon.ico', + connectedAvatarName: 'Uniswap', +}; + +export const ConnectedSiteChaosItem = (args) => ( +
+ +
+); +ConnectedSiteChaosItem.args = { + identity: CHAOS_IDENTITY, + connectedAvatar: 'https://uniswap.org/favicon.ico', + connectedAvatarName: 'Uniswap', +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/account-list-item/account-list-item.test.js b/ui/components/multichain/account-list-item/account-list-item.test.js new file mode 100644 index 000000000000..b8e3e292e360 --- /dev/null +++ b/ui/components/multichain/account-list-item/account-list-item.test.js @@ -0,0 +1,103 @@ +/* eslint-disable jest/require-top-level-describe */ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { shortenAddress } from '../../../helpers/utils/util'; +import { AccountListItem } from '.'; + +const identity = { + ...mockState.metamask.identities[ + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' + ], + balance: '0x152387ad22c3f0', +}; + +const DEFAULT_PROPS = { + identity, + onClick: jest.fn(), +}; + +const render = (props = {}) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + const allProps = { ...DEFAULT_PROPS, ...props }; + return renderWithProvider(, store); +}; + +describe('AccountListItem', () => { + it('renders AccountListItem component and shows account name, address, and balance', () => { + const { container } = render(); + expect(screen.getByText(identity.name)).toBeInTheDocument(); + expect( + screen.getByText(shortenAddress(identity.address)), + ).toBeInTheDocument(); + expect(document.querySelector('[title="0.006 ETH"]')).toBeInTheDocument(); + + expect(container).toMatchSnapshot(); + }); + + it('renders selected block when account is selected', () => { + render({ selected: true }); + expect( + document.querySelector('.multichain-account-list-item--selected'), + ).toBeInTheDocument(); + }); + + it('renders the account name tooltip for long names', () => { + render({ + selected: true, + identity: { + ...identity, + name: 'This is a super long name that requires tooltip', + }, + }); + expect( + document.querySelector('.multichain-account-list-item__tooltip'), + ).toBeInTheDocument(); + }); + + it('renders the tree-dot menu to lauch the details menu', () => { + render(); + const optionsButton = document.querySelector( + '[aria-label="Test Account Options"]', + ); + expect(optionsButton).toBeInTheDocument(); + fireEvent.click(optionsButton); + expect(document.querySelector('.menu__background')).toBeInTheDocument(); + }); + + it('executes the action when the item is clicked', () => { + const onClick = jest.fn(); + render({ onClick }); + const item = document.querySelector('.multichain-account-list-item'); + fireEvent.click(item); + expect(onClick).toHaveBeenCalled(); + }); + + it('clicking the three-dot menu opens up options', () => { + const onClick = jest.fn(); + render({ onClick }); + const item = document.querySelector( + '[data-testid="account-list-item-menu-button"]', + ); + fireEvent.click(item); + expect( + document.querySelector('[data-testid="account-list-menu-open-explorer"]'), + ).toBeInTheDocument(); + }); + + it('renders connected site icon', () => { + const connectedAvatarName = 'Uniswap'; + const { getByAltText } = render({ + connectedAvatar: 'https://uniswap.org/favicon.ico', + connectedAvatarName, + }); + + expect(getByAltText(`${connectedAvatarName} logo`)).toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-list-item/index.js b/ui/components/multichain/account-list-item/index.js new file mode 100644 index 000000000000..0926db843bc8 --- /dev/null +++ b/ui/components/multichain/account-list-item/index.js @@ -0,0 +1 @@ +export { AccountListItem } from './account-list-item'; diff --git a/ui/components/multichain/account-list-item/index.scss b/ui/components/multichain/account-list-item/index.scss new file mode 100644 index 000000000000..5d74ee152a41 --- /dev/null +++ b/ui/components/multichain/account-list-item/index.scss @@ -0,0 +1,32 @@ +.multichain-account-list-item { + position: relative; + width: 100%; + + &:not(.account-list-item--selected) { + &:hover, + &:focus-within { + background: var(--color-background-default-hover); + } + } + + &__selected-indicator { + width: 4px; + height: calc(100% - 8px); + position: absolute; + top: 4px; + left: 4px; + } + + &__content { + overflow: hidden; + flex: 1; + } + + .currency-display-component { + flex-wrap: nowrap; + } + + &__tooltip { + display: inline; + } +} diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js new file mode 100644 index 000000000000..c054b115d01d --- /dev/null +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -0,0 +1,196 @@ +import React, { useState, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import Fuse from 'fuse.js'; +import { useDispatch, useSelector } from 'react-redux'; +import Box from '../../ui/box/box'; +import { + ButtonLink, + ICON_NAMES, + TextFieldSearch, + Text, +} from '../../component-library'; +import { AccountListItem } from '..'; +import { + BLOCK_SIZES, + Size, + TextColor, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import Popover from '../../ui/popover'; +import { + getSelectedAccount, + getMetaMaskAccountsOrdered, + getConnectedSubjectsForAllAddresses, + getOriginOfCurrentTab, +} from '../../../selectors'; +import { toggleAccountMenu, setSelectedAccount } from '../../../store/actions'; +import { EVENT_NAMES, EVENT } from '../../../../shared/constants/metametrics'; +import { + IMPORT_ACCOUNT_ROUTE, + NEW_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, +} from '../../../helpers/constants/routes'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; + +export const AccountListMenu = ({ onClose }) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const accounts = useSelector(getMetaMaskAccountsOrdered); + const selectedAccount = useSelector(getSelectedAccount); + const connectedSites = useSelector(getConnectedSubjectsForAllAddresses); + const currentTabOrigin = useSelector(getOriginOfCurrentTab); + const history = useHistory(); + const dispatch = useDispatch(); + + const [searchQuery, setSearchQuery] = useState(''); + + let searchResults = accounts; + if (searchQuery) { + const fuse = new Fuse(accounts, { + threshold: 0.2, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: ['name', 'address'], + }); + fuse.setCollection(accounts); + searchResults = fuse.search(searchQuery); + } + + return ( + + + {/* Search box */} + + setSearchQuery(e.target.value)} + /> + + {/* Account list block */} + + {searchResults.length === 0 && searchQuery !== '' ? ( + + {t('noAccountsFound')} + + ) : null} + {searchResults.map((account) => { + const connectedSite = connectedSites[account.address]?.find( + ({ origin }) => origin === currentTabOrigin, + ); + + return ( + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.NAV_ACCOUNT_SWITCHED, + properties: { + location: 'Main Menu', + }, + }); + dispatch(setSelectedAccount(account.address)); + }} + identity={account} + key={account.address} + selected={selectedAccount.address === account.address} + closeMenu={onClose} + connectedAvatar={connectedSite?.iconUrl} + connectedAvatarName={connectedSite?.name} + /> + ); + })} + + {/* Add / Import / Hardware */} + + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.ACCOUNT_ADD_SELECTED, + properties: { + account_type: EVENT.ACCOUNT_TYPES.DEFAULT, + location: 'Main Menu', + }, + }); + history.push(NEW_ACCOUNT_ROUTE); + }} + > + {t('addAccount')} + + + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.ACCOUNT_ADD_SELECTED, + properties: { + account_type: EVENT.ACCOUNT_TYPES.IMPORTED, + location: 'Main Menu', + }, + }); + history.push(IMPORT_ACCOUNT_ROUTE); + }} + > + {t('importAccount')} + + + + { + dispatch(toggleAccountMenu()); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.ACCOUNT_ADD_SELECTED, + properties: { + account_type: EVENT.ACCOUNT_TYPES.HARDWARE, + location: 'Main Menu', + }, + }); + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser( + CONNECT_HARDWARE_ROUTE, + ); + } else { + history.push(CONNECT_HARDWARE_ROUTE); + } + }} + > + {t('hardwareWallet')} + + + + + + ); +}; + +AccountListMenu.propTypes = { + /** + * Function that executes when the menu closes + */ + onClose: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.stories.js b/ui/components/multichain/account-list-menu/account-list-menu.stories.js new file mode 100644 index 000000000000..30abe820e7d6 --- /dev/null +++ b/ui/components/multichain/account-list-menu/account-list-menu.stories.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { AccountListMenu } from '.'; + +export default { + title: 'Components/Multichain/AccountListMenu', + component: AccountListMenu, + argTypes: { + onClose: { + action: 'onClose', + }, + }, +}; + +export const DefaultStory = (args) => ; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.test.js b/ui/components/multichain/account-list-menu/account-list-menu.test.js new file mode 100644 index 000000000000..2d34652b79a2 --- /dev/null +++ b/ui/components/multichain/account-list-menu/account-list-menu.test.js @@ -0,0 +1,103 @@ +/* eslint-disable jest/require-top-level-describe */ +import React from 'react'; +import reactRouterDom from 'react-router-dom'; +import { fireEvent, renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, +} from '../../../helpers/constants/routes'; +import { AccountListMenu } from '.'; + +const render = (props = { onClose: () => jest.fn() }) => { + const store = configureStore({ + activeTab: { + id: 113, + title: 'E2E Test Dapp', + origin: 'https://metamask.github.io', + protocol: 'https:', + url: 'https://metamask.github.io/test-dapp/', + }, + metamask: { + ...mockState.metamask, + }, + }); + return renderWithProvider(, store); +}; + +describe('AccountListMenu', () => { + const historyPushMock = jest.fn(); + + jest + .spyOn(reactRouterDom, 'useHistory') + .mockImplementation() + .mockReturnValue({ push: historyPushMock }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('displays important controls', () => { + const { getByPlaceholderText, getByText } = render(); + + expect(getByPlaceholderText('Search accounts')).toBeInTheDocument(); + expect(getByText('Add account')).toBeInTheDocument(); + expect(getByText('Import account')).toBeInTheDocument(); + expect(getByText('Hardware wallet')).toBeInTheDocument(); + }); + + it('navigates to new account screen when clicked', () => { + const { getByText } = render(); + fireEvent.click(getByText('Add account')); + expect(historyPushMock).toHaveBeenCalledWith(NEW_ACCOUNT_ROUTE); + }); + + it('navigates to import account screen when clicked', () => { + const { getByText } = render(); + fireEvent.click(getByText('Import account')); + expect(historyPushMock).toHaveBeenCalledWith(IMPORT_ACCOUNT_ROUTE); + }); + + it('navigates to hardware wallet connection screen when clicked', () => { + const { getByText } = render(); + fireEvent.click(getByText('Hardware wallet')); + expect(historyPushMock).toHaveBeenCalledWith(CONNECT_HARDWARE_ROUTE); + }); + + it('displays accounts for list and filters by search', () => { + render(); + const listItems = document.querySelectorAll( + '.multichain-account-list-item', + ); + expect(listItems).toHaveLength(4); + + const searchBox = document.querySelector('input[type=search]'); + fireEvent.change(searchBox, { + target: { value: 'Le' }, + }); + + const filteredListItems = document.querySelectorAll( + '.multichain-account-list-item', + ); + expect(filteredListItems).toHaveLength(1); + }); + + it('displays the "no accounts" message when search finds nothing', () => { + const { getByTestId } = render(); + + const searchBox = document.querySelector('input[type=search]'); + fireEvent.change(searchBox, { + target: { value: 'adslfkjlx' }, + }); + + const filteredListItems = document.querySelectorAll( + '.multichain-account-list-item', + ); + expect(filteredListItems).toHaveLength(0); + expect( + getByTestId('multichain-account-menu-no-results'), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/components/multichain/account-list-menu/index.js b/ui/components/multichain/account-list-menu/index.js new file mode 100644 index 000000000000..c7c28c3b86c4 --- /dev/null +++ b/ui/components/multichain/account-list-menu/index.js @@ -0,0 +1 @@ +export { AccountListMenu } from './account-list-menu'; diff --git a/ui/components/multichain/account-list-menu/index.scss b/ui/components/multichain/account-list-menu/index.scss new file mode 100644 index 000000000000..5bbb6f0f112a --- /dev/null +++ b/ui/components/multichain/account-list-menu/index.scss @@ -0,0 +1,6 @@ +.multichain-account-menu { + &__list { + max-height: 200px; + overflow: auto; + } +} diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js new file mode 100644 index 000000000000..49c3563e4e24 --- /dev/null +++ b/ui/components/multichain/index.js @@ -0,0 +1,3 @@ +export { AccountListItem } from './account-list-item'; +export { AccountListItemMenu } from './account-list-item-menu'; +export { AccountListMenu } from './account-list-menu'; diff --git a/ui/components/multichain/index.scss b/ui/components/multichain/index.scss new file mode 100644 index 000000000000..bef86b1dde89 --- /dev/null +++ b/ui/components/multichain/index.scss @@ -0,0 +1,2 @@ +@import 'account-list-item/index'; +@import 'account-list-menu/index'; diff --git a/ui/components/ui/menu/menu-item.js b/ui/components/ui/menu/menu-item.js index c3c409cc43ec..abdebf108bf4 100644 --- a/ui/components/ui/menu/menu-item.js +++ b/ui/components/ui/menu/menu-item.js @@ -2,7 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Icon, ICON_SIZES } from '../../component-library'; +import { Text, Icon, ICON_SIZES } from '../../component-library'; +import { TextVariant } from '../../../helpers/constants/design-system'; const MenuItem = ({ children, @@ -22,7 +23,7 @@ const MenuItem = ({ ) : null}
{children}
- {subtitle ?
{subtitle}
: null} + {subtitle ? {subtitle} : null}
); diff --git a/ui/components/ui/popover/index.scss b/ui/components/ui/popover/index.scss index acb22a510096..0a22355eb844 100644 --- a/ui/components/ui/popover/index.scss +++ b/ui/components/ui/popover/index.scss @@ -22,6 +22,10 @@ &-header { position: relative; + + &__title--center { + flex: 1; + } } &-bg { diff --git a/ui/components/ui/popover/popover.component.js b/ui/components/ui/popover/popover.component.js index 1defc3741e73..23a06f759472 100644 --- a/ui/components/ui/popover/popover.component.js +++ b/ui/components/ui/popover/popover.component.js @@ -15,6 +15,8 @@ import { Size, BorderColor, IconColor, + TEXT_ALIGN, + BLOCK_SIZES, } from '../../../helpers/constants/design-system'; import { ButtonIcon, @@ -76,9 +78,10 @@ const Popover = ({ {onBack ? ( @@ -90,7 +93,13 @@ const Popover = ({ size={Size.SM} /> ) : null} - + {title} {onClose ? ( diff --git a/ui/css/index.scss b/ui/css/index.scss index d861b721019e..46e1e7f2b095 100644 --- a/ui/css/index.scss +++ b/ui/css/index.scss @@ -10,6 +10,7 @@ @import './base-styles.scss'; @import '../components/component-library/component-library-components.scss'; @import '../components/app/app-components'; +@import '../components/multichain/index.scss'; @import '../components/ui/ui-components'; @import '../pages/pages'; @import './errors.scss'; diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts index a80999cbf56a..bc17859c0975 100644 --- a/ui/helpers/constants/design-system.ts +++ b/ui/helpers/constants/design-system.ts @@ -303,6 +303,7 @@ export const TEXT_ALIGN = { RIGHT: 'right', JUSTIFY: 'justify', END: 'end', + START: 'start', }; export const TEXT_TRANSFORM = { diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index c6cb8d89bf08..8cb160764239 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -90,6 +90,7 @@ import { SEND_STAGES } from '../../ducks/send'; import DeprecatedTestNetworks from '../../components/ui/deprecated-test-networks/deprecated-test-networks'; import NewNetworkInfo from '../../components/ui/new-network-info/new-network-info'; import { ThemeType } from '../../../shared/constants/preferences'; +import { AccountListMenu } from '../../components/multichain'; export default class Routes extends Component { static propTypes = { @@ -125,6 +126,8 @@ export default class Routes extends Component { forgottenPassword: PropTypes.bool, isCurrentProviderCustom: PropTypes.bool, completedOnboarding: PropTypes.bool, + isAccountMenuOpen: PropTypes.bool, + toggleAccountMenu: PropTypes.func, }; static contextTypes = { @@ -427,6 +430,8 @@ export default class Routes extends Component { shouldShowSeedPhraseReminder, isCurrentProviderCustom, completedOnboarding, + isAccountMenuOpen, + toggleAccountMenu, } = this.props; const loadMessage = loadingMessage || isNetworkLoading @@ -483,7 +488,10 @@ export default class Routes extends Component { )} {this.showOnboardingHeader() && } {completedOnboarding ? : null} - + {process.env.MULTICHAIN ? null : } + {process.env.MULTICHAIN && isAccountMenuOpen ? ( + toggleAccountMenu()} /> + ) : null}
{isLoading ? : null} {!isLoading && isNetworkLoading ? : null} diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index baa22c162504..c132bcfa87c7 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -18,6 +18,7 @@ import { setCurrentCurrency, setLastActiveTime, setMouseUserState, + toggleAccountMenu, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -55,6 +56,7 @@ function mapStateToProps(state) { forgottenPassword: state.metamask.forgottenPassword, isCurrentProviderCustom: isCurrentProviderCustom(state), completedOnboarding, + isAccountMenuOpen: state.metamask.isAccountMenuOpen, }; } @@ -67,6 +69,7 @@ function mapDispatchToProps(dispatch) { setLastActiveTime: () => dispatch(setLastActiveTime()), pageChanged: (path) => dispatch(pageChanged(path)), prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()), + toggleAccountMenu: () => dispatch(toggleAccountMenu()), }; } diff --git a/ui/selectors/permissions.js b/ui/selectors/permissions.js index 27f5c4c88f6a..d4b3d90de780 100644 --- a/ui/selectors/permissions.js +++ b/ui/selectors/permissions.js @@ -100,6 +100,24 @@ export function getConnectedSubjectsForSelectedAddress(state) { return connectedSubjects; } +export function getConnectedSubjectsForAllAddresses(state) { + const subjects = getPermissionSubjects(state); + const subjectMetadata = getSubjectMetadata(state); + + const accountsToConnections = {}; + Object.entries(subjects).forEach(([subjectKey, subjectValue]) => { + const exposedAccounts = getAccountsFromSubject(subjectValue); + exposedAccounts.forEach((address) => { + if (!accountsToConnections[address]) { + accountsToConnections[address] = []; + } + accountsToConnections[address].push(subjectMetadata[subjectKey] || {}); + }); + }); + + return accountsToConnections; +} + export function getSubjectsWithPermission(state, permissionName) { const subjects = getPermissionSubjects(state); From cedef121a36184cde4eae448ef510733fa301072 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Wed, 22 Mar 2023 16:39:58 +0530 Subject: [PATCH 10/20] updated keyringType (#18273) --- .storybook/test-data.js | 8 +++----- .../account-list-item/account-list-item.js | 18 ++++++++---------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.storybook/test-data.js b/.storybook/test-data.js index a7a893354e12..3f133fcba5e3 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1182,11 +1182,9 @@ const state = { ], }, { - type: HardwareKeyringTypes.ledger, - accounts: [ - '0x9d0ba4ddac06032527b140912ec808ab9451b788' - ], - } + type: KeyringType.ledger, + accounts: ['0x9d0ba4ddac06032527b140912ec808ab9451b788'], + }, ], networkConfigurations: { 'test-networkConfigurationId-1': { diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index f0a2aea8c820..bbfd277a4570 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -30,10 +30,8 @@ import { Size, BorderColor, } from '../../../helpers/constants/design-system'; -import { - HardwareKeyringTypes, - HardwareKeyringNames, -} from '../../../../shared/constants/hardware-wallets'; +import { HardwareKeyringNames } from '../../../../shared/constants/hardware-wallets'; +import { KeyringType } from '../../../../shared/constants/keyring'; import UserPreferencedCurrencyDisplay from '../../app/user-preferenced-currency-display/user-preferenced-currency-display.component'; import { SECONDARY, PRIMARY } from '../../../helpers/constants/common'; import { findKeyringForAddress } from '../../../ducks/metamask/metamask'; @@ -45,15 +43,15 @@ const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 17; function getLabel(keyring = {}, t) { const { type } = keyring; switch (type) { - case HardwareKeyringTypes.qr: + case KeyringType.qr: return HardwareKeyringNames.qr; - case HardwareKeyringTypes.imported: + case KeyringType.imported: return t('imported'); - case HardwareKeyringTypes.trezor: + case KeyringType.trezor: return HardwareKeyringNames.trezor; - case HardwareKeyringTypes.ledger: + case KeyringType.ledger: return HardwareKeyringNames.ledger; - case HardwareKeyringTypes.lattice: + case KeyringType.lattice: return HardwareKeyringNames.lattice; default: return null; @@ -212,7 +210,7 @@ export const AccountListItem = ({ blockExplorerUrlSubTitle={blockExplorerUrlSubTitle} identity={identity} onClose={() => setAccountOptionsMenuOpen(false)} - isRemovable={keyring?.type !== HardwareKeyringTypes.hdKeyTree} + isRemovable={keyring?.type !== KeyringType.hdKeyTree} closeMenu={closeMenu} /> ) : null} From 735f86cac23680792711e07b6f995a84660dfbe1 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Wed, 22 Mar 2023 12:18:17 +0000 Subject: [PATCH 11/20] Remove personal message manager (#18260) --- app/scripts/lib/personal-message-manager.js | 370 ------------------ .../lib/personal-message-manager.test.js | 182 --------- 2 files changed, 552 deletions(-) delete mode 100644 app/scripts/lib/personal-message-manager.js delete mode 100644 app/scripts/lib/personal-message-manager.test.js diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js deleted file mode 100644 index bb0a2b0c1e04..000000000000 --- a/app/scripts/lib/personal-message-manager.js +++ /dev/null @@ -1,370 +0,0 @@ -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import { bufferToHex } from 'ethereumjs-util'; -import { ethErrors } from 'eth-rpc-errors'; -import log from 'loglevel'; -import { MESSAGE_TYPE } from '../../../shared/constants/app'; -import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; -import createId from '../../../shared/modules/random-id'; -import { EVENT } from '../../../shared/constants/metametrics'; -import { detectSIWE } from '../../../shared/modules/siwe'; -import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; -import { addHexPrefix } from './util'; - -const hexRe = /^[0-9A-Fa-f]+$/gu; - -/** - * Represents, and contains data about, an 'personal_sign' type signature request. These are created when a - * signature for an personal_sign call is requested. - * - * @see {@link https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html#sign} - * @typedef {object} PersonalMessage - * @property {number} id An id to track and identify the message object - * @property {object} msgParams The parameters to pass to the personal_sign method once the signature request is - * approved. - * @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. - * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request - * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' - * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will - * always have a 'personal_sign' type. - */ - -export default class PersonalMessageManager extends EventEmitter { - /** - * Controller in charge of managing - storing, adding, removing, updating - PersonalMessage. - * - * @param options - * @param options.metricsEvent - * @param options.securityProviderRequest - */ - constructor({ metricsEvent, securityProviderRequest }) { - super(); - this.memStore = new ObservableStore({ - unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgCount: 0, - }); - - this.resetState = () => { - this.memStore.updateState({ - unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgCount: 0, - }); - }; - - this.messages = []; - this.metricsEvent = metricsEvent; - this.securityProviderRequest = securityProviderRequest; - } - - /** - * A getter for the number of 'unapproved' PersonalMessages in this.messages - * - * @returns {number} The number of 'unapproved' PersonalMessages in this.messages - */ - get unapprovedPersonalMsgCount() { - return Object.keys(this.getUnapprovedMsgs()).length; - } - - /** - * A getter for the 'unapproved' PersonalMessages in this.messages - * - * @returns {object} An index of PersonalMessage ids to PersonalMessages, for all 'unapproved' PersonalMessages in - * this.messages - */ - getUnapprovedMsgs() { - return this.messages - .filter((msg) => msg.status === 'unapproved') - .reduce((result, msg) => { - result[msg.id] = msg; - return result; - }, {}); - } - - /** - * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to - * this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {promise} When the message has been signed or rejected - */ - addUnapprovedMessageAsync(msgParams, req) { - return new Promise((resolve, reject) => { - if (!msgParams.from) { - reject( - new Error('MetaMask Message Signature: from field is required.'), - ); - return; - } - this.addUnapprovedMessage(msgParams, req).then((msgId) => { - this.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - resolve(data.rawSig); - return; - case 'rejected': - reject( - ethErrors.provider.userRejectedRequest( - 'MetaMask Message Signature: User denied message signature.', - ), - ); - return; - case 'errored': - reject(new Error(`MetaMask Message Signature: ${data.error}`)); - return; - default: - reject( - new Error( - `MetaMask Message Signature: Unknown problem: ${JSON.stringify( - msgParams, - )}`, - ), - ); - } - }); - }); - }); - } - - /** - * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add - * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to - * this.memStore. - * - * @param {object} msgParams - The params for the eth_sign call to be made after the message is approved. - * @param {object} [req] - The original request object possibly containing the origin - * @returns {number} The id of the newly created PersonalMessage. - */ - async addUnapprovedMessage(msgParams, req) { - log.debug( - `PersonalMessageManager addUnapprovedMessage: ${JSON.stringify( - msgParams, - )}`, - ); - // add origin from request - if (req) { - msgParams.origin = req.origin; - } - msgParams.data = this.normalizeMsgData(msgParams.data); - - // check for SIWE message - const siwe = detectSIWE(msgParams); - msgParams.siwe = siwe; - - // create txData obj with parameters and meta data - const time = new Date().getTime(); - const msgId = createId(); - const msgData = { - id: msgId, - msgParams, - time, - status: 'unapproved', - type: MESSAGE_TYPE.PERSONAL_SIGN, - }; - this.addMsg(msgData); - - const securityProviderResponse = await this.securityProviderRequest( - msgData, - msgData.type, - ); - - msgData.securityProviderResponse = securityProviderResponse; - - // signal update - this.emit('update'); - return msgId; - } - - /** - * Adds a passed PersonalMessage to this.messages, and calls this._saveMsgList() to save the unapproved PersonalMessages from that - * list to this.memStore. - * - * @param {Message} msg - The PersonalMessage to add to this.messages - */ - addMsg(msg) { - this.messages.push(msg); - this._saveMsgList(); - } - - /** - * Returns a specified PersonalMessage. - * - * @param {number} msgId - The id of the PersonalMessage to get - * @returns {PersonalMessage|undefined} The PersonalMessage with the id that matches the passed msgId, or undefined - * if no PersonalMessage has that id. - */ - getMsg(msgId) { - return this.messages.find((msg) => msg.id === msgId); - } - - /** - * Approves a PersonalMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise - * with any the message params modified for proper signing. - * - * @param {object} msgParams - The msgParams to be used when eth_sign is called, plus data added by MetaMask. - * @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask. - * @returns {Promise} Promises the msgParams object with metamaskId removed. - */ - approveMessage(msgParams) { - this.setMsgStatusApproved(msgParams.metamaskId); - return this.prepMsgForSigning(msgParams); - } - - /** - * Sets a PersonalMessage status to 'approved' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the PersonalMessage to approve. - */ - setMsgStatusApproved(msgId) { - this._setMsgStatus(msgId, 'approved'); - } - - /** - * Sets a PersonalMessage status to 'signed' via a call to this._setMsgStatus and updates that PersonalMessage in - * this.messages by adding the raw signature data of the signature request to the PersonalMessage - * - * @param {number} msgId - The id of the PersonalMessage to sign. - * @param {buffer} rawSig - The raw data of the signature request - */ - setMsgStatusSigned(msgId, rawSig) { - const msg = this.getMsg(msgId); - msg.rawSig = rawSig; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'signed'); - } - - /** - * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams - * - * @param {object} msgParams - The msgParams to modify - * @returns {Promise} Promises the msgParams with the metamaskId property removed - */ - async prepMsgForSigning(msgParams) { - delete msgParams.metamaskId; - return msgParams; - } - - /** - * Sets a PersonalMessage status to 'rejected' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the PersonalMessage to reject. - * @param reason - */ - rejectMsg(msgId, reason = undefined) { - if (reason) { - const msg = this.getMsg(msgId); - this.metricsEvent({ - event: reason, - category: EVENT.CATEGORIES.TRANSACTIONS, - properties: { - action: 'Sign Request', - type: msg.type, - }, - }); - } - this._setMsgStatus(msgId, 'rejected'); - } - - /** - * Sets a Message status to 'errored' via a call to this._setMsgStatus. - * - * @param {number} msgId - The id of the Message to error - * @param error - */ - errorMessage(msgId, error) { - const msg = this.getMsg(msgId); - msg.error = error; - this._updateMsg(msg); - this._setMsgStatus(msgId, 'errored'); - } - - /** - * Clears all unapproved messages from memory. - */ - clearUnapproved() { - this.messages = this.messages.filter((msg) => msg.status !== 'unapproved'); - this._saveMsgList(); - } - - /** - * Updates the status of a PersonalMessage in this.messages via a call to this._updateMsg - * - * @private - * @param {number} msgId - The id of the PersonalMessage to update. - * @param {string} status - The new status of the PersonalMessage. - * @throws A 'PersonalMessageManager - PersonalMessage not found for id: "${msgId}".' if there is no PersonalMessage - * in this.messages with an id equal to the passed msgId - * @fires An event with a name equal to `${msgId}:${status}`. The PersonalMessage is also fired. - * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along - * with the PersonalMessage - */ - _setMsgStatus(msgId, status) { - const msg = this.getMsg(msgId); - if (!msg) { - throw new Error( - `PersonalMessageManager - Message not found for id: "${msgId}".`, - ); - } - msg.status = status; - this._updateMsg(msg); - this.emit(`${msgId}:${status}`, msg); - if (status === 'rejected' || status === 'signed') { - this.emit(`${msgId}:finished`, msg); - } - } - - /** - * Sets a PersonalMessage in this.messages to the passed PersonalMessage if the ids are equal. Then saves the - * unapprovedPersonalMsgs index to storage via this._saveMsgList - * - * @private - * @param {PersonalMessage} msg - A PersonalMessage that will replace an existing PersonalMessage (with the same - * id) in this.messages - */ - _updateMsg(msg) { - const index = this.messages.findIndex((message) => message.id === msg.id); - if (index !== -1) { - this.messages[index] = msg; - } - this._saveMsgList(); - } - - /** - * Saves the unapproved PersonalMessages, and their count, to this.memStore - * - * @private - * @fires 'updateBadge' - */ - _saveMsgList() { - const unapprovedPersonalMsgs = this.getUnapprovedMsgs(); - const unapprovedPersonalMsgCount = Object.keys( - unapprovedPersonalMsgs, - ).length; - this.memStore.updateState({ - unapprovedPersonalMsgs, - unapprovedPersonalMsgCount, - }); - this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); - } - - /** - * A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. - * - * @param {any} data - The buffer data to convert to a hex - * @returns {string} A hex string conversion of the buffer data - */ - normalizeMsgData(data) { - try { - const stripped = stripHexPrefix(data); - if (stripped.match(hexRe)) { - return addHexPrefix(stripped); - } - } catch (e) { - log.debug(`Message was not hex encoded, interpreting as utf8.`); - } - - return bufferToHex(Buffer.from(data, 'utf8')); - } -} diff --git a/app/scripts/lib/personal-message-manager.test.js b/app/scripts/lib/personal-message-manager.test.js deleted file mode 100644 index e565c0229b69..000000000000 --- a/app/scripts/lib/personal-message-manager.test.js +++ /dev/null @@ -1,182 +0,0 @@ -import { TransactionStatus } from '../../../shared/constants/transaction'; -import PersonalMessageManager from './personal-message-manager'; - -describe('Personal Message Manager', () => { - let messageManager; - - beforeEach(() => { - messageManager = new PersonalMessageManager({ - metricsEvent: jest.fn(), - securityProviderRequest: jest.fn(), - }); - }); - - describe('#getMsgList', () => { - it('when new should return empty array', () => { - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(0); - }); - }); - - describe('#addMsg', () => { - it('adds a Msg returned in getMsgList', () => { - const Msg = { - id: 1, - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].id).toStrictEqual(1); - }); - }); - - describe('#setMsgStatusApproved', () => { - it('sets the Msg status to approved', () => { - const Msg = { - id: 1, - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.setMsgStatusApproved(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.approved); - }); - }); - - describe('#rejectMsg', () => { - it('sets the Msg status to rejected', () => { - const Msg = { - id: 1, - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }; - messageManager.addMsg(Msg); - messageManager.rejectMsg(1); - const result = messageManager.messages; - expect(Array.isArray(result)).toStrictEqual(true); - expect(result).toHaveLength(1); - expect(result[0].status).toStrictEqual(TransactionStatus.rejected); - }); - }); - - describe('#_updateMsg', () => { - it('replaces the Msg with the same id', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - messageManager._updateMsg({ - id: '1', - status: 'blah', - hash: 'foo', - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getMsg('1'); - expect(result.hash).toStrictEqual('foo'); - }); - }); - - describe('#getUnapprovedMsgs', () => { - it('returns unapproved Msgs in a hash', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - const result = messageManager.getUnapprovedMsgs(); - expect(typeof result).toStrictEqual('object'); - expect(result['1'].status).toStrictEqual(TransactionStatus.unapproved); - expect(result['2']).toBeUndefined(); - }); - }); - - describe('#getMsg', () => { - it('returns a Msg with the requested id', () => { - messageManager.addMsg({ - id: '1', - status: TransactionStatus.unapproved, - metamaskNetworkId: 'unit test', - }); - messageManager.addMsg({ - id: '2', - status: TransactionStatus.approved, - metamaskNetworkId: 'unit test', - }); - expect(messageManager.getMsg('1').status).toStrictEqual( - TransactionStatus.unapproved, - ); - expect(messageManager.getMsg('2').status).toStrictEqual( - TransactionStatus.approved, - ); - }); - }); - - describe('#normalizeMsgData', () => { - it('converts text to a utf8 hex string', () => { - const input = 'hello'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x68656c6c6f'); - }); - - it('tolerates a hex prefix', () => { - const input = '0x12'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x12'); - }); - - it('tolerates normal hex', () => { - const input = '12'; - const output = messageManager.normalizeMsgData(input); - expect(output).toStrictEqual('0x12'); - }); - }); - - describe('#addUnapprovedMessage', () => { - const origin = 'http://localhost:8080'; - const from = '0xFb2C15004343904e5f4082578c4e8e11105cF7e3'; - const msgParams = { - from, - data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e', - }; - - it('should detect SIWE messages', async () => { - const request = { origin }; - const nonSiweMsgParams = { - from, - data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', - }; - // siwe message - const msgId = await messageManager.addUnapprovedMessage( - msgParams, - request, - ); - const result = messageManager.getMsg(msgId); - expect(result.msgParams.siwe.isSIWEMessage).toStrictEqual(true); - // non-siwe message - const msgId2 = await messageManager.addUnapprovedMessage( - nonSiweMsgParams, - request, - ); - const result2 = messageManager.getMsg(msgId2); - expect(result2.msgParams.siwe.isSIWEMessage).toStrictEqual(false); - }); - }); -}); From 1eb102fd964952d5d2b9a53070b0c868cbfe59bd Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Wed, 22 Mar 2023 15:43:03 +0000 Subject: [PATCH 12/20] Fix(18190): add tabs to permission when initializing app (#18218) --- app/manifest/v2/_base.json | 14 -------------- app/manifest/v2/chrome.json | 16 +++++++++++++++- app/manifest/v2/firefox.json | 17 ++++++++++++++++- app/manifest/v3/_base.json | 10 ---------- app/manifest/v3/chrome.json | 16 +++++++++++++++- app/manifest/v3/firefox.json | 17 ++++++++++++++++- 6 files changed, 62 insertions(+), 28 deletions(-) diff --git a/app/manifest/v2/_base.json b/app/manifest/v2/_base.json index f962de618d51..39b289197617 100644 --- a/app/manifest/v2/_base.json +++ b/app/manifest/v2/_base.json @@ -60,19 +60,5 @@ }, "manifest_version": 2, "name": "__MSG_appName__", - "permissions": [ - "storage", - "unlimitedStorage", - "clipboardWrite", - "http://localhost:8545/", - "https://*.infura.io/", - "https://*.codefi.network/", - "https://chainid.network/chains.json", - "https://lattice.gridplus.io/*", - "activeTab", - "webRequest", - "*://*.eth/", - "notifications" - ], "short_name": "__MSG_appName__" } diff --git a/app/manifest/v2/chrome.json b/app/manifest/v2/chrome.json index a152130d89b2..9c0e95ec5d6f 100644 --- a/app/manifest/v2/chrome.json +++ b/app/manifest/v2/chrome.json @@ -4,5 +4,19 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "80" + "minimum_chrome_version": "80", + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ] } diff --git a/app/manifest/v2/firefox.json b/app/manifest/v2/firefox.json index d50b26a27ff8..5adf0471356d 100644 --- a/app/manifest/v2/firefox.json +++ b/app/manifest/v2/firefox.json @@ -4,5 +4,20 @@ "id": "webextension@metamask.io", "strict_min_version": "78.0" } - } + }, + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "activeTab", + "tabs", + "webRequest", + "*://*.eth/", + "notifications" + ] } diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json index 1b9456fd8d93..3beeb73790d4 100644 --- a/app/manifest/v3/_base.json +++ b/app/manifest/v3/_base.json @@ -65,15 +65,5 @@ }, "manifest_version": 3, "name": "__MSG_appName__", - "permissions": [ - "activeTab", - "alarms", - "clipboardWrite", - "notifications", - "scripting", - "storage", - "unlimitedStorage", - "webRequest" - ], "short_name": "__MSG_appName__" } diff --git a/app/manifest/v3/chrome.json b/app/manifest/v3/chrome.json index 486692539eb4..dbb0ee22cca8 100644 --- a/app/manifest/v3/chrome.json +++ b/app/manifest/v3/chrome.json @@ -6,5 +6,19 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "80" + "minimum_chrome_version": "80", + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ] } diff --git a/app/manifest/v3/firefox.json b/app/manifest/v3/firefox.json index 5f0e5672fdbe..67ecf7b09891 100644 --- a/app/manifest/v3/firefox.json +++ b/app/manifest/v3/firefox.json @@ -22,5 +22,20 @@ "default_title": "MetaMask", "default_popup": "popup.html" }, - "manifest_version": 2 + "manifest_version": 2, + "permissions": [ + "storage", + "unlimitedStorage", + "clipboardWrite", + "http://localhost:8545/", + "https://*.infura.io/", + "https://*.codefi.network/", + "https://chainid.network/chains.json", + "https://lattice.gridplus.io/*", + "tabs", + "activeTab", + "webRequest", + "*://*.eth/", + "notifications" + ] } From 16fa967740d0903a1e4c630692d269bffa9e9d7b Mon Sep 17 00:00:00 2001 From: vthomas13 <10986371+vthomas13@users.noreply.github.com> Date: Wed, 22 Mar 2023 12:51:37 -0400 Subject: [PATCH 13/20] Revert "What's new - OpenSea security provider (#16831)" (#17968) This reverts commit 932282e638902621fd40d297c840dbf175ca8885. --- app/_locales/de/messages.json | 21 --- app/_locales/el/messages.json | 21 --- app/_locales/en/messages.json | 21 --- app/_locales/es/messages.json | 21 --- app/_locales/fr/messages.json | 21 --- app/_locales/hi/messages.json | 21 --- app/_locales/id/messages.json | 21 --- app/_locales/ja/messages.json | 21 --- app/_locales/ko/messages.json | 21 --- app/_locales/pt/messages.json | 21 --- app/_locales/ru/messages.json | 21 --- app/_locales/tl/messages.json | 21 --- app/_locales/tr/messages.json | 21 --- app/_locales/vi/messages.json | 21 --- app/_locales/zh_CN/messages.json | 21 --- app/images/open-sea-security-provider.svg | 91 ------------- app/scripts/controllers/preferences.js | 11 -- app/scripts/controllers/preferences.test.js | 24 ---- app/scripts/metamask-controller.js | 4 - test/e2e/fixture-builder.js | 4 - .../app/open-sea-whats-new-popover/index.js | 1 - .../app/open-sea-whats-new-popover/index.scss | 6 - .../open-sea-whats-new-popover.js | 124 ------------------ ui/pages/home/home.component.js | 2 - ui/selectors/selectors.js | 6 - ui/store/actions.ts | 16 +-- 26 files changed, 1 insertion(+), 603 deletions(-) delete mode 100644 app/images/open-sea-security-provider.svg delete mode 100644 ui/components/app/open-sea-whats-new-popover/index.js delete mode 100644 ui/components/app/open-sea-whats-new-popover/index.scss delete mode 100644 ui/components/app/open-sea-whats-new-popover/open-sea-whats-new-popover.js diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index a92a7de6aee1..a920e0116afe 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -329,9 +329,6 @@ "message": "$1 erlauben, bis zu dem folgenden Betrag abzuheben und auszugeben:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Gehen Sie immer mit Sorgfalt vor, bevor Sie irgendwelche Anfragen genehmigen." - }, "amount": { "message": "Betrag" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Verwenden Sie die OpenSea's API, um NFT-Daten abzurufen. Die NFT-Auto-Erkennung basiert auf der OpenSea's API und wird nicht verfügbar sein, wenn diese deaktiviert ist." }, - "enableOpenSeaSecurityProvider": { - "message": "Sicherheitsanbieter aktivieren" - }, "enableSmartTransactions": { "message": "Intelligente Transaktionen ermöglichen" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Allgemein" }, - "getWarningsFromOpenSea": { - "message": "Erhalten Sie Warnungen von OpenSea, wenn Sie eine bekannte bösartige Anfrage erhalten." - }, "goBack": { "message": "Zurück" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Nicht genügend Gas" }, - "notNow": { - "message": "Nicht jetzt" - }, "notifications": { "message": "Benachrichtigungen" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "OpenSea-Sicherheitsanbieter" - }, - "openSeaDescription": { - "message": "OpenSea ist der erset Sicherheitsanbieter für diese Funktion. Mehr Anbieter folgen in Kürze!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Nicht verbunden" }, - "staySafeWithOpenSea": { - "message": "Mit OpenSea sicher bleiben" - }, "step1LatticeWallet": { "message": "Verbinden Sie Ihr Lattice1" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index bb17e4e33696..6a97d9d71ceb 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -329,9 +329,6 @@ "message": "Επιτρέψτε στο $1 να κάνει ανάληψη και να ξοδέψει μέχρι το ακόλουθο ποσό:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Φροντίστε πάντα να κάνετε τη δική σας επιμελή έρευνα προτού εγκρίνετε οποιαδήποτε αιτήματα." - }, "amount": { "message": "Ποσό" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Χρησιμοποιήστε το API OpenSea για λήψη δεδομένων NFT. Η αυτόματη ανίχνευση NFT βασίζεται στο API του OpenSea, και δεν θα είναι διαθέσιμη όταν αυτό είναι απενεργοποιημένο." }, - "enableOpenSeaSecurityProvider": { - "message": "Ενεργοποίηση παρόχου ασφάλειας" - }, "enableSmartTransactions": { "message": "Ενεργοποίηση Έξυπνων Συναλλαγών" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Γενικά" }, - "getWarningsFromOpenSea": { - "message": "Λάβετε προειδοποιήσεις από το OpenSea όποτε λαβαίνετε ένα αναγνωρισμένο κακόβουλο αίτημα." - }, "goBack": { "message": "Μετάβαση Πίσω" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Δεν Υπάρχει Αρκετό τέλος συναλλαγής" }, - "notNow": { - "message": "Όχι τώρα" - }, "notifications": { "message": "Ειδοποιήσεις" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Πάροχος ασφάλειας OpenSea" - }, - "openSeaDescription": { - "message": "Το OpenSea είναι ο πρώτος πάροχος ασφάλειας για αυτή τη λειτουργία. Σύντομα θα προστεθούν και άλλοι πάροχοι!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Δεν έχει συνδεθεί" }, - "staySafeWithOpenSea": { - "message": "Μείνετε ασφαλείς με το OpenSea" - }, "step1LatticeWallet": { "message": "Συνδέστε το Lattice1 σας" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ccaa85c05ca6..1aca00b814d9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -342,9 +342,6 @@ "message": "Allow $1 to withdraw and spend up to the following amount:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Always be sure to do your own due diligence before approving any requests." - }, "amount": { "message": "Amount" }, @@ -1249,9 +1246,6 @@ "enableOpenSeaAPIDescription": { "message": "Use OpenSea's API to fetch NFT data. NFT auto-detection relies on OpenSea's API, and will not be available when this is turned off." }, - "enableOpenSeaSecurityProvider": { - "message": "Enable security provider" - }, "enableSmartTransactions": { "message": "Enable smart transactions" }, @@ -1555,9 +1549,6 @@ "general": { "message": "General" }, - "getWarningsFromOpenSea": { - "message": "Get warnings from OpenSea whenever you receive a known malicious request." - }, "goBack": { "message": "Go back" }, @@ -2332,9 +2323,6 @@ "notEnoughGas": { "message": "Not enough gas" }, - "notNow": { - "message": "Not now" - }, "notifications": { "message": "Notifications" }, @@ -2695,12 +2683,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "OpenSea security provider" - }, - "openSeaDescription": { - "message": "OpenSea is the first security provider for this feature. More providers coming soon!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3709,9 +3691,6 @@ "statusNotConnected": { "message": "Not connected" }, - "staySafeWithOpenSea": { - "message": "Stay safe with OpenSea" - }, "step1LatticeWallet": { "message": "Connect your Lattice1" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index e987868a466d..a2dc1a9fe61b 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -329,9 +329,6 @@ "message": "Permitir que se retire $1 y gastar hasta el siguiente importe:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Asegúrese siempre de hacer su propia diligencia debida antes de aprobar cualquier solicitud." - }, "amount": { "message": "Importe" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Utilice la API de OpenSea para obtener los datos de NFT. La autodetección de NFT depende de la API de OpenSea y no estará disponible si la API está desactivada." }, - "enableOpenSeaSecurityProvider": { - "message": "Habilitar proveedor de seguridad" - }, "enableSmartTransactions": { "message": "Habilitar transacciones inteligentes" }, @@ -1519,9 +1513,6 @@ "general": { "message": "General" }, - "getWarningsFromOpenSea": { - "message": "Reciba advertencias de OpenSea cada vez que reciba una solicitud maliciosa conocida." - }, "goBack": { "message": "Volver" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "No hay gas suficiente" }, - "notNow": { - "message": "Por ahora no" - }, "notifications": { "message": "Notificaciones" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Proveedor de seguridad de OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea es el primer proveedor de seguridad para esta función. ¡Más proveedores próximamente!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "No conectado" }, - "staySafeWithOpenSea": { - "message": "Manténgase seguro con OpenSea" - }, "step1LatticeWallet": { "message": "Conecte su Lattice1" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index e2f21611a001..1906a3a1f8f8 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -329,9 +329,6 @@ "message": "Permettre à $1 de retirer et de dépenser jusqu’au montant suivant :", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Vous devez faire preuve de diligence raisonnable avant d’approuver toute demande." - }, "amount": { "message": "Montant" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Utilisez l’API OpenSea pour récupérer les données de NFT. La détection automatique de NFT repose sur l’API OpenSea et ne sera pas disponible si elle est désactivée." }, - "enableOpenSeaSecurityProvider": { - "message": "Activer le fournisseur de services de sécurité" - }, "enableSmartTransactions": { "message": "Activer les transactions intelligentes" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Général" }, - "getWarningsFromOpenSea": { - "message": "OpenSea vous avertira dès que vous recevrez une requête malveillante." - }, "goBack": { "message": "Retour" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Pas assez de gaz" }, - "notNow": { - "message": "Pas maintenant" - }, "notifications": { "message": "Notifications" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Fournisseur de services de sécurité OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea est le premier fournisseur de services de sécurité à offrir cette fonctionnalité. D’autres fournisseurs seront bientôt ajoutés !" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Non connecté" }, - "staySafeWithOpenSea": { - "message": "Restez en sécurité avec OpenSea" - }, "step1LatticeWallet": { "message": "Connectez votre Lattice1" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 2c2da2a6ebf0..03307179f588 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -329,9 +329,6 @@ "message": "$1 को निम्नलिखित तक राशि निकालने और खर्च करने की अनुमति दें:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "किसी भी अनुरोध को मंजूरी देने से पहले हमेशा अपनी खुद की उचित कर्मठता सुनिश्चित करें।" - }, "amount": { "message": "राशि" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "NFT डेटा लाने के लिए OpenSea के API का उपयोग करें। NFT ऑटो-डिटेक्शन OpenSea के API पर निर्भर करता है, और इसके बंद होने पर उपलब्ध नहीं होगा।" }, - "enableOpenSeaSecurityProvider": { - "message": "सुरक्षा प्रदाता सक्षम करें" - }, "enableSmartTransactions": { "message": "स्मार्ट लेनदेन को सक्षम करें" }, @@ -1519,9 +1513,6 @@ "general": { "message": "सामान्य" }, - "getWarningsFromOpenSea": { - "message": "जब भी आप एक ज्ञात दुर्भावनापूर्ण अनुरोध प्राप्त करते हैं, OpenSea से चेतावनी प्राप्त करें।" - }, "goBack": { "message": "वापस जाएं" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "पर्याप्त गैस नहीं" }, - "notNow": { - "message": "अभी नहीं" - }, "notifications": { "message": "सूचनाएं" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (बीटा)" }, - "openSeaAltText": { - "message": "OpenSea सुरक्षा प्रदाता" - }, - "openSeaDescription": { - "message": "OpenSea इस सुविधा के लिए पहला सुरक्षा प्रदाता है।अधिक प्रदाता जल्द ही आ रहे हैं!" - }, "openSeaNew": { "message": "ओपनसी" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "कनेक्ट नहीं है" }, - "staySafeWithOpenSea": { - "message": "OpenSea के साथ सुरक्षित रहें" - }, "step1LatticeWallet": { "message": "अपना Lattice1 कनेक्ट करें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 370ffdf72441..07aae2ce06e4 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -329,9 +329,6 @@ "message": "Izinkan $1 untuk ditarik dan digunakan hingga jumlah berikut:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Selalu pastikan untuk melakukan uji tuntas Anda sendiri sebelum menyetujui permintaan apa pun." - }, "amount": { "message": "Jumlah" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Gunakan API OpenSea untuk mengambil data NFT. Deteksi otomatis NFT bergantung pada API OpenSea, dan tidak akan tersedia saat API ditutup." }, - "enableOpenSeaSecurityProvider": { - "message": "Aktifkan penyedia keamanan" - }, "enableSmartTransactions": { "message": "Aktifkan transaksi pintar" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Umum" }, - "getWarningsFromOpenSea": { - "message": "Dapatkan peringatan dari OpenSea setiap kali Anda menerima permintaan berbahaya yang diketahui." - }, "goBack": { "message": "Kembali" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Gas tidak cukup" }, - "notNow": { - "message": "Tidak sekarang" - }, "notifications": { "message": "Notifikasi" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Penyedia keamanan OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea merupakan penyedia keamanan pertama untuk fitur ini. Penyedia lainnya akan segera hadir!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Tidak terhubung" }, - "staySafeWithOpenSea": { - "message": "Tetap aman bersama OpenSea" - }, "step1LatticeWallet": { "message": "Hubungkan Lattice1 Anda" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 57d53796f4ca..3320c1442012 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -329,9 +329,6 @@ "message": "$1に以下の額までの引き出しと使用を許可します。", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "リクエストを承認する前に、必ず独自のデューデリジェンスを行ってください。" - }, "amount": { "message": "金額" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "OpenSea APIを使用してNFTデータを取得します。NFT自動検出はOpenSea APIを使用するため、この設定をオフにすると利用できなくなります。" }, - "enableOpenSeaSecurityProvider": { - "message": "セキュリティプロバイダーを有効にする" - }, "enableSmartTransactions": { "message": "スマートトランザクションを有効にする" }, @@ -1519,9 +1513,6 @@ "general": { "message": "一般" }, - "getWarningsFromOpenSea": { - "message": "既知の悪質なリクエストを受けた際に OpenSea から警告を受けられます。" - }, "goBack": { "message": "戻る" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "ガスが不足しています" }, - "notNow": { - "message": "また後で" - }, "notifications": { "message": "通知" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (ベータ)" }, - "openSeaAltText": { - "message": "OpenSea セキュリティプロバイダー" - }, - "openSeaDescription": { - "message": "OpenSea は、この機能を提供する最初のセキュリティプロバイダーです。他のプロバイダーも近日追加予定です!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "未接続" }, - "staySafeWithOpenSea": { - "message": "OpenSea で安全を確保" - }, "step1LatticeWallet": { "message": "Lattice1を接続する" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 2826089ef4ab..01f792772edd 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -329,9 +329,6 @@ "message": "$1에서 다음 금액까지 인출 및 지출하도록 허용:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "모든 요청을 승인하기 전에 주의 깊게 직접 확인하세요." - }, "amount": { "message": "금액" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "OpenSea의 API를 사용하여 NFT 데이터를 가져옵니다. NFT 자동 감지는 OpenSea의 API에 의존하며 이 API가 꺼져 있으면 사용할 수 없습니다." }, - "enableOpenSeaSecurityProvider": { - "message": "보안 업체 활성화" - }, "enableSmartTransactions": { "message": "스마트 트랜잭션 활성화" }, @@ -1519,9 +1513,6 @@ "general": { "message": "일반" }, - "getWarningsFromOpenSea": { - "message": "알려진 악성 요청을 받을 때마다 OpenSea로부터 경고 알림을 받으세요." - }, "goBack": { "message": "뒤로 가기" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "가스 부족" }, - "notNow": { - "message": "나중에" - }, "notifications": { "message": "알림" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (베타)" }, - "openSeaAltText": { - "message": "OpenSea 보안 업체" - }, - "openSeaDescription": { - "message": "이 기능의 1차 보안 업체는 OpenSea입니다. 곧 다른 보안 업체도 마련하겠습니다!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "연결되지 않음" }, - "staySafeWithOpenSea": { - "message": "OpenSea로 보안을 유지하세요" - }, "step1LatticeWallet": { "message": "Lattice1을 연결하세요." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 46fc9910facf..544cc1998a2d 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -329,9 +329,6 @@ "message": "Permitir que $1 saque e gaste até o seguinte valor:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Certifique-se sempre de fazer sua devida diligência antes de aprovar qualquer solicitação." - }, "amount": { "message": "Valor" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Use a API OpenSea para recuperar dados de NFTs. A detecção automática de NFTs depende da API OpenSea e não estará disponível quando essa opção estiver desativada." }, - "enableOpenSeaSecurityProvider": { - "message": "Habilitar provedor de segurança" - }, "enableSmartTransactions": { "message": "Ativar transações inteligentes" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Geral" }, - "getWarningsFromOpenSea": { - "message": "Receba avisos do OpenSea sempre que receber uma solicitação maliciosa conhecida." - }, "goBack": { "message": "Voltar" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Não há gás suficiente" }, - "notNow": { - "message": "Agora não" - }, "notifications": { "message": "Notificações" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Provedor de segurança OpenSea" - }, - "openSeaDescription": { - "message": "O OpenSea é o primeiro provedor de segurança para este recurso. Mais provedores chegarão em breve!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Não conectado" }, - "staySafeWithOpenSea": { - "message": "Fique seguro com o OpenSea" - }, "step1LatticeWallet": { "message": "Conecte seu Lattice1" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index d906e91fec79..89293012dc4a 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -329,9 +329,6 @@ "message": "Разрешить $1 снять и потратить до следующей суммы:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Всегда обязательно проводите собственную комплексную проверку, прежде чем утверждать какие-либо запросы." - }, "amount": { "message": "Сумма" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Используйте API OpenSea для получения данных NFT. Для автоматического обнаружения NFT используется API OpenSea, и такое обнаружение будет недоступно, если этот API отключен." }, - "enableOpenSeaSecurityProvider": { - "message": "Включить поставщика услуг безопасности" - }, "enableSmartTransactions": { "message": "Включить смарт-транзакции" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Общее" }, - "getWarningsFromOpenSea": { - "message": "Получайте предупреждения от OpenSea всякий раз, когда вы получаете заведомо вредоносный запрос." - }, "goBack": { "message": "Назад" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Недостаточно газа" }, - "notNow": { - "message": "Не сейчас" - }, "notifications": { "message": "Уведомления" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (бета-версия)" }, - "openSeaAltText": { - "message": "Поставщик услуг безопасности OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea является первым поставщиком услуг безопасности для этой функции. Скоро появятся новые поставщики!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Не подключено" }, - "staySafeWithOpenSea": { - "message": "Оставайтесь в безопасности с OpenSea" - }, "step1LatticeWallet": { "message": "Подключите Lattice1" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 334c05393a01..ef944e308713 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -329,9 +329,6 @@ "message": "Payagan ang $1 na mag-withdraw at gastusin ang sumusunod na halaga:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Laging siguraduhin na gawin ang iyong sariling angkop na pagsusumikap bago aprubahan ang anumang mga kahilingan." - }, "amount": { "message": "Halaga" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Gamitin ang API ng Opensea upang kunin ang NFT data. ang NFT auto-detection ay umaasa sa API ng OpenSea, at hindi magiging available kapag ito ay isinara." }, - "enableOpenSeaSecurityProvider": { - "message": "Paganahin ang tagapagbigay ng seguridad" - }, "enableSmartTransactions": { "message": "Payagan ang mga smart transaction" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Pangkalahatan" }, - "getWarningsFromOpenSea": { - "message": "Makakuha ng mga babala mula sa OpenSea sa tuwing makakatanggap ka ng kilalang malisyosong kahilingan." - }, "goBack": { "message": "Bumalik" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Hindi Sapat ang Gas" }, - "notNow": { - "message": "Hindi ngayon" - }, "notifications": { "message": "Mga Abiso" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea na (Beta)" }, - "openSeaAltText": { - "message": "Tagapagbigay ng seguridad ng OpenSea" - }, - "openSeaDescription": { - "message": "Ang OpenSea ay ang unang tagapagbigay ng seguridad para sa tampok na ito. Marami pang mga provider ang paparating!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Hindi konektado" }, - "staySafeWithOpenSea": { - "message": "Manatiling ligtas sa OpenSea" - }, "step1LatticeWallet": { "message": "Ikonekta ang iyong Lattice1" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3b85a7dc8da7..eb3841356acd 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -329,9 +329,6 @@ "message": "$1 için şu tutara kadar para çekme ve harcama izni ver:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Talepleri onaylamadan önce her zaman bunu gerekli özeni göstererek yaptığınızdan emin olun." - }, "amount": { "message": "Tutar" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "NFT verilerini almak için OpenSea API'sini kullanın. NFT otomatik algılama OpenSea API'ye dayalıdır ve bu kapatılırsa mevcut olmayacaktır." }, - "enableOpenSeaSecurityProvider": { - "message": "Güvenlik sağlayıcısını etkinleştir" - }, "enableSmartTransactions": { "message": "Akıllı işlemleri etkinleştir" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Genel" }, - "getWarningsFromOpenSea": { - "message": "Bilinen bir kötü amaçlı talep aldığınızda OpenSea'den uyarı alın." - }, "goBack": { "message": "Geri git" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Yeterli gaz yok" }, - "notNow": { - "message": "Şimdi değil" - }, "notifications": { "message": "Bildirimler" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "OpenSea güvenlik sağlayıcısı" - }, - "openSeaDescription": { - "message": "OpenSea bu özellik için birinci güvenlik sağlayıcısıdır. Daha fazla sağlayıcı çok yakında gelecektir!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Bağlanmadı" }, - "staySafeWithOpenSea": { - "message": "OpenSea ile güvende kalın" - }, "step1LatticeWallet": { "message": "Lattice1'inizi bağlayın" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 00897f945df9..1aebc6b67c6c 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -329,9 +329,6 @@ "message": "Cho phép $1 rút và chi tiêu tối đa số tiền sau đây:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "Nhớ luôn tự thẩm định trước khi phê duyệt bất kỳ yêu cầu nào." - }, "amount": { "message": "Số tiền" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "Sử dụng API của OpenSea để tìm nạp dữ liệu NFT. Tính năng tự động phát hiện NFT dựa vào API của OpenSea và sẽ không khả dụng nếu tính năng này bị tắt." }, - "enableOpenSeaSecurityProvider": { - "message": "Kích hoạt nhà cung cấp bảo mật" - }, "enableSmartTransactions": { "message": "Bật giao dịch thông minh" }, @@ -1519,9 +1513,6 @@ "general": { "message": "Chung" }, - "getWarningsFromOpenSea": { - "message": "Nhận cảnh báo từ OpenSea bất cứ khi nào bạn nhận được một yêu cầu độc hại đã biết." - }, "goBack": { "message": "Quay Lại" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "Không đủ gas" }, - "notNow": { - "message": "Không phải bây giờ" - }, "notifications": { "message": "Thông báo" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea (Beta)" }, - "openSeaAltText": { - "message": "Nhà cung cấp bảo mật OpenSea" - }, - "openSeaDescription": { - "message": "OpenSea là nhà cung cấp bảo mật đầu tiên cho tính năng này. Sắp có thêm nhiều nhà cung cấp khác!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "Chưa kết nối" }, - "staySafeWithOpenSea": { - "message": "Đảm bảo an toàn với OpenSea" - }, "step1LatticeWallet": { "message": "Kết nối với Lattice1" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 096dcd1e4c29..78d5099ef13e 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -329,9 +329,6 @@ "message": "允许 $1 提取和消费最高以下金额:", "description": "The url of the site that requested permission to 'withdraw and spend'" }, - "alwaysBeSureTo": { - "message": "在批准任何请求之前,始终确保执行自身尽职调查。" - }, "amount": { "message": "数额" }, @@ -1225,9 +1222,6 @@ "enableOpenSeaAPIDescription": { "message": "使用 OpenSea 的 API 获取 NFT 数据。NFT 自动检测依赖于 OpenSea 的 API,在后者关闭时自动检测将不可用。" }, - "enableOpenSeaSecurityProvider": { - "message": "启用安全服务提供商" - }, "enableSmartTransactions": { "message": "启用智能交易" }, @@ -1519,9 +1513,6 @@ "general": { "message": "常规" }, - "getWarningsFromOpenSea": { - "message": "每当收到已知恶意请求,即从 OpenSea 获取警告。" - }, "goBack": { "message": "返回" }, @@ -2275,9 +2266,6 @@ "notEnoughGas": { "message": "燃料不足" }, - "notNow": { - "message": "暂时不" - }, "notifications": { "message": "通知" }, @@ -2608,12 +2596,6 @@ "openSea": { "message": "OpenSea(测试版)" }, - "openSeaAltText": { - "message": "OpenSea 安全服务提供商" - }, - "openSeaDescription": { - "message": "OpenSea 是该功能的第一个安全服务提供商。即将推出更多提供商!" - }, "openSeaNew": { "message": "OpenSea" }, @@ -3508,9 +3490,6 @@ "statusNotConnected": { "message": "未连接" }, - "staySafeWithOpenSea": { - "message": "通过 OpenSea 确保安全" - }, "step1LatticeWallet": { "message": "关联您的 Lattice1" }, diff --git a/app/images/open-sea-security-provider.svg b/app/images/open-sea-security-provider.svg deleted file mode 100644 index ac79c0026adb..000000000000 --- a/app/images/open-sea-security-provider.svg +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 455fcdc1e0ee..c33e0f12adf4 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -67,7 +67,6 @@ export default class PreferencesController { : LedgerTransportTypes.u2f, transactionSecurityCheckEnabled: false, theme: ThemeType.os, - openSeaTransactionSecurityProviderPopoverHasBeenShown: false, ...opts.initState, }; @@ -204,16 +203,6 @@ export default class PreferencesController { }); } - /** - * Setter for the `openSeaTransactionSecurityProviderPopoverHasBeenShown` property - * - */ - setOpenSeaTransactionSecurityProviderPopoverHasBeenShown() { - this.store.updateState({ - openSeaTransactionSecurityProviderPopoverHasBeenShown: true, - }); - } - /** * Add new methodData to state, to avoid requesting this information again through Infura * diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index f129de94011f..21267195c322 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -330,28 +330,4 @@ describe('preferences controller', function () { ); }); }); - - describe('setOpenSeaTransactionSecurityProviderPopoverHasBeenShown', function () { - it('should default to value "false"', function () { - const state = preferencesController.store.getState(); - assert.equal( - state.openSeaTransactionSecurityProviderPopoverHasBeenShown, - false, - ); - }); - - it('should set the openSeaTransactionSecurityProviderPopoverHasBeenShown to true', function () { - const state = preferencesController.store.getState(); - assert.equal( - state.openSeaTransactionSecurityProviderPopoverHasBeenShown, - false, - ); - preferencesController.setOpenSeaTransactionSecurityProviderPopoverHasBeenShown(); - assert.equal( - preferencesController.store.getState() - .openSeaTransactionSecurityProviderPopoverHasBeenShown, - true, - ); - }); - }); }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0a395106a9d2..6a2c1107d07c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1861,10 +1861,6 @@ export default class MetamaskController extends EventEmitter { preferencesController.setTransactionSecurityCheckEnabled.bind( preferencesController, ), - setOpenSeaTransactionSecurityProviderPopoverHasBeenShown: - preferencesController.setOpenSeaTransactionSecurityProviderPopoverHasBeenShown.bind( - preferencesController, - ), // AssetsContractController getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index a56197f4e694..6fffc84a9958 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -245,8 +245,6 @@ function defaultFixture() { useTokenDetection: false, useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, - transactionSecurityCheckEnabled: true, - openSeaTransactionSecurityProviderPopoverHasBeenShown: true, }, SmartTransactionsController: { smartTransactionsState: { @@ -360,8 +358,6 @@ function onboardingFixture() { useTokenDetection: false, useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, - transactionSecurityCheckEnabled: true, - openSeaTransactionSecurityProviderPopoverHasBeenShown: true, }, SmartTransactionsController: { smartTransactionsState: { diff --git a/ui/components/app/open-sea-whats-new-popover/index.js b/ui/components/app/open-sea-whats-new-popover/index.js deleted file mode 100644 index abd91185ed53..000000000000 --- a/ui/components/app/open-sea-whats-new-popover/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './open-sea-whats-new-popover'; diff --git a/ui/components/app/open-sea-whats-new-popover/index.scss b/ui/components/app/open-sea-whats-new-popover/index.scss deleted file mode 100644 index ff307b957902..000000000000 --- a/ui/components/app/open-sea-whats-new-popover/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.open-sea-whats-new-popover { - &__enable-security-provider-button { - width: fit-content; - font-weight: 400; - } -} diff --git a/ui/components/app/open-sea-whats-new-popover/open-sea-whats-new-popover.js b/ui/components/app/open-sea-whats-new-popover/open-sea-whats-new-popover.js deleted file mode 100644 index 30bfda33a30f..000000000000 --- a/ui/components/app/open-sea-whats-new-popover/open-sea-whats-new-popover.js +++ /dev/null @@ -1,124 +0,0 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { I18nContext } from '../../../contexts/i18n'; -import Popover from '../../ui/popover'; -import { - DISPLAY, - FLEX_DIRECTION, - FONT_WEIGHT, - TextColor, - TextVariant, -} from '../../../helpers/constants/design-system'; -import Button from '../../ui/button'; -import Box from '../../ui/box'; -import { - setOpenSeaTransactionSecurityProviderPopoverHasBeenShown, - setTransactionSecurityCheckEnabled, -} from '../../../store/actions'; -import { getHasTheOpenSeaTransactionSecurityProviderPopoverBeenShown } from '../../../selectors'; -import { Text } from '../../component-library'; -import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes'; - -export default function OpenSeaWhatsNewPopover() { - const t = useContext(I18nContext); - const dispatch = useDispatch(); - const history = useHistory(); - - const hasThePopoverBeenShown = useSelector( - getHasTheOpenSeaTransactionSecurityProviderPopoverBeenShown, - ); - - return ( - process.env.TRANSACTION_SECURITY_PROVIDER && - !hasThePopoverBeenShown && ( - - {t('staySafeWithOpenSea')} - - } - footer={ - <> - - - - - - - - } - footerClassName="smart-transactions-popover__footer" - className="smart-transactions-popover" - onClose={() => - dispatch(setOpenSeaTransactionSecurityProviderPopoverHasBeenShown()) - } - > - - - {t('openSeaAltText')} - - - {t('getWarningsFromOpenSea')} - - - {t('openSeaDescription')} - - - {t('alwaysBeSureTo')} - - - - ) - ); -} diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 1d8c8330d7e6..cdc6663178cb 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -54,7 +54,6 @@ import { ONBOARDING_SECURE_YOUR_WALLET_ROUTE, } from '../../helpers/constants/routes'; import ZENDESK_URLS from '../../helpers/constants/zendesk-url'; -import OpenSeaWhatsNewPopover from '../../components/app/open-sea-whats-new-popover/open-sea-whats-new-popover'; ///: BEGIN:ONLY_INCLUDE_IN(main) import { SUPPORT_LINK } from '../../../shared/lib/ui-utils'; ///: END:ONLY_INCLUDE_IN @@ -632,7 +631,6 @@ export default class Home extends PureComponent { />
{showWhatsNew ? : null} - {showWhatsNew ? : null} {!showWhatsNew && showRecoveryPhraseReminder ? ( { - return async (dispatch) => { + return async () => { try { await submitRequestToBackground('setTransactionSecurityCheckEnabled', [ transactionSecurityCheckEnabled, ]); - await forceUpdateMetamaskState(dispatch); } catch (error) { logErrorWithMessage(error); } @@ -4591,19 +4590,6 @@ export function setFirstTimeUsedNetwork(chainId: string) { return submitRequestToBackground('setFirstTimeUsedNetwork', [chainId]); } -export function setOpenSeaTransactionSecurityProviderPopoverHasBeenShown(): ThunkAction< - void, - MetaMaskReduxState, - unknown, - AnyAction -> { - return async () => { - await submitRequestToBackground( - 'setOpenSeaTransactionSecurityProviderPopoverHasBeenShown', - ); - }; -} - // QR Hardware Wallets export async function submitQRHardwareCryptoHDKey(cbor: Hex) { await submitRequestToBackground('submitQRHardwareCryptoHDKey', [cbor]); From 34ba62470ad3dd13eb84004a65b82d7050f3a5bb Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Wed, 22 Mar 2023 18:06:05 +0000 Subject: [PATCH 14/20] bump contract metadata package version (#18278) * bump contract metadata package version * bump contract metadata package version --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index be6a45a14417..6441e7a86a24 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,7 @@ "@metamask/approval-controller": "^1.0.0", "@metamask/assets-controllers": "^4.0.1", "@metamask/base-controller": "^1.0.0", - "@metamask/contract-metadata": "^2.2.0", + "@metamask/contract-metadata": "^2.3.1", "@metamask/controller-utils": "^1.0.0", "@metamask/design-tokens": "^1.9.0", "@metamask/desktop": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index ddf886249df2..6e98cfa41564 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3704,10 +3704,10 @@ __metadata: languageName: node linkType: hard -"@metamask/contract-metadata@npm:^2.1.0, @metamask/contract-metadata@npm:^2.2.0": - version: 2.2.0 - resolution: "@metamask/contract-metadata@npm:2.2.0" - checksum: 31b94274c7c0a2d03377f3fa5e46574cde6607fd3a3b571cb7ffa4f7ce6dc90e2bfe2a4b2eb9eb4ebcd07cb7d6918da893356b699e91bc6e005b472ca7279370 +"@metamask/contract-metadata@npm:^2.1.0, @metamask/contract-metadata@npm:^2.3.1": + version: 2.3.1 + resolution: "@metamask/contract-metadata@npm:2.3.1" + checksum: 95dcc27f661a3e380c0cca8c6d90fb1777e31ab3fe16909fd5c67844125510e3f8e9eca05c9069fde34c77df3b66e56111c7a908a07623e88052ef147eff4315 languageName: node linkType: hard @@ -24259,7 +24259,7 @@ __metadata: "@metamask/assets-controllers": ^4.0.1 "@metamask/auto-changelog": ^2.1.0 "@metamask/base-controller": ^1.0.0 - "@metamask/contract-metadata": ^2.2.0 + "@metamask/contract-metadata": ^2.3.1 "@metamask/controller-utils": ^1.0.0 "@metamask/design-tokens": ^1.9.0 "@metamask/desktop": ^0.3.0 From 349c9d4a03c100dbc056d6ff757d449b6638c22f Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 22 Mar 2023 17:20:59 -0500 Subject: [PATCH 15/20] Refactor network client constructor (#17652) --- .../network/create-network-client.test.js | 7 + .../network/create-network-client.ts | 191 ++++++++++++++++++ .../controllers/network/createInfuraClient.js | 49 ----- .../network/createInfuraClient.test.js | 5 - .../network/createJsonRpcClient.js | 61 ------ .../network/createJsonRpcClient.test.js | 5 - .../controllers/network/network-controller.js | 32 ++- .../network/network-controller.test.js | 10 +- .../network/provider-api-tests/helpers.js | 22 +- .../provider-api-tests/shared-tests.js | 6 +- lavamoat/browserify/beta/policy.json | 54 ++--- lavamoat/browserify/desktop/policy.json | 54 ++--- lavamoat/browserify/flask/policy.json | 54 ++--- lavamoat/browserify/main/policy.json | 54 ++--- package.json | 7 +- shared/constants/network.ts | 9 +- types/eth-json-rpc-filters/index.d.ts | 1 + .../subscriptionManager.d.ts | 1 + yarn.lock | 71 ++++--- 19 files changed, 342 insertions(+), 351 deletions(-) create mode 100644 app/scripts/controllers/network/create-network-client.test.js create mode 100644 app/scripts/controllers/network/create-network-client.ts delete mode 100644 app/scripts/controllers/network/createInfuraClient.js delete mode 100644 app/scripts/controllers/network/createInfuraClient.test.js delete mode 100644 app/scripts/controllers/network/createJsonRpcClient.js delete mode 100644 app/scripts/controllers/network/createJsonRpcClient.test.js create mode 100644 types/eth-json-rpc-filters/index.d.ts create mode 100644 types/eth-json-rpc-filters/subscriptionManager.d.ts diff --git a/app/scripts/controllers/network/create-network-client.test.js b/app/scripts/controllers/network/create-network-client.test.js new file mode 100644 index 000000000000..7e674392b75c --- /dev/null +++ b/app/scripts/controllers/network/create-network-client.test.js @@ -0,0 +1,7 @@ +import { NetworkClientType } from './create-network-client'; +import { testsForProviderType } from './provider-api-tests/shared-tests'; + +describe('createNetworkClient', () => { + testsForProviderType(NetworkClientType.Infura); + testsForProviderType(NetworkClientType.Custom); +}); diff --git a/app/scripts/controllers/network/create-network-client.ts b/app/scripts/controllers/network/create-network-client.ts new file mode 100644 index 000000000000..6e96b71f5c9a --- /dev/null +++ b/app/scripts/controllers/network/create-network-client.ts @@ -0,0 +1,191 @@ +import { + createAsyncMiddleware, + createScaffoldMiddleware, + JsonRpcEngine, + mergeMiddleware, + JsonRpcMiddleware, +} from 'json-rpc-engine'; +import { + createBlockCacheMiddleware, + createBlockRefMiddleware, + createBlockRefRewriteMiddleware, + createBlockTrackerInspectorMiddleware, + createInflightCacheMiddleware, + createFetchMiddleware, + createRetryOnEmptyMiddleware, +} from '@metamask/eth-json-rpc-middleware'; +import { + providerFromEngine, + providerFromMiddleware, + SafeEventEmitterProvider, +} from '@metamask/eth-json-rpc-provider'; +import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; +import type { Hex } from '@metamask/utils/dist'; +import { PollingBlockTracker } from 'eth-block-tracker/dist'; +import { SECOND } from '../../../../shared/constants/time'; +import { + BUILT_IN_INFURA_NETWORKS, + BuiltInInfuraNetwork, +} from '../../../../shared/constants/network'; + +export enum NetworkClientType { + Custom = 'custom', + Infura = 'infura', +} + +type CustomNetworkConfiguration = { + chainId: Hex; + rpcUrl: string; + type: NetworkClientType.Custom; +}; + +type InfuraNetworkConfiguration = { + network: BuiltInInfuraNetwork; + infuraProjectId: string; + type: NetworkClientType.Infura; +}; + +/** + * Create a JSON RPC network client for a specific network. + * + * @param networkConfig - The network configuration. + * @returns + */ +export function createNetworkClient( + networkConfig: CustomNetworkConfiguration | InfuraNetworkConfiguration, +): { provider: SafeEventEmitterProvider; blockTracker: PollingBlockTracker } { + const rpcApiMiddleware = + networkConfig.type === NetworkClientType.Infura + ? createInfuraMiddleware({ + network: networkConfig.network, + projectId: networkConfig.infuraProjectId, + maxAttempts: 5, + source: 'metamask', + }) + : createFetchMiddleware({ + btoa: global.btoa, + fetch: global.fetch, + rpcUrl: networkConfig.rpcUrl, + }); + + const rpcProvider = providerFromMiddleware(rpcApiMiddleware); + + const blockTrackerOpts = + process.env.IN_TEST && networkConfig.type === 'custom' + ? { pollingInterval: SECOND } + : {}; + const blockTracker = new PollingBlockTracker({ + ...blockTrackerOpts, + provider: rpcProvider, + }); + + const networkMiddleware = + networkConfig.type === NetworkClientType.Infura + ? createInfuraNetworkMiddleware({ + blockTracker, + network: networkConfig.network, + rpcProvider, + rpcApiMiddleware, + }) + : createCustomNetworkMiddleware({ + blockTracker, + chainId: networkConfig.chainId, + rpcApiMiddleware, + }); + + const engine = new JsonRpcEngine(); + + engine.push(networkMiddleware); + + const provider = providerFromEngine(engine); + + return { provider, blockTracker }; +} + +function createInfuraNetworkMiddleware({ + blockTracker, + network, + rpcProvider, + rpcApiMiddleware, +}: { + blockTracker: PollingBlockTracker; + network: BuiltInInfuraNetwork; + rpcProvider: SafeEventEmitterProvider; + rpcApiMiddleware: JsonRpcMiddleware; +}) { + return mergeMiddleware([ + createNetworkAndChainIdMiddleware({ network }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), + createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ]); +} + +function createNetworkAndChainIdMiddleware({ + network, +}: { + network: BuiltInInfuraNetwork; +}) { + if (!BUILT_IN_INFURA_NETWORKS[network]) { + throw new Error(`createInfuraClient - unknown network "${network}"`); + } + + const { chainId, networkId } = BUILT_IN_INFURA_NETWORKS[network]; + + return createScaffoldMiddleware({ + eth_chainId: chainId, + net_version: networkId, + }); +} + +const createChainIdMiddleware = ( + chainId: string, +): JsonRpcMiddleware => { + return (req, res, next, end) => { + if (req.method === 'eth_chainId') { + res.result = chainId; + return end(); + } + return next(); + }; +}; + +function createCustomNetworkMiddleware({ + blockTracker, + chainId, + rpcApiMiddleware, +}: { + blockTracker: PollingBlockTracker; + chainId: string; + rpcApiMiddleware: any; +}) { + const testMiddlewares = process.env.IN_TEST + ? [createEstimateGasDelayTestMiddleware()] + : []; + + return mergeMiddleware([ + ...testMiddlewares, + createChainIdMiddleware(chainId), + createBlockRefRewriteMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightCacheMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + rpcApiMiddleware, + ]); +} + +/** + * For use in tests only. + * Adds a delay to `eth_estimateGas` calls. + */ +function createEstimateGasDelayTestMiddleware() { + return createAsyncMiddleware(async (req, _, next) => { + if (req.method === 'eth_estimateGas') { + await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); + } + return next(); + }); +} diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js deleted file mode 100644 index 76461eb6226e..000000000000 --- a/app/scripts/controllers/network/createInfuraClient.js +++ /dev/null @@ -1,49 +0,0 @@ -import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'; -import { - createBlockRefMiddleware, - createRetryOnEmptyMiddleware, - createBlockCacheMiddleware, - createInflightCacheMiddleware, - createBlockTrackerInspectorMiddleware, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-middleware'; - -import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; -import { PollingBlockTracker } from 'eth-block-tracker'; - -import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; - -export default function createInfuraClient({ network, projectId }) { - const infuraMiddleware = createInfuraMiddleware({ - network, - projectId, - maxAttempts: 5, - source: 'metamask', - }); - const infuraProvider = providerFromMiddleware(infuraMiddleware); - const blockTracker = new PollingBlockTracker({ provider: infuraProvider }); - - const networkMiddleware = mergeMiddleware([ - createNetworkAndChainIdMiddleware({ network }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockRefMiddleware({ blockTracker, provider: infuraProvider }), - createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }), - createBlockTrackerInspectorMiddleware({ blockTracker }), - infuraMiddleware, - ]); - return { networkMiddleware, blockTracker }; -} - -function createNetworkAndChainIdMiddleware({ network }) { - if (!BUILT_IN_NETWORKS[network]) { - throw new Error(`createInfuraClient - unknown network "${network}"`); - } - - const { chainId, networkId } = BUILT_IN_NETWORKS[network]; - - return createScaffoldMiddleware({ - eth_chainId: chainId, - net_version: networkId, - }); -} diff --git a/app/scripts/controllers/network/createInfuraClient.test.js b/app/scripts/controllers/network/createInfuraClient.test.js deleted file mode 100644 index d1b1a7ccfc62..000000000000 --- a/app/scripts/controllers/network/createInfuraClient.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import { testsForProviderType } from './provider-api-tests/shared-tests'; - -describe('createInfuraClient', () => { - testsForProviderType('infura'); -}); diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js deleted file mode 100644 index b8cf0e7aaeb0..000000000000 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ /dev/null @@ -1,61 +0,0 @@ -import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine'; -import { - createFetchMiddleware, - createBlockRefRewriteMiddleware, - createBlockCacheMiddleware, - createInflightCacheMiddleware, - createBlockTrackerInspectorMiddleware, - providerFromMiddleware, -} from '@metamask/eth-json-rpc-middleware'; -import { PollingBlockTracker } from 'eth-block-tracker'; -import { SECOND } from '../../../../shared/constants/time'; - -export default function createJsonRpcClient({ rpcUrl, chainId }) { - const blockTrackerOpts = process.env.IN_TEST - ? { pollingInterval: SECOND } - : {}; - const fetchMiddleware = createFetchMiddleware({ rpcUrl }); - const blockProvider = providerFromMiddleware(fetchMiddleware); - const blockTracker = new PollingBlockTracker({ - ...blockTrackerOpts, - provider: blockProvider, - }); - const testMiddlewares = process.env.IN_TEST - ? [createEstimateGasDelayTestMiddleware()] - : []; - - const networkMiddleware = mergeMiddleware([ - ...testMiddlewares, - createChainIdMiddleware(chainId), - createBlockRefRewriteMiddleware({ blockTracker }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockTrackerInspectorMiddleware({ blockTracker }), - fetchMiddleware, - ]); - - return { networkMiddleware, blockTracker }; -} - -function createChainIdMiddleware(chainId) { - return (req, res, next, end) => { - if (req.method === 'eth_chainId') { - res.result = chainId; - return end(); - } - return next(); - }; -} - -/** - * For use in tests only. - * Adds a delay to `eth_estimateGas` calls. - */ -function createEstimateGasDelayTestMiddleware() { - return createAsyncMiddleware(async (req, _, next) => { - if (req.method === 'eth_estimateGas') { - await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); - } - return next(); - }); -} diff --git a/app/scripts/controllers/network/createJsonRpcClient.test.js b/app/scripts/controllers/network/createJsonRpcClient.test.js deleted file mode 100644 index 1c3443d25814..000000000000 --- a/app/scripts/controllers/network/createJsonRpcClient.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import { testsForProviderType } from './provider-api-tests/shared-tests'; - -describe('createJsonRpcClient', () => { - testsForProviderType('custom'); -}); diff --git a/app/scripts/controllers/network/network-controller.js b/app/scripts/controllers/network/network-controller.js index 20869f227e55..06d236e26c83 100644 --- a/app/scripts/controllers/network/network-controller.js +++ b/app/scripts/controllers/network/network-controller.js @@ -1,8 +1,6 @@ import { strict as assert } from 'assert'; import EventEmitter from 'events'; import { ComposedStore, ObservableStore } from '@metamask/obs-store'; -import { JsonRpcEngine } from 'json-rpc-engine'; -import { providerFromEngine } from '@metamask/eth-json-rpc-middleware'; import log from 'loglevel'; import { createSwappableProxy, @@ -24,8 +22,7 @@ import { isSafeChainId, } from '../../../../shared/modules/network.utils'; import { EVENT } from '../../../../shared/constants/metametrics'; -import createInfuraClient from './createInfuraClient'; -import createJsonRpcClient from './createJsonRpcClient'; +import { createNetworkClient } from './create-network-client'; /** * @typedef {object} NetworkConfiguration @@ -428,7 +425,10 @@ export default class NetworkController extends EventEmitter { // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type); if (isInfura) { - this._configureInfuraProvider(type, this._infuraProjectId); + this._configureInfuraProvider({ + type, + infuraProjectId: this._infuraProjectId, + }); // url-based rpc endpoints } else if (type === NETWORK_TYPES.RPC) { this._configureStandardProvider(rpcUrl, chainId); @@ -439,25 +439,23 @@ export default class NetworkController extends EventEmitter { } } - _configureInfuraProvider(type, projectId) { + _configureInfuraProvider({ type, infuraProjectId }) { log.info('NetworkController - configureInfuraProvider', type); - const networkClient = createInfuraClient({ + const { provider, blockTracker } = createNetworkClient({ network: type, - projectId, + infuraProjectId, + type: 'infura', }); - this._setNetworkClient(networkClient); + this._setProviderAndBlockTracker({ provider, blockTracker }); } _configureStandardProvider(rpcUrl, chainId) { log.info('NetworkController - configureStandardProvider', rpcUrl); - const networkClient = createJsonRpcClient({ rpcUrl, chainId }); - this._setNetworkClient(networkClient); - } - - _setNetworkClient({ networkMiddleware, blockTracker }) { - const engine = new JsonRpcEngine(); - engine.push(networkMiddleware); - const provider = providerFromEngine(engine); + const { provider, blockTracker } = createNetworkClient({ + chainId, + rpcUrl, + type: 'custom', + }); this._setProviderAndBlockTracker({ provider, blockTracker }); } diff --git a/app/scripts/controllers/network/network-controller.test.js b/app/scripts/controllers/network/network-controller.test.js index a3743ae56819..cd4e9a78254f 100644 --- a/app/scripts/controllers/network/network-controller.test.js +++ b/app/scripts/controllers/network/network-controller.test.js @@ -3,15 +3,15 @@ import { isMatch } from 'lodash'; import { v4 } from 'uuid'; import nock from 'nock'; import sinon from 'sinon'; -import * as ethJsonRpcMiddlewareModule from '@metamask/eth-json-rpc-middleware'; +import * as ethJsonRpcProvider from '@metamask/eth-json-rpc-provider'; import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network'; import { EVENT } from '../../../../shared/constants/metametrics'; import NetworkController from './network-controller'; -jest.mock('@metamask/eth-json-rpc-middleware', () => { +jest.mock('@metamask/eth-json-rpc-provider', () => { return { __esModule: true, - ...jest.requireActual('@metamask/eth-json-rpc-middleware'), + ...jest.requireActual('@metamask/eth-json-rpc-provider'), }; }); @@ -1760,7 +1760,7 @@ describe('NetworkController', () => { }, ]; jest - .spyOn(ethJsonRpcMiddlewareModule, 'providerFromEngine') + .spyOn(ethJsonRpcProvider, 'providerFromEngine') .mockImplementationOnce(() => fakeProviders[0]) .mockImplementationOnce(() => fakeProviders[1]); await withoutCallingLookupNetwork({ @@ -1876,7 +1876,7 @@ describe('NetworkController', () => { }, ]; jest - .spyOn(ethJsonRpcMiddlewareModule, 'providerFromEngine') + .spyOn(ethJsonRpcProvider, 'providerFromEngine') .mockImplementationOnce(() => fakeProviders[0]) .mockImplementationOnce(() => fakeProviders[1]); await withoutCallingLookupNetwork({ diff --git a/app/scripts/controllers/network/provider-api-tests/helpers.js b/app/scripts/controllers/network/provider-api-tests/helpers.js index a5d92b91e5d2..afa91a7bba6c 100644 --- a/app/scripts/controllers/network/provider-api-tests/helpers.js +++ b/app/scripts/controllers/network/provider-api-tests/helpers.js @@ -1,10 +1,7 @@ import nock from 'nock'; import sinon from 'sinon'; -import { JsonRpcEngine } from 'json-rpc-engine'; -import { providerFromEngine } from '@metamask/eth-json-rpc-middleware'; import EthQuery from 'eth-query'; -import createInfuraClient from '../createInfuraClient'; -import createJsonRpcClient from '../createJsonRpcClient'; +import { createNetworkClient } from '../create-network-client'; /** * @typedef {import('nock').Scope} NockScope @@ -414,20 +411,21 @@ export async function withNetworkClient( delete process.env.IN_TEST; const clientUnderTest = providerType === 'infura' - ? createInfuraClient({ + ? createNetworkClient({ network: infuraNetwork, - projectId: MOCK_INFURA_PROJECT_ID, + infuraProjectId: MOCK_INFURA_PROJECT_ID, + type: 'infura', }) - : createJsonRpcClient({ rpcUrl: customRpcUrl, chainId: customChainId }); + : createNetworkClient({ + chainId: customChainId, + rpcUrl: customRpcUrl, + type: 'custom', + }); process.env.IN_TEST = inTest; - const { networkMiddleware, blockTracker } = clientUnderTest; + const { provider, blockTracker } = clientUnderTest; - const engine = new JsonRpcEngine(); - engine.push(networkMiddleware); - const provider = providerFromEngine(engine); const ethQuery = new EthQuery(provider); - const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request); const makeRpcCallsInSeries = async (requests) => { const responses = []; diff --git a/app/scripts/controllers/network/provider-api-tests/shared-tests.js b/app/scripts/controllers/network/provider-api-tests/shared-tests.js index d615956707b9..04412d3f0841 100644 --- a/app/scripts/controllers/network/provider-api-tests/shared-tests.js +++ b/app/scripts/controllers/network/provider-api-tests/shared-tests.js @@ -291,9 +291,9 @@ export function testsForProviderType(providerType) { // tests on the core side. { name: 'net_listening', numberOfParameters: 0 }, - - { name: 'eth_subscribe', numberOfParameters: 1 }, - { name: 'eth_unsubscribe', numberOfParameters: 1 }, + // TODO: Methods to add back when we add testing for subscribe middleware + // { name: 'eth_subscribe', numberOfParameters: 1 }, + // { name: 'eth_unsubscribe', numberOfParameters: 1 }, { name: 'custom_rpc_method', numberOfParameters: 1 }, { name: 'net_peerCount', numberOfParameters: 0 }, { name: 'parity_nextNonce', numberOfParameters: 1 }, diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 17687c27bbdf..7d933f82d451 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -871,8 +871,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -881,57 +882,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -947,6 +910,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -2610,6 +2579,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index ceadebd907b7..46ce15ef8d00 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -925,8 +925,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -935,57 +936,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -1001,6 +964,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -3051,6 +3020,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ceadebd907b7..46ce15ef8d00 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -925,8 +925,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -935,57 +936,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -1001,6 +964,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -3051,6 +3020,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 17687c27bbdf..7d933f82d451 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -871,8 +871,9 @@ "setTimeout": true }, "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": true, + "@metamask/eth-json-rpc-infura>@metamask/utils": true, "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, @@ -881,57 +882,19 @@ "vinyl>clone": true } }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": true, - "ethereumjs-abi": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util": { - "packages": { - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": true, - "bn.js": true, - "browserify>assert": true, - "browserify>buffer": true, - "ethereumjs-util>create-hash": true, - "ethereumjs-util>rlp": true, - "ethereumjs-wallet>safe-buffer": true, - "ganache>secp256k1>elliptic": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethereum-cryptography": { - "packages": { - "browserify>buffer": true, - "ethereumjs-util>ethereum-cryptography>keccak": true, - "ethereumjs-util>ethereum-cryptography>secp256k1": true, - "ethereumjs-wallet>randombytes": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>eth-sig-util>ethereumjs-util>ethjs-util": { - "packages": { - "browserify>buffer": true, - "ethjs>ethjs-util>is-hex-prefixed": true, - "ethjs>ethjs-util>strip-hex-prefix": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, - "btoa": true, "console.error": true, - "fetch": true, "setTimeout": true }, "packages": { "@metamask/eth-json-rpc-middleware>@metamask/utils": true, "@metamask/eth-json-rpc-middleware>pify": true, + "@metamask/eth-json-rpc-middleware>safe-stable-stringify": true, "@metamask/eth-keyring-controller>@metamask/eth-sig-util": true, - "browserify>browser-resolve": true, "eth-rpc-errors": true, "json-rpc-engine": true, - "json-rpc-engine>@metamask/safe-event-emitter": true, - "lavamoat>json-stable-stringify": true, "vinyl>clone": true } }, @@ -947,6 +910,12 @@ "semver": true } }, + "@metamask/eth-json-rpc-provider": { + "packages": { + "json-rpc-engine": true, + "json-rpc-engine>@metamask/safe-event-emitter": true + } + }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/eth-keyring-controller>@metamask/eth-hd-keyring": true, @@ -2610,6 +2579,11 @@ "document.createElement": true } }, + "btoa": { + "packages": { + "browserify>buffer": true + } + }, "classnames": { "globals": { "classNames": "write", diff --git a/package.json b/package.json index 6441e7a86a24..e76f1c517b2b 100644 --- a/package.json +++ b/package.json @@ -232,8 +232,9 @@ "@metamask/controller-utils": "^1.0.0", "@metamask/design-tokens": "^1.9.0", "@metamask/desktop": "^0.3.0", - "@metamask/eth-json-rpc-infura": "^7.0.0", - "@metamask/eth-json-rpc-middleware": "^10.0.0", + "@metamask/eth-json-rpc-infura": "^8.0.0", + "@metamask/eth-json-rpc-middleware": "^11.0.0", + "@metamask/eth-json-rpc-provider": "^1.0.0", "@metamask/eth-keyring-controller": "^10.0.1", "@metamask/eth-ledger-bridge-keyring": "^0.13.0", "@metamask/eth-token-tracker": "^4.0.0", @@ -285,7 +286,7 @@ "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", "end-of-stream": "^1.4.4", - "eth-block-tracker": "^6.0.0", + "eth-block-tracker": "^7.0.0", "eth-ens-namehash": "^2.0.8", "eth-json-rpc-filters": "^6.0.0", "eth-lattice-keyring": "^0.12.3", diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 16bafb208dda..0774126239b1 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -1,4 +1,4 @@ -import { capitalize } from 'lodash'; +import { capitalize, pick } from 'lodash'; /** * A type representing any valid value for 'type' for setProviderType and other * methods that add or manipulate networks in MetaMask state. @@ -301,6 +301,13 @@ export const BUILT_IN_NETWORKS = { }, } as const; +export const BUILT_IN_INFURA_NETWORKS = pick( + BUILT_IN_NETWORKS, + INFURA_PROVIDER_TYPES, +); + +export type BuiltInInfuraNetwork = keyof typeof BUILT_IN_INFURA_NETWORKS; + export const NETWORK_TO_NAME_MAP = { [NETWORK_TYPES.MAINNET]: MAINNET_DISPLAY_NAME, [NETWORK_TYPES.GOERLI]: GOERLI_DISPLAY_NAME, diff --git a/types/eth-json-rpc-filters/index.d.ts b/types/eth-json-rpc-filters/index.d.ts new file mode 100644 index 000000000000..f7515bccd652 --- /dev/null +++ b/types/eth-json-rpc-filters/index.d.ts @@ -0,0 +1 @@ +declare module 'eth-json-rpc-filters'; diff --git a/types/eth-json-rpc-filters/subscriptionManager.d.ts b/types/eth-json-rpc-filters/subscriptionManager.d.ts new file mode 100644 index 000000000000..a79ff8ee4483 --- /dev/null +++ b/types/eth-json-rpc-filters/subscriptionManager.d.ts @@ -0,0 +1 @@ +declare module 'eth-json-rpc-filters/subscriptionManager'; diff --git a/yarn.lock b/yarn.lock index 6e98cfa41564..6ea77ae8ff53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3849,35 +3849,43 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-infura@npm:^7.0.0": - version: 7.0.0 - resolution: "@metamask/eth-json-rpc-infura@npm:7.0.0" +"@metamask/eth-json-rpc-infura@npm:^8.0.0": + version: 8.0.0 + resolution: "@metamask/eth-json-rpc-infura@npm:8.0.0" dependencies: "@metamask/utils": ^3.0.1 - eth-json-rpc-middleware: ^8.1.0 + eth-json-rpc-middleware: ^9.0.0 eth-rpc-errors: ^4.0.3 json-rpc-engine: ^6.1.0 node-fetch: ^2.6.7 - checksum: 6230cb289b66db39d27f08ffc72cfb79274e632e4e14eb52ca72d19167d17bbf22c58718d80f801818058631dec0638bcc21ef4b229fa8d53e7f9328be870fc6 + checksum: e8c3a4b75d4f2bb09f68d7d2ac6b992f264df893921b50a05c35968ee684b7bba90180870eebecfc89b7fbf40d11de2a545ab68f1d511f569ce0a6519c64b0aa languageName: node linkType: hard -"@metamask/eth-json-rpc-middleware@npm:^10.0.0": - version: 10.0.0 - resolution: "@metamask/eth-json-rpc-middleware@npm:10.0.0" +"@metamask/eth-json-rpc-middleware@npm:^11.0.0": + version: 11.0.0 + resolution: "@metamask/eth-json-rpc-middleware@npm:11.0.0" dependencies: + "@metamask/eth-json-rpc-provider": ^1.0.0 "@metamask/eth-sig-util": ^5.0.0 - "@metamask/safe-event-emitter": ^2.0.0 "@metamask/utils": ^3.0.3 - btoa: ^1.2.1 clone: ^2.1.1 - eth-block-tracker: ^5.0.1 + eth-block-tracker: ^7.0.0 eth-rpc-errors: ^4.0.3 json-rpc-engine: ^6.1.0 - json-stable-stringify: ^1.0.1 - node-fetch: ^2.6.7 pify: ^3.0.0 - checksum: c754b3a39f175698070b4d07076e692d3080738bd25157c3b93114d286c975ee6895d5793b4188ca3d0abbcdef04bfde9e2d4835251a6b725b002d3750bf98de + safe-stable-stringify: ^2.3.2 + checksum: c866d07a199ab480ceeb7ab8df61c08284640b5ac13aee3dd81dae9e0e5575f4a425d95728070ab5402c0c6cd5f9237fb5f4f22dbcdc99fe0b50bb47df561830 + languageName: node + linkType: hard + +"@metamask/eth-json-rpc-provider@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/eth-json-rpc-provider@npm:1.0.0" + dependencies: + "@metamask/safe-event-emitter": ^2.0.0 + json-rpc-engine: ^6.1.0 + checksum: 27865d84d90030db1a9e5a66bc0b0ae079706fb7be635ec1e9bd4f64771e819aae78f0a026c6629d3a1a2eb277fcd51977315c049c47a70df1dd95d1d4106982 languageName: node linkType: hard @@ -15563,15 +15571,16 @@ __metadata: languageName: node linkType: hard -"eth-block-tracker@npm:^6.0.0": - version: 6.0.0 - resolution: "eth-block-tracker@npm:6.0.0" +"eth-block-tracker@npm:^7.0.0": + version: 7.0.0 + resolution: "eth-block-tracker@npm:7.0.0" dependencies: + "@metamask/eth-json-rpc-provider": ^1.0.0 "@metamask/safe-event-emitter": ^2.0.0 "@metamask/utils": ^3.0.1 json-rpc-random-id: ^1.0.1 pify: ^3.0.0 - checksum: ad1199b822a9a3ff2673ecc92ca7cda0a37828e5bfd1927fd917a8085a99904fc29d3ef2392068bcfb14e47589df097940ef28f3e9025d1681e56a89b07e284e + checksum: b76f6ba022947eec0161e5592bc5386e8f05bff8a2c3e0e10c76bce21bc51900ef1cb153eb8bf31858fb0027e929c6a85a159bdf84aa1e3ef77b24e53e82ba84 languageName: node linkType: hard @@ -15640,21 +15649,22 @@ __metadata: languageName: node linkType: hard -"eth-json-rpc-middleware@npm:^8.1.0": - version: 8.1.0 - resolution: "eth-json-rpc-middleware@npm:8.1.0" +"eth-json-rpc-middleware@npm:^9.0.0": + version: 9.0.1 + resolution: "eth-json-rpc-middleware@npm:9.0.1" dependencies: + "@metamask/eth-sig-util": ^5.0.0 "@metamask/safe-event-emitter": ^2.0.0 + "@metamask/utils": ^3.0.3 btoa: ^1.2.1 clone: ^2.1.1 eth-block-tracker: ^5.0.1 eth-rpc-errors: ^4.0.3 - eth-sig-util: ^1.4.2 json-rpc-engine: ^6.1.0 json-stable-stringify: ^1.0.1 node-fetch: ^2.6.7 pify: ^3.0.0 - checksum: ec10bbc04e3b7696f82db2db528b052c8f6de811c90a12d4eb32f23cbe6ea198d86656afa5b53c52de06b631fef633cf29409bb56c04a16f173da94ee1d89ab6 + checksum: 9512829a6958df6ef739b891a0c0804b51a140407fd2e3ddaaa6b18d975796646cfcf7f7305a18beb7903db09e0c7a91b06dc5434b6bd2d6cdb85d992d9fd3ab languageName: node linkType: hard @@ -24268,8 +24278,9 @@ __metadata: "@metamask/eslint-config-mocha": ^9.0.0 "@metamask/eslint-config-nodejs": ^9.0.0 "@metamask/eslint-config-typescript": ^9.0.1 - "@metamask/eth-json-rpc-infura": ^7.0.0 - "@metamask/eth-json-rpc-middleware": ^10.0.0 + "@metamask/eth-json-rpc-infura": ^8.0.0 + "@metamask/eth-json-rpc-middleware": ^11.0.0 + "@metamask/eth-json-rpc-provider": ^1.0.0 "@metamask/eth-keyring-controller": ^10.0.1 "@metamask/eth-ledger-bridge-keyring": ^0.13.0 "@metamask/eth-token-tracker": ^4.0.0 @@ -24402,7 +24413,7 @@ __metadata: eslint-plugin-react: ^7.23.1 eslint-plugin-react-hooks: ^4.2.0 eslint-plugin-storybook: ^0.6.4 - eth-block-tracker: ^6.0.0 + eth-block-tracker: ^7.0.0 eth-ens-namehash: ^2.0.8 eth-json-rpc-filters: ^6.0.0 eth-lattice-keyring: ^0.12.3 @@ -30453,10 +30464,10 @@ __metadata: languageName: node linkType: hard -"safe-stable-stringify@npm:^2.1.0": - version: 2.3.1 - resolution: "safe-stable-stringify@npm:2.3.1" - checksum: a0a0bad0294c3e2a9d1bf3cf2b1096dfb83c162d09a5e4891e488cce082120bd69161d2a92aae7fc48255290f17700decae9c89a07fe139794e61b5c8b411377 +"safe-stable-stringify@npm:^2.1.0, safe-stable-stringify@npm:^2.3.2": + version: 2.4.2 + resolution: "safe-stable-stringify@npm:2.4.2" + checksum: 0324ba2e40f78cae63e31a02b1c9bdf1b786621f9e8760845608eb9e81aef401944ac2078e5c9c1533cf516aea34d08fa8052ca853637ced84b791caaf1e394e languageName: node linkType: hard From 68f928c8a2de6ef6a5503381079e48f007600b0b Mon Sep 17 00:00:00 2001 From: George Marshall Date: Wed, 22 Mar 2023 17:17:19 -0700 Subject: [PATCH 16/20] Adding ModalContent component (#18175) * Adding ModalContent component * Using different component api for ref * use imperative handle * Updating size * Updating stories and docs as well as component api * Fixing import --- .../component-library-components.scss | 1 + ui/components/component-library/index.js | 1 + .../modal-content/README.mdx | 115 ++++++++++++++ .../__snapshots__/modal-content.test.tsx.snap | 11 ++ .../component-library/modal-content/index.ts | 3 + .../modal-content/modal-content.scss | 17 +++ .../modal-content/modal-content.stories.tsx | 144 ++++++++++++++++++ .../modal-content/modal-content.test.tsx | 39 +++++ .../modal-content/modal-content.tsx | 37 +++++ .../modal-content/modal-content.types.ts | 40 +++++ 10 files changed, 408 insertions(+) create mode 100644 ui/components/component-library/modal-content/README.mdx create mode 100644 ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap create mode 100644 ui/components/component-library/modal-content/index.ts create mode 100644 ui/components/component-library/modal-content/modal-content.scss create mode 100644 ui/components/component-library/modal-content/modal-content.stories.tsx create mode 100644 ui/components/component-library/modal-content/modal-content.test.tsx create mode 100644 ui/components/component-library/modal-content/modal-content.tsx create mode 100644 ui/components/component-library/modal-content/modal-content.types.ts diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss index 6a10d8f99656..618b089ad81f 100644 --- a/ui/components/component-library/component-library-components.scss +++ b/ui/components/component-library/component-library-components.scss @@ -29,3 +29,4 @@ @import 'form-text-field/form-text-field'; @import 'banner-alert/banner-alert'; @import 'banner-tip/banner-tip'; +@import 'modal-content/modal-content'; diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js index fbf0282f809b..e1b2f1408fdc 100644 --- a/ui/components/component-library/index.js +++ b/ui/components/component-library/index.js @@ -31,6 +31,7 @@ export { Text, TEXT_DIRECTIONS, INVISIBLE_CHARACTER } from './text'; export { Input, INPUT_TYPES } from './input'; export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field'; export { TextFieldSearch } from './text-field-search'; +export { ModalContent, ModalContentSize } from './modal-content'; // Molecules export { BannerBase } from './banner-base'; diff --git a/ui/components/component-library/modal-content/README.mdx b/ui/components/component-library/modal-content/README.mdx new file mode 100644 index 000000000000..3d755d954388 --- /dev/null +++ b/ui/components/component-library/modal-content/README.mdx @@ -0,0 +1,115 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; + +import { ModalContent } from './modal-content'; + +# ModalContent + +`ModalContent` is the container for the modal dialog's content + + + + + +## Props + +The `ModalContent` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props + + + +### Children + +Use the `children` prop to render the content of `ModalContent` + + + + + +```jsx +import { ModalContent, Text } from '../../component-library'; + + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio, + reiciendis assumenda dolorum mollitia saepe, optio at aliquam molestias + omnis quae corporis nesciunt natus, quas tempore ut ullam eaque fuga. Velit. + +; +``` + +### Size + +Currently the `ModalContent` supports a single size, this decision was made after we ran an audit on all modal sizes in the extension codebase and found that all modals could be made to fit the `ModalContentSize.Sm`(360px) size. + +If you do require a larger modal size you can use the Box props or add a className to override the default size. + + + + + +```jsx +import { BLOCK_SIZES } from '../../../helpers/constants/design-system'; +import { ModalContent,s Text } from '../../component-library'; + + + ModalContentSize.Sm default and only size 360px max-width + + + + Using width Box props and responsive array props
[ + BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF, + BLOCK_SIZES.ONE_THIRD, ] +
+
+ + Adding a className and setting a max width (max-width: 800px) + +``` + +### Modal Content Ref + +Use the `modalContentRef` prop to pass a ref to the `ModalContent` component. This is primarily used with the `closeOnOutsideClick` prop on the `Modal` component. It allows the `Modal` to close when the user clicks outside of the `ModalContent` component. + + + + + +```jsx +import React, { useEffect, useRef, useState } from 'react'; +import { ModalContent, Text } from '../../component-library'; + +const [show, setShow] = useState(false); +const modalContentRef = useRef(null); + +const handleClickOutside = (event: MouseEvent) => { + if ( + modalContentRef?.current && + !modalContentRef.current.contains(event.target as Node) + ) { + setShow(false); + } +}; + +useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; +}, []); + + +{show && ( + + Click outside of this ModalContent to close + +)} +``` diff --git a/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap b/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap new file mode 100644 index 000000000000..c8fa28c96d0a --- /dev/null +++ b/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalContent should match snapshot 1`] = ` +
+
+ test +
+
+`; diff --git a/ui/components/component-library/modal-content/index.ts b/ui/components/component-library/modal-content/index.ts new file mode 100644 index 000000000000..ef6fb6595300 --- /dev/null +++ b/ui/components/component-library/modal-content/index.ts @@ -0,0 +1,3 @@ +export { ModalContent } from './modal-content'; +export { ModalContentSize } from './modal-content.types'; +export type { ModalContentProps } from './modal-content.types'; diff --git a/ui/components/component-library/modal-content/modal-content.scss b/ui/components/component-library/modal-content/modal-content.scss new file mode 100644 index 000000000000..3c3bc00fb64f --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.scss @@ -0,0 +1,17 @@ +.mm-modal-content { + --modal-content-size: var(--size, 360px); + + // Currently there is only use case for one size of ModalContent in the extension + // See audit https://www.figma.com/file/hxYqloYgmVcgsoiVqmGZ8K/Modal-Audit?node-id=481%3A244&t=XITeuRB1pRc09hiG-1 + // Not to say there won't be more in the future, but to prevent redundant code there is only one for now + &--size-sm { + --size: 360px; + + max-width: var(--modal-content-size); + } + + position: relative; + box-shadow: var(--shadow-size-lg) var(--color-shadow-default); + max-height: calc(100% - 32px); // allow for 16px padding on top and bottom + overflow: auto; +} diff --git a/ui/components/component-library/modal-content/modal-content.stories.tsx b/ui/components/component-library/modal-content/modal-content.stories.tsx new file mode 100644 index 000000000000..020e170262d1 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.stories.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { + DISPLAY, + JustifyContent, + AlignItems, + BLOCK_SIZES, + TextVariant, + TEXT_ALIGN, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box'; + +import { Button, Text } from '..'; + +import { ModalContent } from './modal-content'; +import { ModalContentSize } from './modal-content.types'; + +import README from './README.mdx'; + +export default { + title: 'Components/ComponentLibrary/ModalContent', + component: ModalContent, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + className: { + control: 'text', + }, + children: { + control: 'text', + }, + size: { + control: 'select', + options: Object.values(ModalContentSize).map((value) => + value.toLowerCase(), + ), + }, + }, + args: { + children: 'Modal Content', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const DefaultStory = Template.bind({}); +DefaultStory.storyName = 'Default'; + +/* + * !!TODO: Replace with ModalHeader component + */ +const ModalHeader = () => ( + <> + + + + Modal Header + + + + +); + +export const Children: ComponentStory = (args) => ( + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio, + reiciendis assumenda dolorum mollitia saepe, optio at aliquam molestias + omnis quae corporis nesciunt natus, quas tempore ut ullam eaque fuga. + Velit. + + +); + +export const Size: ComponentStory = (args) => ( + <> + + ModalContentSize.Sm default and only size 360px max-width + + + + Using width Box props and responsive array props
[ + BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF, + BLOCK_SIZES.ONE_THIRD, ] +
+
+ + Adding a className and setting a max width (max-width: 800px) + + +); + +export const ModalContentRef: ComponentStory = (args) => { + const [show, setShow] = useState(false); + const modalContentRef = useRef(null); + const handleClickOutside = (event: MouseEvent) => { + if ( + modalContentRef?.current && + !modalContentRef.current.contains(event.target as Node) + ) { + setShow(false); + } + }; + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + return ( + <> + + {show && ( + + Click outside of this ModalContent to close + + )} + + ); +}; diff --git a/ui/components/component-library/modal-content/modal-content.test.tsx b/ui/components/component-library/modal-content/modal-content.test.tsx new file mode 100644 index 000000000000..64b6a96dfe49 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.test.tsx @@ -0,0 +1,39 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; + +import { ModalContent } from './modal-content'; +import { ModalContentSize } from './modal-content.types'; + +describe('ModalContent', () => { + it('should render with text inside the ModalContent', () => { + const { getByText } = render(test); + expect(getByText('test')).toBeDefined(); + expect(getByText('test')).toHaveClass('mm-modal-content'); + }); + it('should match snapshot', () => { + const { container } = render(test); + expect(container).toMatchSnapshot(); + }); + it('should render with and additional className', () => { + const { getByText } = render( + test, + ); + expect(getByText('test')).toHaveClass('test-class'); + }); + it('should render with size sm', () => { + const { getByText } = render( + <> + default + sm + , + ); + expect(getByText('sm')).toHaveClass('mm-modal-content--size-sm'); + expect(getByText('default')).toHaveClass('mm-modal-content--size-sm'); + }); + it('should render with a ref', () => { + const ref = React.createRef(); + render(test); + expect(ref.current).toBeDefined(); + }); +}); diff --git a/ui/components/component-library/modal-content/modal-content.tsx b/ui/components/component-library/modal-content/modal-content.tsx new file mode 100644 index 000000000000..064a4b4e71cc --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import classnames from 'classnames'; + +import { + BackgroundColor, + BorderRadius, + BLOCK_SIZES, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box/box'; + +import { ModalContentProps, ModalContentSize } from './modal-content.types'; + +export const ModalContent = ({ + className = '', + children, + size = ModalContentSize.Sm, + width, + modalContentRef, // Would have preferred to forwardRef but it's not trivial in TypeScript. Will update once we have an established pattern + ...props +}: ModalContentProps) => ( + + {children} + +); diff --git a/ui/components/component-library/modal-content/modal-content.types.ts b/ui/components/component-library/modal-content/modal-content.types.ts new file mode 100644 index 000000000000..801a04b36323 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.types.ts @@ -0,0 +1,40 @@ +import React from 'react'; +import type { BoxProps, BoxWidth, BoxWidthArray } from '../../ui/box/box.d'; +import { Size } from '../../../helpers/constants/design-system'; + +/* + * ModalContent sizes + * Currently there is only use case for one size of ModalContent in the extension + * See audit https://www.figma.com/file/hxYqloYgmVcgsoiVqmGZ8K/Modal-Audit?node-id=481%3A244&t=XITeuRB1pRc09hiG-1 + * Not to say there won't be more in the future, but to prevent redundant code there is only one for now + */ +export enum ModalContentSize { + Sm = Size.SM, +} + +export interface ModalContentProps extends BoxProps { + /** + * The additional className of the ModalContent component + */ + className?: string; + /** + * The content of the ModalContent component + */ + children?: React.ReactNode; + /** + * The size of ModalContent + * Currently only one size is supported ModalContentSize.Sm 360px + * See docs for more info + */ + size?: ModalContentSize; + /** + * To override the default width of the ModalContent component + * Accepts all BLOCK_SIZES from design-system + */ + width?: BoxWidth | BoxWidthArray; + /** + * The ref of the ModalContent component + * Used with Modal and closeOnOutsideClick prop + */ + modalContentRef?: React.RefObject; +} From fcfb8a8938a310a4f68b0e5a3175535e8dc9ed91 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Thu, 23 Mar 2023 15:38:33 +0530 Subject: [PATCH 17/20] UX: Multichain: Added TokenList Component (#17859) * added redesign storybook * updated token list * updated css * fixed lint error * updated the new token list component * fixed redesign folder error * reverted changes in settings.json * updated redesign to multichain * added feature flag * reverted settings.json * added detect token banner * added button componeny * fixed lint errors * removed settings * fixed lint errors * added stories for multichain * updated no token found string * updated lint error * updated padding values * updated padding values * updated tabs with role button * updated hover state * updated components with multichain * fixed lint errors * updated multichain import token link * updated a tag * updated fixes * updated onClick to handleClick * updated setShowDetectedTokens proptype * updated multichain tokenlist with item suffix * updated tests * updated tests * updated token list css * updated snapshot * updated text * reverted story * added story for multichain token list * updated story * updated tooltip * updated the new token list component * fixed redesign folder error * added feature flag * reverted unused setting change * removed token list * fixed lint error * updated status * updated tooltip * updated token-list-item changes * updated actionbutton click for detect token banner * updated snapshot * updated symbol * updated styles * updated eth decimal and token url * updated snapshot * updated scripts * updated snapshots --- ui/components/app/asset-list/asset-list.js | 94 +++++++--- ui/components/app/token-cell/token-cell.js | 70 +++++--- .../detected-token-banner.test.js.snap | 27 +++ .../detected-token-banner.js | 60 +++++++ .../detected-token-banner.stories.js | 14 ++ .../detected-token-banner.test.js | 39 +++++ .../multichain/detected-token-banner/index.js | 1 + ui/components/multichain/index.js | 3 + ui/components/multichain/index.scss | 2 - .../multichain/multichain-components.scss | 9 + .../multichain-import-token-link.test.js.snap | 75 ++++++++ .../multichain-import-token-link/index.js | 1 + .../multichain-import-token-link.js | 87 ++++++++++ .../multichain-import-token-link.stories.js | 11 ++ .../multichain-import-token-link.test.js | 101 +++++++++++ .../multichain-token-list-item.test.js.snap | 67 ++++++++ .../multichain-token-list-item/index.js | 1 + .../multichain-token-list-item.js | 162 ++++++++++++++++++ .../multichain-token-list-item.scss | 8 + .../multichain-token-list-item.stories.js | 78 +++++++++ .../multichain-token-list-item.test.js | 57 ++++++ ui/components/ui/tooltip/index.scss | 8 +- ui/css/index.scss | 2 +- 23 files changed, 919 insertions(+), 58 deletions(-) create mode 100644 ui/components/multichain/detected-token-banner/__snapshots__/detected-token-banner.test.js.snap create mode 100644 ui/components/multichain/detected-token-banner/detected-token-banner.js create mode 100644 ui/components/multichain/detected-token-banner/detected-token-banner.stories.js create mode 100644 ui/components/multichain/detected-token-banner/detected-token-banner.test.js create mode 100644 ui/components/multichain/detected-token-banner/index.js delete mode 100644 ui/components/multichain/index.scss create mode 100644 ui/components/multichain/multichain-components.scss create mode 100644 ui/components/multichain/multichain-import-token-link/__snapshots__/multichain-import-token-link.test.js.snap create mode 100644 ui/components/multichain/multichain-import-token-link/index.js create mode 100644 ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js create mode 100644 ui/components/multichain/multichain-import-token-link/multichain-import-token-link.stories.js create mode 100644 ui/components/multichain/multichain-import-token-link/multichain-import-token-link.test.js create mode 100644 ui/components/multichain/multichain-token-list-item/__snapshots__/multichain-token-list-item.test.js.snap create mode 100644 ui/components/multichain/multichain-token-list-item/index.js create mode 100644 ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js create mode 100644 ui/components/multichain/multichain-token-list-item/multichain-token-list-item.scss create mode 100644 ui/components/multichain/multichain-token-list-item/multichain-token-list-item.stories.js create mode 100644 ui/components/multichain/multichain-token-list-item/multichain-token-list-item.test.js diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index 427e60690984..e83606e8bfe7 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -12,21 +12,26 @@ import { getNativeCurrencyImage, getDetectedTokensInCurrentNetwork, getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, + getTokenList, } from '../../../selectors'; import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; -import Typography from '../../ui/typography/typography'; import Box from '../../ui/box/box'; import { Color, - TypographyVariant, - FONT_WEIGHT, - JustifyContent, + TextVariant, + TEXT_ALIGN, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics'; import DetectedToken from '../detected-token/detected-token'; +import { + DetectedTokensBanner, + MultichainTokenListItem, + MultichainImportTokenLink, +} from '../../multichain'; +import { Text } from '../../component-library'; import DetectedTokensLink from './detetcted-tokens-link/detected-tokens-link'; const AssetList = ({ onClickAsset }) => { @@ -69,20 +74,38 @@ const AssetList = ({ onClickAsset }) => { const istokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector( getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, ); - + const tokenList = useSelector(getTokenList); + const tokenData = Object.values(tokenList).find( + (token) => token.symbol === primaryCurrencyProperties.suffix, + ); + const title = tokenData?.name || primaryCurrencyProperties.suffix; return ( <> - onClickAsset(nativeCurrency)} - data-testid="wallet-balance" - primary={ - primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value - } - tokenSymbol={primaryCurrencyProperties.suffix} - secondary={showFiat ? secondaryCurrencyDisplay : undefined} - tokenImage={balanceIsLoading ? null : primaryTokenImage} - identiconBorder - /> + {process.env.MULTICHAIN ? ( + onClickAsset(nativeCurrency)} + title={title} + primary={ + primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value + } + tokenSymbol={primaryCurrencyProperties.suffix} + secondary={showFiat ? secondaryCurrencyDisplay : undefined} + tokenImage={balanceIsLoading ? null : primaryTokenImage} + /> + ) : ( + onClickAsset(nativeCurrency)} + data-testid="wallet-balance" + primary={ + primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value + } + tokenSymbol={primaryCurrencyProperties.suffix} + secondary={showFiat ? secondaryCurrencyDisplay : undefined} + tokenImage={balanceIsLoading ? null : primaryTokenImage} + identiconBorder + /> + )} + { onClickAsset(tokenAddress); @@ -98,19 +121,36 @@ const AssetList = ({ onClickAsset }) => { /> {detectedTokens.length > 0 && !istokenDetectionInactiveOnNonMainnetSupportedNetwork && ( - + <> + {process.env.MULTICHAIN ? ( + setShowDetectedTokens(true)} + margin={4} + /> + ) : ( + + )} + )} 0 ? 0 : 4}> - - - {t('missingToken')} - - - + {process.env.MULTICHAIN ? ( + + ) : ( + <> + + {t('missingToken')} + + + + + )} {showDetectedTokens && ( diff --git a/ui/components/app/token-cell/token-cell.js b/ui/components/app/token-cell/token-cell.js index 1a18f9c838c7..60d8f304a879 100644 --- a/ui/components/app/token-cell/token-cell.js +++ b/ui/components/app/token-cell/token-cell.js @@ -3,9 +3,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import { useSelector } from 'react-redux'; import AssetListItem from '../asset-list-item'; -import { getSelectedAddress } from '../../../selectors'; +import { getSelectedAddress, getTokenList } from '../../../selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; +import { MultichainTokenListItem } from '../../multichain'; +import { ButtonLink, Text } from '../../component-library'; +import { TextColor } from '../../../helpers/constants/design-system'; export default function TokenCell({ address, @@ -19,39 +22,58 @@ export default function TokenCell({ }) { const userAddress = useSelector(getSelectedAddress); const t = useI18nContext(); - + const tokenList = useSelector(getTokenList); + const tokenData = Object.values(tokenList).find( + (token) => token.symbol === symbol, + ); + const title = tokenData?.name || symbol; + const tokenImage = tokenData?.iconUrl || image; const formattedFiat = useTokenFiatAmount(address, string, symbol); const warning = balanceError ? ( - + {t('troubleTokenBalances')} - event.stopPropagation()} - style={{ color: 'var(--color-warning-default)' }} + textProps={{ + color: TextColor.warningDefault, + }} > {t('here')} - - + + ) : null; return ( - + <> + {process.env.MULTICHAIN ? ( + onClick(address)} + tokenSymbol={symbol} + tokenImage={tokenImage} + primary={`${string || 0}`} + secondary={formattedFiat} + title={title} + /> + ) : ( + onClick(address)} + tokenAddress={address} + tokenSymbol={symbol} + tokenDecimals={decimals} + tokenImage={image} + warning={warning} + primary={`${string || 0}`} + secondary={formattedFiat} + isERC721={isERC721} + /> + )} + ); } diff --git a/ui/components/multichain/detected-token-banner/__snapshots__/detected-token-banner.test.js.snap b/ui/components/multichain/detected-token-banner/__snapshots__/detected-token-banner.test.js.snap new file mode 100644 index 000000000000..cbf587cb2064 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/__snapshots__/detected-token-banner.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetectedTokensBanner should render correctly 1`] = ` +
+
+ +
+

+ 3 new tokens found in this account +

+ +
+
+
+`; diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.js b/ui/components/multichain/detected-token-banner/detected-token-banner.js new file mode 100644 index 000000000000..790630ef71d2 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.js @@ -0,0 +1,60 @@ +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { getDetectedTokensInCurrentNetwork } from '../../../selectors'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics'; +import { BannerAlert } from '../../component-library'; + +export const DetectedTokensBanner = ({ + className, + actionButtonOnClick, + ...props +}) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + + const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); + const detectedTokensDetails = detectedTokens.map( + ({ address, symbol }) => `${symbol} - ${address}`, + ); + + const handleOnClick = () => { + actionButtonOnClick(); + trackEvent({ + event: EVENT_NAMES.TOKEN_IMPORT_CLICKED, + category: EVENT.CATEGORIES.WALLET, + properties: { + source: EVENT.SOURCE.TOKEN.DETECTED, + tokens: detectedTokensDetails, + }, + }); + }; + return ( + + {detectedTokens.length === 1 + ? t('numberOfNewTokensDetectedSingular') + : t('numberOfNewTokensDetectedPlural', [detectedTokens.length])} + + ); +}; + +DetectedTokensBanner.propTypes = { + /** + * Handler to be passed to the DetectedTokenBanner component + */ + actionButtonOnClick: PropTypes.func.isRequired, + /** + * An additional className to the DetectedTokenBanner component + */ + className: PropTypes.string, +}; diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.stories.js b/ui/components/multichain/detected-token-banner/detected-token-banner.stories.js new file mode 100644 index 000000000000..080b476effd3 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.stories.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { DetectedTokensBanner } from './detected-token-banner'; + +export default { + title: 'Components/Multichain/DetectedTokensBanner', + component: DetectedTokensBanner, + argTypes: { + actionButtonOnClick: { action: 'setShowDetectedTokens' }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.test.js b/ui/components/multichain/detected-token-banner/detected-token-banner.test.js new file mode 100644 index 000000000000..e7c78b47b3b6 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.test.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { renderWithProvider, screen, fireEvent } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; + +import { DetectedTokensBanner } from './detected-token-banner'; + +describe('DetectedTokensBanner', () => { + let setShowDetectedTokensSpy; + + const args = {}; + + beforeEach(() => { + setShowDetectedTokensSpy = jest.fn(); + args.actionButtonOnClick = setShowDetectedTokensSpy; + }); + + it('should render correctly', () => { + const store = configureStore(testData); + const { getByTestId, container } = renderWithProvider( + , + store, + ); + + expect(getByTestId('detected-token-banner')).toBeDefined(); + expect(container).toMatchSnapshot(); + }); + it('should render number of tokens detected link', () => { + const store = configureStore(testData); + renderWithProvider(, store); + + expect( + screen.getByText('3 new tokens found in this account'), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Import tokens')); + expect(setShowDetectedTokensSpy).toHaveBeenCalled(); + }); +}); diff --git a/ui/components/multichain/detected-token-banner/index.js b/ui/components/multichain/detected-token-banner/index.js new file mode 100644 index 000000000000..0fe2b61deaa6 --- /dev/null +++ b/ui/components/multichain/detected-token-banner/index.js @@ -0,0 +1 @@ +export { DetectedTokensBanner } from './detected-token-banner'; diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index 49c3563e4e24..d34e427510d9 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -1,3 +1,6 @@ export { AccountListItem } from './account-list-item'; export { AccountListItemMenu } from './account-list-item-menu'; export { AccountListMenu } from './account-list-menu'; +export { DetectedTokensBanner } from './detected-token-banner'; +export { MultichainImportTokenLink } from './multichain-import-token-link'; +export { MultichainTokenListItem } from './multichain-token-list-item'; diff --git a/ui/components/multichain/index.scss b/ui/components/multichain/index.scss deleted file mode 100644 index bef86b1dde89..000000000000 --- a/ui/components/multichain/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'account-list-item/index'; -@import 'account-list-menu/index'; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss new file mode 100644 index 000000000000..e8ee40ffa49e --- /dev/null +++ b/ui/components/multichain/multichain-components.scss @@ -0,0 +1,9 @@ +/** +* Please import your styles in order of atomicity. +* The most atomic styles should be imported first. +* This will help improve specificity and reduce the chance of +* unintended overrides. +**/ +@import 'account-list-item/index'; +@import 'account-list-menu/index'; +@import 'multichain-token-list-item/multichain-token-list-item'; diff --git a/ui/components/multichain/multichain-import-token-link/__snapshots__/multichain-import-token-link.test.js.snap b/ui/components/multichain/multichain-import-token-link/__snapshots__/multichain-import-token-link.test.js.snap new file mode 100644 index 000000000000..925788efa313 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/__snapshots__/multichain-import-token-link.test.js.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Import Token Link should match snapshot for goerli chainId 1`] = ` +
+ +
+`; + +exports[`Import Token Link should match snapshot for mainnet chainId 1`] = ` +
+ +
+`; diff --git a/ui/components/multichain/multichain-import-token-link/index.js b/ui/components/multichain/multichain-import-token-link/index.js new file mode 100644 index 000000000000..c9899d1191b9 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/index.js @@ -0,0 +1 @@ +export { MultichainImportTokenLink } from './multichain-import-token-link'; diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js new file mode 100644 index 000000000000..fc6dae84a9c6 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js @@ -0,0 +1,87 @@ +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import Box from '../../ui/box/box'; +import { ButtonLink, ICON_NAMES } from '../../component-library'; +import { + AlignItems, + DISPLAY, + Size, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes'; +import { detectNewTokens } from '../../../store/actions'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics'; +import { + getIsTokenDetectionSupported, + getIsTokenDetectionInactiveOnMainnet, +} from '../../../selectors'; + +export const MultichainImportTokenLink = ({ className, ...props }) => { + const trackEvent = useContext(MetaMetricsContext); + const t = useI18nContext(); + const history = useHistory(); + + const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported); + const isTokenDetectionInactiveOnMainnet = useSelector( + getIsTokenDetectionInactiveOnMainnet, + ); + + const isTokenDetectionAvailable = + isTokenDetectionSupported || + isTokenDetectionInactiveOnMainnet || + Boolean(process.env.IN_TEST); + return ( + + + { + history.push(IMPORT_TOKEN_ROUTE); + trackEvent({ + event: EVENT_NAMES.TOKEN_IMPORT_BUTTON_CLICKED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + location: 'Home', + }, + }); + }} + > + {isTokenDetectionAvailable + ? t('importTokensCamelCase') + : t('importTokensCamelCase').charAt(0).toUpperCase() + + t('importTokensCamelCase').slice(1)} + + + + detectNewTokens()} + > + {t('refreshList')} + + + + ); +}; + +MultichainImportTokenLink.propTypes = { + /** + * An additional className to apply to the TokenList. + */ + className: PropTypes.string, +}; diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.stories.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.stories.js new file mode 100644 index 000000000000..1cb7b4395148 --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { MultichainImportTokenLink } from './multichain-import-token-link'; + +export default { + title: 'Components/Multichain/MultichainImportTokenLink', + component: MultichainImportTokenLink, +}; + +export const DefaultStory = () => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.test.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.test.js new file mode 100644 index 000000000000..dd376c52ba0c --- /dev/null +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.test.js @@ -0,0 +1,101 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent, screen } from '@testing-library/react'; +import { detectNewTokens } from '../../../store/actions'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { MultichainImportTokenLink } from './multichain-import-token-link'; + +const mockPushHistory = jest.fn(); + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + return { + ...original, + useLocation: jest.fn(() => ({ search: '' })), + useHistory: () => ({ + push: mockPushHistory, + }), + }; +}); + +jest.mock('../../../store/actions.ts', () => ({ + detectNewTokens: jest.fn(), +})); + +describe('Import Token Link', () => { + it('should match snapshot for goerli chainId', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x5', + }, + }, + }; + + const store = configureMockStore()(mockState); + + const { container } = renderWithProvider( + , + store, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot for mainnet chainId', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x1', + }, + }, + }; + + const store = configureMockStore()(mockState); + + const { container } = renderWithProvider( + , + store, + ); + + expect(container).toMatchSnapshot(); + }); + + it('should detectNewTokens when clicking refresh', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x5', + }, + }, + }; + + const store = configureMockStore()(mockState); + + renderWithProvider(, store); + + const refreshList = screen.getByTestId('refresh-list-button'); + fireEvent.click(refreshList); + + expect(detectNewTokens).toHaveBeenCalled(); + }); + + it('should push import token route', () => { + const mockState = { + metamask: { + provider: { + chainId: '0x5', + }, + }, + }; + + const store = configureMockStore()(mockState); + + renderWithProvider(, store); + + const importToken = screen.getByTestId('import-token-button'); + fireEvent.click(importToken); + + expect(mockPushHistory).toHaveBeenCalledWith('/import-token'); + }); +}); diff --git a/ui/components/multichain/multichain-token-list-item/__snapshots__/multichain-token-list-item.test.js.snap b/ui/components/multichain/multichain-token-list-item/__snapshots__/multichain-token-list-item.test.js.snap new file mode 100644 index 000000000000..b4af82ccb0a6 --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/__snapshots__/multichain-token-list-item.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MultichainTokenListItem should render correctly 1`] = ` + +`; diff --git a/ui/components/multichain/multichain-token-list-item/index.js b/ui/components/multichain/multichain-token-list-item/index.js new file mode 100644 index 000000000000..bccfac8ab4e9 --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/index.js @@ -0,0 +1 @@ +export { MultichainTokenListItem } from './multichain-token-list-item'; diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js new file mode 100644 index 000000000000..82ed7752403a --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js @@ -0,0 +1,162 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import classnames from 'classnames'; +import { + BLOCK_SIZES, + BorderColor, + DISPLAY, + FLEX_DIRECTION, + FONT_WEIGHT, + JustifyContent, + Size, + TextColor, + TextVariant, + TEXT_ALIGN, +} from '../../../helpers/constants/design-system'; +import { + AvatarNetwork, + AvatarToken, + BadgeWrapper, + Text, +} from '../../component-library'; +import Box from '../../ui/box/box'; +import { getNativeCurrencyImage } from '../../../selectors'; +import Tooltip from '../../ui/tooltip'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +export const MultichainTokenListItem = ({ + className, + onClick, + tokenSymbol, + tokenImage, + primary, + secondary, + title, +}) => { + const t = useI18nContext(); + const primaryTokenImage = useSelector(getNativeCurrencyImage); + const dataTheme = document.documentElement.getAttribute('data-theme'); + return ( + + { + e.preventDefault(); + onClick(); + }} + > + + } + marginRight={3} + > + + + + + + + + {title === 'ETH' ? t('networkNameEthereum') : title} + + + + + {secondary} + + + + {Number(primary).toFixed(3)} {tokenSymbol}{' '} + + + + + ); +}; + +MultichainTokenListItem.propTypes = { + /** + * An additional className to apply to the TokenList. + */ + className: PropTypes.string, + /** + * The onClick handler to be passed to the MultichainTokenListItem component + */ + onClick: PropTypes.func, + /** + * tokenSymbol represents the symbol of the Token + */ + tokenSymbol: PropTypes.string, + /** + * title represents the name of the token and if name is not available then Symbol + */ + title: PropTypes.string, + /** + * tokenImage represnts the image of the token icon + */ + tokenImage: PropTypes.string, + /** + * primary represents the balance + */ + primary: PropTypes.string, + /** + * secondary represents the balance in dollars + */ + secondary: PropTypes.string, +}; diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.scss b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.scss new file mode 100644 index 000000000000..f6863f8b32b2 --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.scss @@ -0,0 +1,8 @@ +.multichain-token-list-item { + &__container-cell { + &:hover, + &:focus-within { + background-color: var(--color-background-default-hover); + } + } +} diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.stories.js b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.stories.js new file mode 100644 index 000000000000..168e6ed1aa0f --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.stories.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import testData from '../../../../.storybook/test-data'; +import configureStore from '../../../store/store'; +import { MultichainTokenListItem } from './multichain-token-list-item'; + +export default { + title: 'Components/Multichain/MultichainTokenListItem', + component: MultichainTokenListItem, + argTypes: { + tokenSymbol: { + control: 'text', + }, + tokenImage: { + control: 'text', + }, + primary: { + control: 'text', + }, + secondary: { + control: 'text', + }, + title: { + control: 'text', + }, + onClick: { + action: 'onClick', + }, + }, + args: { + secondary: '$9.80 USD', + primary: '88.00687889', + tokenImage: './images/eth_logo.svg', + tokenSymbol: 'ETH', + title: 'Ethereum', + }, +}; + +const customNetworkData = { + ...testData, + metamask: { ...testData.metamask, nativeCurrency: '' }, +}; +const customNetworkStore = configureStore(customNetworkData); + +const Template = (args) => { + return ; +}; + +export const DefaultStory = Template.bind({}); + +export const ChaosStory = (args) => ( +
+ +
+); +ChaosStory.storyName = 'ChaosStory'; + +ChaosStory.args = { + title: 'Really long, long name', + secondary: '$94556756776.80 USD', + primary: '34449765768526.00', +}; + +export const NoImagesStory = Template.bind({}); + +NoImagesStory.decorators = [ + (Story) => ( + + + + ), +]; + +NoImagesStory.args = { + tokenImage: '', +}; diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.test.js b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.test.js new file mode 100644 index 000000000000..6f04982984ae --- /dev/null +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; + +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { MultichainTokenListItem } from './multichain-token-list-item'; + +const state = { + metamask: { + provider: { + ticker: 'ETH', + nickname: '', + chainId: '0x1', + type: 'mainnet', + }, + useTokenDetection: false, + nativeCurrency: 'ETH', + }, +}; + +describe('MultichainTokenListItem', () => { + const props = { + onClick: jest.fn(), + }; + it('should render correctly', () => { + const store = configureMockStore()(state); + const { getByTestId, container } = renderWithProvider( + , + store, + ); + expect(getByTestId('multichain-token-list-item')).toBeDefined(); + expect(container).toMatchSnapshot(); + }); + + it('should render with custom className', () => { + const store = configureMockStore()(state); + const { getByTestId } = renderWithProvider( + , + store, + ); + expect(getByTestId('multichain-token-list-item')).toHaveClass( + 'multichain-token-list-item-test', + ); + }); + + it('handles click action and fires onClick', () => { + const store = configureMockStore()(state); + const { queryByTestId } = renderWithProvider( + , + store, + ); + + fireEvent.click(queryByTestId('multichain-token-list-button')); + + expect(props.onClick).toHaveBeenCalled(); + }); +}); diff --git a/ui/components/ui/tooltip/index.scss b/ui/components/ui/tooltip/index.scss index 8ae602e33f7b..3bc86f9102b2 100644 --- a/ui/components/ui/tooltip/index.scss +++ b/ui/components/ui/tooltip/index.scss @@ -26,19 +26,19 @@ } } - &[x-placement^=top] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='top'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-top-color: var(--color-background-default); } - &[x-placement^=right] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='right'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-right-color: var(--color-background-default); } - &[x-placement^=left] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='left'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-left-color: var(--color-background-default); } - &[x-placement^=bottom] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { + &[x-placement^='bottom'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] { border-bottom-color: var(--color-background-default); } } diff --git a/ui/css/index.scss b/ui/css/index.scss index 46e1e7f2b095..a26c7ae09cf6 100644 --- a/ui/css/index.scss +++ b/ui/css/index.scss @@ -10,8 +10,8 @@ @import './base-styles.scss'; @import '../components/component-library/component-library-components.scss'; @import '../components/app/app-components'; -@import '../components/multichain/index.scss'; @import '../components/ui/ui-components'; +@import '../components/multichain/multichain-components.scss'; @import '../pages/pages'; @import './errors.scss'; @import './loading.scss'; From 8c310a0a8b6b83e139ba56f17c8641a9cfb7fec9 Mon Sep 17 00:00:00 2001 From: aleksandar-mihajlovic <120601632+aleksandar-mihajlovic@users.noreply.github.com> Date: Thu, 23 Mar 2023 11:35:25 +0100 Subject: [PATCH 18/20] Fix for hovering any Address parsed on Signed Type Data screen does not display the pointer anymore (#18046) * Signed Type Data address hover fix * Snapshots fix * Snapshots update fix * Update snapshot --- .../signature-request.component.test.js.snap | 20 +++++++++---------- .../decoding/address/address.component.js | 2 +- .../components/decoding/address/index.scss | 19 +++++++++--------- .../__snapshots__/index.test.js.snap | 10 +++++----- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap index 8a8ddd8a5aba..d1e6c7708ee2 100644 --- a/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap +++ b/ui/components/app/signature-request/__snapshots__/signature-request.component.test.js.snap @@ -369,7 +369,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xCD2...D826
@@ -445,7 +445,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xDea...beeF
@@ -580,7 +580,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xbBb...BBbB
@@ -656,7 +656,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xB0B...Ea57
@@ -732,7 +732,7 @@ exports[`Signature Request Component render should match snapshot when we are us
0xB0B...0000
@@ -1144,7 +1144,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xCD2...D826
@@ -1220,7 +1220,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xDea...beeF
@@ -1355,7 +1355,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xbBb...BBbB
@@ -1431,7 +1431,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xB0B...Ea57
@@ -1507,7 +1507,7 @@ exports[`Signature Request Component render should match snapshot when we want t
0xB0B...0000
diff --git a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js index 9b576b5b3384..21220448ab5c 100644 --- a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js +++ b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js @@ -58,7 +58,7 @@ const Address = ({
setShowNicknamePopovers(true)} > {recipientToRender} diff --git a/ui/components/app/transaction-decoding/components/decoding/address/index.scss b/ui/components/app/transaction-decoding/components/decoding/address/index.scss index ca821dae80e2..71bf88fadc64 100644 --- a/ui/components/app/transaction-decoding/components/decoding/address/index.scss +++ b/ui/components/app/transaction-decoding/components/decoding/address/index.scss @@ -1,12 +1,13 @@ -.tx-insight-content { - .tx-insight-component-address { - display: flex; - align-items: center; - cursor: pointer; - overflow: visible; +.tx-insight-component-address { + display: flex; + align-items: center; + overflow: visible; + + &__sender-icon { + padding-right: 5px; + } - &__sender-icon { - padding-right: 5px; - } + &__name { + cursor: pointer; } } diff --git a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap index e795ab35d5f7..fbf3c10c168d 100644 --- a/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap +++ b/ui/pages/confirm-signature-request/__snapshots__/index.test.js.snap @@ -368,7 +368,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xCD2...D826
@@ -444,7 +444,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xDea...beeF
@@ -579,7 +579,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xbBb...BBbB
@@ -655,7 +655,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xB0B...Ea57
@@ -731,7 +731,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
0xB0B...0000
From 447435959166a9194c229baf09a70ff5f29fb70b Mon Sep 17 00:00:00 2001 From: Ariella Vu <20778143+digiwand@users.noreply.github.com> Date: Thu, 23 Mar 2023 05:55:00 -0700 Subject: [PATCH 19/20] Fix overflowing UI & buttons in Signature Request screens (#18247) * signature-req: fix overflow ui * signature-req: fix uneven btn height --- .../app/signature-request-original/index.scss | 10 +++++----- ui/components/ui/page-container/index.scss | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/components/app/signature-request-original/index.scss b/ui/components/app/signature-request-original/index.scss index 01182eda8edb..281fae02fa07 100644 --- a/ui/components/app/signature-request-original/index.scss +++ b/ui/components/app/signature-request-original/index.scss @@ -62,11 +62,11 @@ } &__body { - width: 100%; - height: 100%; display: flex; flex-flow: column; - flex: 1 1 auto; + flex: 1 1 0; + min-height: 0; + max-width: 100%; } &__origin { @@ -92,18 +92,18 @@ } &__rows { - height: 100%; overflow-y: auto; overflow-x: hidden; border-top: 1px solid var(--color-border-default); display: flex; flex-flow: column; + flex: 1 1 0; + min-height: 0; } &__row { display: flex; flex-flow: column; - flex: 1 0 auto; } &__row-title { diff --git a/ui/components/ui/page-container/index.scss b/ui/components/ui/page-container/index.scss index cec98678aa43..1e37799af474 100644 --- a/ui/components/ui/page-container/index.scss +++ b/ui/components/ui/page-container/index.scss @@ -190,7 +190,7 @@ margin-right: 0; &:first-of-type { - margin-bottom: 0; + margin-right: 4px; } } } From 99bdf8458cef8243c6ee39f69ba9f646a01b6214 Mon Sep 17 00:00:00 2001 From: George Marshall Date: Thu, 23 Mar 2023 06:36:09 -0700 Subject: [PATCH 20/20] Adding ModalOverlay component (#18161) --- .../component-library-components.scss | 2 + ui/components/component-library/index.js | 1 + .../modal-overlay/README.mdx | 40 +++++++++++++++++ .../__snapshots__/modal-overlay.test.tsx.snap | 9 ++++ .../component-library/modal-overlay/index.ts | 2 + .../modal-overlay/modal-overlay.scss | 7 +++ .../modal-overlay/modal-overlay.stories.tsx | 44 +++++++++++++++++++ .../modal-overlay/modal-overlay.test.tsx | 32 ++++++++++++++ .../modal-overlay/modal-overlay.tsx | 28 ++++++++++++ .../modal-overlay/modal-overlay.types.ts | 13 ++++++ 10 files changed, 178 insertions(+) create mode 100644 ui/components/component-library/modal-overlay/README.mdx create mode 100644 ui/components/component-library/modal-overlay/__snapshots__/modal-overlay.test.tsx.snap create mode 100644 ui/components/component-library/modal-overlay/index.ts create mode 100644 ui/components/component-library/modal-overlay/modal-overlay.scss create mode 100644 ui/components/component-library/modal-overlay/modal-overlay.stories.tsx create mode 100644 ui/components/component-library/modal-overlay/modal-overlay.test.tsx create mode 100644 ui/components/component-library/modal-overlay/modal-overlay.tsx create mode 100644 ui/components/component-library/modal-overlay/modal-overlay.types.ts diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss index 618b089ad81f..4068f6af5cf6 100644 --- a/ui/components/component-library/component-library-components.scss +++ b/ui/components/component-library/component-library-components.scss @@ -30,3 +30,5 @@ @import 'banner-alert/banner-alert'; @import 'banner-tip/banner-tip'; @import 'modal-content/modal-content'; +@import 'modal-overlay/modal-overlay'; + diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js index e1b2f1408fdc..33e8ab60480e 100644 --- a/ui/components/component-library/index.js +++ b/ui/components/component-library/index.js @@ -32,6 +32,7 @@ export { Input, INPUT_TYPES } from './input'; export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field'; export { TextFieldSearch } from './text-field-search'; export { ModalContent, ModalContentSize } from './modal-content'; +export { ModalOverlay } from './modal-overlay'; // Molecules export { BannerBase } from './banner-base'; diff --git a/ui/components/component-library/modal-overlay/README.mdx b/ui/components/component-library/modal-overlay/README.mdx new file mode 100644 index 000000000000..cb42da5d003f --- /dev/null +++ b/ui/components/component-library/modal-overlay/README.mdx @@ -0,0 +1,40 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; + +import { ModalOverlay } from './modal-overlay'; + +# ModalOverlay + +`ModalOverlay` is a transparent overlay that covers the entire screen. It is used to dim the background when a modal is open. + + + + + +## Props + +The `ModalOverlay` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props + + + +### On Click + +Use the `onClick` prop to handle clicks on the overlay + + + + + +```jsx +import React, { useState } from 'react'; +import { ModalOverlay } from '../../component-library'; + +const [open, setOpen] = useState(false); +const handleOnClick = () => { + setOpen(!open); +}; + +; +{ + open && ; +} +``` diff --git a/ui/components/component-library/modal-overlay/__snapshots__/modal-overlay.test.tsx.snap b/ui/components/component-library/modal-overlay/__snapshots__/modal-overlay.test.tsx.snap new file mode 100644 index 000000000000..10f22ae8d9b9 --- /dev/null +++ b/ui/components/component-library/modal-overlay/__snapshots__/modal-overlay.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalOverlay should match snapshot 1`] = ` +
+
+
+`; diff --git a/ui/components/component-library/modal-overlay/index.ts b/ui/components/component-library/modal-overlay/index.ts new file mode 100644 index 000000000000..bc283f8985c2 --- /dev/null +++ b/ui/components/component-library/modal-overlay/index.ts @@ -0,0 +1,2 @@ +export { ModalOverlay } from './modal-overlay'; +export type { ModalOverlayProps } from './modal-overlay.types'; diff --git a/ui/components/component-library/modal-overlay/modal-overlay.scss b/ui/components/component-library/modal-overlay/modal-overlay.scss new file mode 100644 index 000000000000..afa528a1853c --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.scss @@ -0,0 +1,7 @@ +.mm-modal-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} diff --git a/ui/components/component-library/modal-overlay/modal-overlay.stories.tsx b/ui/components/component-library/modal-overlay/modal-overlay.stories.tsx new file mode 100644 index 000000000000..fb2cec5970de --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.stories.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { ModalOverlay } from './modal-overlay'; + +import README from './README.mdx'; + +export default { + title: 'Components/ComponentLibrary/ModalOverlay', + component: ModalOverlay, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + className: { + control: 'text', + }, + onClick: { + action: 'onClick', + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const DefaultStory = Template.bind({}); +DefaultStory.storyName = 'Default'; + +export const OnClick: ComponentStory = (args) => { + const [open, setOpen] = useState(false); + const handleOnClick = () => { + setOpen(!open); + }; + return ( + <> + + {open && } + + ); +}; diff --git a/ui/components/component-library/modal-overlay/modal-overlay.test.tsx b/ui/components/component-library/modal-overlay/modal-overlay.test.tsx new file mode 100644 index 000000000000..e3347176676b --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { ModalOverlay } from './modal-overlay'; + +describe('ModalOverlay', () => { + it('should render ModalOverlay without error', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('modal-overlay')).toBeDefined(); + expect(getByTestId('modal-overlay')).toHaveClass('mm-modal-overlay'); + }); + it('should match snapshot', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it('should render with and additional className', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('modal-overlay')).toHaveClass('test-class'); + }); + it('should fire the onClick function when clicked', () => { + const onClick = jest.fn(); + const { getByTestId } = render( + , + ); + getByTestId('modal-overlay').click(); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/ui/components/component-library/modal-overlay/modal-overlay.tsx b/ui/components/component-library/modal-overlay/modal-overlay.tsx new file mode 100644 index 000000000000..76b9586cba88 --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import classnames from 'classnames'; + +import { + BackgroundColor, + BLOCK_SIZES, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box/box'; + +import { ModalOverlayProps } from './modal-overlay.types'; + +export const ModalOverlay: React.FC = ({ + onClick, + className = '', + ...props +}) => ( + +); + +export default ModalOverlay; diff --git a/ui/components/component-library/modal-overlay/modal-overlay.types.ts b/ui/components/component-library/modal-overlay/modal-overlay.types.ts new file mode 100644 index 000000000000..a1045bc96783 --- /dev/null +++ b/ui/components/component-library/modal-overlay/modal-overlay.types.ts @@ -0,0 +1,13 @@ +import { BoxProps } from '../../ui/box/box.d'; + +export interface ModalOverlayProps extends BoxProps { + /** + * onClick handler for the overlay + * Not necessary when used with Modal and closeOnClickOutside is true + */ + onClick?: (event: React.MouseEvent) => void; + /** + * Additional className to add to the ModalOverlay + */ + className?: string; +}