From 2968d94ed6003506df7daddf63d73b97a892feb0 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 12 Sep 2024 09:00:52 -0400 Subject: [PATCH 01/71] make adjustments to show notification call --- app/scripts/metamask-controller.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 479fe748c609..f79712b993f6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2654,14 +2654,19 @@ export default class MetamaskController extends EventEmitter { origin, args.message, ), - showInAppNotification: (origin, args) => + showInAppNotification: (origin, args) => { + const notificationArgs = { ...args }; + notificationArgs.interfaceId = notificationArgs.content; + delete notificationArgs.content; + delete notificationArgs.type; this.controllerMessenger.call( 'RateLimitController:call', origin, 'showInAppNotification', origin, - args.message, - ), + notificationArgs, + ); + }, updateSnapState: this.controllerMessenger.call.bind( this.controllerMessenger, 'SnapController:updateSnapState', From 39f7d1b1060ed319a652caf9a97949e25efc2326 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 24 Sep 2024 20:48:44 +0200 Subject: [PATCH 02/71] Bump Snaps packages --- builds.yml | 8 ++--- package.json | 12 ++++---- test/e2e/snaps/enums.js | 2 +- yarn.lock | 68 ++++++++++++++++++++--------------------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/builds.yml b/builds.yml index acee49063822..06abc43a5b72 100644 --- a/builds.yml +++ b/builds.yml @@ -26,7 +26,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -46,7 +46,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -67,7 +67,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -90,7 +90,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.7.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://support.metamask-institutional.io - SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io diff --git a/package.json b/package.json index 12d1cd30d956..bf7f87121abd 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,7 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.5.1", + "@metamask/snaps-sdk": "^6.6.0", "@swc/types@0.1.5": "^0.1.6", "@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -354,11 +354,11 @@ "@metamask/selected-network-controller": "^18.0.1", "@metamask/signature-controller": "^19.0.0", "@metamask/smart-transactions-controller": "^13.0.0", - "@metamask/snaps-controllers": "^9.7.0", - "@metamask/snaps-execution-environments": "^6.7.2", - "@metamask/snaps-rpc-methods": "^11.1.1", - "@metamask/snaps-sdk": "^6.5.1", - "@metamask/snaps-utils": "^8.1.1", + "@metamask/snaps-controllers": "^9.9.0", + "@metamask/snaps-execution-environments": "^6.8.0", + "@metamask/snaps-rpc-methods": "^11.2.0", + "@metamask/snaps-sdk": "^6.6.0", + "@metamask/snaps-utils": "^8.2.0", "@metamask/transaction-controller": "^37.0.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.1.0", diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 2b1a6bc6532d..a0a0e856095e 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.13.1/', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.0', }; diff --git a/yarn.lock b/yarn.lock index e6329f1b9a03..2d8aacff3198 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6265,9 +6265,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.6.0, @metamask/snaps-controllers@npm:^9.7.0": - version: 9.7.0 - resolution: "@metamask/snaps-controllers@npm:9.7.0" +"@metamask/snaps-controllers@npm:^9.6.0, @metamask/snaps-controllers@npm:^9.9.0": + version: 9.9.0 + resolution: "@metamask/snaps-controllers@npm:9.9.0" dependencies: "@metamask/approval-controller": "npm:^7.0.2" "@metamask/base-controller": "npm:^6.0.2" @@ -6279,9 +6279,9 @@ __metadata: "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-rpc-methods": "npm:^11.2.0" + "@metamask/snaps-sdk": "npm:^6.6.0" + "@metamask/snaps-utils": "npm:^8.2.0" "@metamask/utils": "npm:^9.2.1" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6294,30 +6294,30 @@ __metadata: readable-web-to-node-stream: "npm:^3.0.2" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.7.1 + "@metamask/snaps-execution-environments": ^6.8.0 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/8a353819e60330ef3e338a40b1115d4c830b92b1cc0c92afb2b34bf46fbc906e6da5f905654e1d486cacd40b7025ec74d3cd01cb935090035ce9f1021ce5469f + checksum: 10/c13bfa077f6e3b378b03634f7bea8ece4297985239505ba1791c0e18eb55e74d3db43a5c8d2da7a9d6bdde93db969746d7aba3742436d76f1b21fd4e480b2b19 languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.7.2": - version: 6.7.2 - resolution: "@metamask/snaps-execution-environments@npm:6.7.2" +"@metamask/snaps-execution-environments@npm:^6.8.0": + version: 6.8.0 + resolution: "@metamask/snaps-execution-environments@npm:6.8.0" dependencies: "@metamask/json-rpc-engine": "npm:^9.0.2" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.6.0" + "@metamask/snaps-utils": "npm:^8.2.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/4b8ec4c0f6e628feeffd92fe4378fd204d2ed78012a1ed5282b24b00c78cebc3b6d7cb1306903b045a2ca887ecc0adafb2c96da4a19f2730a268f4912b36bec3 + checksum: 10/579c51e37efa4efa9820d663cadd9e146084cf2eadfcae35a61c574f1e54a6ac2a86d7145d1bc9764da31920aabfd81beea47178be0d2226d42ad636e3513780 languageName: node linkType: hard @@ -6333,32 +6333,32 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.1.1": - version: 11.1.1 - resolution: "@metamask/snaps-rpc-methods@npm:11.1.1" +"@metamask/snaps-rpc-methods@npm:^11.2.0": + version: 11.2.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.2.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/permission-controller": "npm:^11.0.0" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.5.0" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-sdk": "npm:^6.6.0" + "@metamask/snaps-utils": "npm:^8.2.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" - checksum: 10/e23279dabc6f4ffe2c6c4a7003a624cd5e79b558d7981ec12c23e54a5da25cb7be9bc7bddfa8b2ce84af28a89b42076a2c14ab004b7a976a4426bf1e1de71b5b + checksum: 10/95aee0edf9295dabd16446192b0e24144bc1637dee78c54adfb5d5fb4dea8d7101ff8a614627d8ff0b08be437afb30656906aabfe59a4c27697d959beb14cd9c languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.5.1": - version: 6.5.1 - resolution: "@metamask/snaps-sdk@npm:6.5.1" +"@metamask/snaps-sdk@npm:^6.6.0": + version: 6.6.0 + resolution: "@metamask/snaps-sdk@npm:6.6.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - checksum: 10/7831fb2ca61a32ad43e971de9307b221f6bd2f65c84a3286f350cfdd2396166c58db6cd2fac9711654a211c8dc2049e591a79ab720b3f5ad562e434f75e95d32 + checksum: 10/509b5f695a9fe183061f001298985ac6fd3c54c55aecc9cbddfbdea9ff2e93dd9b601e5f3ad002e50f0a155dad81e52e796dec8645da1a7bb313e24f2107cdda languageName: node linkType: hard @@ -6393,9 +6393,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.1.1": - version: 8.1.1 - resolution: "@metamask/snaps-utils@npm:8.1.1" +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.2.0": + version: 8.2.0 + resolution: "@metamask/snaps-utils@npm:8.2.0" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6405,7 +6405,7 @@ __metadata: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-sdk": "npm:^6.6.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" @@ -6420,7 +6420,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f + checksum: 10/6f38bf39c4c2561f13347859fdd8c0f319027f6819e17c899f5e6fa7a2d8b0ceb8eaadcb3f104cae68341e0cc20044688b815563e38dfa0cf60d4a2c77143d1b languageName: node linkType: hard @@ -26146,11 +26146,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^18.0.1" "@metamask/signature-controller": "npm:^19.0.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" - "@metamask/snaps-controllers": "npm:^9.7.0" - "@metamask/snaps-execution-environments": "npm:^6.7.2" - "@metamask/snaps-rpc-methods": "npm:^11.1.1" - "@metamask/snaps-sdk": "npm:^6.5.1" - "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/snaps-controllers": "npm:^9.9.0" + "@metamask/snaps-execution-environments": "npm:^6.8.0" + "@metamask/snaps-rpc-methods": "npm:^11.2.0" + "@metamask/snaps-sdk": "npm:^6.6.0" + "@metamask/snaps-utils": "npm:^8.2.0" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" "@metamask/transaction-controller": "npm:^37.0.0" From bdd1935623c34fc1d7edd67f4bdc93a53e126a90 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Fri, 27 Sep 2024 17:16:48 +0200 Subject: [PATCH 03/71] Bump dependencies again --- builds.yml | 8 ++--- package.json | 12 ++++---- test/e2e/snaps/enums.js | 2 +- yarn.lock | 68 ++++++++++++++++++++--------------------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/builds.yml b/builds.yml index 06abc43a5b72..6a968119d6da 100644 --- a/builds.yml +++ b/builds.yml @@ -26,7 +26,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.0/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -46,7 +46,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.0/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -67,7 +67,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.0/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -90,7 +90,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.8.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.9.0/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://support.metamask-institutional.io - SUPPORT_REQUEST_LINK: https://support.metamask-institutional.io diff --git a/package.json b/package.json index bf7f87121abd..82fd140a8427 100644 --- a/package.json +++ b/package.json @@ -229,7 +229,7 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.6.0", + "@metamask/snaps-sdk": "^6.7.0", "@swc/types@0.1.5": "^0.1.6", "@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -354,11 +354,11 @@ "@metamask/selected-network-controller": "^18.0.1", "@metamask/signature-controller": "^19.0.0", "@metamask/smart-transactions-controller": "^13.0.0", - "@metamask/snaps-controllers": "^9.9.0", - "@metamask/snaps-execution-environments": "^6.8.0", - "@metamask/snaps-rpc-methods": "^11.2.0", - "@metamask/snaps-sdk": "^6.6.0", - "@metamask/snaps-utils": "^8.2.0", + "@metamask/snaps-controllers": "^9.10.0", + "@metamask/snaps-execution-environments": "^6.9.0", + "@metamask/snaps-rpc-methods": "^11.3.0", + "@metamask/snaps-sdk": "^6.7.0", + "@metamask/snaps-utils": "^8.3.0", "@metamask/transaction-controller": "^37.0.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.1.0", diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index a0a0e856095e..ea806d4c80d6 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.0', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.15.1', }; diff --git a/yarn.lock b/yarn.lock index 2d8aacff3198..e7a8482fe85c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6265,9 +6265,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.6.0, @metamask/snaps-controllers@npm:^9.9.0": - version: 9.9.0 - resolution: "@metamask/snaps-controllers@npm:9.9.0" +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.6.0": + version: 9.10.0 + resolution: "@metamask/snaps-controllers@npm:9.10.0" dependencies: "@metamask/approval-controller": "npm:^7.0.2" "@metamask/base-controller": "npm:^6.0.2" @@ -6279,9 +6279,9 @@ __metadata: "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-rpc-methods": "npm:^11.2.0" - "@metamask/snaps-sdk": "npm:^6.6.0" - "@metamask/snaps-utils": "npm:^8.2.0" + "@metamask/snaps-rpc-methods": "npm:^11.3.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/utils": "npm:^9.2.1" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6294,30 +6294,30 @@ __metadata: readable-web-to-node-stream: "npm:^3.0.2" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.8.0 + "@metamask/snaps-execution-environments": ^6.9.0 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/c13bfa077f6e3b378b03634f7bea8ece4297985239505ba1791c0e18eb55e74d3db43a5c8d2da7a9d6bdde93db969746d7aba3742436d76f1b21fd4e480b2b19 + checksum: 10/330af13da1bfd276dd6eaf9629b00fdcbd221d52fd7920c615104afe30e75aba0b347035d65db7675ee1805b6e1fa00cde76026a2e8f430550a656ed6947c570 languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.8.0": - version: 6.8.0 - resolution: "@metamask/snaps-execution-environments@npm:6.8.0" +"@metamask/snaps-execution-environments@npm:^6.9.0": + version: 6.9.0 + resolution: "@metamask/snaps-execution-environments@npm:6.9.0" dependencies: "@metamask/json-rpc-engine": "npm:^9.0.2" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.6.0" - "@metamask/snaps-utils": "npm:^8.2.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/579c51e37efa4efa9820d663cadd9e146084cf2eadfcae35a61c574f1e54a6ac2a86d7145d1bc9764da31920aabfd81beea47178be0d2226d42ad636e3513780 + checksum: 10/07aa1b10ac945c18d40b379a1665b20c1b55d6fa53fe9cdfe206b61c4a3bc6a19c4e2d68add62b662651600688a052af81d86326888b98e4bcaaed7576c0c999 languageName: node linkType: hard @@ -6333,32 +6333,32 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.2.0": - version: 11.2.0 - resolution: "@metamask/snaps-rpc-methods@npm:11.2.0" +"@metamask/snaps-rpc-methods@npm:^11.3.0": + version: 11.3.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.3.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/permission-controller": "npm:^11.0.0" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.6.0" - "@metamask/snaps-utils": "npm:^8.2.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" - checksum: 10/95aee0edf9295dabd16446192b0e24144bc1637dee78c54adfb5d5fb4dea8d7101ff8a614627d8ff0b08be437afb30656906aabfe59a4c27697d959beb14cd9c + checksum: 10/be00cf6688755bd51ff6936c6e0a7ca65df4b19794355ba9f6cec5ee6f115298854f16444d52c53d3b72c7a7a192565fbc92a44a29315d5adec66ef043c62157 languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.6.0": - version: 6.6.0 - resolution: "@metamask/snaps-sdk@npm:6.6.0" +"@metamask/snaps-sdk@npm:^6.7.0": + version: 6.7.0 + resolution: "@metamask/snaps-sdk@npm:6.7.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" - checksum: 10/509b5f695a9fe183061f001298985ac6fd3c54c55aecc9cbddfbdea9ff2e93dd9b601e5f3ad002e50f0a155dad81e52e796dec8645da1a7bb313e24f2107cdda + checksum: 10/1f3148f509e7212e40b615f9050dbb5a5085ae17cd2301f471b828b90a61902a3ae21f9ac520b3a2ffa291585823dd063486a314a2738cd86479b4358e39cfac languageName: node linkType: hard @@ -6393,9 +6393,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.2.0": - version: 8.2.0 - resolution: "@metamask/snaps-utils@npm:8.2.0" +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0": + version: 8.3.0 + resolution: "@metamask/snaps-utils@npm:8.3.0" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6405,7 +6405,7 @@ __metadata: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.6.0" + "@metamask/snaps-sdk": "npm:^6.7.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" @@ -6420,7 +6420,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/6f38bf39c4c2561f13347859fdd8c0f319027f6819e17c899f5e6fa7a2d8b0ceb8eaadcb3f104cae68341e0cc20044688b815563e38dfa0cf60d4a2c77143d1b + checksum: 10/43844f4057d698088f03c30937e59bfeed86bfe05ec82a5c25248a0d0f4019994611060913d61751a692bf7791a374459b88f6c1a6f119f5f883069307e8278c languageName: node linkType: hard @@ -26146,11 +26146,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^18.0.1" "@metamask/signature-controller": "npm:^19.0.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" - "@metamask/snaps-controllers": "npm:^9.9.0" - "@metamask/snaps-execution-environments": "npm:^6.8.0" - "@metamask/snaps-rpc-methods": "npm:^11.2.0" - "@metamask/snaps-sdk": "npm:^6.6.0" - "@metamask/snaps-utils": "npm:^8.2.0" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-execution-environments": "npm:^6.9.0" + "@metamask/snaps-rpc-methods": "npm:^11.3.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" "@metamask/transaction-controller": "npm:^37.0.0" From 0e30d3a17e8aa639a45ca62e7fecfc1198e7f851 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 30 Sep 2024 09:11:25 -0400 Subject: [PATCH 04/71] update notification controller, update raw snap notification type --- app/scripts/metamask-controller.js | 4 ++-- package.json | 2 +- ui/pages/notifications/snap/types/types.ts | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 01d6e2d62a09..afe295a7a951 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1397,11 +1397,11 @@ export default class MetamaskController extends EventEmitter { rateLimitTimeout: 300000, }, showInAppNotification: { - method: (origin, message) => { + method: (origin, notificationData) => { this.controllerMessenger.call( 'NotificationController:show', origin, - message, + notificationData, ); return null; diff --git a/package.json b/package.json index d54962b53b24..5867cb3c26ed 100644 --- a/package.json +++ b/package.json @@ -334,7 +334,7 @@ "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/name-controller": "^8.0.0", "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", - "@metamask/notification-controller": "^6.0.0", + "@metamask/notification-controller": "^7.0.0", "@metamask/notification-services-controller": "^0.7.0", "@metamask/object-multiplex": "^2.0.0", "@metamask/obs-store": "^9.0.0", diff --git a/ui/pages/notifications/snap/types/types.ts b/ui/pages/notifications/snap/types/types.ts index 20a675b1e6d7..b4fee20411e9 100644 --- a/ui/pages/notifications/snap/types/types.ts +++ b/ui/pages/notifications/snap/types/types.ts @@ -1,8 +1,15 @@ export const SNAP = 'SNAP' as const; +export type SnapNotificationExpandedView = { + title: string; + interfaceId: string; + footerLink?: { href: string; text: string }; +}; + export type RawSnapNotification = { id: string; message: string; + expandedView?: SnapNotificationExpandedView; origin: string; createdDate: number; readDate?: number; From 3d5a9c4b8ba4abe9797b5b64159e09a0862d10da Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 30 Sep 2024 16:32:34 -0400 Subject: [PATCH 05/71] add snap notification modal --- .../snaps/snap-notification-modal/index.ts | 1 + .../snap-notification-modal.tsx | 103 ++++++++++++++++++ .../notification-components/snap/snap.tsx | 55 +++++++--- 3 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 ui/components/app/snaps/snap-notification-modal/index.ts create mode 100644 ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx diff --git a/ui/components/app/snaps/snap-notification-modal/index.ts b/ui/components/app/snaps/snap-notification-modal/index.ts new file mode 100644 index 000000000000..2b6e4d856ed4 --- /dev/null +++ b/ui/components/app/snaps/snap-notification-modal/index.ts @@ -0,0 +1 @@ +export { default } from './snap-notification-modal'; \ No newline at end of file diff --git a/ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx b/ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx new file mode 100644 index 000000000000..cb8650d0f605 --- /dev/null +++ b/ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { Box, ButtonLink, Icon, IconName, IconSize, Modal, ModalContent, ModalHeader, ModalOverlay, Text } from '../../../component-library'; +import { AlignItems, BackgroundColor, BorderRadius, Display, FlexDirection, TextAlign, TextColor, TextVariant } from '../../../../helpers/constants/design-system'; +import { RawSnapNotification } from '../../../../pages/notifications/snap/types/types'; +import { hasProperty } from '@metamask/utils'; +import { SnapIcon } from '../snap-icon'; +import { getSnapMetadata } from '../../../../selectors'; +import { SnapUIMarkdown } from '../snap-ui-markdown'; +import { SnapUIRenderer } from '../snap-ui-renderer'; + +type SnapNotificationModalProps = { + handleClose: () => void; + isOpen: boolean; + data: RawSnapNotification; +} + +const SnapNotificationModal = ({ handleClose, isOpen, data }: SnapNotificationModalProps) => { + const { expandedView } = data; + const hasFooter = hasProperty(expandedView, 'footerLink'); + const { name: snapName } = useSelector((state) => + // @ts-expect-error incorrectly typed + getSnapMetadata(state, data.origin), + ); + + const SnapNotificationModalFooter = () => { + const { footerLink: { href, text } } = expandedView; + return ( + + {text} + + + ) + } + + const SnapNotificationContentHeader = () => { + return ( + + + + {snapName} + + + {data.message} + + + ) + } + + const SnapNotificationContent = () => { + return ( + + + + ) + } + + return ( + + + + + + {data.expandedView.title} + + + + + + + {hasFooter && } + + + ); +} + +export default SnapNotificationModal; \ No newline at end of file diff --git a/ui/pages/notifications/notification-components/snap/snap.tsx b/ui/pages/notifications/notification-components/snap/snap.tsx index 95ccb16e416f..7726cf32d3ed 100644 --- a/ui/pages/notifications/notification-components/snap/snap.tsx +++ b/ui/pages/notifications/notification-components/snap/snap.tsx @@ -1,6 +1,7 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { hasProperty } from '@metamask/utils'; import { MetaMetricsEventCategory, MetaMetricsEventName, @@ -11,6 +12,7 @@ import type { SnapNotification } from '../../snap/types/types'; import { getSnapsMetadata } from '../../../../selectors'; import { markNotificationsAsRead } from '../../../../store/actions'; import { getSnapRoute, getSnapName } from '../../../../helpers/utils/util'; +import SnapNotificationModal from '../../../../components/app/snaps/snap-notification-modal/snap-notification-modal'; type SnapComponentProps = { snapNotification: SnapNotification; @@ -20,11 +22,18 @@ export const SnapComponent = ({ snapNotification }: SnapComponentProps) => { const dispatch = useDispatch(); const history = useHistory(); const trackEvent = useContext(MetaMetricsContext); + const [isOpen, setIsOpen] = useState(false); const snapsMetadata = useSelector(getSnapsMetadata); const snapsNameGetter = getSnapName(snapsMetadata); + const hasExpandedView = hasProperty(snapNotification.data, 'expandedView'); + + const handleClose = () => { + setIsOpen(false); + }; + const handleSnapClick = () => { dispatch(markNotificationsAsRead([snapNotification.id])); trackEvent({ @@ -36,6 +45,9 @@ export const SnapComponent = ({ snapNotification }: SnapComponentProps) => { previously_read: snapNotification.isRead, }, }); + if (hasExpandedView) { + setIsOpen(true); + } }; const handleSnapButton = () => { @@ -53,21 +65,30 @@ export const SnapComponent = ({ snapNotification }: SnapComponentProps) => { }; return ( - + <> + {hasExpandedView && ( + + )} + + ); }; From 306d898aa0f1e9f9512493d2457c35f34d57ea5c Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Wed, 2 Oct 2024 10:14:07 +0200 Subject: [PATCH 06/71] feat(snaps): Connect `getCurrencyRate` hook to `multichainRateController` (#27489) --- app/scripts/metamask-controller.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 30112ee61a3c..3ccab3a3cd69 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -5928,6 +5928,19 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getAll', ), + getCurrencyRate: (currency) => { + const rate = this.multichainRatesController.state.rates[currency]; + const { fiatCurrency } = this.multichainRatesController.state; + + if (!rate) { + return undefined; + } + + return { + ...rate, + currency: fiatCurrency, + }; + }, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hasPermission: this.permissionController.hasPermission.bind( this.permissionController, From f375215a52a79e261fe8b9791459798a7528bbdb Mon Sep 17 00:00:00 2001 From: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:44:47 -0400 Subject: [PATCH 07/71] feat: Integrate MetaMask links into Snaps `Link` component (#27377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? Previously, developers couldn't navigate to parts of the extension with the existing functionality provided to them through snaps UI 2. What is the improvement/solution? The Snaps `Link` component now checks for if the url is a MetaMask url and navigates to the proper path accordingly. ## **Screenshots/Recordings** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 1 + .../app/snaps/snap-ui-link/snap-ui-link.js | 23 ++++++++++++++++++- ui/hooks/snaps/useSnapNavigation.js | 22 ++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 ui/hooks/snaps/useSnapNavigation.js diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3ccab3a3cd69..ffcd750a2f2e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1462,6 +1462,7 @@ export default class MetamaskController extends EventEmitter { `${this.phishingController.name}:testOrigin`, `${this.approvalController.name}:hasRequest`, `${this.approvalController.name}:acceptRequest`, + `${this.snapController.name}:get`, ], }); diff --git a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js index 46439c523a68..a1289543fd45 100644 --- a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js +++ b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js @@ -9,18 +9,39 @@ import { IconSize, } from '../../../component-library'; import SnapLinkWarning from '../snap-link-warning'; +import useSnapNavigation from '../../../../hooks/snaps/useSnapNavigation'; export const SnapUILink = ({ href, children }) => { const [isOpen, setIsOpen] = useState(false); + const isMetaMaskUrl = href.startsWith('metamask:'); + const { navigate } = useSnapNavigation(); + const handleLinkClick = () => { - setIsOpen(true); + if (isMetaMaskUrl) { + navigate(href); + } else { + setIsOpen(true); + } }; const handleModalClose = () => { setIsOpen(false); }; + if (isMetaMaskUrl) { + return ( + + {children} + + ); + } + return ( <> diff --git a/ui/hooks/snaps/useSnapNavigation.js b/ui/hooks/snaps/useSnapNavigation.js new file mode 100644 index 000000000000..00c49dc94d27 --- /dev/null +++ b/ui/hooks/snaps/useSnapNavigation.js @@ -0,0 +1,22 @@ +import { useHistory } from 'react-router-dom'; +import { parseMetaMaskUrl } from '@metamask/snaps-utils'; +import { getSnapRoute } from '../../helpers/utils/util'; + +const useSnapNavigation = () => { + const history = useHistory(); + const navigate = (url) => { + let path; + const linkData = parseMetaMaskUrl(url); + if (linkData.snapId) { + path = getSnapRoute(linkData.snapId); + } else { + path = linkData.path; + } + history.push(path); + }; + return { + navigate, + }; +}; + +export default useSnapNavigation; From aad36dbd0e44d63e3ad9d621e783cba3f6dc6148 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Mon, 7 Oct 2024 23:51:50 -0400 Subject: [PATCH 08/71] integrate snaps expanded view into notification system --- .../snaps/snap-notification-modal/index.ts | 1 - .../snap-notification-modal.tsx | 103 ------------ .../notification-detail-button.tsx | 36 +++-- .../useNotifications.ts | 13 +- .../notification-details-body.tsx | 8 +- .../notification-details-footer.tsx | 5 + .../notification-components/index.ts | 16 +- .../notification-components/node-guard.ts | 5 +- .../notification-components/snap/snap.tsx | 148 +++++++++--------- .../types/notifications/notifications.ts | 25 ++- .../notifications/notifications-list-item.tsx | 22 ++- ui/pages/notifications/notifications-list.tsx | 5 - ui/pages/notifications/notifications.tsx | 7 +- ui/pages/notifications/snap/types/types.ts | 4 +- ui/pages/notifications/snap/utils/utils.ts | 4 +- ui/store/actions.ts | 3 +- 16 files changed, 192 insertions(+), 213 deletions(-) delete mode 100644 ui/components/app/snaps/snap-notification-modal/index.ts delete mode 100644 ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx diff --git a/ui/components/app/snaps/snap-notification-modal/index.ts b/ui/components/app/snaps/snap-notification-modal/index.ts deleted file mode 100644 index 2b6e4d856ed4..000000000000 --- a/ui/components/app/snaps/snap-notification-modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './snap-notification-modal'; \ No newline at end of file diff --git a/ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx b/ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx deleted file mode 100644 index cb8650d0f605..000000000000 --- a/ui/components/app/snaps/snap-notification-modal/snap-notification-modal.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { Box, ButtonLink, Icon, IconName, IconSize, Modal, ModalContent, ModalHeader, ModalOverlay, Text } from '../../../component-library'; -import { AlignItems, BackgroundColor, BorderRadius, Display, FlexDirection, TextAlign, TextColor, TextVariant } from '../../../../helpers/constants/design-system'; -import { RawSnapNotification } from '../../../../pages/notifications/snap/types/types'; -import { hasProperty } from '@metamask/utils'; -import { SnapIcon } from '../snap-icon'; -import { getSnapMetadata } from '../../../../selectors'; -import { SnapUIMarkdown } from '../snap-ui-markdown'; -import { SnapUIRenderer } from '../snap-ui-renderer'; - -type SnapNotificationModalProps = { - handleClose: () => void; - isOpen: boolean; - data: RawSnapNotification; -} - -const SnapNotificationModal = ({ handleClose, isOpen, data }: SnapNotificationModalProps) => { - const { expandedView } = data; - const hasFooter = hasProperty(expandedView, 'footerLink'); - const { name: snapName } = useSelector((state) => - // @ts-expect-error incorrectly typed - getSnapMetadata(state, data.origin), - ); - - const SnapNotificationModalFooter = () => { - const { footerLink: { href, text } } = expandedView; - return ( - - {text} - - - ) - } - - const SnapNotificationContentHeader = () => { - return ( - - - - {snapName} - - - {data.message} - - - ) - } - - const SnapNotificationContent = () => { - return ( - - - - ) - } - - return ( - - - - - - {data.expandedView.title} - - - - - - - {hasFooter && } - - - ); -} - -export default SnapNotificationModal; \ No newline at end of file diff --git a/ui/components/multichain/notification-detail-button/notification-detail-button.tsx b/ui/components/multichain/notification-detail-button/notification-detail-button.tsx index e9b2f8d21903..034fda825692 100644 --- a/ui/components/multichain/notification-detail-button/notification-detail-button.tsx +++ b/ui/components/multichain/notification-detail-button/notification-detail-button.tsx @@ -12,10 +12,13 @@ import { IconName, } from '../../component-library'; import { BlockSize } from '../../../helpers/constants/design-system'; +import { SnapNotification } from '../../../pages/notifications/snap/types/types'; +import { TRIGGER_TYPES } from '../../../pages/notifications/notification-components'; +import useSnapNavigation from '../../../hooks/snaps/useSnapNavigation'; -type Notification = NotificationServicesController.Types.INotification; - -const { TRIGGER_TYPES } = NotificationServicesController.Constants; +type Notification = + | NotificationServicesController.Types.INotification + | SnapNotification; type NotificationDetailButtonProps = { notification: Notification; @@ -37,6 +40,16 @@ export const NotificationDetailButton = ({ endIconName = true, }: NotificationDetailButtonProps) => { const trackEvent = useContext(MetaMetricsContext); + const { navigate } = useSnapNavigation(); + const isMetaMaskUrl = href.startsWith('metamask:'); + + // this logic can be expanded once this detail button is used outside of the current use cases + const getClickedItem = () => { + if (notification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT) { + return 'block_explorer'; + } + return isExternal ? 'external_link' : 'internal_link'; + }; const onClick = () => { trackEvent({ @@ -45,20 +58,25 @@ export const NotificationDetailButton = ({ properties: { notification_id: notification.id, notification_type: notification.type, - ...(notification.type !== TRIGGER_TYPES.FEATURES_ANNOUNCEMENT && { - chain_id: notification?.chain_id, - }), - clicked_item: 'block_explorer', + ...(notification.type !== TRIGGER_TYPES.FEATURES_ANNOUNCEMENT && + notification.type !== TRIGGER_TYPES.SNAP && { + chain_id: notification?.chain_id, + }), + clicked_item: getClickedItem(), }, }); + + if (notification.type === TRIGGER_TYPES.SNAP && isMetaMaskUrl) { + navigate(href); + } }; return ( + `; diff --git a/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap index 9f7014dea03e..e98ec1921081 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/currency.test.tsx.snap @@ -12,7 +12,6 @@ exports[`ConfirmInfoRowCurrency should display in currency passed 1`] = ` > $82.65 @@ -38,7 +37,6 @@ exports[`ConfirmInfoRowCurrency should display value in user preferred currency > 0.14861879 diff --git a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap index 3559558bc2cb..c3958d886710 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap @@ -22,16 +22,16 @@ exports[`ConfirmInfoExpandableRow should match snapshot 1`] = `
+ Expandable Value - Expandable Value
- +
diff --git a/ui/components/app/confirm/info/row/constants.ts b/ui/components/app/confirm/info/row/constants.ts index 415358aa5252..f260f9bce282 100644 --- a/ui/components/app/confirm/info/row/constants.ts +++ b/ui/components/app/confirm/info/row/constants.ts @@ -3,9 +3,8 @@ export const TEST_ADDRESS = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; export enum RowAlertKey { EstimatedFee = 'estimatedFee', SigningInWith = 'signingInWith', - RequestFrom = 'requestFrom', - Resimulation = 'resimulation', Speed = 'speed', + RequestFrom = 'requestFrom', } export enum AlertActionKey { diff --git a/ui/components/app/confirm/info/row/copy-icon.tsx b/ui/components/app/confirm/info/row/copy-icon.tsx index 16f6604a53d4..ce349089dac3 100644 --- a/ui/components/app/confirm/info/row/copy-icon.tsx +++ b/ui/components/app/confirm/info/row/copy-icon.tsx @@ -1,20 +1,12 @@ -import React, { CSSProperties, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useCopyToClipboard } from '../../../../../hooks/useCopyToClipboard'; import { IconColor } from '../../../../../helpers/constants/design-system'; -import { - ButtonIcon, - ButtonIconSize, - IconName, -} from '../../../../component-library'; +import { Icon, IconName, IconSize } from '../../../../component-library'; type CopyCallback = (text: string) => void; -export const CopyIcon: React.FC<{ - copyText: string; - color?: IconColor; - style?: CSSProperties; -}> = ({ copyText, color, style = {} }) => { +export const CopyIcon: React.FC<{ copyText: string }> = ({ copyText }) => { const [copied, handleCopy] = useCopyToClipboard(); const handleClick = useCallback(async () => { @@ -22,19 +14,12 @@ export const CopyIcon: React.FC<{ }, [copyText]); return ( - ); }; diff --git a/ui/components/app/confirm/info/row/expandable-row.tsx b/ui/components/app/confirm/info/row/expandable-row.tsx index a6e88fc17a81..23fa7f7d5fd0 100644 --- a/ui/components/app/confirm/info/row/expandable-row.tsx +++ b/ui/components/app/confirm/info/row/expandable-row.tsx @@ -40,19 +40,19 @@ export const ConfirmInfoExpandableRow = ( <> + {children} - {children} { ); expect(container).toMatchSnapshot(); }); - - it('should be expandable when collapsed is true', () => { - render( - - Some text - , - ); - expect(screen.queryByText('Some text')).not.toBeInTheDocument(); - fireEvent.click(screen.getByTestId('sectionCollapseButton')); - expect(screen.queryByText('Some text')).toBeInTheDocument(); - }); }); diff --git a/ui/components/app/confirm/info/row/row.tsx b/ui/components/app/confirm/info/row/row.tsx index 63d2242461ea..7616ccae5f21 100644 --- a/ui/components/app/confirm/info/row/row.tsx +++ b/ui/components/app/confirm/info/row/row.tsx @@ -1,9 +1,7 @@ -import React, { createContext, useState } from 'react'; +import React, { createContext } from 'react'; import Tooltip from '../../../../ui/tooltip/tooltip'; import { Box, - ButtonIcon, - ButtonIconSize, Icon, IconName, IconSize, @@ -42,7 +40,6 @@ export type ConfirmInfoRowProps = { copyEnabled?: boolean; copyText?: string; 'data-testid'?: string; - collapsed?: boolean; }; const BACKGROUND_COLORS = { @@ -82,101 +79,71 @@ export const ConfirmInfoRow: React.FC = ({ labelChildren, color, copyEnabled = false, - copyText, + copyText = undefined, 'data-testid': dataTestId, - collapsed, -}) => { - const [expanded, setExpanded] = useState(!collapsed); - - const isCollapsible = collapsed !== undefined; - - return ( - +}) => ( + + + {copyEnabled && } - {copyEnabled && ( - - )} - {isCollapsible && ( - setExpanded(!expanded)} - data-testid="sectionCollapseButton" - ariaLabel="collapse-button" - /> - )} - - - - {label} - - {labelChildren} - {!labelChildren && tooltip?.length && ( - - - - )} - + + + {label} + + {labelChildren} + {!labelChildren && tooltip?.length && ( + + + + )} - {expanded && - (typeof children === 'string' ? ( - - {children} - - ) : ( - children - ))} - - ); -}; + {typeof children === 'string' ? ( + + {children} + + ) : ( + children + )} + + +); diff --git a/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap b/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap index 2482a916a5a9..f9823b5af9ac 100644 --- a/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap +++ b/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap @@ -36,7 +36,6 @@ exports[`CurrencyInput Component rendering should disable unit input 1`] = ` > $0.00 @@ -90,7 +89,6 @@ exports[`CurrencyInput Component rendering should render properly with a fiat va > 0.004327880204275946 @@ -185,7 +183,6 @@ exports[`CurrencyInput Component rendering should render properly with an ETH va > $231.06 @@ -240,7 +237,6 @@ exports[`CurrencyInput Component rendering should render properly without a suff > $0.00 diff --git a/ui/components/app/detected-token/detected-token-details/detected-token-details.js b/ui/components/app/detected-token/detected-token-details/detected-token-details.js index fe8f12618305..3fb3219630a1 100644 --- a/ui/components/app/detected-token/detected-token-details/detected-token-details.js +++ b/ui/components/app/detected-token/detected-token-details/detected-token-details.js @@ -1,23 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; -import { - AvatarNetwork, - AvatarNetworkSize, - AvatarToken, - AvatarTokenSize, - BadgeWrapper, - Box, -} from '../../../component-library'; + +import { Box } from '../../../component-library'; +import Identicon from '../../../ui/identicon'; import DetectedTokenValues from '../detected-token-values/detected-token-values'; import DetectedTokenAddress from '../detected-token-address/detected-token-address'; import DetectedTokenAggregators from '../detected-token-aggregators/detected-token-aggregators'; import { Display } from '../../../../helpers/constants/design-system'; -import { - getCurrentNetwork, - getTestNetworkBackgroundColor, - getTokenList, -} from '../../../../selectors'; +import { getTokenList } from '../../../../selectors'; const DetectedTokenDetails = ({ token, @@ -26,8 +17,6 @@ const DetectedTokenDetails = ({ }) => { const tokenList = useSelector(getTokenList); const tokenData = tokenList[token.address?.toLowerCase()]; - const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); - const currentNetwork = useSelector(getCurrentNetwork); return ( - - } - marginRight={2} + - - - + address={token.address} + diameter={40} + /> <0.000001 @@ -27,7 +26,6 @@ exports[`CancelTransactionGasFee Component should render 1`] = ` > <0.000001 diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap index da6f598dfa5e..cfa08b3eee01 100644 --- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap +++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap @@ -706,50 +706,12 @@ exports[`NameDetails renders with recognized name 1`] = `
-
-
-
- - - - - -
-
-
+

diff --git a/ui/components/app/name/name-details/name-details.test.tsx b/ui/components/app/name/name-details/name-details.test.tsx index 8043d432d526..0f0df9f5b5f6 100644 --- a/ui/components/app/name/name-details/name-details.test.tsx +++ b/ui/components/app/name/name-details/name-details.test.tsx @@ -1,21 +1,18 @@ +import React from 'react'; import { NameType } from '@metamask/name-controller'; +import configureStore from 'redux-mock-store'; import { fireEvent } from '@testing-library/react'; -import React from 'react'; import { act } from 'react-dom/test-utils'; -import { useDispatch, useSelector } from 'react-redux'; -import configureStore from 'redux-mock-store'; +import { useDispatch } from 'react-redux'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import { setName, updateProposedNames } from '../../../../store/actions'; +import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; import { CHAIN_IDS } from '../../../../../shared/constants/network'; -import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { mockNetworkState } from '../../../../../test/stub/networks'; -import { MetaMetricsContext } from '../../../../contexts/metametrics'; -import { getDomainResolutions } from '../../../../ducks/domains'; -import { getNames, getNameSources } from '../../../../selectors'; -import { getNftContractsByAddressByChain } from '../../../../selectors/nft'; -import { setName, updateProposedNames } from '../../../../store/actions'; import NameDetails from './name-details'; jest.mock('../../../../store/actions', () => ({ @@ -26,7 +23,6 @@ jest.mock('../../../../store/actions', () => ({ jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useDispatch: jest.fn(), - useSelector: jest.fn(), })); jest.useFakeTimers(); @@ -154,74 +150,10 @@ describe('NameDetails', () => { const setNameMock = jest.mocked(setName); const updateProposedNamesMock = jest.mocked(updateProposedNames); const useDispatchMock = jest.mocked(useDispatch); - const useSelectorMock = jest.mocked(useSelector); beforeEach(() => { jest.resetAllMocks(); useDispatchMock.mockReturnValue(jest.fn()); - - useSelectorMock.mockImplementation((selector) => { - if (selector === getNames) { - return { - [NameType.ETHEREUM_ADDRESS]: { - [ADDRESS_SAVED_NAME_MOCK]: { - [VARIATION_MOCK]: { - name: SAVED_NAME_MOCK, - proposedNames: { - [SOURCE_ID_MOCK]: { - proposedNames: [PROPOSED_NAME_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - [SOURCE_ID_2_MOCK]: { - proposedNames: [PROPOSED_NAME_2_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - }, - }, - }, - [ADDRESS_NO_NAME_MOCK]: { - [VARIATION_MOCK]: { - name: SAVED_NAME_MOCK, - proposedNames: { - [SOURCE_ID_MOCK]: { - proposedNames: [PROPOSED_NAME_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - [SOURCE_ID_2_MOCK]: { - proposedNames: [PROPOSED_NAME_2_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - }, - }, - }, - }, - }; - } else if (selector === getNftContractsByAddressByChain) { - return { - [VARIATION_MOCK]: { - [ADDRESS_RECOGNIZED_MOCK]: { - name: 'iZUMi Bond USD', - }, - }, - }; - } else if (selector === getDomainResolutions) { - return [ - { - resolvedAddress: ADDRESS_SAVED_NAME_MOCK, - domainName: 'Domain name', - }, - ]; - } else if (selector === getNameSources) { - return { - [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, - }; - } - return undefined; - }); }); it('renders when no address value is passed', () => { @@ -239,33 +171,6 @@ describe('NameDetails', () => { }); it('renders with no saved name', () => { - useSelectorMock.mockImplementation((selector) => { - if (selector === getNames) { - return { - [NameType.ETHEREUM_ADDRESS]: {}, - }; - } else if (selector === getNftContractsByAddressByChain) { - return { - [VARIATION_MOCK]: { - [ADDRESS_RECOGNIZED_MOCK]: { - name: 'iZUMi Bond USD', - }, - }, - }; - } else if (selector === getDomainResolutions) { - return [ - { - resolvedAddress: ADDRESS_SAVED_NAME_MOCK, - }, - ]; - } else if (selector === getNameSources) { - return { - [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, - }; - } - return undefined; - }); - const { baseElement } = renderWithProvider( { }); it('sends created event', async () => { - useSelectorMock.mockImplementation((selector) => { - if (selector === getNames) { - return { - [NameType.ETHEREUM_ADDRESS]: { - [ADDRESS_NO_NAME_MOCK]: { - [VARIATION_MOCK]: { - proposedNames: { - [SOURCE_ID_MOCK]: { - proposedNames: [PROPOSED_NAME_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - [SOURCE_ID_2_MOCK]: { - proposedNames: [PROPOSED_NAME_2_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - }, - }, - }, - }, - }; - } else if (selector === getNftContractsByAddressByChain) { - return { - [VARIATION_MOCK]: { - [ADDRESS_RECOGNIZED_MOCK]: { - name: 'iZUMi Bond USD', - }, - }, - }; - } else if (selector === getDomainResolutions) { - return [ - { - resolvedAddress: ADDRESS_SAVED_NAME_MOCK, - }, - ]; - } else if (selector === getNameSources) { - return { - [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, - }; - } - return undefined; - }); - const trackEventMock = jest.fn(); const component = renderWithProvider( @@ -563,7 +424,7 @@ describe('NameDetails', () => { await saveNameUsingDropdown(component, PROPOSED_NAME_MOCK); - expect(trackEventMock).toHaveBeenNthCalledWith(2, { + expect(trackEventMock).toHaveBeenCalledWith({ event: MetaMetricsEventName.PetnameCreated, category: MetaMetricsEventCategory.Petnames, properties: { @@ -575,69 +436,6 @@ describe('NameDetails', () => { }); it('sends updated event', async () => { - useSelectorMock.mockImplementation((selector) => { - if (selector === getNames) { - return { - [NameType.ETHEREUM_ADDRESS]: { - [ADDRESS_SAVED_NAME_MOCK]: { - [CHAIN_ID_MOCK]: { - proposedNames: { - [SOURCE_ID_MOCK]: { - proposedNames: [PROPOSED_NAME_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - [SOURCE_ID_2_MOCK]: { - proposedNames: [PROPOSED_NAME_2_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - }, - name: SAVED_NAME_MOCK, - sourceId: SOURCE_ID_MOCK, - }, - }, - [ADDRESS_NO_NAME_MOCK]: { - [CHAIN_ID_MOCK]: { - proposedNames: { - [SOURCE_ID_MOCK]: { - proposedNames: [PROPOSED_NAME_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - [SOURCE_ID_2_MOCK]: { - proposedNames: [PROPOSED_NAME_2_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - }, - name: null, - }, - }, - }, - }; - } else if (selector === getNftContractsByAddressByChain) { - return { - [VARIATION_MOCK]: { - [ADDRESS_RECOGNIZED_MOCK]: { - name: 'iZUMi Bond USD', - }, - }, - }; - } else if (selector === getDomainResolutions) { - return [ - { - resolvedAddress: ADDRESS_SAVED_NAME_MOCK, - }, - ]; - } else if (selector === getNameSources) { - return { - [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, - }; - } - return undefined; - }); - const trackEventMock = jest.fn(); const component = renderWithProvider( @@ -654,7 +452,7 @@ describe('NameDetails', () => { await saveNameUsingDropdown(component, PROPOSED_NAME_2_MOCK); - expect(trackEventMock).toHaveBeenNthCalledWith(2, { + expect(trackEventMock).toHaveBeenCalledWith({ event: MetaMetricsEventName.PetnameUpdated, category: MetaMetricsEventCategory.Petnames, properties: { @@ -667,69 +465,6 @@ describe('NameDetails', () => { }); it('sends deleted event', async () => { - useSelectorMock.mockImplementation((selector) => { - if (selector === getNames) { - return { - [NameType.ETHEREUM_ADDRESS]: { - [ADDRESS_SAVED_NAME_MOCK]: { - [CHAIN_ID_MOCK]: { - proposedNames: { - [SOURCE_ID_MOCK]: { - proposedNames: [PROPOSED_NAME_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - [SOURCE_ID_2_MOCK]: { - proposedNames: [PROPOSED_NAME_2_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - }, - name: SAVED_NAME_MOCK, - sourceId: SOURCE_ID_MOCK, - }, - }, - [ADDRESS_NO_NAME_MOCK]: { - [CHAIN_ID_MOCK]: { - proposedNames: { - [SOURCE_ID_MOCK]: { - proposedNames: [PROPOSED_NAME_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - [SOURCE_ID_2_MOCK]: { - proposedNames: [PROPOSED_NAME_2_MOCK], - lastRequestTime: null, - retryDelay: null, - }, - }, - name: null, - }, - }, - }, - }; - } else if (selector === getNftContractsByAddressByChain) { - return { - [VARIATION_MOCK]: { - [ADDRESS_RECOGNIZED_MOCK]: { - name: 'iZUMi Bond USD', - }, - }, - }; - } else if (selector === getDomainResolutions) { - return [ - { - resolvedAddress: ADDRESS_SAVED_NAME_MOCK, - }, - ]; - } else if (selector === getNameSources) { - return { - [SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' }, - }; - } - return undefined; - }); - const trackEventMock = jest.fn(); const component = renderWithProvider( @@ -746,7 +481,7 @@ describe('NameDetails', () => { await saveNameUsingTextField(component, ''); - expect(trackEventMock).toHaveBeenNthCalledWith(2, { + expect(trackEventMock).toHaveBeenCalledWith({ event: MetaMetricsEventName.PetnameDeleted, category: MetaMetricsEventCategory.Petnames, properties: { diff --git a/ui/components/app/name/name.stories.tsx b/ui/components/app/name/name.stories.tsx index 732c9059b530..fb23334a8776 100644 --- a/ui/components/app/name/name.stories.tsx +++ b/ui/components/app/name/name.stories.tsx @@ -3,75 +3,108 @@ import React from 'react'; import { NameType } from '@metamask/name-controller'; import { Provider } from 'react-redux'; import configureStore from '../../../store/store'; -import Name, { NameProps } from './name'; -import mockState from '../../../../test/data/mock-state.json'; -import { - EXPERIENCES_TYPE, - FIRST_PARTY_CONTRACT_NAMES, -} from '../../../../shared/constants/first-party-contracts'; -import { cloneDeep } from 'lodash'; +import Name from './name'; +import { mockNetworkState } from '../../../../test/stub/networks'; -const ADDRESS_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54978'; -const ADDRESS_NFT_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54979'; -const VARIATION_MOCK = '0x1'; -const NAME_MOCK = 'Saved Name'; +const addressNoSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54978'; +const addressSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54977'; +const addressSavedTokenMock = '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d'; +const addressUnsavedTokenMock = '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8'; +const chainIdMock = '0x1'; -const ADDRESS_FIRST_PARTY_MOCK = - FIRST_PARTY_CONTRACT_NAMES[EXPERIENCES_TYPE.METAMASK_BRIDGE][ - VARIATION_MOCK - ].toLowerCase(); - -const PROPOSED_NAMES_MOCK = { - ens: { - proposedNames: ['test.eth'], - lastRequestTime: 123, - retryDelay: null, - }, - etherscan: { - proposedNames: ['TestContract'], - lastRequestTime: 123, - retryDelay: null, - }, - token: { - proposedNames: ['Test Token'], - lastRequestTime: 123, - retryDelay: null, - }, - lens: { - proposedNames: ['test.lens'], - lastRequestTime: 123, - retryDelay: null, - }, -}; - -const STATE_MOCK = { - ...mockState, +const storeMock = configureStore({ metamask: { - ...mockState.metamask, + ...mockNetworkState({chainId: chainIdMock}), useTokenDetection: true, - tokensChainsCache: {}, + tokenList: { + '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': { + address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d', + symbol: 'IUSD', + name: 'iZUMi Bond USD', + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', + }, + '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8': { + address: '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8', + symbol: 'USX', + name: 'dForce USD', + iconUrl: + 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8.png', + }, + }, names: { [NameType.ETHEREUM_ADDRESS]: { - [ADDRESS_MOCK]: { - [VARIATION_MOCK]: { - proposedNames: PROPOSED_NAMES_MOCK, + [addressNoSavedNameMock]: { + [chainIdMock]: { + proposedNames: { + ens: { + proposedNames: ['test.eth'], + lastRequestTime: 123, + retryDelay: null, + }, + etherscan: { + proposedNames: ['TestContract'], + lastRequestTime: 123, + retryDelay: null, + }, + token: { + proposedNames: ['Test Token'], + lastRequestTime: 123, + retryDelay: null, + }, + lens: { + proposedNames: ['test.lens'], + lastRequestTime: 123, + retryDelay: null, + }, + }, }, }, - [ADDRESS_NFT_MOCK]: { - [VARIATION_MOCK]: { - proposedNames: PROPOSED_NAMES_MOCK, + [addressSavedNameMock]: { + [chainIdMock]: { + proposedNames: { + ens: { + proposedNames: ['test.eth'], + lastRequestTime: 123, + retryDelay: null, + }, + etherscan: { + proposedNames: ['TestContract'], + lastRequestTime: 123, + retryDelay: null, + }, + token: { + proposedNames: ['Test Token'], + lastRequestTime: 123, + retryDelay: null, + }, + lens: { + proposedNames: ['test.lens'], + lastRequestTime: 123, + retryDelay: null, + }, + }, + name: 'Test Token', + sourceId: 'token', }, }, - [ADDRESS_FIRST_PARTY_MOCK]: { - [VARIATION_MOCK]: { - proposedNames: PROPOSED_NAMES_MOCK, + [addressSavedTokenMock]: { + [chainIdMock]: { + proposedNames: {}, + name: 'Saved Token Name', + sourceId: 'token', }, }, }, }, - nameSources: {}, + nameSources: { + ens: { label: 'Ethereum Name Service (ENS)' }, + etherscan: { label: 'Etherscan (Verified Contract Name)' }, + token: { label: 'Blockchain (Token Name)' }, + lens: { label: 'Lens Protocol' }, + }, }, -}; +}); /** * Displays the saved name for a raw value such as an Ethereum address.

@@ -92,10 +125,6 @@ export default { description: `The type of value.

Limited to the values in the \`NameType\` enum.`, }, - variation: { - control: 'text', - description: `The variation of the value.

For example, the chain ID if the type is Ethereum address.`, - }, disableEdit: { control: 'boolean', description: `Whether to prevent the modal from opening when the component is clicked.`, @@ -105,141 +134,68 @@ export default { }, }, args: { - value: ADDRESS_MOCK, + value: addressNoSavedNameMock, type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, disableEdit: false, }, - render: ({ state, ...args }) => { - const finalState = cloneDeep(STATE_MOCK); - state?.(finalState); - - return ( - - - - ); - }, + decorators: [(story) => {story()}], }; +// eslint-disable-next-line jsdoc/require-param /** * No name has been saved for the value and type. */ -export const NoSavedName = { - name: 'No Saved Name', - args: { - value: ADDRESS_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - }, +export const DefaultStory = (args) => { + return ; }; +DefaultStory.storyName = 'No Saved Name'; + /** * A name was previously saved for this value and type.

* The component will still display a modal when clicked to edit the name. */ -export const SavedNameStory = { - name: 'Saved Name', - args: { - value: ADDRESS_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - state: (state) => { - state.metamask.names[NameType.ETHEREUM_ADDRESS][ADDRESS_MOCK][ - VARIATION_MOCK - ].name = NAME_MOCK; - }, - }, +export const SavedNameStory = () => { + return ; }; +SavedNameStory.storyName = 'Saved Name'; + /** * No name was previously saved for this recognized token.

* The component will still display a modal when clicked to edit the name. */ -export const DefaultTokenNameStory = { - name: 'Default ERC-20 Token Name', - args: { - value: ADDRESS_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - state: (state) => { - state.metamask.tokensChainsCache = { - [VARIATION_MOCK]: { - data: { - [ADDRESS_MOCK]: { - address: ADDRESS_MOCK, - symbol: 'IUSD', - name: 'iZUMi Bond USD', - iconUrl: - 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png', - }, - }, - }, - }; - }, - }, +export const UnsavedTokenNameStory = () => { + return ( + + ); }; -/** - * No name was previously saved for this watched NFT.

- * The component will still display a modal when clicked to edit the name. - */ -export const DefaultWatchedNFTNameStory = { - name: 'Default Watched NFT Name', - args: { - value: ADDRESS_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - state: (state) => { - state.metamask.allNftContracts = { - '0x123': { - [VARIATION_MOCK]: [ - { - address: ADDRESS_MOCK, - name: 'Everything I Own', - }, - ], - }, - }; - }, - }, -}; +UnsavedTokenNameStory.storyName = 'Unsaved Token Name'; /** - * No name was previously saved for this recognized NFT.

+ * A name was previously saved for this recognized token.

* The component will still display a modal when clicked to edit the name. */ -export const DefaultNFTNameStory = { - name: 'Default NFT Name', - args: { - value: ADDRESS_NFT_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - }, +export const SavedTokenNameStory = () => { + return ( + + ); }; -/** - * No name was previously saved for this first-party contract.

- * The component will still display a modal when clicked to edit the name. - */ -export const DefaultFirstPartyNameStory = { - name: 'Default First-Party Name', - args: { - value: ADDRESS_FIRST_PARTY_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - }, -}; +SavedTokenNameStory.storyName = 'Saved Token Name'; /** * Clicking the component will not display a modal to edit the name. */ -export const EditDisabledStory = { - name: 'Edit Disabled', - args: { - value: ADDRESS_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - disableEdit: true, - }, +export const EditDisabledStory = () => { + return ( + + ); }; + +EditDisabledStory.storyName = 'Edit Disabled'; diff --git a/ui/components/app/selected-account/selected-account-component.test.js b/ui/components/app/selected-account/selected-account-component.test.js index 9545580bebbb..290e737c0808 100644 --- a/ui/components/app/selected-account/selected-account-component.test.js +++ b/ui/components/app/selected-account/selected-account-component.test.js @@ -111,7 +111,7 @@ describe('SelectedAccount Component', () => { const tooltipTitle = await waitFor(() => container.querySelector( - '[data-original-title="This account is not set up for use with Goerli"]', + '[data-original-title="This account is not set up for use with goerli"]', ), ); diff --git a/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap b/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap index deb95bd48555..7dd3749a6e5f 100644 --- a/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap +++ b/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap @@ -46,7 +46,6 @@ exports[`SnapUIAddress renders Bitcoin address 1`] = `

128Lkh3...Mp8p6

@@ -68,7 +67,6 @@ exports[`SnapUIAddress renders Bitcoin address with blockie 1`] = ` />

128Lkh3...Mp8p6

@@ -122,7 +120,6 @@ exports[`SnapUIAddress renders Cosmos address 1`] = `

cosmos1...6hdc0

@@ -144,7 +141,6 @@ exports[`SnapUIAddress renders Cosmos address with blockie 1`] = ` />

cosmos1...6hdc0

@@ -198,7 +194,6 @@ exports[`SnapUIAddress renders Ethereum address 1`] = `

0xab16a...Bfcdb

@@ -220,7 +215,6 @@ exports[`SnapUIAddress renders Ethereum address with blockie 1`] = ` />

0xab16a...Bfcdb

@@ -274,7 +268,6 @@ exports[`SnapUIAddress renders Hedera address 1`] = `

0.0.123...zbhlt

@@ -296,7 +289,6 @@ exports[`SnapUIAddress renders Hedera address with blockie 1`] = ` />

0.0.123...zbhlt

@@ -350,7 +342,6 @@ exports[`SnapUIAddress renders Polkadot address 1`] = `

5hmuyxw...egmfy

@@ -372,7 +363,6 @@ exports[`SnapUIAddress renders Polkadot address with blockie 1`] = ` />

5hmuyxw...egmfy

@@ -426,7 +416,6 @@ exports[`SnapUIAddress renders Starknet address 1`] = `

0x02dd1...0ab57

@@ -448,7 +437,6 @@ exports[`SnapUIAddress renders Starknet address with blockie 1`] = ` />

0x02dd1...0ab57

@@ -502,7 +490,6 @@ exports[`SnapUIAddress renders legacy Ethereum address 1`] = `

0xab16a...Bfcdb

diff --git a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx index d39cb5adf321..35f1af2ad414 100644 --- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx +++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx @@ -9,29 +9,21 @@ import { AlignItems, Display, TextColor, - TextVariant, } from '../../../../helpers/constants/design-system'; import { shortenAddress } from '../../../../helpers/utils/util'; import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils'; import { SnapUIAvatar } from '../snap-ui-avatar'; -import { useDisplayName } from '../../../../hooks/snaps/useDisplayName'; export type SnapUIAddressProps = { // The address must be a CAIP-10 string. address: string; // This is not currently exposed to Snaps. avatarSize?: 'xs' | 'sm' | 'md' | 'lg'; - truncate?: boolean; - displayName?: boolean; - avatar?: boolean; }; export const SnapUIAddress: React.FunctionComponent = ({ address, avatarSize = 'md', - truncate = true, - displayName = false, - avatar = true, }) => { const caipIdentifier = useMemo(() => { if (isHexString(address)) { @@ -48,17 +40,12 @@ export const SnapUIAddress: React.FunctionComponent = ({ [caipIdentifier], ); - const name = useDisplayName(parsed); - // For EVM addresses, we make sure they are checksummed. const transformedAddress = parsed.chain.namespace === 'eip155' ? toChecksumHexAddress(parsed.address) : parsed.address; - - const formattedAddress = truncate - ? shortenAddress(transformedAddress) - : address; + const shortenedAddress = shortenAddress(transformedAddress); return ( = ({ alignItems={AlignItems.center} gap={2} > - {avatar && } - - {displayName && name ? name : formattedAddress} - + + {shortenedAddress} ); }; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/address.ts b/ui/components/app/snaps/snap-ui-renderer/components/address.ts index 908890ecef9f..1e39966df760 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/address.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/address.ts @@ -6,8 +6,5 @@ export const address: UIComponentFactory = ({ element }) => ({ props: { address: element.props.address, avatarSize: 'xs', - truncate: element.props.truncate, - displayName: element.props.displayName, - avatar: element.props.avatar, }, }); diff --git a/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap b/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap index 4a9fc4d3cf7a..b29efce542e3 100644 --- a/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap +++ b/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap @@ -8,7 +8,6 @@ exports[`UserPreferencedCurrencyDisplay Component rendering should match snapsho > 0 diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 779309858a18..4db61d568f4a 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -16,7 +16,6 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< showCurrencySuffix?: boolean; shouldCheckShowNativeToken?: boolean; isAggregatedFiatOverviewBalance?: boolean; - privacyMode?: boolean; } >; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index a466f7813672..613b731d0a16 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -28,7 +28,6 @@ export default function UserPreferencedCurrencyDisplay({ showNative, showCurrencySuffix, shouldCheckShowNativeToken, - privacyMode = false, ...restProps }) { // NOTE: When displaying currencies, we need the actual account to detect whether we're in a @@ -84,7 +83,6 @@ export default function UserPreferencedCurrencyDisplay({ numberOfDecimals={numberOfDecimals} prefixComponent={prefixComponent} suffix={showCurrencySuffix && !showEthLogo && currency} - privacyMode={privacyMode} /> ); } @@ -128,7 +126,6 @@ const UserPreferencedCurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, shouldCheckShowNativeToken: PropTypes.bool, - privacyMode: PropTypes.bool, }; UserPreferencedCurrencyDisplay.propTypes = diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx index 8da096151908..95e0d92fa2b8 100644 --- a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx @@ -7,7 +7,6 @@ import { getSelectedAccount, getShouldHideZeroBalanceTokens, getTokensMarketData, - getPreferences, } from '../../../selectors'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; @@ -23,7 +22,6 @@ jest.mock('../../../ducks/locale/locale', () => ({ jest.mock('../../../selectors', () => ({ getCurrentCurrency: jest.fn(), getSelectedAccount: jest.fn(), - getPreferences: jest.fn(), getShouldHideZeroBalanceTokens: jest.fn(), getTokensMarketData: jest.fn(), })); @@ -34,7 +32,6 @@ jest.mock('../../../hooks/useAccountTotalFiatBalance', () => ({ const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock; const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock; -const mockGetPreferences = getPreferences as jest.Mock; const mockGetSelectedAccount = getSelectedAccount as unknown as jest.Mock; const mockGetShouldHideZeroBalanceTokens = getShouldHideZeroBalanceTokens as jest.Mock; @@ -162,7 +159,6 @@ describe('AggregatedPercentageOverview', () => { beforeEach(() => { mockGetIntlLocale.mockReturnValue('en-US'); mockGetCurrentCurrency.mockReturnValue('USD'); - mockGetPreferences.mockReturnValue({ privacyMode: false }); mockGetSelectedAccount.mockReturnValue(selectedAccountMock); mockGetShouldHideZeroBalanceTokens.mockReturnValue(false); mockGetTokensMarketData.mockReturnValue(marketDataMock); diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx index 8c609610daa1..94555d3bc0cd 100644 --- a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx +++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx @@ -7,7 +7,6 @@ import { getSelectedAccount, getShouldHideZeroBalanceTokens, getTokensMarketData, - getPreferences, } from '../../../selectors'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; @@ -20,7 +19,7 @@ import { TextColor, TextVariant, } from '../../../helpers/constants/design-system'; -import { Box, SensitiveText } from '../../component-library'; +import { Box, Text } from '../../component-library'; import { getCalculatedTokenAmount1dAgo } from '../../../helpers/utils/util'; // core already has this exported type but its not yet available in this version @@ -35,7 +34,6 @@ export const AggregatedPercentageOverview = () => { useSelector(getTokensMarketData); const locale = useSelector(getIntlLocale); const fiatCurrency = useSelector(getCurrentCurrency); - const { privacyMode } = useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, @@ -112,7 +110,7 @@ export const AggregatedPercentageOverview = () => { let color = TextColor.textDefault; - if (!privacyMode && isValidAmount(amountChange)) { + if (isValidAmount(amountChange)) { if ((amountChange as number) === 0) { color = TextColor.textDefault; } else if ((amountChange as number) > 0) { @@ -120,33 +118,26 @@ export const AggregatedPercentageOverview = () => { } else { color = TextColor.errorDefault; } - } else { - color = TextColor.textAlternative; } - return ( - {formattedAmountChange} - - + {formattedPercentChange} - + ); }; diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index 62e6f5ff82b3..abff2cb2b239 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -11,33 +11,14 @@ import { MultichainNetworks } from '../../../../shared/constants/multichain/netw import { RampsMetaMaskEntry } from '../../../hooks/ramps/useRamps/useRamps'; import { defaultBuyableChains } from '../../../ducks/ramps/constants'; import { setBackgroundConnection } from '../../../store/background-connection'; -import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../../shared/constants/metametrics'; import BtcOverview from './btc-overview'; -// We need to mock `dispatch` since we use it for `setDefaultHomeActiveTabName`. -const mockDispatch = jest.fn().mockReturnValue(() => jest.fn()); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useDispatch: () => mockDispatch, -})); - -jest.mock('../../../store/actions', () => ({ - handleSnapRequest: jest.fn(), - sendMultichainTransaction: jest.fn(), - setDefaultHomeActiveTabName: jest.fn(), -})); - const PORTOFOLIO_URL = 'https://portfolio.test'; const BTC_OVERVIEW_BUY = 'coin-overview-buy'; const BTC_OVERVIEW_BRIDGE = 'coin-overview-bridge'; const BTC_OVERVIEW_RECEIVE = 'coin-overview-receive'; const BTC_OVERVIEW_SWAP = 'token-overview-button-swap'; -const BTC_OVERVIEW_SEND = 'coin-overview-send'; const BTC_OVERVIEW_PRIMARY_CURRENCY = 'coin-overview__primary-currency'; const mockMetaMetricsId = 'deadbeef'; @@ -247,110 +228,9 @@ describe('BtcOverview', () => { }); }); - it('sends an event when clicking the Buy button', () => { - const storeWithBtcBuyable = getStore({ - ramps: { - buyableChains: mockBuyableChainsWithBtc, - }, - }); - - const mockTrackEvent = jest.fn(); - const { queryByTestId } = renderWithProvider( - - - , - storeWithBtcBuyable, - ); - - const buyButton = queryByTestId(BTC_OVERVIEW_BUY); - expect(buyButton).toBeInTheDocument(); - expect(buyButton).not.toBeDisabled(); - fireEvent.click(buyButton as HTMLElement); - - expect(mockTrackEvent).toHaveBeenCalledWith({ - event: MetaMetricsEventName.NavBuyButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - account_type: mockNonEvmAccount.type, - chain_id: MultichainNetworks.BITCOIN, - location: 'Home', - snap_id: mockNonEvmAccount.metadata.snap.id, - text: 'Buy', - }, - }); - }); - it('always show the Receive button', () => { const { queryByTestId } = renderWithProvider(, getStore()); const receiveButton = queryByTestId(BTC_OVERVIEW_RECEIVE); expect(receiveButton).toBeInTheDocument(); }); - - it('"Buy & Sell" button is disabled for testnet accounts', () => { - const storeWithBtcBuyable = getStore({ - metamask: { - ...mockMetamaskStore, - internalAccounts: { - ...mockMetamaskStore.internalAccounts, - accounts: { - [mockNonEvmAccount.id]: { - ...mockNonEvmAccount, - address: 'tb1q9lakrt5sw0w0twnc6ww4vxs7hm0q23e03286k8', - }, - }, - }, - }, - ramps: { - buyableChains: mockBuyableChainsWithBtc, - }, - }); - - const { queryByTestId } = renderWithProvider( - , - storeWithBtcBuyable, - ); - - const buyButton = queryByTestId(BTC_OVERVIEW_BUY); - - expect(buyButton).toBeInTheDocument(); - expect(buyButton).toBeDisabled(); - }); - - it('always show the Send button', () => { - const { queryByTestId } = renderWithProvider(, getStore()); - const sendButton = queryByTestId(BTC_OVERVIEW_SEND); - expect(sendButton).toBeInTheDocument(); - expect(sendButton).not.toBeDisabled(); - }); - - it('sends an event when clicking the Send button', () => { - const mockTrackEvent = jest.fn(); - const { queryByTestId } = renderWithProvider( - - - , - getStore(), - ); - - const sendButton = queryByTestId(BTC_OVERVIEW_SEND); - expect(sendButton).toBeInTheDocument(); - expect(sendButton).not.toBeDisabled(); - fireEvent.click(sendButton as HTMLElement); - - expect(mockTrackEvent).toHaveBeenCalledWith( - { - event: MetaMetricsEventName.NavSendButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - account_type: mockNonEvmAccount.type, - chain_id: MultichainNetworks.BITCOIN, - location: 'Home', - snap_id: mockNonEvmAccount.metadata.snap.id, - text: 'Send', - token_symbol: 'BTC', - }, - }, - expect.any(Object), - ); - }); }); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx index fb315d3ab3b0..dc47df7567b5 100644 --- a/ui/components/app/wallet-overview/btc-overview.tsx +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -1,17 +1,12 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - getMultichainIsMainnet, - ///: END:ONLY_INCLUDE_IF getMultichainProviderConfig, getMultichainSelectedAccountCachedBalance, } from '../../../selectors/multichain'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsBitcoinBuyable } from '../../../ducks/ramps'; -import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; ///: END:ONLY_INCLUDE_IF -import { getSelectedInternalAccount } from '../../../selectors'; import { CoinOverview } from './coin-overview'; type BtcOverviewProps = { @@ -21,18 +16,12 @@ type BtcOverviewProps = { const BtcOverview = ({ className }: BtcOverviewProps) => { const { chainId } = useSelector(getMultichainProviderConfig); const balance = useSelector(getMultichainSelectedAccountCachedBalance); - const account = useSelector(getSelectedInternalAccount); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - const isBtcMainnetAccount = useMultichainSelector( - getMultichainIsMainnet, - account, - ); const isBtcBuyable = useSelector(getIsBitcoinBuyable); ///: END:ONLY_INCLUDE_IF return ( { isSwapsChain={false} ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain={false} - isBuyableChain={isBtcBuyable && isBtcMainnetAccount} + isBuyableChain={isBtcBuyable} ///: END:ONLY_INCLUDE_IF /> ); diff --git a/ui/components/app/wallet-overview/coin-buttons.stories.js b/ui/components/app/wallet-overview/coin-buttons.stories.js index 028ebeee5b40..40a6879673a8 100644 --- a/ui/components/app/wallet-overview/coin-buttons.stories.js +++ b/ui/components/app/wallet-overview/coin-buttons.stories.js @@ -1,13 +1,9 @@ import React from 'react'; -import testData from '../../../../.storybook/test-data'; import CoinButtons from './coin-buttons'; -const { accounts, selectedAccount } = testData.metamask.internalAccounts; - export default { title: 'Components/App/WalletOverview/CoinButtons', args: { - account: accounts[selectedAccount], chainId: '1', trackingLocation: 'home', isSwapsChain: true, diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 3a499aa44dbf..bac7872c79e3 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -24,7 +24,7 @@ import { } from '@metamask/utils'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) -import { BtcAccountType, InternalAccount } from '@metamask/keyring-api'; +import { BtcAccountType } from '@metamask/keyring-api'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { ChainId } from '../../../../shared/constants/network'; @@ -51,6 +51,7 @@ import { getCurrentKeyring, ///: END:ONLY_INCLUDE_IF getUseExternalServices, + getSelectedAccount, ///: BEGIN:ONLY_INCLUDE_IF(build-flask) getMemoizedUnapprovedTemplatedConfirmations, ///: END:ONLY_INCLUDE_IF @@ -89,29 +90,8 @@ import { } from '../../../store/actions'; import { BITCOIN_WALLET_SNAP_ID } from '../../../../shared/lib/accounts/bitcoin-wallet-snap'; ///: END:ONLY_INCLUDE_IF -import { - getMultichainIsEvm, - getMultichainNativeCurrency, -} from '../../../selectors/multichain'; -import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; - -type CoinButtonsProps = { - account: InternalAccount; - chainId: `0x${string}` | CaipChainId | number; - trackingLocation: string; - isSwapsChain: boolean; - isSigningEnabled: boolean; - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - isBridgeChain: boolean; - isBuyableChain: boolean; - defaultSwapsToken?: SwapsEthToken; - ///: END:ONLY_INCLUDE_IF - classPrefix?: string; - iconButtonClassName?: string; -}; const CoinButtons = ({ - account, chainId, trackingLocation, isSwapsChain, @@ -123,13 +103,26 @@ const CoinButtons = ({ ///: END:ONLY_INCLUDE_IF classPrefix = 'coin', iconButtonClassName = '', -}: CoinButtonsProps) => { +}: { + chainId: `0x${string}` | CaipChainId | number; + trackingLocation: string; + isSwapsChain: boolean; + isSigningEnabled: boolean; + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + isBridgeChain: boolean; + isBuyableChain: boolean; + defaultSwapsToken?: SwapsEthToken; + ///: END:ONLY_INCLUDE_IF + classPrefix?: string; + iconButtonClassName?: string; +}) => { const t = useContext(I18nContext); const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); const [showReceiveModal, setShowReceiveModal] = useState(false); + const account = useSelector(getSelectedAccount); const { address: selectedAddress } = account; const history = useHistory(); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -138,16 +131,6 @@ const CoinButtons = ({ const usingHardwareWallet = isHardwareKeyring(keyring?.type); ///: END:ONLY_INCLUDE_IF - // Initially, those events were using a "ETH" as `token_symbol`, so we keep this behavior - // for EVM, no matter the currently selected native token (e.g. SepoliaETH if you are on Sepolia - // network). - const isEvm = useMultichainSelector(getMultichainIsEvm, account); - const multichainNativeToken = useMultichainSelector( - getMultichainNativeCurrency, - account, - ); - const nativeToken = isEvm ? 'ETH' : multichainNativeToken; - const isExternalServicesEnabled = useSelector(getUseExternalServices); const buttonTooltips = { @@ -197,23 +180,6 @@ const CoinButtons = ({ }; ///: END:ONLY_INCLUDE_IF - const getSnapAccountMetaMetricsPropertiesIfAny = ( - internalAccount: InternalAccount, - ): { snap_id?: string } => { - // Some accounts might be Snap accounts, in this case we add some extra properties - // to the metrics: - const snapId = internalAccount.metadata.snap?.id; - if (snapId) { - return { - snap_id: snapId, - }; - } - - // If the account is not a Snap account or that we could not get the Snap ID for - // some reason, we don't add any extra property. - return {}; - }; - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) const mmiPortfolioEnabled = useSelector(getMmiPortfolioEnabled); const mmiPortfolioUrl = useSelector(getMmiPortfolioUrl); @@ -310,21 +276,6 @@ const CoinButtons = ({ ///: END:ONLY_INCLUDE_IF const handleSendOnClick = useCallback(async () => { - trackEvent( - { - event: MetaMetricsEventName.NavSendButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - account_type: account.type, - token_symbol: nativeToken, - location: 'Home', - text: 'Send', - chain_id: chainId, - ...getSnapAccountMetaMetricsPropertiesIfAny(account), - }, - }, - { excludeMetaMetricsId: false }, - ); switch (account.type) { ///: BEGIN:ONLY_INCLUDE_IF(build-flask) case BtcAccountType.P2wpkh: { @@ -340,6 +291,19 @@ const CoinButtons = ({ } ///: END:ONLY_INCLUDE_IF default: { + trackEvent( + { + event: MetaMetricsEventName.NavSendButtonClicked, + category: MetaMetricsEventCategory.Navigation, + properties: { + token_symbol: 'ETH', + location: 'Home', + text: 'Send', + chain_id: chainId, + }, + }, + { excludeMetaMetricsId: false }, + ); await dispatch(startNewDraftTransaction({ type: AssetType.native })); history.push(SEND_ROUTE); } @@ -394,12 +358,10 @@ const CoinButtons = ({ event: MetaMetricsEventName.NavBuyButtonClicked, category: MetaMetricsEventCategory.Navigation, properties: { - account_type: account.type, location: 'Home', text: 'Buy', chain_id: chainId, token_symbol: defaultSwapsToken, - ...getSnapAccountMetaMetricsPropertiesIfAny(account), }, }); }, [chainId, defaultSwapsToken]); diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 2244f3b33e82..2de787ef23c0 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -11,7 +11,6 @@ import { zeroAddress } from 'ethereumjs-util'; import { CaipChainId } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import { InternalAccount } from '@metamask/keyring-api'; import { Box, ButtonIcon, @@ -29,7 +28,6 @@ import { JustifyContent, TextAlign, TextVariant, - IconColor, } from '../../../helpers/constants/design-system'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; @@ -46,6 +44,7 @@ import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display import { PRIMARY } from '../../../helpers/constants/common'; import { getPreferences, + getSelectedAccount, getShouldHideZeroBalanceTokens, getTokensMarketData, getIsTestnet, @@ -62,10 +61,7 @@ import Spinner from '../../ui/spinner'; import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change'; import { getMultichainIsEvm } from '../../../selectors/multichain'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; -import { - setAggregatedBalancePopoverShown, - setPrivacyMode, -} from '../../../store/actions'; +import { setAggregatedBalancePopoverShown } from '../../../store/actions'; import { useTheme } from '../../../hooks/useTheme'; import { getSpecificSettingsRoute } from '../../../helpers/utils/settings-search'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -74,7 +70,6 @@ import CoinButtons from './coin-buttons'; import { AggregatedPercentageOverview } from './aggregated-percentage-overview'; export type CoinOverviewProps = { - account: InternalAccount; balance: string; balanceIsCached: boolean; className?: string; @@ -91,7 +86,6 @@ export type CoinOverviewProps = { }; export const CoinOverview = ({ - account, balance, balanceIsCached, className, @@ -123,6 +117,7 @@ export const CoinOverview = ({ ///: END:ONLY_INCLUDE_IF + const account = useSelector(getSelectedAccount); const showNativeTokenAsMainBalanceRoute = getSpecificSettingsRoute( t, t('general'), @@ -133,17 +128,19 @@ export const CoinOverview = ({ const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover); const isTestnet = useSelector(getIsTestnet); - const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } = - useSelector(getPreferences); + const { showFiatInTestnets } = useSelector(getPreferences); + const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, ); const { totalFiatBalance, loading } = useAccountTotalFiatBalance( - account, + selectedAccount, shouldHideZeroBalanceTokens, ); + const { showNativeTokenAsMainBalance } = useSelector(getPreferences); + const isEvm = useSelector(getMultichainIsEvm); const isNotAggregatedFiatBalance = showNativeTokenAsMainBalance || isTestnet || !isEvm; @@ -166,10 +163,6 @@ export const CoinOverview = ({ dispatch(setAggregatedBalancePopoverShown()); }; - const handleSensitiveToggle = () => { - dispatch(setPrivacyMode(!privacyMode)); - }; - const [referenceElement, setReferenceElement] = useState(null); const setBoxRef = (ref: HTMLSpanElement | null) => { @@ -260,39 +253,26 @@ export const CoinOverview = ({ ref={setBoxRef} > {balanceToDisplay ? ( - <> - - - + ) : ( )} @@ -368,7 +348,6 @@ export const CoinOverview = ({ buttons={ { return ( jest.fn()); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useDispatch: () => mockDispatch, -})); - jest.mock('../../../hooks/useIsOriginalNativeTokenSymbol', () => { return { useIsOriginalNativeTokenSymbol: jest.fn(), @@ -36,10 +24,6 @@ jest.mock('../../../ducks/locale/locale', () => ({ getIntlLocale: jest.fn(), })); -jest.mock('../../../store/actions', () => ({ - startNewDraftTransaction: jest.fn(), -})); - const mockGetIntlLocale = getIntlLocale; let openTabSpy; @@ -48,46 +32,18 @@ describe('EthOverview', () => { useIsOriginalNativeTokenSymbol.mockReturnValue(true); mockGetIntlLocale.mockReturnValue('en-US'); - const mockEvmAccount1 = { - address: '0x1', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: KeyringType.imported, - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }; - - const mockEvmAccount2 = { - address: '0x2', - id: 'e9b992f9-e151-4317-b8b7-c771bb73dd02', - metadata: { - name: 'Account 2', - keyring: { - type: KeyringType.imported, - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }; - const mockStore = { metamask: { ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), accountsByChainId: { [CHAIN_IDS.MAINNET]: { - '0x1': { address: mockEvmAccount1.address, balance: '0x1F4' }, + '0x1': { address: '0x1', balance: '0x1F4' }, }, }, tokenList: [], cachedBalances: { '0x1': { - [mockEvmAccount1.address]: '0x1F4', + '0x1': '0x1F4', }, }, preferences: { @@ -102,22 +58,46 @@ describe('EthOverview', () => { }, }, accounts: { - [mockEvmAccount1.address]: { - address: mockEvmAccount1.address, + '0x1': { + address: '0x1', balance: '0x1F4', }, }, internalAccounts: { accounts: { - [mockEvmAccount1.id]: mockEvmAccount1, - [mockEvmAccount2.id]: mockEvmAccount2, + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0x1', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 1', + keyring: { + type: KeyringType.imported, + }, + }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, + }, + 'e9b992f9-e151-4317-b8b7-c771bb73dd02': { + address: '0x2', + id: 'e9b992f9-e151-4317-b8b7-c771bb73dd02', + metadata: { + name: 'Account 2', + keyring: { + type: KeyringType.imported, + }, + }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, + }, }, - selectedAccount: mockEvmAccount1.id, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', }, keyrings: [ { type: KeyringType.imported, - accounts: [mockEvmAccount1.address, mockEvmAccount2.address], + accounts: ['0x1', '0x2'], }, { type: KeyringType.ledger, @@ -401,36 +381,6 @@ describe('EthOverview', () => { }); }); - it('sends an event when clicking the Buy button: %s', () => { - const mockTrackEvent = jest.fn(); - - const mockedStore = configureMockStore([thunk])(mockStore); - const { queryByTestId } = renderWithProvider( - - - , - mockedStore, - ); - - const buyButton = queryByTestId(ETH_OVERVIEW_BUY); - expect(buyButton).toBeInTheDocument(); - expect(buyButton).not.toBeDisabled(); - fireEvent.click(buyButton); - - expect(mockTrackEvent).toHaveBeenCalledWith({ - event: MetaMetricsEventName.NavBuyButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - account_type: mockEvmAccount1.type, - chain_id: CHAIN_IDS.MAINNET, - location: 'Home', - text: 'Buy', - // We use a `SwapsEthToken` in this case, so we're expecting an entire object here. - token_symbol: expect.any(Object), - }, - }); - }); - describe('Disabled buttons when an account cannot sign transactions', () => { const buttonTestCases = [ { testId: ETH_OVERVIEW_SEND, buttonText: 'Send' }, @@ -441,30 +391,15 @@ describe('EthOverview', () => { it.each(buttonTestCases)( 'should have the $buttonText button disabled when an account cannot sign transactions or user operations', ({ testId, buttonText }) => { - const mockedStoreWithoutSigningMethods = { - ...mockStore, - metamask: { - ...mockStore.metamask, - internalAccounts: { - ...mockStore.metamask.internalAccounts, - accounts: { - [mockEvmAccount1.id]: { - ...mockEvmAccount1, - // Filter out all methods used for signing transactions. - methods: Object.values(EthMethod).filter( - (method) => - method !== EthMethod.SignTransaction && - method !== EthMethod.SignUserOperation, - ), - }, - }, - }, - }, - }; - - const mockedStore = configureMockStore([thunk])( - mockedStoreWithoutSigningMethods, + mockStore.metamask.internalAccounts.accounts[ + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3' + ].methods = Object.values(EthMethod).filter( + (method) => + method !== EthMethod.SignTransaction && + method !== EthMethod.SignUserOperation, ); + + const mockedStore = configureMockStore([thunk])(mockStore); const { queryByTestId, queryByText } = renderWithProvider( , mockedStore, @@ -480,50 +415,4 @@ describe('EthOverview', () => { }, ); }); - - it.each([ - CHAIN_IDS.MAINNET, - // We want to test with a different chain ID than mainnet to make sure the events are still using - // the right `token_symbol`. - CHAIN_IDS.SEPOLIA, - ])('sends an event when clicking the Send button: %s', (chainId) => { - const mockTrackEvent = jest.fn(); - const mockedStoreWithSpecificChainId = { - ...mockStore, - metamask: { - ...mockStore.metamask, - ...mockNetworkState({ chainId }), - }, - }; - - const mockedStore = configureMockStore([thunk])( - mockedStoreWithSpecificChainId, - ); - const { queryByTestId } = renderWithProvider( - - - , - mockedStore, - ); - - const sendButton = queryByTestId(ETH_OVERVIEW_SEND); - expect(sendButton).toBeInTheDocument(); - expect(sendButton).not.toBeDisabled(); - fireEvent.click(sendButton); - - expect(mockTrackEvent).toHaveBeenCalledWith( - { - event: MetaMetricsEventName.NavSendButtonClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - account_type: mockEvmAccount1.type, - chain_id: chainId, - location: 'Home', - text: 'Send', - token_symbol: 'ETH', - }, - }, - expect.any(Object), - ); - }); }); diff --git a/ui/components/app/wallet-overview/index.scss b/ui/components/app/wallet-overview/index.scss index 47dc40200e69..4759af1ffa8c 100644 --- a/ui/components/app/wallet-overview/index.scss +++ b/ui/components/app/wallet-overview/index.scss @@ -9,10 +9,6 @@ flex-direction: column; width: 100%; - &-fullscreen { - align-items: center; - } - &__balance { flex: 1; display: flex; @@ -20,10 +16,6 @@ flex-direction: column; align-items: start; width: 100%; - - .wallet-overview-fullscreen > & { - align-items: center; - } } &__icon_button { @@ -78,8 +70,7 @@ display: flex; max-width: inherit; justify-content: center; - align-items: center; - flex-wrap: nowrap; + flex-wrap: wrap; } &__primary-balance { @@ -143,8 +134,7 @@ display: flex; max-width: inherit; justify-content: center; - align-items: center; - flex-wrap: nowrap; + flex-wrap: wrap; } &__primary-balance { diff --git a/ui/components/app/wallet-overview/wallet-overview.js b/ui/components/app/wallet-overview/wallet-overview.js index 04127276acaf..213a7b2f2317 100644 --- a/ui/components/app/wallet-overview/wallet-overview.js +++ b/ui/components/app/wallet-overview/wallet-overview.js @@ -2,23 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -// TODO: Move this function to shared -// eslint-disable-next-line import/no-restricted-paths -import { getEnvironmentType } from '../../../../app/scripts/lib/util'; -import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; - const WalletOverview = ({ balance, buttons, className }) => { return ( -
+
{balance}
{buttons}
diff --git a/ui/components/component-library/README.md b/ui/components/component-library/README.md index a1f865bfe95a..ec5006d3491f 100644 --- a/ui/components/component-library/README.md +++ b/ui/components/component-library/README.md @@ -4,7 +4,7 @@ This folder contains design system components that are built 1:1 with the Figma ## Architecture -All components are built on top of the `Box` component and accept all `Box` component props. +All components are built on top of the `Box` component and accept all `Box` [component props](/docs/components-componentlibrary-box--docs#props). ### Layout diff --git a/ui/components/component-library/icon/icon.types.ts b/ui/components/component-library/icon/icon.types.ts index 3afd17ef983b..9c87851d0b65 100644 --- a/ui/components/component-library/icon/icon.types.ts +++ b/ui/components/component-library/icon/icon.types.ts @@ -44,7 +44,6 @@ export enum IconName { Book = 'book', Bookmark = 'bookmark', Bridge = 'bridge', - Collapse = 'collapse', Calculator = 'calculator', CardPos = 'card-pos', CardToken = 'card-token', diff --git a/ui/components/component-library/index.ts b/ui/components/component-library/index.ts index 634af093a41b..861fb80bcf2c 100644 --- a/ui/components/component-library/index.ts +++ b/ui/components/component-library/index.ts @@ -69,8 +69,6 @@ export { TagUrl } from './tag-url'; export type { TagUrlProps } from './tag-url'; export { Text, ValidTag, TextDirection, InvisibleCharacter } from './text'; export type { TextProps } from './text'; -export { SensitiveText, SensitiveTextLength } from './sensitive-text'; -export type { SensitiveTextProps } from './sensitive-text'; export { Input, InputType } from './input'; export type { InputProps } from './input'; export { TextField, TextFieldType, TextFieldSize } from './text-field'; diff --git a/ui/components/component-library/sensitive-text/README.mdx b/ui/components/component-library/sensitive-text/README.mdx deleted file mode 100644 index 9e950381e6f3..000000000000 --- a/ui/components/component-library/sensitive-text/README.mdx +++ /dev/null @@ -1,81 +0,0 @@ -import { Controls, Canvas } from '@storybook/blocks'; - -import * as SensitiveTextStories from './sensitive-text.stories'; - -# SensitiveText - -SensitiveText is a component that extends the Text component to handle sensitive information. It provides the ability to hide or show the text content, replacing it with dots when hidden. - - - -## Props - -The `SensitiveText` component extends the `Text` component. See the `Text` component for an extended list of props. - - - -### Children - -The text content to be displayed or hidden. - - - -```jsx -import { SensitiveText } from '../../component-library'; - - - Sensitive Information - -``` - - -### IsHidden - -Use the `isHidden` prop to determine whether the text should be hidden or visible. When `isHidden` is `true`, the component will display dots instead of the actual text. - - - -```jsx -import { SensitiveText } from '../../component-library'; - - - Sensitive Information - -``` - -### Length - -Use the `length` prop to determine the length of the hidden text (number of dots). Can be a predefined `SensitiveTextLength` or a custom string number. - -The following predefined length options are available: - -- `SensitiveTextLength.Short`: `6` -- `SensitiveTextLength.Medium`: `9` -- `SensitiveTextLength.Long`: `12` -- `SensitiveTextLength.ExtraLong`: `20` - -- The number of dots displayed is determined by the `length` prop. -- If an invalid `length` is provided, the component will fall back to `SensitiveTextLength.Short` and log a warning. -- Custom length values can be provided as strings, e.g. `15`. - - - -```jsx -import { SensitiveText, SensitiveTextLength } from '../../component-library'; - - - Length "short" (6 characters) - - - Length "medium" (9 characters) - - - Length "long" (12 characters) - - - Length "extra long" (20 characters) - - - Length "15" (15 characters) - -``` diff --git a/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap b/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap deleted file mode 100644 index 6844feb1783e..000000000000 --- a/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SensitiveText should render correctly 1`] = ` -
-

- Sensitive Information -

-
-`; diff --git a/ui/components/component-library/sensitive-text/index.ts b/ui/components/component-library/sensitive-text/index.ts deleted file mode 100644 index ff89896fd03b..000000000000 --- a/ui/components/component-library/sensitive-text/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { SensitiveText } from './sensitive-text'; -export { SensitiveTextLength } from './sensitive-text.types'; -export type { SensitiveTextProps } from './sensitive-text.types'; diff --git a/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx b/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx deleted file mode 100644 index 142def9118b5..000000000000 --- a/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import React from 'react'; -import { SensitiveText } from '.'; -import { SensitiveTextLength } from './sensitive-text.types'; -import README from './README.mdx'; -import { Box } from '../box'; -import { - Display, - FlexDirection, -} from '../../../helpers/constants/design-system'; - -const meta: Meta = { - title: 'Components/ComponentLibrary/SensitiveText', - component: SensitiveText, - parameters: { - docs: { - page: README, - }, - }, - args: { - children: 'Sensitive information', - isHidden: false, - length: SensitiveTextLength.Short, - }, -} as Meta; - -export default meta; -type Story = StoryObj; - -export const DefaultStory: Story = {}; -DefaultStory.storyName = 'Default'; - -export const Children: Story = { - args: { - children: 'Sensitive information', - }, - render: (args) => ( - - ), -}; - -export const IsHidden: Story = { - args: { - isHidden: true, - }, - render: (args) => ( - - ), -}; - -export const Length: Story = { - args: { - isHidden: true, - }, - render: (args) => ( - - - Length "short" (6 characters) - - - Length "medium" (9 characters) - - - Length "long" (12 characters) - - - Length "extra long" (20 characters) - - - Length "15" (15 characters) - - - ), -}; diff --git a/ui/components/component-library/sensitive-text/sensitive-text.test.tsx b/ui/components/component-library/sensitive-text/sensitive-text.test.tsx deleted file mode 100644 index a4be911ea78d..000000000000 --- a/ui/components/component-library/sensitive-text/sensitive-text.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { SensitiveText } from './sensitive-text'; -import { SensitiveTextLength } from './sensitive-text.types'; - -describe('SensitiveText', () => { - const testProps = { - isHidden: false, - length: SensitiveTextLength.Short, - children: 'Sensitive Information', - }; - - it('should render correctly', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('should display the text when isHidden is false', () => { - render(); - expect(screen.getByText('Sensitive Information')).toBeInTheDocument(); - }); - - it('should hide the text when isHidden is true', () => { - render(); - expect(screen.queryByText('Sensitive Information')).not.toBeInTheDocument(); - expect(screen.getByText('••••••')).toBeInTheDocument(); - }); - - it('should render the correct number of bullets for different lengths', () => { - const lengths = [ - SensitiveTextLength.Short, - SensitiveTextLength.Medium, - SensitiveTextLength.Long, - SensitiveTextLength.ExtraLong, - ]; - - lengths.forEach((length) => { - render(); - expect(screen.getByText('•'.repeat(Number(length)))).toBeInTheDocument(); - }); - }); - - it('should handle all predefined SensitiveTextLength values', () => { - Object.entries(SensitiveTextLength).forEach(([_, value]) => { - render(); - expect(screen.getByText('•'.repeat(Number(value)))).toBeInTheDocument(); - }); - }); - - it('should handle custom length as a string', () => { - render(); - expect(screen.getByText('•'.repeat(15))).toBeInTheDocument(); - }); - - it('should fall back to Short length for invalid custom length', () => { - render(); - expect( - screen.getByText('•'.repeat(Number(SensitiveTextLength.Short))), - ).toBeInTheDocument(); - }); - - it('should log a warning for invalid custom length', () => { - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); - render(); - expect(consoleSpy).toHaveBeenCalledWith( - 'Invalid length provided: abc. Falling back to Short.', - ); - consoleSpy.mockRestore(); - }); - - it('should apply additional props to the Text component', () => { - render(); - expect(screen.getByTestId('sensitive-text')).toBeInTheDocument(); - }); - - it('should forward ref to the Text component', () => { - const ref = React.createRef(); - render(); - expect(ref.current).toBeInstanceOf(HTMLParagraphElement); - }); -}); diff --git a/ui/components/component-library/sensitive-text/sensitive-text.tsx b/ui/components/component-library/sensitive-text/sensitive-text.tsx deleted file mode 100644 index ddabda784fe1..000000000000 --- a/ui/components/component-library/sensitive-text/sensitive-text.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useMemo } from 'react'; -import { Text } from '../text'; -import { - SensitiveTextProps, - SensitiveTextLength, -} from './sensitive-text.types'; - -export const SensitiveText = React.forwardRef< - HTMLParagraphElement, - SensitiveTextProps ->((props, ref) => { - const { - isHidden = false, - length = SensitiveTextLength.Short, - children = '', - ...restProps - } = props; - - const getFallbackLength = useMemo( - () => (len: string) => { - const numLength = Number(len); - return Number.isNaN(numLength) ? 0 : numLength; - }, - [], - ); - - const isValidCustomLength = (value: string): boolean => { - const num = Number(value); - return !Number.isNaN(num) && num > 0; - }; - - let adjustedLength = length; - if (!(length in SensitiveTextLength) && !isValidCustomLength(length)) { - console.warn(`Invalid length provided: ${length}. Falling back to Short.`); - adjustedLength = SensitiveTextLength.Short; - } - - const fallback = useMemo( - () => '•'.repeat(getFallbackLength(adjustedLength)), - [length, getFallbackLength], - ); - - return ( - - {isHidden ? fallback : children} - - ); -}); diff --git a/ui/components/component-library/sensitive-text/sensitive-text.types.ts b/ui/components/component-library/sensitive-text/sensitive-text.types.ts deleted file mode 100644 index 1ea8270d377f..000000000000 --- a/ui/components/component-library/sensitive-text/sensitive-text.types.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { TextProps } from '../text/text.types'; - -/** - * SensitiveText length options. - */ -export const SensitiveTextLength = { - Short: '6', - Medium: '9', - Long: '12', - ExtraLong: '20', -} as const; - -/** - * Type for SensitiveTextLength values. - */ -export type SensitiveTextLengthType = - (typeof SensitiveTextLength)[keyof typeof SensitiveTextLength]; -/** - * Type for custom length values. - */ -export type CustomLength = string; - -export type SensitiveTextProps = Omit< - TextProps, - 'children' -> & { - /** - * Boolean to determine whether the text should be hidden or visible. - * - * @default false - */ - isHidden?: boolean; - /** - * Determines the length of the hidden text (number of asterisks). - * Can be a predefined SensitiveTextLength or a custom string number. - * - * @default SensitiveTextLength.Short - */ - length?: SensitiveTextLengthType | CustomLength; - /** - * The text content to be displayed or hidden. - */ - children?: React.ReactNode; -}; diff --git a/ui/components/component-library/text/README.mdx b/ui/components/component-library/text/README.mdx index 183fcbbca9f8..5b275aa48e23 100644 --- a/ui/components/component-library/text/README.mdx +++ b/ui/components/component-library/text/README.mdx @@ -580,7 +580,7 @@ Values using the `TextAlign` object from `./ui/helpers/constants/design-system.j ### Box Props -Box props are now integrated with the `Text` component. +Box props are now integrated with the `Text` component. Valid Box props: [Box](/docs/components-componentlibrary-box--docs#props) You no longer need to pass these props as an object through `boxProps` diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx index e76dabc7add7..06fe1336ca7e 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx @@ -4,7 +4,7 @@ import { ICustodianType } from '@metamask-institutional/types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { hideModal } from '../../../store/actions'; -import { getSelectedInternalAccount } from '../../../selectors/accounts'; +import { getSelectedInternalAccount } from '../../../selectors/selectors'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { Box, diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx index 7c1d0f60488f..10dc049b8678 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx @@ -56,7 +56,6 @@ const InteractiveReplacementTokenNotification: React.FC< interactiveReplacementToken && Boolean(Object.keys(interactiveReplacementToken).length); - // @ts-expect-error keyring type is wrong maybe? if (!/^Custody/u.test(keyring.type) || !hasInteractiveReplacementToken) { setShowNotification(false); return; @@ -67,7 +66,6 @@ const InteractiveReplacementTokenNotification: React.FC< )) as unknown as string; const custodyAccountDetails = await dispatch( mmiActions.getAllCustodianAccountsWithToken( - // @ts-expect-error keyring type is wrong maybe? keyring.type.split(' - ')[1], token, ), @@ -107,7 +105,6 @@ const InteractiveReplacementTokenNotification: React.FC< interactiveReplacementToken?.oldRefreshToken, isUnlocked, dispatch, - // @ts-expect-error keyring type is wrong maybe? keyring.type, interactiveReplacementToken, mmiActions, 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 index e320bd1de0e3..51f6f2e905f9 100644 --- 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 @@ -242,7 +242,6 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > $100,000.00 @@ -539,7 +538,6 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > 0.006 @@ -583,7 +581,6 @@ exports[`AccountListItem renders AccountListItem component and shows account nam > 0.006 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 bfc8f24369e1..df1c4ed455d5 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -88,7 +88,6 @@ const AccountListItem = ({ startAccessory, onActionClick, shouldScrollToWhenSelected = true, - privacyMode = false, }) => { const t = useI18nContext(); const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); @@ -315,7 +314,6 @@ const AccountListItem = ({ type={PRIMARY} showFiat={showFiat} data-testid="first-currency-display" - privacyMode={privacyMode} /> @@ -363,7 +361,6 @@ const AccountListItem = ({ type={SECONDARY} showNative data-testid="second-currency-display" - privacyMode={privacyMode} /> @@ -511,10 +508,6 @@ AccountListItem.propTypes = { * Determines if list item should be scrolled to when selected */ shouldScrollToWhenSelected: PropTypes.bool, - /** - * Determines if list balance should be obfuscated - */ - privacyMode: PropTypes.bool, }; AccountListItem.displayName = 'AccountListItem'; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index 718d86cf40b5..b9166eb8c85e 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -65,9 +65,6 @@ import { getOriginOfCurrentTab, getSelectedInternalAccount, getUpdatedAndSortedAccounts, - ///: BEGIN:ONLY_INCLUDE_IF(solana) - getIsSolanaSupportEnabled, - ///: END:ONLY_INCLUDE_IF } from '../../../selectors'; import { setSelectedAccount } from '../../../store/actions'; import { @@ -97,14 +94,8 @@ import { hasCreatedBtcMainnetAccount, hasCreatedBtcTestnetAccount, } from '../../../selectors/accounts'; -///: END:ONLY_INCLUDE_IF - -///: BEGIN:ONLY_INCLUDE_IF(build-flask,solana) import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; -import { - WalletClientType, - useMultichainWalletSnapClient, -} from '../../../hooks/accounts/useMultichainWalletSnapClient'; +import { useBitcoinWalletSnapClient } from '../../../hooks/accounts/useBitcoinWalletSnapClient'; ///: END:ONLY_INCLUDE_IF import { InternalAccountWithBalance, @@ -114,12 +105,6 @@ import { import { CreateEthAccount } from '../create-eth-account'; import { ImportAccount } from '../import-account'; import { endTrace, TraceName } from '../../../../shared/lib/trace'; -///: BEGIN:ONLY_INCLUDE_IF(solana) -import { - SOLANA_WALLET_NAME, - SOLANA_WALLET_SNAP_ID, -} from '../../../../shared/lib/accounts/solana-wallet-snap'; -///: END:ONLY_INCLUDE_IF import { HiddenAccountList } from './hidden-account-list'; const ACTION_MODES = { @@ -202,7 +187,6 @@ export const mergeAccounts = ( type AccountListMenuProps = { onClose: () => void; - privacyMode?: boolean; showAccountCreation?: boolean; accountListItemProps?: object; allowedAccountTypes?: KeyringAccountType[]; @@ -210,7 +194,6 @@ type AccountListMenuProps = { export const AccountListMenu = ({ onClose, - privacyMode = false, showAccountCreation = true, accountListItemProps, allowedAccountTypes = [ @@ -283,16 +266,7 @@ export const AccountListMenu = ({ hasCreatedBtcTestnetAccount, ); - const bitcoinWalletSnapClient = useMultichainWalletSnapClient( - WalletClientType.Bitcoin, - ); - ///: END:ONLY_INCLUDE_IF - - ///: BEGIN:ONLY_INCLUDE_IF(solana) - const solanaSupportEnabled = useSelector(getIsSolanaSupportEnabled); - const solanaWalletSnapClient = useMultichainWalletSnapClient( - WalletClientType.Solana, - ); + const bitcoinWalletSnapClient = useBitcoinWalletSnapClient(); ///: END:ONLY_INCLUDE_IF const [searchQuery, setSearchQuery] = useState(''); @@ -476,47 +450,10 @@ export const AccountListMenu = ({ ) : null ///: END:ONLY_INCLUDE_IF } - { - ///: BEGIN:ONLY_INCLUDE_IF(solana) - solanaSupportEnabled && ( - - { - trackEvent({ - category: MetaMetricsEventCategory.Navigation, - event: MetaMetricsEventName.AccountAddSelected, - properties: { - account_type: MetaMetricsEventAccountType.Snap, - snap_id: SOLANA_WALLET_SNAP_ID, - snap_name: SOLANA_WALLET_NAME, - location: 'Main Menu', - }, - }); - - // The account creation + renaming is handled by the - // Snap account bridge, so we need to close the current - // modal - onClose(); - - await solanaWalletSnapClient.createAccount( - MultichainNetworks.SOLANA, - ); - }} - data-testid="multichain-account-menu-popover-add-solana-account" - > - {t('addNewSolanaAccount')} - - - ) - ///: END:ONLY_INCLUDE_IF - } { trackEvent({ category: MetaMetricsEventCategory.Navigation, @@ -705,7 +642,6 @@ export const AccountListMenu = ({ isHidden={Boolean(account.hidden)} currentTabOrigin={currentTabOrigin} isActive={Boolean(account.active)} - privacyMode={privacyMode} {...accountListItemProps} /> diff --git a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap index 247f7aeb5c78..d22597edd89f 100644 --- a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap +++ b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap @@ -616,7 +616,6 @@ exports[`App Header unlocked state matches snapshot: unlocked 1`] = ` >

1

diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap index a0c808186082..9c0bd9c49482 100644 --- a/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap @@ -8,7 +8,6 @@ exports[`AssetBalanceText matches snapshot 1`] = ` > prefix-fiat value diff --git a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap index b4a4836db2d6..d53c8e7d8d8a 100644 --- a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap +++ b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap @@ -358,7 +358,6 @@ exports[`Connect More Accounts Modal should render correctly 1`] = ` > 0 @@ -402,7 +401,6 @@ exports[`Connect More Accounts Modal should render correctly 1`] = ` > 0 diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx index 34ec98e671b9..509a4aa60a2a 100644 --- a/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx +++ b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx @@ -57,7 +57,7 @@ describe('FundingMethodModal', () => { expect(queryByTestId('funding-method-modal')).toBeNull(); }); - it('should call openBuyCryptoInPdapp when the Token Marketplace item is clicked', () => { + it('should call openBuyCryptoInPdapp when the Buy Crypto item is clicked', () => { const { getByText } = renderWithProvider( { store, ); - fireEvent.click(getByText('Token marketplace')); + fireEvent.click(getByText('Buy crypto')); expect(openBuyCryptoInPdapp).toHaveBeenCalled(); }); diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx index baa0e234a32a..47d6ed22c2e8 100644 --- a/ui/components/multichain/funding-method-modal/funding-method-modal.tsx +++ b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx @@ -115,8 +115,8 @@ export const FundingMethodModal: React.FC = ({ { handleNotificationsClick()} - data-testid="notifications-menu-item" > {rpcEndpoint.name ?? new URL(rpcEndpoint.url).host} diff --git a/ui/components/multichain/notifications-tag-counter/notifications-tag-counter.tsx b/ui/components/multichain/notifications-tag-counter/notifications-tag-counter.tsx index ce68f05d0f14..066c95ab7c6d 100644 --- a/ui/components/multichain/notifications-tag-counter/notifications-tag-counter.tsx +++ b/ui/components/multichain/notifications-tag-counter/notifications-tag-counter.tsx @@ -48,7 +48,6 @@ export const NotificationsTagCounter = ({ color={TextColor.errorInverse} variant={TextVariant.bodyXs} className="notifications-tag-counter__unread-dot" - data-testid="notifications-tag-counter__unread-dot" textAlign={TextAlign.Center} > {notificationsUnreadCount > 10 ? '9+' : notificationsUnreadCount} diff --git a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap index ad2dc490d7c0..afd02098086f 100644 --- a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap +++ b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap @@ -297,7 +297,6 @@ exports[`Connections Content should render correctly 1`] = ` > 966.988 @@ -341,7 +340,6 @@ exports[`Connections Content should render correctly 1`] = ` > 966.988 diff --git a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx index d5ca0b816d48..9f77238bda9a 100644 --- a/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx +++ b/ui/components/multichain/pages/review-permissions-page/site-cell/site-cell.tsx @@ -82,15 +82,6 @@ export const SiteCell: React.FC = ({ ]) : t('requestingFor'); - const networkMessageConnectedState = - selectedChainIdsLength === 1 - ? t('connectedWithNetworkName', [selectedNetworks[0].name]) - : t('connectedWithNetwork', [selectedChainIdsLength]); - const networkMessageNotConnectedState = - selectedChainIdsLength === 1 - ? t('requestingForNetwork', [selectedNetworks[0].name]) - : t('requestingFor'); - return ( <> = ({ { setShowEditNetworksModal(true); diff --git a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap index 7b0605b7ea60..814dc934fc9a 100644 --- a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap +++ b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap @@ -474,7 +474,6 @@ exports[`SendPage render and initialization should render correctly even when a > $0.00 @@ -518,7 +517,6 @@ exports[`SendPage render and initialization should render correctly even when a > 0 diff --git a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap index 71431a330f94..9fbf7e29879b 100644 --- a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap +++ b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap @@ -248,7 +248,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 966.988 @@ -292,7 +291,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 966.988 @@ -547,7 +545,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -591,7 +588,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -846,7 +842,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -890,7 +885,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1154,7 +1148,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1198,7 +1191,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1453,7 +1445,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1497,7 +1488,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1765,7 +1755,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 @@ -1809,7 +1798,6 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` > 0 diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js index 5fb91272ff83..ac72f4c5112f 100644 --- a/ui/components/multichain/ramps-card/ramps-card.js +++ b/ui/components/multichain/ramps-card/ramps-card.js @@ -30,6 +30,7 @@ const darkenGradient = export const RAMPS_CARD_VARIANT_TYPES = { TOKEN: 'token', + NFT: 'nft', ACTIVITY: 'activity', BTC: 'btc', }; @@ -40,8 +41,15 @@ export const RAMPS_CARD_VARIANTS = { gradient: // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #0189EC 0%, #4B7AED 35%, #6774EE 58%, #706AF4 80.5%, #7C5BFC 100%)', - title: 'tipsForUsingAWallet', - body: 'tipsForUsingAWalletDescription', + title: 'fundYourWallet', + body: 'getStartedByFundingWallet', + }, + [RAMPS_CARD_VARIANT_TYPES.NFT]: { + illustrationSrc: './images/ramps-card-nft-illustration.png', + // eslint-disable-next-line @metamask/design-tokens/color-no-hex + gradient: 'linear-gradient(90deg, #F6822D 0%, #F894A7 52%, #ED94FB 92.5%)', + title: 'getStartedWithNFTs', + body: 'getStartedWithNFTsDescription', }, [RAMPS_CARD_VARIANT_TYPES.ACTIVITY]: { illustrationSrc: './images/ramps-card-activity-illustration.png', @@ -49,21 +57,22 @@ export const RAMPS_CARD_VARIANTS = { // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #57C5DC 0%, #06BFDD 49.39%, #35A9C7 100%)', - title: 'tipsForUsingAWallet', - body: 'tipsForUsingAWalletDescription', + title: 'startYourJourney', + body: 'startYourJourneyDescription', }, [RAMPS_CARD_VARIANT_TYPES.BTC]: { illustrationSrc: './images/ramps-card-btc-illustration.png', gradient: // eslint-disable-next-line @metamask/design-tokens/color-no-hex 'linear-gradient(90deg, #017ED9 0%, #446FD9 35%, #5E6AD9 58%, #635ED9 80.5%, #6855D9 92.5%, #6A4FD9 100%)', - title: 'tipsForUsingAWallet', - body: 'tipsForUsingAWalletDescription', + title: 'fundYourWallet', + body: 'fundYourWalletDescription', }, }; const metamaskEntryMap = { [RAMPS_CARD_VARIANT_TYPES.TOKEN]: RampsMetaMaskEntry.TokensBanner, + [RAMPS_CARD_VARIANT_TYPES.NFT]: RampsMetaMaskEntry.NftBanner, [RAMPS_CARD_VARIANT_TYPES.ACTIVITY]: RampsMetaMaskEntry.ActivityBanner, [RAMPS_CARD_VARIANT_TYPES.BTC]: RampsMetaMaskEntry.BtcBanner, }; @@ -78,6 +87,8 @@ export const RampsCard = ({ variant, handleOnClick }) => { const { chainId, nickname } = useSelector(getMultichainCurrentNetwork); const { symbol } = useSelector(getMultichainDefaultToken); + const isTokenVariant = variant === RAMPS_CARD_VARIANT_TYPES.TOKEN; + useEffect(() => { trackEvent({ event: MetaMetricsEventName.EmptyBuyBannerDisplayed, @@ -99,7 +110,7 @@ export const RampsCard = ({ variant, handleOnClick }) => { category: MetaMetricsEventCategory.Navigation, properties: { location: `${variant} tab`, - text: `Token Marketplace`, + text: `Buy ${symbol}`, // FIXME: This might not be a number for non-EVM networks chain_id: chainId, token_symbol: symbol, @@ -121,14 +132,14 @@ export const RampsCard = ({ variant, handleOnClick }) => { }} > - {t(title)} + {t(title, [symbol])} - {t(body)} + {t(body, [symbol])} - {t('tokenMarketplace')} + {isTokenVariant ? t('getStarted') : t('buyToken', [symbol])} ); diff --git a/ui/components/multichain/ramps-card/ramps-card.stories.js b/ui/components/multichain/ramps-card/ramps-card.stories.js index 903ea3d27f9a..2a4dce444c7e 100644 --- a/ui/components/multichain/ramps-card/ramps-card.stories.js +++ b/ui/components/multichain/ramps-card/ramps-card.stories.js @@ -24,6 +24,12 @@ export const TokensStory = (args) => ( TokensStory.storyName = 'Tokens'; +export const NFTsStory = (args) => ( + +); + +NFTsStory.storyName = 'NFTs'; + export const ActivityStory = (args) => ( ); diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index 265b334fdb57..0c3c46114541 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -34,8 +34,6 @@ import { ModalFooter, ModalHeader, ModalOverlay, - SensitiveText, - SensitiveTextLength, Text, } from '../../component-library'; import { @@ -84,7 +82,6 @@ type TokenListItemProps = { address?: string | null; showPercentage?: boolean; isPrimaryTokenSymbolHidden?: boolean; - privacyMode?: boolean; }; export const TokenListItem = ({ @@ -102,7 +99,6 @@ export const TokenListItem = ({ isStakeable = false, address = null, showPercentage = false, - privacyMode = false, }: TokenListItemProps) => { const t = useI18nContext(); const isEvm = useSelector(getMultichainIsEvm); @@ -302,7 +298,6 @@ export const TokenListItem = ({ as="span" fontWeight={FontWeight.Medium} variant={TextVariant.bodyMd} - display={Display.Block} ellipsis > {isStakeable ? ( @@ -380,19 +375,17 @@ export const TokenListItem = ({ ariaLabel={''} /> - {primary}{' '} {isNativeCurrency || isPrimaryTokenSymbolHidden ? '' : tokenSymbol} - + ) : ( - {secondary} - - + {primary}{' '} {isNativeCurrency || isPrimaryTokenSymbolHidden ? '' : tokenSymbol} - + )} diff --git a/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap b/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap index eeb40144894b..44ba7be60b6f 100644 --- a/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap +++ b/ui/components/ui/currency-display/__snapshots__/currency-display.component.test.js.snap @@ -8,7 +8,6 @@ exports[`CurrencyDisplay Component should match default snapshot 1`] = ` >
@@ -22,7 +21,6 @@ exports[`CurrencyDisplay Component should render text with a className 1`] = ` > $123.45 @@ -38,7 +36,6 @@ exports[`CurrencyDisplay Component should render text with a prefix 1`] = ` > - $123.45 diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index 7e2569ffaee3..ca9322661d79 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import { EtherDenomination } from '../../../../shared/constants/common'; -import { SensitiveText, Box } from '../../component-library'; +import { Text, Box } from '../../component-library'; import { AlignItems, Display, @@ -33,7 +33,6 @@ export default function CurrencyDisplay({ textProps = {}, suffixProps = {}, isAggregatedFiatOverviewBalance = false, - privacyMode = false, ...props }) { const [title, parts] = useCurrencyDisplay(value, { @@ -69,33 +68,26 @@ export default function CurrencyDisplay({ {prefixComponent} ) : null} - {parts.prefix} {parts.value} - + {parts.suffix ? ( - {parts.suffix} - + ) : null} ); @@ -123,7 +115,6 @@ const CurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, isAggregatedFiatOverviewBalance: PropTypes.bool, - privacyMode: PropTypes.bool, }; CurrencyDisplay.propTypes = CurrencyDisplayPropTypes; diff --git a/ui/components/ui/token-currency-display/token-currency-display.stories.tsx b/ui/components/ui/token-currency-display/token-currency-display.stories.tsx index 932d54210b84..7cf850c42c84 100644 --- a/ui/components/ui/token-currency-display/token-currency-display.stories.tsx +++ b/ui/components/ui/token-currency-display/token-currency-display.stories.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; +import { Meta, Story } from '@storybook/react'; import TokenCurrencyDisplay from './token-currency-display.component'; +import { TokenCurrencyDisplayProps } from './token-currency-display.types'; -const meta: Meta = { +export default { title: 'Components/UI/TokenCurrencyDisplay', component: TokenCurrencyDisplay, argTypes: { @@ -11,15 +12,14 @@ const meta: Meta = { token: { control: 'object' }, prefix: { control: 'text' }, }, - args: { - className: '', - transactionData: '0x123', - token: { symbol: 'ETH' }, - prefix: '', - }, -}; +} as Meta; -export default meta; -type Story = StoryObj; +const Template: Story = (args) => ; -export const Default: Story = {}; +export const Default = Template.bind({}); +Default.args = { + className: '', + transactionData: '0x123', + token: { symbol: 'ETH' }, + prefix: '', +}; diff --git a/ui/contexts/assetPolling.tsx b/ui/contexts/assetPolling.tsx deleted file mode 100644 index 63cef9667fbd..000000000000 --- a/ui/contexts/assetPolling.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { ReactNode } from 'react'; -import useCurrencyRatePolling from '../hooks/useCurrencyRatePolling'; -import useTokenRatesPolling from '../hooks/useTokenRatesPolling'; - -// This provider is a step towards making controller polling fully UI based. -// Eventually, individual UI components will call the use*Polling hooks to -// poll and return particular data. This polls globally in the meantime. -export const AssetPollingProvider = ({ children }: { children: ReactNode }) => { - useCurrencyRatePolling(); - useTokenRatesPolling(); - - return <>{children}; -}; diff --git a/ui/contexts/currencyRate.js b/ui/contexts/currencyRate.js new file mode 100644 index 000000000000..6739b730a882 --- /dev/null +++ b/ui/contexts/currencyRate.js @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import useCurrencyRatePolling from '../hooks/useCurrencyRatePolling'; + +export const CurrencyRateProvider = ({ children }) => { + useCurrencyRatePolling(); + + return <>{children}; +}; + +CurrencyRateProvider.propTypes = { + children: PropTypes.node, +}; diff --git a/ui/contexts/metametrics.js b/ui/contexts/metametrics.js index b7870babfbb7..6445362f1f60 100644 --- a/ui/contexts/metametrics.js +++ b/ui/contexts/metametrics.js @@ -26,7 +26,7 @@ import { trackMetaMetricsEvent, trackMetaMetricsPage } from '../store/actions'; // type imports /** - * @typedef {import('../../shared/constants/metametrics').UnsanitizedMetaMetricsEventPayload} MetaMetricsEventPayload + * @typedef {import('../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload * @typedef {import('../../shared/constants/metametrics').MetaMetricsEventOptions} MetaMetricsEventOptions * @typedef {import('../../shared/constants/metametrics').MetaMetricsPageObject} MetaMetricsPageObject * @typedef {import('../../shared/constants/metametrics').MetaMetricsReferrerObject} MetaMetricsReferrerObject diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 27b20a5841b3..9a7a93ea958b 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -339,23 +339,4 @@ describe('App State', () => { expect(state.showDataDeletionErrorModal).toStrictEqual(false); }); - - it('displays error in settings', () => { - const state = reduceApp(metamaskState, { - type: actions.SHOW_SETTINGS_PAGE_ERROR, - payload: 'settings page error', - }); - - expect(state.errorInSettings).toStrictEqual('settings page error'); - }); - - it('hides error in settings', () => { - const displayErrorInSettings = { errorInSettings: 'settings page error' }; - const oldState = { ...metamaskState, ...displayErrorInSettings }; - const state = reduceApp(oldState, { - type: actions.HIDE_SETTINGS_PAGE_ERROR, - }); - - expect(state.errorInSettings).toBeNull(); - }); }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index 81f875446f1e..e6a7855ce7a5 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -105,7 +105,6 @@ type AppState = { snapsInstallPrivacyWarningShown: boolean; isAddingNewNetwork: boolean; isMultiRpcOnboarding: boolean; - errorInSettings: string | null; }; export type AppSliceState = { @@ -193,7 +192,6 @@ const initialState: AppState = { snapsInstallPrivacyWarningShown: false, isAddingNewNetwork: false, isMultiRpcOnboarding: false, - errorInSettings: null, }; export default function reduceApp( @@ -634,16 +632,6 @@ export default function reduceApp( ...appState, showDataDeletionErrorModal: false, }; - case actionConstants.SHOW_SETTINGS_PAGE_ERROR: - return { - ...appState, - errorInSettings: action.payload, - }; - case actionConstants.HIDE_SETTINGS_PAGE_ERROR: - return { - ...appState, - errorInSettings: null, - }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { @@ -732,27 +720,6 @@ export function setCustomTokenAmount(payload: string): PayloadAction { return { type: actionConstants.SET_CUSTOM_TOKEN_AMOUNT, payload }; } -/** - * An action creator for display a error to the user in various places in the - * UI. It will not be cleared until a new warning replaces it or `hideWarning` - * is called. - * - * @param payload - The warning to show. - * @returns The action to display the warning. - */ -export function displayErrorInSettings(payload: string): PayloadAction { - return { - type: actionConstants.SHOW_SETTINGS_PAGE_ERROR, - payload, - }; -} - -export function hideErrorInSettings() { - return { - type: actionConstants.HIDE_SETTINGS_PAGE_ERROR, - }; -} - // Selectors export function getQrCodeData(state: AppSliceState): { type?: string | null; diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index f045c0dfbc12..5e50b004b774 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -11,31 +11,31 @@ import { import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import { MetaMaskReduxDispatch } from '../../store/store'; -import { QuoteRequest } from '../../pages/bridge/types'; import { bridgeSlice } from './bridge'; const { - setToChainId, + setToChainId: setToChainId_, setFromToken, setToToken, setFromTokenInputValue, resetInputFields, + switchToAndFromTokens, } = bridgeSlice.actions; export { - setToChainId, - resetInputFields, - setToToken, setFromToken, + setToToken, setFromTokenInputValue, + switchToAndFromTokens, + resetInputFields, }; const callBridgeControllerMethod = ( bridgeAction: BridgeUserAction | BridgeBackgroundAction, - args?: T, + args?: T[], ) => { return async (dispatch: MetaMaskReduxDispatch) => { - await submitRequestToBackground(bridgeAction, [args]); + await submitRequestToBackground(bridgeAction, args); await forceUpdateMetamaskState(dispatch); }; }; @@ -49,40 +49,24 @@ export const setBridgeFeatureFlags = () => { }; }; -export const resetBridgeState = () => { - return async (dispatch: MetaMaskReduxDispatch) => { - dispatch(resetInputFields()); - dispatch(callBridgeControllerMethod(BridgeBackgroundAction.RESET_STATE)); - }; -}; - // User actions export const setFromChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { dispatch( - callBridgeControllerMethod( - BridgeUserAction.SELECT_SRC_NETWORK, + callBridgeControllerMethod(BridgeUserAction.SELECT_SRC_NETWORK, [ chainId, - ), + ]), ); }; }; export const setToChain = (chainId: Hex) => { return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(setToChainId_(chainId)); dispatch( - callBridgeControllerMethod( - BridgeUserAction.SELECT_DEST_NETWORK, + callBridgeControllerMethod(BridgeUserAction.SELECT_DEST_NETWORK, [ chainId, - ), - ); - }; -}; - -export const updateQuoteRequestParams = (params: Partial) => { - return async (dispatch: MetaMaskReduxDispatch) => { - await dispatch( - callBridgeControllerMethod(BridgeUserAction.UPDATE_QUOTE_PARAMS, params), + ]), ); }; }; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index dc9596fcafba..f4a566c233b5 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -1,6 +1,5 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { zeroAddress } from 'ethereumjs-util'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { setBackgroundConnection } from '../../store/background-connection'; @@ -19,9 +18,7 @@ import { setToToken, setFromChain, resetInputFields, - setToChainId, - updateQuoteRequestParams, - resetBridgeState, + switchToAndFromTokens, } from './actions'; const middleware = [thunk]; @@ -34,23 +31,9 @@ describe('Ducks - Bridge', () => { store.clearActions(); }); - describe('setToChainId', () => { - it('calls the "bridge/setToChainId" action', () => { - const state = store.getState().bridge; - const actionPayload = CHAIN_IDS.OPTIMISM; - - store.dispatch(setToChainId(actionPayload as never) as never); - - // Check redux state - const actions = store.getActions(); - expect(actions[0].type).toStrictEqual('bridge/setToChainId'); - const newState = bridgeReducer(state, actions[0]); - expect(newState.toChainId).toStrictEqual(actionPayload); - }); - }); - describe('setToChain', () => { - it('calls the selectDestNetwork background action', () => { + it('calls the "bridge/setToChainId" action and the selectDestNetwork background action', () => { + const state = store.getState().bridge; const actionPayload = CHAIN_IDS.OPTIMISM; const mockSelectDestNetwork = jest.fn().mockReturnValue({}); @@ -60,6 +43,11 @@ describe('Ducks - Bridge', () => { store.dispatch(setToChain(actionPayload as never) as never); + // Check redux state + const actions = store.getActions(); + expect(actions[0].type).toStrictEqual('bridge/setToChainId'); + const newState = bridgeReducer(state, actions[0]); + expect(newState.toChainId).toStrictEqual(actionPayload); // Check background state expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1); expect(mockSelectDestNetwork).toHaveBeenCalledWith( @@ -73,7 +61,7 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' }; - store.dispatch(setFromToken(actionPayload as never) as never); + store.dispatch(setFromToken(actionPayload)); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromToken'); const newState = bridgeReducer(state, actions[0]); @@ -85,8 +73,7 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setToToken" action', () => { const state = store.getState().bridge; const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' }; - - store.dispatch(setToToken(actionPayload as never) as never); + store.dispatch(setToToken(actionPayload)); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setToToken'); const newState = bridgeReducer(state, actions[0]); @@ -98,8 +85,7 @@ describe('Ducks - Bridge', () => { it('calls the "bridge/setFromTokenInputValue" action', () => { const state = store.getState().bridge; const actionPayload = '10'; - - store.dispatch(setFromTokenInputValue(actionPayload as never) as never); + store.dispatch(setFromTokenInputValue(actionPayload)); const actions = store.getActions(); expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue'); const newState = bridgeReducer(state, actions[0]); @@ -151,59 +137,29 @@ describe('Ducks - Bridge', () => { }); }); - describe('updateQuoteRequestParams', () => { - it('dispatches quote params to the bridge controller', () => { - const mockUpdateParams = jest.fn(); - setBackgroundConnection({ - [BridgeUserAction.UPDATE_QUOTE_PARAMS]: mockUpdateParams, - } as never); - - store.dispatch( - updateQuoteRequestParams({ - srcChainId: 1, - srcTokenAddress: zeroAddress(), - destTokenAddress: undefined, - }) as never, - ); - - expect(mockUpdateParams).toHaveBeenCalledTimes(1); - expect(mockUpdateParams).toHaveBeenCalledWith( - { - srcChainId: 1, - srcTokenAddress: zeroAddress(), - destTokenAddress: undefined, - }, - expect.anything(), - ); - }); - }); - - describe('resetBridgeState', () => { - it('dispatches action to the bridge controller', () => { + describe('switchToAndFromTokens', () => { + it('switches to and from input values', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockStore = configureMockStore(middleware)( - createBridgeMockStore({}, { fromTokenInputValue: '10' }), + const bridgeStore = configureMockStore(middleware)( + createBridgeMockStore( + {}, + { + toChainId: CHAIN_IDS.MAINNET, + fromToken: { symbol: 'WETH', address: '0x13341432' }, + toToken: { symbol: 'USDC', address: '0x13341431' }, + fromTokenInputValue: '10', + }, + ), ); - const state = mockStore.getState().bridge; - const mockResetBridgeState = jest.fn(); - setBackgroundConnection({ - [BridgeBackgroundAction.RESET_STATE]: mockResetBridgeState, - } as never); - - mockStore.dispatch(resetBridgeState() as never); - - expect(mockResetBridgeState).toHaveBeenCalledTimes(1); - expect(mockResetBridgeState).toHaveBeenCalledWith( - undefined, - expect.anything(), - ); - const actions = mockStore.getActions(); - expect(actions[0].type).toStrictEqual('bridge/resetInputFields'); + const state = bridgeStore.getState().bridge; + bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON)); + const actions = bridgeStore.getActions(); + expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens'); const newState = bridgeReducer(state, actions[0]); expect(newState).toStrictEqual({ - toChainId: null, - fromToken: null, - toToken: null, + toChainId: CHAIN_IDS.POLYGON, + fromToken: { symbol: 'USDC', address: '0x13341431' }, + toToken: { symbol: 'WETH', address: '0x13341432' }, fromTokenInputValue: null, }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index c75030c7591d..9ec744d9e953 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -39,6 +39,12 @@ const bridgeSlice = createSlice({ resetInputFields: () => ({ ...initialState, }), + switchToAndFromTokens: (state, { payload }) => ({ + toChainId: payload, + fromToken: state.toToken, + toToken: state.fromToken, + fromTokenInputValue: null, + }), }, }); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index e39f73f2fa15..6be67515e6e4 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -6,10 +6,8 @@ import { } from '../../../shared/constants/network'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { mockNetworkState } from '../../../test/stub/networks'; -import mockErc20Erc20Quotes from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; import { getAllBridgeableNetworks, - getBridgeQuotes, getFromAmount, getFromChain, getFromChains, @@ -397,7 +395,7 @@ describe('Bridge selectors', () => { const state = createBridgeMockStore(); const result = getToAmount(state as never); - expect(result).toStrictEqual(undefined); + expect(result).toStrictEqual('0'); }); }); @@ -493,81 +491,4 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]); }); }); - - describe('getBridgeQuotes', () => { - it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=5', () => { - const state = createBridgeMockStore( - { extensionConfig: { maxRefreshCount: 5 } }, - { toChainId: '0x1' }, - { - quoteRequest: { insufficientBal: false }, - quotes: mockErc20Erc20Quotes, - quotesFetchStatus: 1, - quotesRefreshCount: 5, - quotesLastFetched: 100, - srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, - srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], - }, - ); - const result = getBridgeQuotes(state as never); - - expect(result).toStrictEqual({ - quotes: mockErc20Erc20Quotes, - quotesLastFetchedMs: 100, - isLoading: false, - quotesRefreshCount: 5, - isQuoteGoingToRefresh: false, - }); - }); - - it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=2', () => { - const state = createBridgeMockStore( - { extensionConfig: { maxRefreshCount: 5 } }, - { toChainId: '0x1' }, - { - quoteRequest: { insufficientBal: false }, - quotes: mockErc20Erc20Quotes, - quotesFetchStatus: 1, - quotesRefreshCount: 2, - quotesLastFetched: 100, - srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, - srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], - }, - ); - const result = getBridgeQuotes(state as never); - - expect(result).toStrictEqual({ - quotes: mockErc20Erc20Quotes, - quotesLastFetchedMs: 100, - isLoading: false, - quotesRefreshCount: 2, - isQuoteGoingToRefresh: true, - }); - }); - - it('returns quote list and fetch data, insufficientBal=true', () => { - const state = createBridgeMockStore( - { extensionConfig: { maxRefreshCount: 5 } }, - { toChainId: '0x1' }, - { - quoteRequest: { insufficientBal: true }, - quotes: mockErc20Erc20Quotes, - quotesFetchStatus: 1, - quotesRefreshCount: 1, - quotesLastFetched: 100, - srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } }, - srcTopAssets: [{ address: '0x00', symbol: 'TEST' }], - }, - ); - const result = getBridgeQuotes(state as never); - - expect(result).toStrictEqual({ - quotes: mockErc20Erc20Quotes, - quotesLastFetchedMs: 100, - isLoading: false, - quotesRefreshCount: 1, - isQuoteGoingToRefresh: false, - }); - }); - }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 60704aa6f094..8cd56928fc66 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -3,13 +3,12 @@ import { NetworkState, } from '@metamask/network-controller'; import { uniqBy } from 'lodash'; -import { createSelector } from 'reselect'; import { getNetworkConfigurationsByChainId, getIsBridgeEnabled, getSwapsDefaultToken, SwapsEthToken, -} from '../../selectors/selectors'; +} from '../../selectors'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { BridgeControllerState, @@ -20,10 +19,6 @@ import { import { createDeepEqualSelector } from '../../selectors/util'; import { getProviderConfig } from '../metamask/metamask'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; -import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { BridgeState } from './bridge'; type BridgeAppState = { @@ -115,7 +110,7 @@ export const getToTokens = (state: BridgeAppState) => { export const getFromToken = ( state: BridgeAppState, -): SwapsTokenObject | SwapsEthToken | null => { +): SwapsTokenObject | SwapsEthToken => { return state.bridge.fromToken?.address ? state.bridge.fromToken : getSwapsDefaultToken(state); @@ -129,61 +124,10 @@ export const getToToken = ( export const getFromAmount = (state: BridgeAppState): string | null => state.bridge.fromTokenInputValue; - -export const getQuoteRequest = (state: BridgeAppState) => { - const { quoteRequest } = state.metamask.bridgeState; - return quoteRequest; +export const getToAmount = (_state: BridgeAppState) => { + return '0'; }; -export const getBridgeQuotesConfig = (state: BridgeAppState) => - state.metamask.bridgeState?.bridgeFeatureFlags[ - BridgeFeatureFlagsKey.EXTENSION_CONFIG - ] ?? {}; - -export const getBridgeQuotes = createSelector( - (state: BridgeAppState) => state.metamask.bridgeState.quotes, - (state: BridgeAppState) => state.metamask.bridgeState.quotesLastFetched, - (state: BridgeAppState) => - state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING, - (state: BridgeAppState) => state.metamask.bridgeState.quotesRefreshCount, - getBridgeQuotesConfig, - getQuoteRequest, - ( - quotes, - quotesLastFetchedMs, - isLoading, - quotesRefreshCount, - { maxRefreshCount }, - { insufficientBal }, - ) => { - return { - quotes, - quotesLastFetchedMs, - isLoading, - quotesRefreshCount, - isQuoteGoingToRefresh: insufficientBal - ? false - : quotesRefreshCount < maxRefreshCount, - }; - }, -); - -export const getRecommendedQuote = createSelector( - getBridgeQuotes, - ({ quotes }) => { - return quotes[0]; - }, -); - -export const getToAmount = createSelector(getRecommendedQuote, (quote) => - quote - ? calcTokenAmount( - quote.quote.destTokenAmount, - quote.quote.destAsset.decimals, - ) - : undefined, -); - export const getIsBridgeTx = createDeepEqualSelector( getFromChain, getToChain, diff --git a/ui/ducks/locale/locale.js b/ui/ducks/locale/locale.js new file mode 100644 index 000000000000..5118a749ab58 --- /dev/null +++ b/ui/ducks/locale/locale.js @@ -0,0 +1,42 @@ +import { createSelector } from 'reselect'; +import * as actionConstants from '../../store/actionConstants'; + +export default function reduceLocaleMessages(state = {}, { type, payload }) { + switch (type) { + case actionConstants.SET_CURRENT_LOCALE: + return { + ...state, + current: payload.messages, + currentLocale: payload.locale, + }; + default: + return state; + } +} + +/** + * This selector returns a code from file://./../../../app/_locales/index.json. + * + * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. + * + * @param state + * @returns {string} the user's selected locale. + * These codes are not safe to use with the Intl API. + */ +export const getCurrentLocale = (state) => state.localeMessages.currentLocale; + +/** + * This selector returns a + * [BCP 47 Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag) + * for use with the Intl API. + * + * @returns {Intl.UnicodeBCP47LocaleIdentifier} the user's selected locale. + */ +export const getIntlLocale = createSelector( + getCurrentLocale, + (locale) => Intl.getCanonicalLocales(locale?.replace(/_/gu, '-'))[0], +); + +export const getCurrentLocaleMessages = (state) => state.localeMessages.current; + +export const getEnLocaleMessages = (state) => state.localeMessages.en; diff --git a/ui/ducks/locale/locale.test.ts b/ui/ducks/locale/locale.test.ts index 67627bb73423..37fc1f99e29a 100644 --- a/ui/ducks/locale/locale.test.ts +++ b/ui/ducks/locale/locale.test.ts @@ -1,80 +1,32 @@ // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import locales from '../../../app/_locales/index.json'; -import testData from '../../../test/data/mock-state.json'; +import { getIntlLocale } from './locale'; -import { - getCurrentLocale, - getIntlLocale, - getCurrentLocaleMessages, - getEnLocaleMessages, -} from './locale'; - -// Mock state creation functions const createMockStateWithLocale = (locale: string) => ({ localeMessages: { currentLocale: locale }, }); -describe('Locale Selectors', () => { - describe('getCurrentLocale', () => { - it('returns the current locale from the state', () => { - expect(getCurrentLocale(testData)).toBe('en'); - }); +describe('getIntlLocale', () => { + it('returns the canonical BCP 47 language tag for the currently selected locale', () => { + const mockState = createMockStateWithLocale('ab-cd'); - it('returns undefined if no current locale is set', () => { - const newAppState = { - ...testData, - localeMessages: { - currentLocale: undefined, - }, - }; - expect(getCurrentLocale(newAppState)).toBeUndefined(); - }); + expect(getIntlLocale(mockState)).toBe('ab-CD'); }); - describe('getIntlLocale', () => { - it('returns the canonical BCP 47 language tag for the currently selected locale', () => { - const mockState = createMockStateWithLocale('en_US'); - expect(getIntlLocale(mockState)).toBe('en-US'); - }); + it('throws an error if locale cannot be made into BCP 47 language tag', () => { + const mockState = createMockStateWithLocale('xxxinvalid-locale'); - locales.forEach((locale: { code: string; name: string }) => { - it(`handles supported locale - "${locale.code}"`, () => { - const mockState = createMockStateWithLocale(locale.code); - expect(() => getIntlLocale(mockState)).not.toThrow(); - }); - }); + expect(() => getIntlLocale(mockState)).toThrow(); }); - describe('getCurrentLocaleMessages', () => { - it('returns the current locale messages from the state', () => { - expect(getCurrentLocaleMessages(testData)).toEqual({ user: 'user' }); - }); - - it('returns undefined if there are no current locale messages', () => { - const newAppState = { - ...testData, - localeMessages: { - current: undefined, - }, - }; - expect(getCurrentLocaleMessages(newAppState)).toEqual(undefined); - }); - }); + // @ts-expect-error This is missing from the Mocha type definitions + it.each(locales)( + 'handles all supported locales – "%s"', + (locale: { code: string; name: string }) => { + const mockState = createMockStateWithLocale(locale.code); - describe('getEnLocaleMessages', () => { - it('returns the English locale messages from the state', () => { - expect(getEnLocaleMessages(testData)).toEqual({ user: 'user' }); - }); - - it('returns undefined if there are no English locale messages', () => { - const newAppState = { - ...testData, - localeMessages: { - en: undefined, - }, - }; - expect(getEnLocaleMessages(newAppState)).toBeUndefined(); - }); - }); + expect(() => getIntlLocale(mockState)).not.toThrow(); + }, + ); }); diff --git a/ui/ducks/locale/locale.ts b/ui/ducks/locale/locale.ts deleted file mode 100644 index 2ca0cbaac4dc..000000000000 --- a/ui/ducks/locale/locale.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { createSelector } from 'reselect'; -import { Action } from 'redux'; // Import types for actions -import * as actionConstants from '../../store/actionConstants'; -import { FALLBACK_LOCALE } from '../../../shared/modules/i18n'; - -/** - * Type for the locale messages part of the state - */ -type LocaleMessagesState = { - current?: { [key: string]: string }; // Messages for the current locale - currentLocale?: string; // User's selected locale (unsafe for Intl API) - en?: { [key: string]: string }; // English locale messages -}; - -/** - * Payload for the SET_CURRENT_LOCALE action - */ -type SetCurrentLocaleAction = Action & { - type: typeof actionConstants.SET_CURRENT_LOCALE; - payload: { - messages: { [key: string]: string }; - locale: string; - }; -}; - -/** - * Type for actions that can be handled by reduceLocaleMessages - */ -type LocaleMessagesActions = SetCurrentLocaleAction; - -/** - * Initial state for localeMessages reducer - */ -const initialState: LocaleMessagesState = {}; - -/** - * Reducer for localeMessages - * - * @param state - The current state - * @param action - The action being dispatched - * @returns The updated locale messages state - */ -export default function reduceLocaleMessages( - // eslint-disable-next-line @typescript-eslint/default-param-last - state: LocaleMessagesState = initialState, - action: LocaleMessagesActions, -): LocaleMessagesState { - switch (action.type) { - case actionConstants.SET_CURRENT_LOCALE: - return { - ...state, - current: action.payload.messages, - currentLocale: action.payload.locale, - }; - default: - return state; - } -} - -/** - * Type for the overall Redux state - */ -type AppState = { - localeMessages: LocaleMessagesState; -}; - -/** - * This selector returns a code from file://./../../../app/_locales/index.json. - * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. - * - * @param state - The overall state - * @returns The user's selected locale - */ -export const getCurrentLocale = (state: AppState): string | undefined => - state.localeMessages.currentLocale; - -/** - * This selector returns a BCP 47 Language Tag for use with the Intl API. - * - * @returns The user's selected locale in BCP 47 format - */ -export const getIntlLocale = createSelector( - getCurrentLocale, - (locale): string => - Intl.getCanonicalLocales( - locale ? locale.replace(/_/gu, '-') : FALLBACK_LOCALE, - )[0], -); - -/** - * This selector returns the current locale messages. - * - * @param state - The overall state - * @returns The current locale's messages - */ -export const getCurrentLocaleMessages = ( - state: AppState, -): Record | undefined => state.localeMessages.current; - -/** - * This selector returns the English locale messages. - * - * @param state - The overall state - * @returns The English locale's messages - */ -export const getEnLocaleMessages = ( - state: AppState, -): Record | undefined => state.localeMessages.en; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 63ff92a11ccc..05cc6d46cb27 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -17,9 +17,9 @@ import { checkNetworkAndAccountSupports1559, getAddressBook, getSelectedNetworkClientId, + getSelectedInternalAccount, getNetworkConfigurationsByChainId, -} from '../../selectors/selectors'; -import { getSelectedInternalAccount } from '../../selectors/accounts'; +} from '../../selectors'; import * as actionConstants from '../../store/actionConstants'; import { updateTransactionGasFees } from '../../store/actions'; import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck'; @@ -47,10 +47,9 @@ const initialState = { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: true, + smartTransactionsOptInStatus: false, petnamesEnabled: true, featureNotificationsEnabled: false, - privacyMode: false, showMultiRpcModal: false, }, firstTimeFlowType: null, diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts index 8bd6865295d8..3cd543a65219 100644 --- a/ui/ducks/ramps/ramps.test.ts +++ b/ui/ducks/ramps/ramps.test.ts @@ -205,7 +205,7 @@ describe('rampsSlice', () => { }); it('should return true when Bitcoin is buyable and current chain is Bitcoin', () => { - getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN); getMultichainIsBitcoinMock.mockReturnValue(true); const mockBuyableChains = [ { chainId: MultichainNetworks.BITCOIN, active: true }, @@ -219,7 +219,7 @@ describe('rampsSlice', () => { }); it('should return false when Bitcoin is not buyable and current chain is Bitcoin', () => { - getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); + getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN); getMultichainIsBitcoinMock.mockReturnValue(true); const mockBuyableChains = [ { chainId: MultichainNetworks.BITCOIN, active: false }, diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 30cbc6eeb5dd..700fd466004d 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -2615,12 +2615,7 @@ export function updateSendAsset( let missingProperty = STANDARD_TO_REQUIRED_PROPERTIES[ providedDetails.standard - ]?.find((property) => { - if (providedDetails.collection && property === 'symbol') { - return providedDetails.collection[property] === undefined; - } - return providedDetails[property] === undefined; - }); + ]?.find((property) => providedDetails[property] === undefined); let details; @@ -2657,9 +2652,10 @@ export function updateSendAsset( providedDetails.address, sendingAddress, providedDetails.tokenId, - ).catch(() => { + ).catch((error) => { // prevent infinite stuck loading state dispatch(hideLoadingIndication()); + throw error; })), }; } @@ -2674,7 +2670,7 @@ export function updateSendAsset( if (details.standard === TokenStandard.ERC20) { asset.balance = - details.balance && details.decimals !== undefined + details.balance && typeof details.decimals === 'number' ? addHexPrefix( calcTokenAmount(details.balance, details.decimals).toString(16), ) diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index d23c0ce69381..8dd7336d7a62 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -858,7 +858,6 @@ export const fetchQuotesAndSetQuoteState = ( stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), - gas_included: newSelectedQuote.isGasIncludedTrade, anonymizedData: true, }, }); diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index 232bcdae5aff..c22b0cbcf183 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -1,8 +1,4 @@ /* eslint-disable @metamask/design-tokens/color-no-hex*/ -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { getPlatform } from '../../../app/scripts/lib/util'; -import { PLATFORM_FIREFOX } from '../../../shared/constants/app'; import { IconName } from '../../components/component-library'; import { ADVANCED_ROUTE, @@ -23,7 +19,6 @@ import { * # @param {string} route tab route with appended arbitrary, unique anchor tag / hash route * # @param {string} iconName * # @param {string} featureFlag ENV variable name. If the ENV value exists, the route will be searchable; else, route will not be searchable. - * # @param {boolean} hidden If true, the route will not be searchable. */ /** @type {SettingRouteConfig[]} */ @@ -159,16 +154,6 @@ const SETTINGS_CONSTANTS = [ route: `${ADVANCED_ROUTE}#export-data`, icon: 'fas fa-download', }, - // advanced settingsRefs[11] - { - tabMessage: (t) => t('advanced'), - sectionMessage: (t) => t('overrideContentSecurityPolicyHeader'), - descriptionMessage: (t) => - t('overrideContentSecurityPolicyHeaderDescription'), - route: `${ADVANCED_ROUTE}#override-content-security-policy-header`, - icon: 'fas fa-sliders-h', - hidden: getPlatform() !== PLATFORM_FIREFOX, - }, { tabMessage: (t) => t('contacts'), sectionMessage: (t) => t('contacts'), diff --git a/ui/helpers/utils/notification.util.ts b/ui/helpers/utils/notification.util.ts index afbba2b88172..489f1ca2f272 100644 --- a/ui/helpers/utils/notification.util.ts +++ b/ui/helpers/utils/notification.util.ts @@ -420,11 +420,9 @@ export const getNetworkFees = async (notification: OnChainRawNotification) => { const rpcUrl = getRpcUrlByChainId(`0x${chainId}` as HexChainId); const connection = { url: rpcUrl, - headers: process.env.STORYBOOK - ? undefined - : { - 'Infura-Source': 'metamask/metamask', - }, + headers: { + 'Infura-Source': 'metamask/metamask', + }, }; const provider = new JsonRpcProvider(connection); diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js index 8c11ad8fad52..07b4501c0208 100644 --- a/ui/helpers/utils/settings-search.js +++ b/ui/helpers/utils/settings-search.js @@ -8,10 +8,8 @@ export function getSettingsRoutes() { if (settingsRoutes) { return settingsRoutes; } - settingsRoutes = SETTINGS_CONSTANTS.filter( - (routeObject) => - (routeObject.featureFlag ? process.env[routeObject.featureFlag] : true) && - !routeObject.hidden, + settingsRoutes = SETTINGS_CONSTANTS.filter((routeObject) => + routeObject.featureFlag ? process.env[routeObject.featureFlag] : true, ); return settingsRoutes; } diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index 30af3ee6b9da..cc7b875d8c5e 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -68,10 +68,6 @@ const t = (key) => { return 'Dismiss Secret Recovery Phrase backup reminder'; case 'dismissReminderDescriptionField': return 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds'; - case 'overrideContentSecurityPolicyHeader': - return 'Override Content-Security-Policy header'; - case 'overrideContentSecurityPolicyHeaderDescription': - return "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility."; case 'Contacts': return 'Contacts'; case 'securityAndPrivacy': @@ -151,12 +147,9 @@ describe('Settings Search Utils', () => { describe('getSettingsRoutes', () => { it('should be an array of settings routes objects', () => { const NUM_OF_ENV_FEATURE_FLAG_SETTINGS = 4; - const NUM_OF_HIDDEN_SETTINGS = 1; expect(getSettingsRoutes()).toHaveLength( - SETTINGS_CONSTANTS.length - - NUM_OF_ENV_FEATURE_FLAG_SETTINGS - - NUM_OF_HIDDEN_SETTINGS, + SETTINGS_CONSTANTS.length - NUM_OF_ENV_FEATURE_FLAG_SETTINGS, ); }); }); diff --git a/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts b/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts new file mode 100644 index 000000000000..6032a7636128 --- /dev/null +++ b/ui/hooks/accounts/useBitcoinWalletSnapClient.test.ts @@ -0,0 +1,57 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { HandlerType } from '@metamask/snaps-utils'; +import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; +import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; +import { BITCOIN_WALLET_SNAP_ID } from '../../../shared/lib/accounts/bitcoin-wallet-snap'; +import { + handleSnapRequest, + multichainUpdateBalance, +} from '../../store/actions'; +import { useBitcoinWalletSnapClient } from './useBitcoinWalletSnapClient'; + +jest.mock('../../store/actions', () => ({ + handleSnapRequest: jest.fn(), + multichainUpdateBalance: jest.fn(), +})); + +const mockHandleSnapRequest = handleSnapRequest as jest.Mock; +const mockMultichainUpdateBalance = multichainUpdateBalance as jest.Mock; + +describe('useBitcoinWalletSnapClient', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockAccount = { + address: 'tb1q2hjrlnf8kmtt5dj6e49gqzy6jnpe0sj7ty50cl', + id: '11a33c6b-0d46-43f4-a401-01587d575fd0', + options: {}, + methods: [BtcMethod.SendMany], + type: BtcAccountType.P2wpkh, + }; + + it('dispatch a Snap keyring request to create a Bitcoin account', async () => { + const { result } = renderHook(() => useBitcoinWalletSnapClient()); + const bitcoinWalletSnapClient = result.current; + + mockHandleSnapRequest.mockResolvedValue(mockAccount); + + await bitcoinWalletSnapClient.createAccount(MultichainNetworks.BITCOIN); + expect(mockHandleSnapRequest).toHaveBeenCalledWith({ + origin: 'metamask', + snapId: BITCOIN_WALLET_SNAP_ID, + handler: HandlerType.OnKeyringRequest, + request: expect.any(Object), + }); + }); + + it('force fetches the balance after creating a Bitcoin account', async () => { + const { result } = renderHook(() => useBitcoinWalletSnapClient()); + const bitcoinWalletSnapClient = result.current; + + mockHandleSnapRequest.mockResolvedValue(mockAccount); + + await bitcoinWalletSnapClient.createAccount(MultichainNetworks.BITCOIN); + expect(mockMultichainUpdateBalance).toHaveBeenCalledWith(mockAccount.id); + }); +}); diff --git a/ui/hooks/accounts/useMultichainWalletSnapClient.ts b/ui/hooks/accounts/useBitcoinWalletSnapClient.ts similarity index 60% rename from ui/hooks/accounts/useMultichainWalletSnapClient.ts rename to ui/hooks/accounts/useBitcoinWalletSnapClient.ts index 98dfa9b429d3..debe911ac391 100644 --- a/ui/hooks/accounts/useMultichainWalletSnapClient.ts +++ b/ui/hooks/accounts/useBitcoinWalletSnapClient.ts @@ -1,54 +1,32 @@ import { KeyringClient, Sender } from '@metamask/keyring-api'; import { HandlerType } from '@metamask/snaps-utils'; import { CaipChainId, Json, JsonRpcRequest } from '@metamask/utils'; -import { SnapId } from '@metamask/snaps-sdk'; import { useMemo } from 'react'; import { handleSnapRequest, multichainUpdateBalance, } from '../../store/actions'; import { BITCOIN_WALLET_SNAP_ID } from '../../../shared/lib/accounts/bitcoin-wallet-snap'; -import { SOLANA_WALLET_SNAP_ID } from '../../../shared/lib/accounts/solana-wallet-snap'; - -export enum WalletClientType { - Bitcoin = 'bitcoin-wallet-snap', - Solana = 'solana-wallet-snap', -} - -const SNAP_ID_MAP: Record = { - [WalletClientType.Bitcoin]: BITCOIN_WALLET_SNAP_ID, - [WalletClientType.Solana]: SOLANA_WALLET_SNAP_ID, -}; - -export class MultichainWalletSnapSender implements Sender { - private snapId: SnapId; - - constructor(snapId: SnapId) { - this.snapId = snapId; - } +export class BitcoinWalletSnapSender implements Sender { send = async (request: JsonRpcRequest): Promise => { // We assume the caller of this module is aware of this. If we try to use this module // without having the pre-installed Snap, this will likely throw an error in // the `handleSnapRequest` action. return (await handleSnapRequest({ origin: 'metamask', - snapId: this.snapId, + snapId: BITCOIN_WALLET_SNAP_ID, handler: HandlerType.OnKeyringRequest, request, })) as Json; }; } -export class MultichainWalletSnapClient { +export class BitcoinWalletSnapClient { readonly #client: KeyringClient; - constructor(clientType: WalletClientType) { - const snapId = SNAP_ID_MAP[clientType]; - if (!snapId) { - throw new Error(`Unsupported client type: ${clientType}`); - } - this.#client = new KeyringClient(new MultichainWalletSnapSender(snapId)); + constructor() { + this.#client = new KeyringClient(new BitcoinWalletSnapSender()); } async createAccount(scope: CaipChainId) { @@ -65,10 +43,10 @@ export class MultichainWalletSnapClient { } } -export function useMultichainWalletSnapClient(clientType: WalletClientType) { +export function useBitcoinWalletSnapClient() { const client = useMemo(() => { - return new MultichainWalletSnapClient(clientType); - }, [clientType]); + return new BitcoinWalletSnapClient(); + }, []); return client; } diff --git a/ui/hooks/accounts/useMultichainWalletSnapClient.test.ts b/ui/hooks/accounts/useMultichainWalletSnapClient.test.ts deleted file mode 100644 index d177986fee44..000000000000 --- a/ui/hooks/accounts/useMultichainWalletSnapClient.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { HandlerType } from '@metamask/snaps-utils'; -import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; -import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; -import { BITCOIN_WALLET_SNAP_ID } from '../../../shared/lib/accounts/bitcoin-wallet-snap'; -import { SOLANA_WALLET_SNAP_ID } from '../../../shared/lib/accounts/solana-wallet-snap'; -import { - handleSnapRequest, - multichainUpdateBalance, -} from '../../store/actions'; -import { - useMultichainWalletSnapClient, - WalletClientType, -} from './useMultichainWalletSnapClient'; - -jest.mock('../../store/actions', () => ({ - handleSnapRequest: jest.fn(), - multichainUpdateBalance: jest.fn(), -})); - -const mockHandleSnapRequest = handleSnapRequest as jest.Mock; -const mockMultichainUpdateBalance = multichainUpdateBalance as jest.Mock; - -describe('useMultichainWalletSnapClient', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - const testCases = [ - { - clientType: WalletClientType.Bitcoin, - network: MultichainNetworks.BITCOIN, - snapId: BITCOIN_WALLET_SNAP_ID, - mockAccount: { - address: 'tb1q2hjrlnf8kmtt5dj6e49gqzy6jnpe0sj7ty50cl', - id: '11a33c6b-0d46-43f4-a401-01587d575fd0', - options: {}, - methods: [BtcMethod.SendMany], - type: BtcAccountType.P2wpkh, - }, - }, - { - clientType: WalletClientType.Solana, - network: MultichainNetworks.SOLANA, - snapId: SOLANA_WALLET_SNAP_ID, - mockAccount: { - address: '4mip4tgbhxf8dpqvtb3zhzzapwfvznanhssqzgjyp7ha', - id: '22b44d7c-1e57-4b5b-8502-02698e686fd1', - options: {}, - methods: ['someMethod'], - // TODO: Update when keyring-api is published with Solana types - type: BtcAccountType.P2wpkh, - }, - }, - ]; - - testCases.forEach(({ clientType, network, snapId, mockAccount }) => { - it(`dispatches a Snap keyring request to create a ${clientType} account`, async () => { - const { result } = renderHook(() => - useMultichainWalletSnapClient(clientType), - ); - const multichainWalletSnapClient = result.current; - - mockHandleSnapRequest.mockResolvedValue(mockAccount); - - await multichainWalletSnapClient.createAccount(network); - expect(mockHandleSnapRequest).toHaveBeenCalledWith({ - origin: 'metamask', - snapId, - handler: HandlerType.OnKeyringRequest, - request: expect.any(Object), - }); - }); - - it(`force fetches the balance after creating a ${clientType} account`, async () => { - const { result } = renderHook(() => - useMultichainWalletSnapClient(clientType), - ); - const multichainWalletSnapClient = result.current; - - mockHandleSnapRequest.mockResolvedValue(mockAccount); - - await multichainWalletSnapClient.createAccount(network); - expect(mockMultichainUpdateBalance).toHaveBeenCalledWith(mockAccount.id); - }); - }); -}); diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index fe7a21e2206f..a68aeb361bdd 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -43,7 +43,6 @@ const useBridging = () => { const isMarketingEnabled = useSelector(getDataCollectionForMarketing); const providerConfig = useSelector(getProviderConfig); const keyring = useSelector(getCurrentKeyring); - // @ts-expect-error keyring type is wrong maybe? const usingHardwareWallet = isHardwareKeyring(keyring.type); const isBridgeSupported = useSelector(getIsBridgeEnabled); diff --git a/ui/hooks/bridge/useCountdownTimer.test.ts b/ui/hooks/bridge/useCountdownTimer.test.ts deleted file mode 100644 index f2cd1190b1ba..000000000000 --- a/ui/hooks/bridge/useCountdownTimer.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { renderHookWithProvider } from '../../../test/lib/render-helpers'; -import { createBridgeMockStore } from '../../../test/jest/mock-store'; -import { flushPromises } from '../../../test/lib/timer-helpers'; -import { useCountdownTimer } from './useCountdownTimer'; - -jest.useFakeTimers(); -const renderUseCountdownTimer = (mockStoreState: object) => - renderHookWithProvider(() => useCountdownTimer(), mockStoreState); - -describe('useCountdownTimer', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.clearAllTimers(); - }); - - it('returns time remaining', async () => { - const quotesLastFetched = Date.now(); - const { result } = renderUseCountdownTimer( - createBridgeMockStore( - {}, - {}, - { - quotesLastFetched, - quotesRefreshCount: 0, - bridgeFeatureFlags: { - extensionConfig: { maxRefreshCount: 5, refreshRate: 40000 }, - }, - }, - ), - ); - - let i = 0; - while (i <= 40) { - const secondsLeft = Math.min(41, 40 - i + 2); - expect(result.current).toStrictEqual( - `0:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`, - ); - i += 10; - jest.advanceTimersByTime(10000); - await flushPromises(); - } - expect(result.current).toStrictEqual('0:00'); - }); -}); diff --git a/ui/hooks/bridge/useCountdownTimer.ts b/ui/hooks/bridge/useCountdownTimer.ts deleted file mode 100644 index 39e7ac9d2eca..000000000000 --- a/ui/hooks/bridge/useCountdownTimer.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Duration } from 'luxon'; -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - getBridgeQuotes, - getBridgeQuotesConfig, -} from '../../ducks/bridge/selectors'; -import { SECOND } from '../../../shared/constants/time'; - -/** - * Custom hook that provides a countdown timer based on the last fetched quotes timestamp. - * - * This hook calculates the remaining time until the next refresh interval and updates every second. - * - * @returns The formatted remaining time in 'm:ss' format. - */ -export const useCountdownTimer = () => { - const { quotesLastFetchedMs } = useSelector(getBridgeQuotes); - const { refreshRate } = useSelector(getBridgeQuotesConfig); - - const [timeRemaining, setTimeRemaining] = useState(refreshRate); - - useEffect(() => { - if (quotesLastFetchedMs) { - setTimeRemaining( - refreshRate - (Date.now() - quotesLastFetchedMs) + SECOND, - ); - } - }, [quotesLastFetchedMs]); - - useEffect(() => { - const interval = setInterval(() => { - setTimeRemaining(Math.max(0, timeRemaining - SECOND)); - }, SECOND); - return () => clearInterval(interval); - }, [timeRemaining]); - - return Duration.fromMillis(timeRemaining).toFormat('m:ss'); -}; diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts index 6d79672e4550..d1186c3eeb91 100644 --- a/ui/hooks/bridge/useLatestBalance.test.ts +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -61,7 +61,7 @@ describe('useLatestBalance', () => { expect(mockGetBalance).toHaveBeenCalledTimes(1); expect(mockGetBalance).toHaveBeenCalledWith( - '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', ); expect(mockFetchTokenBalance).toHaveBeenCalledTimes(0); }); diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index 524d65503249..dfe868a04090 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -1,14 +1,17 @@ import { useSelector } from 'react-redux'; +import { zeroAddress } from 'ethereumjs-util'; +import { Web3Provider } from '@ethersproject/providers'; import { Hex } from '@metamask/utils'; +import { BigNumber } from 'ethers'; import { Numeric } from '../../../shared/modules/Numeric'; import { DEFAULT_PRECISION } from '../useCurrencyDisplay'; +import { fetchTokenBalance } from '../../../shared/lib/token-util'; import { getCurrentChainId, getSelectedInternalAccount, SwapsEthToken, } from '../../selectors'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; -import { calcLatestSrcBalance } from '../../../shared/modules/bridge-utils/balance'; import { useAsyncResult } from '../useAsyncResult'; /** @@ -25,25 +28,21 @@ const useLatestBalance = ( const { address: selectedAddress } = useSelector(getSelectedInternalAccount); const currentChainId = useSelector(getCurrentChainId); - const { value: latestBalance } = useAsyncResult< - Numeric | undefined - >(async () => { - if (token?.address && chainId && currentChainId === chainId) { - return await calcLatestSrcBalance( - global.ethereumProvider, - selectedAddress, + const { value: latestBalance } = useAsyncResult(async () => { + if (token && chainId && currentChainId === chainId) { + if (!token.address || token.address === zeroAddress()) { + const ethersProvider = new Web3Provider(global.ethereumProvider); + return await ethersProvider.getBalance(selectedAddress); + } + return await fetchTokenBalance( token.address, - chainId, + selectedAddress, + global.ethereumProvider, ); } + return undefined; - }, [ - chainId, - currentChainId, - token, - selectedAddress, - global.ethereumProvider, - ]); + }, [token, selectedAddress, global.ethereumProvider]); if (token && !token.decimals) { throw new Error( @@ -56,7 +55,7 @@ const useLatestBalance = ( return { formattedBalance: token && latestBalance - ? latestBalance + ? Numeric.from(latestBalance.toString(), 10) .shiftedBy(tokenDecimals) .round(DEFAULT_PRECISION) .toString() diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx new file mode 100644 index 000000000000..951cec333ade --- /dev/null +++ b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { renderHook, act } from '@testing-library/react-hooks'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { waitFor } from '@testing-library/react'; +import * as actions from '../../store/actions'; +import { + useEnableProfileSyncing, + useDisableProfileSyncing, + useAccountSyncingEffect, + useDeleteAccountSyncingDataFromUserStorage, +} from './useProfileSyncing'; + +const middlewares = [thunk]; +const mockStore = configureStore(middlewares); + +jest.mock('../../store/actions', () => ({ + performSignIn: jest.fn(), + performSignOut: jest.fn(), + enableProfileSyncing: jest.fn(), + disableProfileSyncing: jest.fn(), + showLoadingIndication: jest.fn(), + hideLoadingIndication: jest.fn(), + syncInternalAccountsWithUserStorage: jest.fn(), + deleteAccountSyncingDataFromUserStorage: jest.fn(), +})); + +type ArrangeMocksMetamaskStateOverrides = { + isSignedIn?: boolean; + isProfileSyncingEnabled?: boolean; + isUnlocked?: boolean; + useExternalServices?: boolean; + completedOnboarding?: boolean; +}; + +const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = { + isSignedIn: false, + isProfileSyncingEnabled: false, + isUnlocked: true, + useExternalServices: true, + completedOnboarding: true, +}; + +const arrangeMocks = ( + metamaskStateOverrides?: ArrangeMocksMetamaskStateOverrides, +) => { + const store = mockStore({ + metamask: { + ...initialMetamaskState, + ...metamaskStateOverrides, + participateInMetaMetrics: false, + internalAccounts: { + accounts: { + '0x123': { + address: '0x123', + id: 'account1', + metadata: {}, + options: {}, + methods: [], + type: 'eip155:eoa', + }, + }, + }, + }, + }); + + store.dispatch = jest.fn().mockImplementation((action) => { + if (typeof action === 'function') { + return action(store.dispatch, store.getState); + } + return Promise.resolve(); + }); + + jest.clearAllMocks(); + + return { store }; +}; + +describe('useProfileSyncing', () => { + it('should enable profile syncing', async () => { + const { store } = arrangeMocks(); + + const { result } = renderHook(() => useEnableProfileSyncing(), { + wrapper: ({ children }) => {children}, + }); + + act(() => { + result.current.enableProfileSyncing(); + }); + + expect(actions.enableProfileSyncing).toHaveBeenCalled(); + }); + + it('should disable profile syncing', async () => { + const { store } = arrangeMocks(); + + const { result } = renderHook(() => useDisableProfileSyncing(), { + wrapper: ({ children }) => {children}, + }); + + act(() => { + result.current.disableProfileSyncing(); + }); + + expect(actions.disableProfileSyncing).toHaveBeenCalled(); + }); + + it('should dispatch account syncing when conditions are met', async () => { + const { store } = arrangeMocks({ + isSignedIn: true, + isProfileSyncingEnabled: true, + }); + + renderHook(() => useAccountSyncingEffect(), { + wrapper: ({ children }) => {children}, + }); + + await waitFor(() => { + expect(actions.syncInternalAccountsWithUserStorage).toHaveBeenCalled(); + }); + }); + + it('should not dispatch account syncing when conditions are not met', async () => { + const { store } = arrangeMocks(); + + renderHook(() => useAccountSyncingEffect(), { + wrapper: ({ children }) => {children}, + }); + + await waitFor(() => { + expect( + actions.syncInternalAccountsWithUserStorage, + ).not.toHaveBeenCalled(); + }); + }); + + it('should dispatch account sync data deletion', async () => { + const { store } = arrangeMocks(); + + const { result } = renderHook( + () => useDeleteAccountSyncingDataFromUserStorage(), + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); + + act(() => { + result.current.dispatchDeleteAccountSyncingDataFromUserStorage(); + }); + + expect(actions.deleteAccountSyncingDataFromUserStorage).toHaveBeenCalled(); + }); +}); diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing.ts similarity index 53% rename from ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts rename to ui/hooks/metamask-notifications/useProfileSyncing.ts index 5c073fdf6d94..67899aa73927 100644 --- a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts +++ b/ui/hooks/metamask-notifications/useProfileSyncing.ts @@ -1,21 +1,35 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import type { InternalAccount } from '@metamask/keyring-api'; import log from 'loglevel'; -import { useMetamaskNotificationsContext } from '../../../contexts/metamask-notifications/metamask-notifications'; import { disableProfileSyncing as disableProfileSyncingAction, enableProfileSyncing as enableProfileSyncingAction, setIsProfileSyncingEnabled as setIsProfileSyncingEnabledAction, hideLoadingIndication, -} from '../../../store/actions'; + syncInternalAccountsWithUserStorage, + deleteAccountSyncingDataFromUserStorage, +} from '../../store/actions'; -import { selectIsSignedIn } from '../../../selectors/metamask-notifications/authentication'; -import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing'; -import { getUseExternalServices } from '../../../selectors'; +import { selectIsSignedIn } from '../../selectors/metamask-notifications/authentication'; +import { selectIsProfileSyncingEnabled } from '../../selectors/metamask-notifications/profile-syncing'; +import { getUseExternalServices } from '../../selectors'; import { getIsUnlocked, getCompletedOnboarding, -} from '../../../ducks/metamask/metamask'; +} from '../../ducks/metamask/metamask'; + +// Define KeyringType interface +export type KeyringType = { + type: string; +}; + +// Define AccountType interface +export type AccountType = InternalAccount & { + balance: string; + keyring: KeyringType; + label: string; +}; /** * Custom hook to enable profile syncing. This hook handles the process of signing in @@ -60,7 +74,6 @@ export function useDisableProfileSyncing(): { error: string | null; } { const dispatch = useDispatch(); - const { listNotifications } = useMetamaskNotificationsContext(); const [error, setError] = useState(null); @@ -70,9 +83,6 @@ export function useDisableProfileSyncing(): { try { // disable profile syncing await dispatch(disableProfileSyncingAction()); - - // list notifications to update the counter - await listNotifications(); } catch (e) { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e ?? ''); @@ -114,29 +124,92 @@ export function useSetIsProfileSyncingEnabled(): { } /** - * A utility used internally to decide if syncing features should be dispatched - * Considers factors like basic functionality; unlocked; finished onboarding, and is logged in + * Custom hook to dispatch account syncing. * - * @returns a boolean if internally we can perform syncing features or not. + * @returns An object containing the `dispatchAccountSyncing` function, boolean `shouldDispatchAccountSyncing`, + * and error state. */ -export const useShouldDispatchProfileSyncing = () => { +export const useAccountSyncing = () => { + const dispatch = useDispatch(); + + const [error, setError] = useState(null); + const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled); - const basicFunctionality: boolean | undefined = useSelector( - getUseExternalServices, - ); - const isUnlocked: boolean | undefined = useSelector(getIsUnlocked); + const basicFunctionality = useSelector(getUseExternalServices); + const isUnlocked = useSelector(getIsUnlocked); const isSignedIn = useSelector(selectIsSignedIn); - const completedOnboarding: boolean | undefined = useSelector( - getCompletedOnboarding, - ); + const completedOnboarding = useSelector(getCompletedOnboarding); - const shouldDispatchProfileSyncing: boolean = Boolean( - basicFunctionality && + const shouldDispatchAccountSyncing = useMemo( + () => + basicFunctionality && isProfileSyncingEnabled && isUnlocked && isSignedIn && completedOnboarding, + [ + basicFunctionality, + isProfileSyncingEnabled, + isUnlocked, + isSignedIn, + completedOnboarding, + ], ); - return shouldDispatchProfileSyncing; + const dispatchAccountSyncing = useCallback(() => { + setError(null); + + try { + if (!shouldDispatchAccountSyncing) { + return; + } + dispatch(syncInternalAccountsWithUserStorage()); + } catch (e) { + log.error(e); + setError(e instanceof Error ? e.message : 'An unexpected error occurred'); + } + }, [dispatch, shouldDispatchAccountSyncing]); + + return { + dispatchAccountSyncing, + shouldDispatchAccountSyncing, + error, + }; +}; + +/** + * Custom hook to delete a user's account syncing data from user storage + */ + +export const useDeleteAccountSyncingDataFromUserStorage = () => { + const dispatch = useDispatch(); + + const [error, setError] = useState(null); + + const dispatchDeleteAccountSyncingDataFromUserStorage = useCallback(() => { + setError(null); + + try { + dispatch(deleteAccountSyncingDataFromUserStorage()); + } catch (e) { + log.error(e); + setError(e instanceof Error ? e.message : 'An unexpected error occurred'); + } + }, [dispatch]); + + return { + dispatchDeleteAccountSyncingDataFromUserStorage, + error, + }; +}; + +/** + * Custom hook to apply account syncing effect. + */ +export const useAccountSyncingEffect = () => { + const { dispatchAccountSyncing } = useAccountSyncing(); + + useEffect(() => { + dispatchAccountSyncing(); + }, [dispatchAccountSyncing]); }; diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx deleted file mode 100644 index 604466b3a75c..000000000000 --- a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { waitFor } from '@testing-library/react'; -import { act } from '@testing-library/react-hooks'; -import { renderHookWithProviderTyped } from '../../../../test/lib/render-helpers'; -import * as actions from '../../../store/actions'; -import { - useAccountSyncingEffect, - useDeleteAccountSyncingDataFromUserStorage, -} from './accountSyncing'; -import * as ProfileSyncModule from './profileSyncing'; - -describe('useDeleteAccountSyncingDataFromUserStorage()', () => { - it('should dispatch account sync data deletion', async () => { - const mockDeleteAccountSyncAction = jest.spyOn( - actions, - 'deleteAccountSyncingDataFromUserStorage', - ); - - const { result } = renderHookWithProviderTyped( - () => useDeleteAccountSyncingDataFromUserStorage(), - {}, - ); - - await act(async () => { - await result.current.dispatchDeleteAccountData(); - }); - - expect(mockDeleteAccountSyncAction).toHaveBeenCalled(); - }); -}); - -describe('useAccountSyncingEffect', () => { - const arrangeMocks = () => { - const mockUseShouldProfileSync = jest.spyOn( - ProfileSyncModule, - 'useShouldDispatchProfileSyncing', - ); - const mockSyncAccountsAction = jest.spyOn( - actions, - 'syncInternalAccountsWithUserStorage', - ); - return { - mockUseShouldProfileSync, - mockSyncAccountsAction, - }; - }; - - const arrangeAndAct = (props: { profileSyncConditionsMet: boolean }) => { - const mocks = arrangeMocks(); - mocks.mockUseShouldProfileSync.mockReturnValue( - props.profileSyncConditionsMet, - ); - - renderHookWithProviderTyped(() => useAccountSyncingEffect(), {}); - return mocks; - }; - - it('should run effect if profile sync conditions are met', async () => { - const mocks = arrangeAndAct({ profileSyncConditionsMet: true }); - await waitFor(() => { - expect(mocks.mockSyncAccountsAction).toHaveBeenCalled(); - }); - }); - - it('should not run effect if profile sync conditions are not met', async () => { - const mocks = arrangeAndAct({ profileSyncConditionsMet: false }); - await waitFor(() => { - expect(mocks.mockSyncAccountsAction).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts deleted file mode 100644 index cef4dc80fa75..000000000000 --- a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts +++ /dev/null @@ -1,66 +0,0 @@ -import log from 'loglevel'; -import { useCallback, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { - deleteAccountSyncingDataFromUserStorage, - syncInternalAccountsWithUserStorage, -} from '../../../store/actions'; -import { useShouldDispatchProfileSyncing } from './profileSyncing'; - -/** - * Custom hook to dispatch account syncing. - * - * @returns An object containing the `dispatchAccountSyncing` function, boolean `shouldDispatchAccountSyncing`, - * and error state. - */ -const useAccountSyncing = () => { - const dispatch = useDispatch(); - - const shouldDispatchAccountSyncing = useShouldDispatchProfileSyncing(); - - const dispatchAccountSyncing = useCallback(() => { - try { - if (!shouldDispatchAccountSyncing) { - return; - } - dispatch(syncInternalAccountsWithUserStorage()); - } catch (e) { - log.error(e); - } - }, [dispatch, shouldDispatchAccountSyncing]); - - return { - dispatchAccountSyncing, - shouldDispatchAccountSyncing, - }; -}; - -/** - * Custom hook to apply account syncing effect. - */ -export const useAccountSyncingEffect = () => { - const shouldSync = useShouldDispatchProfileSyncing(); - const { dispatchAccountSyncing } = useAccountSyncing(); - - useEffect(() => { - if (shouldSync) { - dispatchAccountSyncing(); - } - }, [shouldSync, dispatchAccountSyncing]); -}; - -/** - * Custom hook to delete a user's account syncing data from user storage - */ -export const useDeleteAccountSyncingDataFromUserStorage = () => { - const dispatch = useDispatch(); - const dispatchDeleteAccountData = useCallback(async () => { - try { - await dispatch(deleteAccountSyncingDataFromUserStorage()); - } catch { - // Do Nothing - } - }, []); - - return { dispatchDeleteAccountData }; -}; diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/index.ts b/ui/hooks/metamask-notifications/useProfileSyncing/index.ts deleted file mode 100644 index 9a6cda8468fb..000000000000 --- a/ui/hooks/metamask-notifications/useProfileSyncing/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - useDisableProfileSyncing, - useEnableProfileSyncing, - useSetIsProfileSyncingEnabled, -} from './profileSyncing'; -export { - useAccountSyncingEffect, - useDeleteAccountSyncingDataFromUserStorage, -} from './accountSyncing'; diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx deleted file mode 100644 index 99d3064085ea..000000000000 --- a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { act } from '@testing-library/react-hooks'; -import { renderHookWithProviderTyped } from '../../../../test/lib/render-helpers'; -import { MetamaskNotificationsProvider } from '../../../contexts/metamask-notifications'; -import * as actions from '../../../store/actions'; -import { - useDisableProfileSyncing, - useEnableProfileSyncing, - useShouldDispatchProfileSyncing, -} from './profileSyncing'; - -type ArrangeMocksMetamaskStateOverrides = { - isSignedIn?: boolean; - isProfileSyncingEnabled?: boolean; - isUnlocked?: boolean; - useExternalServices?: boolean; - completedOnboarding?: boolean; -}; - -const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = { - isSignedIn: false, - isProfileSyncingEnabled: false, - isUnlocked: true, - useExternalServices: true, - completedOnboarding: true, -}; - -const arrangeMockState = ( - metamaskStateOverrides?: ArrangeMocksMetamaskStateOverrides, -) => { - const state = { - metamask: { - ...initialMetamaskState, - ...metamaskStateOverrides, - }, - }; - - return { state }; -}; - -describe('useEnableProfileSyncing()', () => { - it('should enable profile syncing', async () => { - const mockEnableProfileSyncingAction = jest.spyOn( - actions, - 'enableProfileSyncing', - ); - - const { state } = arrangeMockState(); - const { result } = renderHookWithProviderTyped( - () => useEnableProfileSyncing(), - state, - ); - await act(async () => { - await result.current.enableProfileSyncing(); - }); - - expect(mockEnableProfileSyncingAction).toHaveBeenCalled(); - }); -}); - -describe('useDisableProfileSyncing()', () => { - it('should disable profile syncing', async () => { - const mockDisableProfileSyncingAction = jest.spyOn( - actions, - 'disableProfileSyncing', - ); - - const { state } = arrangeMockState(); - - const { result } = renderHookWithProviderTyped( - () => useDisableProfileSyncing(), - state, - undefined, - MetamaskNotificationsProvider, - ); - - await act(async () => { - await result.current.disableProfileSyncing(); - }); - - expect(mockDisableProfileSyncingAction).toHaveBeenCalled(); - }); -}); - -describe('useShouldDispatchProfileSyncing()', () => { - const testCases = (() => { - const properties = [ - 'isSignedIn', - 'isProfileSyncingEnabled', - 'isUnlocked', - 'useExternalServices', - 'completedOnboarding', - ] as const; - const baseState = { - isSignedIn: true, - isProfileSyncingEnabled: true, - isUnlocked: true, - useExternalServices: true, - completedOnboarding: true, - }; - - const failureStateCases: { - state: ArrangeMocksMetamaskStateOverrides; - failingField: string; - }[] = []; - - // Generate test cases by toggling each property - properties.forEach((property) => { - const state = { ...baseState, [property]: false }; - failureStateCases.push({ state, failingField: property }); - }); - - const successTestCase = { state: baseState }; - - return { successTestCase, failureStateCases }; - })(); - - it('should return true if all conditions are met', () => { - const { state } = arrangeMockState(testCases.successTestCase.state); - const hook = renderHookWithProviderTyped( - () => useShouldDispatchProfileSyncing(), - state, - ); - expect(hook.result.current).toBe(true); - }); - - testCases.failureStateCases.forEach(({ state, failingField }) => { - it(`should return false if not all conditions are met [${failingField} = false]`, () => { - const { state: newState } = arrangeMockState(state); - const hook = renderHookWithProviderTyped( - () => useShouldDispatchProfileSyncing(), - newState, - ); - expect(hook.result.current).toBe(false); - }); - }); -}); diff --git a/ui/hooks/snaps/useDisplayName.ts b/ui/hooks/snaps/useDisplayName.ts deleted file mode 100644 index 6a6d3d7e6b51..000000000000 --- a/ui/hooks/snaps/useDisplayName.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { NamespaceId } from '@metamask/snaps-utils'; -import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; -import { useSelector } from 'react-redux'; -import { - getMemoizedAccountName, - getAddressBookEntryByNetwork, - AddressBookMetaMaskState, - AccountsMetaMaskState, -} from '../../selectors/snaps'; -import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; -import { decimalToHex } from '../../../shared/modules/conversion.utils'; - -export type UseDisplayNameParams = { - chain: { - namespace: NamespaceId; - reference: string; - }; - chainId: CaipChainId; - address: string; -}; - -/** - * Get the display name for an address. - * This will look for an account name in the state, and if not found, it will look for an address book entry. - * - * @param params - The parsed CAIP-10 ID. - * @returns The display name for the address. - */ -export const useDisplayName = ( - params: UseDisplayNameParams, -): string | undefined => { - const { - address, - chain: { namespace, reference }, - } = params; - - const isEip155 = namespace === KnownCaipNamespace.Eip155; - - const parsedAddress = isEip155 ? toChecksumHexAddress(address) : address; - - const accountName = useSelector((state: AccountsMetaMaskState) => - getMemoizedAccountName(state, parsedAddress), - ); - - const addressBookEntry = useSelector((state: AddressBookMetaMaskState) => - getAddressBookEntryByNetwork( - state, - parsedAddress, - `0x${decimalToHex(isEip155 ? reference : `0`)}`, - ), - ); - - return accountName || (isEip155 && addressBookEntry?.name) || undefined; -}; diff --git a/ui/hooks/useAccountTotalFiatBalance.js b/ui/hooks/useAccountTotalFiatBalance.js index 7b4a4675225a..b0c9b293c906 100644 --- a/ui/hooks/useAccountTotalFiatBalance.js +++ b/ui/hooks/useAccountTotalFiatBalance.js @@ -51,6 +51,7 @@ export const useAccountTotalFiatBalance = ( const tokens = detectedTokens?.[currentChainId]?.[account?.address] ?? []; // This selector returns all the tokens, we need it to get the image of token const allTokenList = useSelector(getTokenList); + const allTokenListValues = Object.values(allTokenList); const primaryTokenImage = useSelector(getNativeCurrencyImage); const nativeCurrency = useSelector(getNativeCurrency); @@ -91,18 +92,20 @@ export const useAccountTotalFiatBalance = ( }; // To match the list of detected tokens with the entire token list to find the image for tokens - const findMatchingTokens = (tokenList, _tokensWithBalances) => { + const findMatchingTokens = (array1, array2) => { const result = []; - _tokensWithBalances.forEach((token) => { - const matchingToken = tokenList[token.address.toLowerCase()]; + array2.forEach((token2) => { + const matchingToken = array1.find( + (token1) => token1.symbol === token2.symbol, + ); if (matchingToken) { result.push({ ...matchingToken, - balance: token.balance, - string: token.string, - balanceError: token.balanceError, + balance: token2.balance, + string: token2.string, + balanceError: token2.balanceError, }); } }); @@ -110,7 +113,10 @@ export const useAccountTotalFiatBalance = ( return result; }; - const matchingTokens = findMatchingTokens(allTokenList, tokensWithBalances); + const matchingTokens = findMatchingTokens( + allTokenListValues, + tokensWithBalances, + ); // Combine native token, detected token with image in an array const allTokensWithFiatValues = [ diff --git a/ui/hooks/useCurrencyRatePolling.ts b/ui/hooks/useCurrencyRatePolling.ts index e7ad21adedf5..fb14b1c94797 100644 --- a/ui/hooks/useCurrencyRatePolling.ts +++ b/ui/hooks/useCurrencyRatePolling.ts @@ -1,30 +1,24 @@ import { useSelector } from 'react-redux'; import { - getNetworkConfigurationsByChainId, + getSelectedNetworkClientId, getUseCurrencyRateCheck, } from '../selectors'; import { - currencyRateStartPolling, + currencyRateStartPollingByNetworkClientId, currencyRateStopPollingByPollingToken, } from '../store/actions'; import { getCompletedOnboarding } from '../ducks/metamask/metamask'; import usePolling from './usePolling'; -const useCurrencyRatePolling = () => { +const useCurrencyRatePolling = (networkClientId?: string) => { const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const completedOnboarding = useSelector(getCompletedOnboarding); - const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); - - const nativeCurrencies = [ - ...new Set( - Object.values(networkConfigurations).map((n) => n.nativeCurrency), - ), - ]; + const selectedNetworkClientId = useSelector(getSelectedNetworkClientId); usePolling({ - startPolling: currencyRateStartPolling, + startPollingByNetworkClientId: currencyRateStartPollingByNetworkClientId, stopPollingByPollingToken: currencyRateStopPollingByPollingToken, - input: nativeCurrencies, + networkClientId: networkClientId ?? selectedNetworkClientId, enabled: useCurrencyRateCheck && completedOnboarding, }); }; diff --git a/ui/hooks/useDisplayName.test.ts b/ui/hooks/useDisplayName.test.ts index 896e974c0025..5c36b0a97ed2 100644 --- a/ui/hooks/useDisplayName.test.ts +++ b/ui/hooks/useDisplayName.test.ts @@ -1,23 +1,19 @@ import { NameType } from '@metamask/name-controller'; import { CHAIN_IDS } from '@metamask/transaction-controller'; -import { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; +import { Hex } from '@metamask/utils'; +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import mockState from '../../test/data/mock-state.json'; import { EXPERIENCES_TYPE, FIRST_PARTY_CONTRACT_NAMES, } from '../../shared/constants/first-party-contracts'; -import mockState from '../../test/data/mock-state.json'; -import { renderHookWithProvider } from '../../test/lib/render-helpers'; -import { getDomainResolutions } from '../ducks/domains'; import { useDisplayName } from './useDisplayName'; -import { useNames } from './useName'; import { useNftCollectionsMetadata } from './useNftCollectionsMetadata'; +import { useNames } from './useName'; jest.mock('./useName'); jest.mock('./useNftCollectionsMetadata'); -jest.mock('../ducks/domains', () => ({ - getDomainResolutions: jest.fn(), -})); const VALUE_MOCK = 'testvalue'; const VARIATION_MOCK = CHAIN_IDS.GOERLI; @@ -26,7 +22,6 @@ const ERC20_TOKEN_NAME_MOCK = 'testName2'; const WATCHED_NFT_NAME_MOCK = 'testName3'; const NFT_NAME_MOCK = 'testName4'; const FIRST_PARTY_CONTRACT_NAME_MOCK = 'testName5'; -const ENS_NAME_MOCK = 'vitalik.eth'; const SYMBOL_MOCK = 'tes'; const NFT_IMAGE_MOCK = 'testNftImage'; const ERC20_IMAGE_MOCK = 'testImage'; @@ -35,7 +30,6 @@ const OTHER_NAME_TYPE = 'test' as NameType; describe('useDisplayName', () => { const useNamesMock = jest.mocked(useNames); const useNftCollectionsMetadataMock = jest.mocked(useNftCollectionsMetadata); - const domainResolutionsMock = jest.mocked(getDomainResolutions); // eslint-disable-next-line @typescript-eslint/no-explicit-any let state: any; @@ -93,18 +87,6 @@ describe('useDisplayName', () => { }); } - function mockDomainResolutions(address: string, ensName: string) { - domainResolutionsMock.mockReturnValue([ - { - addressBookEntryName: undefined, - domainName: ensName, - protocol: 'Ethereum Name Service', - resolvedAddress: address, - resolvingSnap: 'Ethereum Name Service resolver', - }, - ]); - } - function mockFirstPartyContractName( value: string, variation: string, @@ -421,50 +403,6 @@ describe('useDisplayName', () => { }); }); - describe('Domain Resolutions', () => { - it('returns ENS name if domain resolution for that address exists', () => { - mockDomainResolutions(VALUE_MOCK, ENS_NAME_MOCK); - - const { result } = renderHookWithProvider( - () => - useDisplayName({ - value: VALUE_MOCK, - type: NameType.ETHEREUM_ADDRESS, - variation: VARIATION_MOCK, - }), - mockState, - ); - - expect(result.current).toStrictEqual({ - contractDisplayName: undefined, - hasPetname: false, - image: undefined, - name: ENS_NAME_MOCK, - }); - }); - - it('returns no name if type not address', () => { - mockDomainResolutions(VALUE_MOCK, ENS_NAME_MOCK); - - const { result } = renderHookWithProvider( - () => - useDisplayName({ - value: VALUE_MOCK, - type: OTHER_NAME_TYPE, - variation: VARIATION_MOCK, - }), - mockState, - ); - - expect(result.current).toStrictEqual({ - contractDisplayName: undefined, - hasPetname: false, - image: undefined, - name: null, - }); - }); - }); - describe('Priority', () => { it('uses petname as first priority', () => { mockPetname(PETNAME_MOCK); diff --git a/ui/hooks/useDisplayName.ts b/ui/hooks/useDisplayName.ts index 66463b884338..7b7429c7a0d4 100644 --- a/ui/hooks/useDisplayName.ts +++ b/ui/hooks/useDisplayName.ts @@ -1,14 +1,12 @@ import { NameType } from '@metamask/name-controller'; -import { Hex } from '@metamask/utils'; import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; +import { selectERC20TokensByChain } from '../selectors'; +import { getNftContractsByAddressByChain } from '../selectors/nft'; import { EXPERIENCES_TYPE, FIRST_PARTY_CONTRACT_NAMES, } from '../../shared/constants/first-party-contracts'; -import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; -import { getDomainResolutions } from '../ducks/domains'; -import { selectERC20TokensByChain } from '../selectors'; -import { getNftContractsByAddressByChain } from '../selectors/nft'; import { useNames } from './useName'; import { useNftCollectionsMetadata } from './useNftCollectionsMetadata'; @@ -31,11 +29,9 @@ export function useDisplayNames( ): UseDisplayNameResponse[] { const nameEntries = useNames(requests); const firstPartyContractNames = useFirstPartyContractNames(requests); - const erc20Tokens = useERC20Tokens(requests); const watchedNFTNames = useWatchedNFTNames(requests); const nfts = useNFTs(requests); - const ens = useDomainResolutions(requests); return requests.map((_request, index) => { const nameEntry = nameEntries[index]; @@ -43,7 +39,6 @@ export function useDisplayNames( const erc20Token = erc20Tokens[index]; const watchedNftName = watchedNFTNames[index]; const nft = nfts[index]; - const ensName = ens[index]; const name = nameEntry?.name || @@ -51,7 +46,6 @@ export function useDisplayNames( nft?.name || erc20Token?.name || watchedNftName || - ensName || null; const image = nft?.image || erc20Token?.image; @@ -113,7 +107,6 @@ function useWatchedNFTNames( const contractAddress = value.toLowerCase(); const watchedNftNamesByAddress = watchedNftNamesByAddressByChain[variation]; - return watchedNftNamesByAddress?.[contractAddress]?.name; }); } @@ -154,32 +147,6 @@ function useNFTs( ); } -function useDomainResolutions(nameRequests: UseDisplayNameRequest[]) { - const domainResolutions = useSelector(getDomainResolutions); - - return nameRequests.map(({ type, value }) => { - if (type !== NameType.ETHEREUM_ADDRESS) { - return undefined; - } - - const matchedResolution = domainResolutions?.find( - (resolution: { - addressBookEntryName: string; - domainName: string; - protocol: string; - resolvedAddress: string; - resolvingSnap: string; - }) => - toChecksumHexAddress(resolution.resolvedAddress) === - toChecksumHexAddress(value), - ); - - const ensName = matchedResolution?.domainName; - - return ensName; - }); -} - function useFirstPartyContractNames(nameRequests: UseDisplayNameRequest[]) { return nameRequests.map(({ type, value, variation }) => { if (type !== NameType.ETHEREUM_ADDRESS) { diff --git a/ui/hooks/useGasFeeEstimates.js b/ui/hooks/useGasFeeEstimates.js index abbaf0db0bb9..5ad37925054b 100644 --- a/ui/hooks/useGasFeeEstimates.js +++ b/ui/hooks/useGasFeeEstimates.js @@ -74,10 +74,9 @@ export function useGasFeeEstimates(_networkClientId) { }, [networkClientId]); usePolling({ - startPolling: (input) => - gasFeeStartPollingByNetworkClientId(input.networkClientId), + startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, stopPollingByPollingToken: gasFeeStopPollingByPollingToken, - input: { networkClientId }, + networkClientId, }); return { diff --git a/ui/hooks/useGasFeeEstimates.test.js b/ui/hooks/useGasFeeEstimates.test.js index dd63e10581d0..0187ac793bbe 100644 --- a/ui/hooks/useGasFeeEstimates.test.js +++ b/ui/hooks/useGasFeeEstimates.test.js @@ -8,6 +8,7 @@ import { getIsNetworkBusyByChainId, } from '../ducks/metamask/metamask'; import { + gasFeeStartPollingByNetworkClientId, gasFeeStopPollingByPollingToken, getNetworkConfigurationByNetworkClientId, } from '../store/actions'; @@ -114,9 +115,9 @@ describe('useGasFeeEstimates', () => { renderHook(() => useGasFeeEstimates()); }); expect(usePolling).toHaveBeenCalledWith({ - startPolling: expect.any(Function), + startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, stopPollingByPollingToken: gasFeeStopPollingByPollingToken, - input: { networkClientId: 'selectedNetworkClientId' }, + networkClientId: 'selectedNetworkClientId', }); }); @@ -126,9 +127,9 @@ describe('useGasFeeEstimates', () => { renderHook(() => useGasFeeEstimates('networkClientId1')); }); expect(usePolling).toHaveBeenCalledWith({ - startPolling: expect.any(Function), + startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId, stopPollingByPollingToken: gasFeeStopPollingByPollingToken, - input: { networkClientId: 'networkClientId1' }, + networkClientId: 'networkClientId1', }); }); diff --git a/ui/hooks/useMMICustodySendTransaction.ts b/ui/hooks/useMMICustodySendTransaction.ts index 49634fbf0174..0c05d9e16f96 100644 --- a/ui/hooks/useMMICustodySendTransaction.ts +++ b/ui/hooks/useMMICustodySendTransaction.ts @@ -34,9 +34,9 @@ export function useMMICustodySendTransaction() { const accountType = useSelector(getAccountType); const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); - const { currentConfirmation } = useConfirmContext< - TransactionMeta | undefined - >(); + const { currentConfirmation } = useConfirmContext() as unknown as { + currentConfirmation: TransactionMeta | undefined; + }; const { from } = getConfirmationSender(currentConfirmation); const fromChecksumHexAddress = toChecksumHexAddress(from || ''); diff --git a/ui/hooks/useMultiPolling.test.ts b/ui/hooks/useMultiPolling.test.ts deleted file mode 100644 index a5621728c176..000000000000 --- a/ui/hooks/useMultiPolling.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; - -import useMultiPolling from './useMultiPolling'; - -describe('useMultiPolling', () => { - it('Should start/stop polling when inputs are added/removed, and stop on dismount', async () => { - const promises: Promise[] = []; - const mockStartPolling = jest.fn().mockImplementation((input) => { - const promise = Promise.resolve(`${input}_token`); - promises.push(promise); - return promise; - }); - - const mockStopPollingByPollingToken = jest.fn(); - const inputs = ['foo', 'bar']; - - const { unmount, rerender } = renderHook(() => - useMultiPolling({ - startPolling: mockStartPolling, - stopPollingByPollingToken: mockStopPollingByPollingToken, - input: inputs, - }), - ); - - // All inputs should start polling - await Promise.all(promises); - for (const input of inputs) { - expect(mockStartPolling).toHaveBeenCalledWith(input); - } - - // Remove one input, and add another - inputs[0] = 'baz'; - rerender({ input: inputs }); - expect(mockStopPollingByPollingToken).toHaveBeenCalledWith('foo_token'); - expect(mockStartPolling).toHaveBeenCalledWith('baz'); - - // All inputs should stop polling on dismount - await Promise.all(promises); - unmount(); - for (const input of inputs) { - expect(mockStopPollingByPollingToken).toHaveBeenCalledWith( - `${input}_token`, - ); - } - }); -}); diff --git a/ui/hooks/useMultiPolling.ts b/ui/hooks/useMultiPolling.ts deleted file mode 100644 index 0eff48267221..000000000000 --- a/ui/hooks/useMultiPolling.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useEffect, useRef } from 'react'; - -type UseMultiPollingOptions = { - startPolling: (input: PollingInput) => Promise; - stopPollingByPollingToken: (pollingToken: string) => void; - input: PollingInput[]; -}; - -// A hook that manages multiple polling loops of a polling controller. -// Callers provide an array of inputs, and the hook manages starting -// and stopping polling loops for each input. -const useMultiPolling = ( - usePollingOptions: UseMultiPollingOptions, -) => { - const pollingTokens = useRef>(new Map()); - - useEffect(() => { - // start new polls - for (const input of usePollingOptions.input) { - const key = JSON.stringify(input); - if (!pollingTokens.current.has(key)) { - usePollingOptions - .startPolling(input) - .then((token) => pollingTokens.current.set(key, token)); - } - } - - // stop existing polls - for (const [inputKey, token] of pollingTokens.current.entries()) { - const exists = usePollingOptions.input.some( - (i) => inputKey === JSON.stringify(i), - ); - - if (!exists) { - usePollingOptions.stopPollingByPollingToken(token); - pollingTokens.current.delete(inputKey); - } - } - }, [usePollingOptions.input && JSON.stringify(usePollingOptions.input)]); - - // stop all polling on dismount - useEffect(() => { - return () => { - for (const token of pollingTokens.current.values()) { - usePollingOptions.stopPollingByPollingToken(token); - } - }; - }, []); -}; - -export default useMultiPolling; diff --git a/ui/hooks/useMultichainSelector.ts b/ui/hooks/useMultichainSelector.ts index 9bd979df7e7e..326ac79bf9cd 100644 --- a/ui/hooks/useMultichainSelector.ts +++ b/ui/hooks/useMultichainSelector.ts @@ -11,7 +11,6 @@ export function useMultichainSelector< ) { return useSelector((state: TState) => { // We either pass an account or fallback to the currently selected one - // @ts-expect-error state types don't match return selector(state, account || getSelectedInternalAccount(state)); }); } diff --git a/ui/hooks/useName.test.ts b/ui/hooks/useName.test.ts index b102e9dce7a0..f746c4bb6267 100644 --- a/ui/hooks/useName.test.ts +++ b/ui/hooks/useName.test.ts @@ -15,6 +15,7 @@ jest.mock('react-redux', () => ({ })); jest.mock('../selectors', () => ({ + getCurrentChainId: jest.fn(), getNames: jest.fn(), })); diff --git a/ui/hooks/useNftCollectionsMetadata.test.ts b/ui/hooks/useNftCollectionsMetadata.test.ts index cf7997cb518b..e1e2b6745ad1 100644 --- a/ui/hooks/useNftCollectionsMetadata.test.ts +++ b/ui/hooks/useNftCollectionsMetadata.test.ts @@ -16,6 +16,10 @@ jest.mock('react-redux', () => ({ useSelector: (selector: any) => selector(), })); +jest.mock('../selectors', () => ({ + getCurrentChainId: jest.fn(), +})); + jest.mock('../store/actions', () => ({ getNFTContractInfo: jest.fn(), getTokenStandardAndDetails: jest.fn(), diff --git a/ui/hooks/usePolling.test.js b/ui/hooks/usePolling.test.js index a556bb86be54..9250257d3cbc 100644 --- a/ui/hooks/usePolling.test.js +++ b/ui/hooks/usePolling.test.js @@ -4,12 +4,13 @@ import usePolling from './usePolling'; describe('usePolling', () => { // eslint-disable-next-line jest/no-done-callback - it('calls startPolling and calls back with polling token when component instantiating the hook mounts', (done) => { + it('calls startPollingByNetworkClientId and callback option args with polling token when component instantiating the hook mounts', (done) => { const mockStart = jest.fn().mockImplementation(() => { return Promise.resolve('pollingToken'); }); const mockStop = jest.fn(); const networkClientId = 'mainnet'; + const options = {}; const mockState = { metamask: {}, }; @@ -17,16 +18,17 @@ describe('usePolling', () => { renderHookWithProvider(() => { usePolling({ callback: (pollingToken) => { - expect(mockStart).toHaveBeenCalledWith({ networkClientId }); + expect(mockStart).toHaveBeenCalledWith(networkClientId, options); expect(pollingToken).toBeDefined(); done(); return (_pollingToken) => { // noop }; }, - startPolling: mockStart, + startPollingByNetworkClientId: mockStart, stopPollingByPollingToken: mockStop, - input: { networkClientId }, + networkClientId, + options, }); }, mockState); }); @@ -37,6 +39,7 @@ describe('usePolling', () => { }); const mockStop = jest.fn(); const networkClientId = 'mainnet'; + const options = {}; const mockState = { metamask: {}, }; @@ -51,9 +54,10 @@ describe('usePolling', () => { done(); }; }, - startPolling: mockStart, + startPollingByNetworkClientId: mockStart, stopPollingByPollingToken: mockStop, - input: { networkClientId }, + networkClientId, + options, }), mockState, ); diff --git a/ui/hooks/usePolling.ts b/ui/hooks/usePolling.ts index 613e70cf17b5..1a9d6b1f576e 100644 --- a/ui/hooks/usePolling.ts +++ b/ui/hooks/usePolling.ts @@ -1,16 +1,22 @@ import { useEffect, useRef } from 'react'; -type UsePollingOptions = { +type UsePollingOptions = { callback?: (pollingToken: string) => (pollingToken: string) => void; - startPolling: (input: PollingInput) => Promise; + startPollingByNetworkClientId: ( + networkClientId: string, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: any, + ) => Promise; stopPollingByPollingToken: (pollingToken: string) => void; - input: PollingInput; + networkClientId: string; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options?: any; enabled?: boolean; }; -const usePolling = ( - usePollingOptions: UsePollingOptions, -) => { +const usePolling = (usePollingOptions: UsePollingOptions) => { const pollTokenRef = useRef(null); const cleanupRef = useRef void)>(null); let isMounted = false; @@ -32,7 +38,10 @@ const usePolling = ( // Start polling when the component mounts usePollingOptions - .startPolling(usePollingOptions.input) + .startPollingByNetworkClientId( + usePollingOptions.networkClientId, + usePollingOptions.options, + ) .then((pollToken) => { pollTokenRef.current = pollToken; cleanupRef.current = usePollingOptions.callback?.(pollToken) || null; @@ -47,7 +56,12 @@ const usePolling = ( cleanup(); }; }, [ - usePollingOptions.input && JSON.stringify(usePollingOptions.input), + usePollingOptions.networkClientId, + usePollingOptions.options && + JSON.stringify( + usePollingOptions.options, + Object.keys(usePollingOptions.options).sort(), + ), usePollingOptions.enabled, ]); }; diff --git a/ui/hooks/useTokenRatesPolling.ts b/ui/hooks/useTokenRatesPolling.ts deleted file mode 100644 index 41c1c8793b97..000000000000 --- a/ui/hooks/useTokenRatesPolling.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useSelector } from 'react-redux'; -import { - getMarketData, - getNetworkConfigurationsByChainId, - getTokenExchangeRates, - getTokensMarketData, - getUseCurrencyRateCheck, -} from '../selectors'; -import { - tokenRatesStartPolling, - tokenRatesStopPollingByPollingToken, -} from '../store/actions'; -import useMultiPolling from './useMultiPolling'; - -const useTokenRatesPolling = ({ chainIds }: { chainIds?: string[] } = {}) => { - // Selectors to determine polling input - const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); - const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); - - // Selectors returning state updated by the polling - const tokenExchangeRates = useSelector(getTokenExchangeRates); - const tokensMarketData = useSelector(getTokensMarketData); - const marketData = useSelector(getMarketData); - - useMultiPolling({ - startPolling: tokenRatesStartPolling, - stopPollingByPollingToken: tokenRatesStopPollingByPollingToken, - input: useCurrencyRateCheck - ? chainIds ?? Object.keys(networkConfigurations) - : [], - }); - - return { - tokenExchangeRates, - tokensMarketData, - marketData, - }; -}; - -export default useTokenRatesPolling; diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index b5ebc0a83eb6..95828e3e250e 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -268,17 +268,17 @@ exports[`AssetPage should render a native asset 1`] = `

- Tips for using a wallet + Start your journey with ETH

- Adding tokens unlocks more ways to use web3. + Get started with web3 by adding some ETH to your wallet.

- Tips for using a wallet + Start your journey with ETH

- Adding tokens unlocks more ways to use web3. + Get started with web3 by adding some ETH to your wallet.

$1.00

@@ -777,7 +777,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` style="padding-right: 100%; direction: rtl;" >

$1.00

@@ -1136,17 +1136,17 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = `

- Tips for using a wallet + Start your journey with ETH

- Adding tokens unlocks more ways to use web3. + Get started with web3 by adding some ETH to your wallet.

- Select token and amount + Select token
diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts index 30514dbf7f96..07c35ae57749 100644 --- a/ui/pages/bridge/bridge.util.test.ts +++ b/ui/pages/bridge/bridge.util.test.ts @@ -1,28 +1,13 @@ import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { CHAIN_IDS } from '../../../shared/constants/network'; -import mockBridgeQuotesErc20Erc20 from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; -import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json'; -import { zeroAddress } from '../../__mocks__/ethereumjs-util'; -import { - fetchBridgeFeatureFlags, - fetchBridgeQuotes, - fetchBridgeTokens, -} from './bridge.util'; +import { fetchBridgeFeatureFlags, fetchBridgeTokens } from './bridge.util'; jest.mock('../../../shared/lib/fetch-with-cache'); describe('Bridge utils', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('fetchBridgeFeatureFlags', () => { it('should fetch bridge feature flags successfully', async () => { const mockResponse = { - 'extension-config': { - refreshRate: 3, - maxRefreshCount: 1, - }, 'extension-support': true, 'src-network-allowlist': [1, 10, 59144, 120], 'dest-network-allowlist': [1, 137, 59144, 11111], @@ -43,10 +28,6 @@ describe('Bridge utils', () => { }); expect(result).toStrictEqual({ - extensionConfig: { - maxRefreshCount: 1, - refreshRate: 3, - }, extensionSupport: true, srcNetworkAllowlist: [ CHAIN_IDS.MAINNET, @@ -65,10 +46,8 @@ describe('Bridge utils', () => { it('should use fallback bridge feature flags if response is unexpected', async () => { const mockResponse = { - 'extension-support': 25, - 'src-network-allowlist': ['a', 'b', 1], - a: 'b', - 'dest-network-allowlist': [1, 137, 59144, 11111], + flag1: true, + flag2: false, }; (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); @@ -86,10 +65,6 @@ describe('Bridge utils', () => { }); expect(result).toStrictEqual({ - extensionConfig: { - maxRefreshCount: 5, - refreshRate: 30000, - }, extensionSupport: false, srcNetworkAllowlist: [], destNetworkAllowlist: [], @@ -166,128 +141,4 @@ describe('Bridge utils', () => { await expect(fetchBridgeTokens('0xa')).rejects.toThrowError(mockError); }); }); - - describe('fetchBridgeQuotes', () => { - it('should fetch bridge quotes successfully, no approvals', async () => { - (fetchWithCache as jest.Mock).mockResolvedValue( - mockBridgeQuotesNativeErc20, - ); - const { signal } = new AbortController(); - - const result = await fetchBridgeQuotes( - { - walletAddress: '0x123', - srcChainId: 1, - destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), - srcTokenAmount: '20000', - slippage: 0.5, - }, - signal, - ); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - expect(result).toStrictEqual(mockBridgeQuotesNativeErc20); - }); - - it('should fetch bridge quotes successfully, with approvals', async () => { - (fetchWithCache as jest.Mock).mockResolvedValue([ - ...mockBridgeQuotesErc20Erc20, - { ...mockBridgeQuotesErc20Erc20[0], approval: null }, - { ...mockBridgeQuotesErc20Erc20[0], trade: null }, - ]); - const { signal } = new AbortController(); - - const result = await fetchBridgeQuotes( - { - walletAddress: '0x123', - srcChainId: 1, - destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), - srcTokenAmount: '20000', - slippage: 0.5, - }, - signal, - ); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); - }); - - it('should filter out malformed bridge quotes', async () => { - (fetchWithCache as jest.Mock).mockResolvedValue([ - ...mockBridgeQuotesErc20Erc20, - ...mockBridgeQuotesErc20Erc20.map( - ({ quote, ...restOfQuote }) => restOfQuote, - ), - { - ...mockBridgeQuotesErc20Erc20[0], - quote: { - srcAsset: { - ...mockBridgeQuotesErc20Erc20[0].quote.srcAsset, - decimals: undefined, - }, - }, - }, - { - ...mockBridgeQuotesErc20Erc20[1], - quote: { - srcAsset: { - ...mockBridgeQuotesErc20Erc20[1].quote.destAsset, - address: undefined, - }, - }, - }, - ]); - const { signal } = new AbortController(); - - const result = await fetchBridgeQuotes( - { - walletAddress: '0x123', - srcChainId: 1, - destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), - srcTokenAmount: '20000', - slippage: 0.5, - }, - signal, - ); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); - }); - }); }); diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts index bbdddab53658..915a933e7c02 100644 --- a/ui/pages/bridge/bridge.util.ts +++ b/ui/pages/bridge/bridge.util.ts @@ -11,6 +11,7 @@ import { } from '../../../shared/constants/bridge'; import { MINUTE } from '../../../shared/constants/time'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; +import { validateData } from '../../../shared/lib/swaps-utils'; import { decimalToHex, hexToDecimal, @@ -19,37 +20,43 @@ import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP, SwapsTokenObject, } from '../../../shared/constants/swaps'; +import { TOKEN_VALIDATORS } from '../swaps/swaps.util'; import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, } from '../../../shared/modules/swaps.utils'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants'; -import { - BridgeAsset, - BridgeFlag, - FeatureFlagResponse, - FeeData, - FeeType, - Quote, - QuoteRequest, - QuoteResponse, - TxData, -} from './types'; -import { - FEATURE_FLAG_VALIDATORS, - QUOTE_VALIDATORS, - TX_DATA_VALIDATORS, - TOKEN_VALIDATORS, - validateResponse, - QUOTE_RESPONSE_VALIDATORS, - FEE_DATA_VALIDATORS, -} from './utils/validators'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; +// Types copied from Metabridge API +enum BridgeFlag { + EXTENSION_SUPPORT = 'extension-support', + NETWORK_SRC_ALLOWLIST = 'src-network-allowlist', + NETWORK_DEST_ALLOWLIST = 'dest-network-allowlist', +} + +export type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_SUPPORT]: boolean; + [BridgeFlag.NETWORK_SRC_ALLOWLIST]: number[]; + [BridgeFlag.NETWORK_DEST_ALLOWLIST]: number[]; +}; +// End of copied types + +type Validator = { + property: keyof ExpectedResponse | string; + type: string; + validator: (value: DataToValidate) => boolean; +}; + +const validateResponse = ( + validators: Validator[], + data: unknown, + urlUsed: string, +): data is ExpectedResponse => { + return validateData(validators, data, urlUsed); +}; + export async function fetchBridgeFeatureFlags(): Promise { const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; const rawFeatureFlags = await fetchWithCache({ @@ -60,15 +67,35 @@ export async function fetchBridgeFeatureFlags(): Promise { }); if ( - validateResponse( - FEATURE_FLAG_VALIDATORS, + validateResponse( + [ + { + property: BridgeFlag.EXTENSION_SUPPORT, + type: 'boolean', + validator: (v) => typeof v === 'boolean', + }, + { + property: BridgeFlag.NETWORK_SRC_ALLOWLIST, + type: 'object', + validator: (v): v is number[] => + Object.values(v as { [s: string]: unknown }).every( + (i) => typeof i === 'number', + ), + }, + { + property: BridgeFlag.NETWORK_DEST_ALLOWLIST, + type: 'object', + validator: (v): v is number[] => + Object.values(v as { [s: string]: unknown }).every( + (i) => typeof i === 'number', + ), + }, + ], rawFeatureFlags, url, ) ) { return { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: - rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG], [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: rawFeatureFlags[BridgeFlag.EXTENSION_SUPPORT], [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: rawFeatureFlags[ @@ -81,10 +108,6 @@ export async function fetchBridgeFeatureFlags(): Promise { } return { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - refreshRate: REFRESH_INTERVAL_MS, - maxRefreshCount: 5, - }, // TODO set default to true once bridging is live [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, // TODO set default to ALLOWED_BRIDGE_CHAIN_IDS once bridging is live @@ -119,9 +142,13 @@ export async function fetchBridgeTokens( transformedTokens[nativeToken.address] = nativeToken; } - tokens.forEach((token: unknown) => { + tokens.forEach((token: SwapsTokenObject) => { if ( - validateResponse(TOKEN_VALIDATORS, token, url) && + validateResponse( + TOKEN_VALIDATORS, + token, + url, + ) && !( isSwapsDefaultTokenSymbol(token.symbol, chainId) || isSwapsDefaultTokenAddress(token.address, chainId) @@ -132,56 +159,3 @@ export async function fetchBridgeTokens( }); return transformedTokens; } - -// Returns a list of bridge tx quotes -export async function fetchBridgeQuotes( - request: QuoteRequest, - signal: AbortSignal, -): Promise { - const queryParams = new URLSearchParams({ - walletAddress: request.walletAddress, - srcChainId: request.srcChainId.toString(), - destChainId: request.destChainId.toString(), - srcTokenAddress: request.srcTokenAddress, - destTokenAddress: request.destTokenAddress, - srcTokenAmount: request.srcTokenAmount, - slippage: request.slippage.toString(), - insufficientBal: request.insufficientBal ? 'true' : 'false', - resetApproval: request.resetApproval ? 'true' : 'false', - }); - const url = `${BRIDGE_API_BASE_URL}/getQuote?${queryParams}`; - const quotes = await fetchWithCache({ - url, - fetchOptions: { - method: 'GET', - headers: CLIENT_ID_HEADER, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => { - const { quote, approval, trade } = quoteResponse; - return ( - validateResponse( - QUOTE_RESPONSE_VALIDATORS, - quoteResponse, - url, - ) && - validateResponse(QUOTE_VALIDATORS, quote, url) && - validateResponse(TOKEN_VALIDATORS, quote.srcAsset, url) && - validateResponse(TOKEN_VALIDATORS, quote.destAsset, url) && - validateResponse(TX_DATA_VALIDATORS, trade, url) && - validateResponse( - FEE_DATA_VALIDATORS, - quote.feeData[FeeType.METABRIDGE], - url, - ) && - (approval - ? validateResponse(TX_DATA_VALIDATORS, approval, url) - : true) - ); - }); - return filteredQuotes; -} diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss index bc96dfbbe825..98a3a3ee5c34 100644 --- a/ui/pages/bridge/index.scss +++ b/ui/pages/bridge/index.scss @@ -1,8 +1,6 @@ @use "design-system"; @import 'prepare/index'; -@import 'quotes/index'; - .bridge { max-height: 100vh; diff --git a/ui/pages/bridge/index.test.tsx b/ui/pages/bridge/index.test.tsx index 7d5f813513c5..0d0d4c21c71f 100644 --- a/ui/pages/bridge/index.test.tsx +++ b/ui/pages/bridge/index.test.tsx @@ -8,7 +8,6 @@ import { renderWithProvider, MOCKS, CONSTANTS } from '../../../test/jest'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import CrossChainSwap from '.'; -const mockResetBridgeState = jest.fn(); const middleware = [thunk]; setBackgroundConnection({ resetPostFetchState: jest.fn(), @@ -25,7 +24,6 @@ setBackgroundConnection({ .mockResolvedValue({ chainId: '0x1' }), setBridgeFeatureFlags: jest.fn(), selectSrcNetwork: jest.fn(), - resetState: () => mockResetBridgeState(), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); @@ -75,6 +73,5 @@ describe('Bridge', () => { expect(getByText('Bridge')).toBeInTheDocument(); expect(container).toMatchSnapshot(); - expect(mockResetBridgeState).toHaveBeenCalledTimes(1); }); }); diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx index 62244e5793e5..e81b20670011 100644 --- a/ui/pages/bridge/index.tsx +++ b/ui/pages/bridge/index.tsx @@ -24,7 +24,7 @@ import { Header, } from '../../components/multichain/pages/page'; import { getProviderConfig } from '../../ducks/metamask/metamask'; -import { resetBridgeState, setFromChain } from '../../ducks/bridge/actions'; +import { resetInputFields, setFromChain } from '../../ducks/bridge/actions'; import PrepareBridgePage from './prepare/prepare-bridge-page'; import { BridgeCTAButton } from './prepare/bridge-cta-button'; @@ -45,23 +45,11 @@ const CrossChainSwap = () => { isBridgeEnabled && providerConfig && dispatch(setFromChain(providerConfig.chainId)); - }, [isBridgeChain, isBridgeEnabled, providerConfig]); - - const resetControllerAndInputStates = async () => { - await dispatch(resetBridgeState()); - }; - - useEffect(() => { - // Reset controller and inputs before unloading the page - resetControllerAndInputStates(); - - window.addEventListener('beforeunload', resetControllerAndInputStates); return () => { - window.removeEventListener('beforeunload', resetControllerAndInputStates); - resetControllerAndInputStates(); + dispatch(resetInputFields()); }; - }, []); + }, [isBridgeChain, isBridgeEnabled, providerConfig]); const redirectToDefaultRoute = async () => { history.push({ @@ -70,7 +58,6 @@ const CrossChainSwap = () => { }); dispatch(clearSwapsState()); await dispatch(resetBackgroundSwapsState()); - await resetControllerAndInputStates(); }; return ( diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap index 26e25b8bd4cd..b406cafe0941 100644 --- a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap +++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap @@ -9,7 +9,7 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] = class="mm-box prepare-bridge-page__content" >
$0.00 @@ -130,7 +129,7 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] =
$0.00 @@ -213,7 +211,7 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = ` class="mm-box prepare-bridge-page__content" >
$0.00 @@ -340,7 +337,7 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = `
$0.00 diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx index 91adb422e22b..5e42823c885b 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -3,10 +3,6 @@ import { renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../../shared/constants/network'; -import mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { RequestStatus } from '../../../../app/scripts/controllers/bridge/constants'; import { BridgeCTAButton } from './bridge-cta-button'; describe('BridgeCTAButton', () => { @@ -29,52 +25,6 @@ describe('BridgeCTAButton', () => { expect(getByRole('button')).toBeDisabled(); }); - it('should render the component when amount is missing', () => { - const mockStore = createBridgeMockStore( - { - srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], - destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], - }, - { - fromTokenInputValue: null, - fromToken: 'ETH', - toToken: 'ETH', - toChainId: CHAIN_IDS.LINEA_MAINNET, - }, - {}, - ); - const { getByText, getByRole } = renderWithProvider( - , - configureStore(mockStore), - ); - - expect(getByText('Enter amount')).toBeInTheDocument(); - expect(getByRole('button')).toBeDisabled(); - }); - - it('should render the component when amount and dest token is missing', () => { - const mockStore = createBridgeMockStore( - { - srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], - destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], - }, - { - fromTokenInputValue: null, - fromToken: 'ETH', - toToken: null, - toChainId: CHAIN_IDS.LINEA_MAINNET, - }, - {}, - ); - const { getByText, getByRole } = renderWithProvider( - , - configureStore(mockStore), - ); - - expect(getByText('Select token and amount')).toBeInTheDocument(); - expect(getByRole('button')).toBeDisabled(); - }); - it('should render the component when tx is submittable', () => { const mockStore = createBridgeMockStore( { @@ -87,72 +37,14 @@ describe('BridgeCTAButton', () => { toToken: 'ETH', toChainId: CHAIN_IDS.LINEA_MAINNET, }, - { - quotes: mockBridgeQuotesNativeErc20, - quotesLastFetched: Date.now(), - quotesLoadingStatus: RequestStatus.FETCHED, - }, - ); - const { getByText, getByRole } = renderWithProvider( - , - configureStore(mockStore), - ); - - expect(getByText('Confirm')).toBeInTheDocument(); - expect(getByRole('button')).not.toBeDisabled(); - }); - - it('should disable the component when quotes are loading and there are no existing quotes', () => { - const mockStore = createBridgeMockStore( - { - srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], - destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], - }, - { - fromTokenInputValue: 1, - fromToken: 'ETH', - toToken: 'ETH', - toChainId: CHAIN_IDS.LINEA_MAINNET, - }, - { - quotes: [], - quotesLastFetched: Date.now(), - quotesLoadingStatus: RequestStatus.LOADING, - }, - ); - const { getByText, getByRole } = renderWithProvider( - , - configureStore(mockStore), - ); - - expect(getByText('Fetching quotes...')).toBeInTheDocument(); - expect(getByRole('button')).toBeDisabled(); - }); - - it('should enable the component when quotes are loading and there are existing quotes', () => { - const mockStore = createBridgeMockStore( - { - srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], - destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], - }, - { - fromTokenInputValue: 1, - fromToken: 'ETH', - toToken: 'ETH', - toChainId: CHAIN_IDS.LINEA_MAINNET, - }, - { - quotes: mockBridgeQuotesNativeErc20, - quotesLastFetched: Date.now(), - quotesLoadingStatus: RequestStatus.LOADING, - }, + {}, ); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), ); - expect(getByText('Confirm')).toBeInTheDocument(); + expect(getByText('Bridge')).toBeInTheDocument(); expect(getByRole('button')).not.toBeDisabled(); }); }); diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index 28a1a2c1fbd6..fedcf4d4606a 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -2,7 +2,6 @@ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { Button } from '../../../components/component-library'; import { - getBridgeQuotes, getFromAmount, getFromChain, getFromToken, @@ -23,29 +22,16 @@ export const BridgeCTAButton = () => { const fromAmount = useSelector(getFromAmount); const toAmount = useSelector(getToAmount); - const { isLoading } = useSelector(getBridgeQuotes); - const isTxSubmittable = fromToken && toToken && fromChain && toChain && fromAmount && toAmount; const label = useMemo(() => { - if (isLoading && !isTxSubmittable) { - return t('swapFetchingQuotes'); - } - - if (!fromAmount) { - if (!toToken) { - return t('bridgeSelectTokenAndAmount'); - } - return t('bridgeEnterAmount'); - } - if (isTxSubmittable) { - return t('confirm'); + return t('bridge'); } return t('swapSelectToken'); - }, [isLoading, fromAmount, toToken, isTxSubmittable]); + }, [isTxSubmittable]); return ( - - -
-
-
-`; - -exports[`BridgeQuoteCard should render the recommended quote while loading new quotes 1`] = ` -
-
-
-
-
-
-

- Estimated time -

-
-
- -
-
-
-
-
-

-

-

- 1 minutes -

-
-
-
-
-

- Quote rate -

-
-
-
-

-

-

- 1 ETH = 2465.4630 USDC -

-
-
-
-
-

- Total fees -

-
-
- -
-
-
-
-
-

- 0.01 ETH -

-
-

- $0.01 -

-
-
-
- -
-
-`; diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap deleted file mode 100644 index 41d8a03d1ac1..000000000000 --- a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap +++ /dev/null @@ -1,139 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BridgeQuotesModal should render the modal 1`] = ` - -
-
-
- -
-
- Balance: -
-
- 966.987986 ETH -
-
-
-
-
- - T - -
- test would like to read this message to complete your action -
-
-
-
-
-
- {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"}} - -
-
-
-
- -
- Decrypt message -
-
-
-
-
-
-
- -
-
-`; - -exports[`ConfirmDecryptMessage Component matches snapshot if no unapproved decrypt messages 1`] = `
`; - -exports[`ConfirmDecryptMessage Component shows error on decrypt inline error 1`] = ` -
-
-
-
-
- Decrypt request -
-
-
-
-
-
-
+ {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}} + +
+
+
- - This message cannot be decrypted due to error: Decrypt inline error -
-
-
+
- -
- Decrypt message -
+ Decrypt message
-
+
+
+
+
+
+
+ + + + +
@@ -494,7 +451,7 @@ exports[`ConfirmDecryptMessage Component shows the correct message data 1`] = `
- 966.987986 ETH + 1520956.064158 DEF
@@ -515,66 +472,35 @@ exports[`ConfirmDecryptMessage Component shows the correct message data 1`] = `
-
- raw message - -
-
-
-
- -
- Decrypt message -
-
-
+ {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}} +
+
+
-
- Copy encrypted message -
- + Decrypt message
+