From 8a7f0928013474fd8574bfea99d99d4233f649ef Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 28 Nov 2024 23:18:06 +0000 Subject: [PATCH 01/17] bump semvar version to 7.37.0 && build version to 1506 --- android/app/build.gradle | 4 ++-- bitrise.yml | 8 ++++---- ios/MetaMask.xcodeproj/project.pbxproj | 24 ++++++++++++------------ package.json | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 14f14867baa..6e59998cbf2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -173,8 +173,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.35.1" - versionCode 1502 + versionName "7.37.0" + versionCode 1506 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index b21e82dd457..33276a0fa8d 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1684,16 +1684,16 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.35.1 + VERSION_NAME: 7.37.0 - opts: is_expand: false - VERSION_NUMBER: 1502 + VERSION_NUMBER: 1506 - opts: is_expand: false - FLASK_VERSION_NAME: 7.35.1 + FLASK_VERSION_NAME: 7.37.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 1502 + FLASK_VERSION_NUMBER: 1506 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 6515458244b..5d23a5ecb2c 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1502; + CURRENT_PROJECT_VERSION = 1506; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1318,7 +1318,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.35.1; + MARKETING_VERSION = 7.37.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1346,7 +1346,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1502; + CURRENT_PROJECT_VERSION = 1506; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1381,7 +1381,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.35.1; + MARKETING_VERSION = 7.37.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1409,7 +1409,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1502; + CURRENT_PROJECT_VERSION = 1506; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1442,7 +1442,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; LLVM_LTO = YES; - MARKETING_VERSION = 7.35.1; + MARKETING_VERSION = 7.37.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1470,7 +1470,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1502; + CURRENT_PROJECT_VERSION = 1506; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1501,7 +1501,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; LLVM_LTO = YES; - MARKETING_VERSION = 7.35.1; + MARKETING_VERSION = 7.37.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1624,7 +1624,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1502; + CURRENT_PROJECT_VERSION = 1506; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1661,7 +1661,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.35.1; + MARKETING_VERSION = 7.37.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1692,7 +1692,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1502; + CURRENT_PROJECT_VERSION = 1506; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1727,7 +1727,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.35.1; + MARKETING_VERSION = 7.37.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index 2d3131d71df..5f7a6ebb065 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.35.1", + "version": "7.37.0", "private": true, "scripts": { "audit:ci": "./scripts/yarn-audit.sh", From e95812884afadf31e56813cfd9b48fddb4071d99 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:24:06 -0700 Subject: [PATCH 02/17] chore: chore/7.37.0-Changelog (#12491) This PR updates the change log for 7.37.0 and generates the test plan here [commit.csv](https://github.com/MetaMask/metamask-mobile/blob/release/7.37.0/commits.csv) --------- Co-authored-by: metamaskbot Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com> --- CHANGELOG.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc6668f8c1..02597c48457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,89 @@ ## Current Main Branch +## 7.37.0 - Nov 28, 2024 + +### Added +- [#12091](https://github.com/MetaMask/metamask-mobile/pull/12091): feat: 2020 Add a performance test for iOS in Bitrise (#12091) +- [#12148](https://github.com/MetaMask/metamask-mobile/pull/12148): feat: Enable smart transactions for new users (#12148) +- [#12442](https://github.com/MetaMask/metamask-mobile/pull/12442): test: add a new unit test to cover for multichain feature flags ON (#12442) +- [#12420](https://github.com/MetaMask/metamask-mobile/pull/12420): feat(3598): non permitted chain flow small improvements (#12420) +- [#12198](https://github.com/MetaMask/metamask-mobile/pull/12198): feat: custom names for snap accounts (Flask only) (#12198) +- [#12396](https://github.com/MetaMask/metamask-mobile/pull/12396): feat(ramp): enable buy button in asset overview (#12396) +- [#11613](https://github.com/MetaMask/metamask-mobile/pull/11613): feat(ramp): improve amount editing formatting (#11613) +- [#12393](https://github.com/MetaMask/metamask-mobile/pull/12393): feat: Creating data tree for signed type V1 signatures (#12393) +- [#12160](https://github.com/MetaMask/metamask-mobile/pull/12160): feat: Integrate NFT api to display image & names in simulations includes `erc721`s (#12160) +- [#12324](https://github.com/MetaMask/metamask-mobile/pull/12324): feat: confirmation re-designs add basic page for types sign V1 signature request (#12324) +- [#12452](https://github.com/MetaMask/metamask-mobile/pull/12452): [chore] Merge in feat: updated staking events to use withMetaMetrics helper (#12337) (#12452) +- [#11424](https://github.com/MetaMask/metamask-mobile/pull/11424): feat: add workflow for updating automated test results in TestRail (#11424) +- [#12359](https://github.com/MetaMask/metamask-mobile/pull/12359): feat: v7.35.1 (#12359) +- [#12167](https://github.com/MetaMask/metamask-mobile/pull/12167): feat: v7.35.0 (#12167) +- [#12337](https://github.com/MetaMask/metamask-mobile/pull/12337): feat: updated staking events to use withMetaMetrics helper (#12337) +- [#12363](https://github.com/MetaMask/metamask-mobile/pull/12363): feat: add PooledStaking slice for managing staking state (#12363) +- [#12398](https://github.com/MetaMask/metamask-mobile/pull/12398): feat: limit input digits to 12 in useInputHandler (#12398) +- [#12344](https://github.com/MetaMask/metamask-mobile/pull/12344): feat: upgrade assets controllers to v44 (#12344) +- [#12340](https://github.com/MetaMask/metamask-mobile/pull/12340): feat: upgrade assets controllers to version 43 (#12340) +- [#12270](https://github.com/MetaMask/metamask-mobile/pull/12270): feat: upgrade assets controllers to 42 with multichain token rates (#12270) + +### Changed +- [#12356](https://github.com/MetaMask/metamask-mobile/pull/12356): chore: Remove unnecessary event prop (#12356) +- [#12425](https://github.com/MetaMask/metamask-mobile/pull/12425): ci: create ci workflow for multichain flow (#12425) +- [#12350](https://github.com/MetaMask/metamask-mobile/pull/12350): chore: Bump Snaps packages (#12350) +- [#11409](https://github.com/MetaMask/metamask-mobile/pull/11409): refactor: use `withKeyring` to batch account restore operation (#11409) +- [#12339](https://github.com/MetaMask/metamask-mobile/pull/12339): chore: Update accounts-controller @v19.0.0 and keyring-controller @v18.0.0 (#12339) +- [#12440](https://github.com/MetaMask/metamask-mobile/pull/12440): chore(ramp): upgrade sdk to 1.28.7 (#12440) +- [#12351](https://github.com/MetaMask/metamask-mobile/pull/12351): refactor(ramp): remove anonymous events (#12351) +- [#12355](https://github.com/MetaMask/metamask-mobile/pull/12355): chore: Add missing confirmation unit tests (#12355) +- [#12369](https://github.com/MetaMask/metamask-mobile/pull/12369): chore: upgrade transaction controller to increase polling rate (#12369) +- [#12202](https://github.com/MetaMask/metamask-mobile/pull/12202): refactor: update swaps quote poll count (#12202) +- [#10743](https://github.com/MetaMask/metamask-mobile/pull/10743): chore: @metamask/swaps-controller v9 -> v10 (#10743) +- [#12415](https://github.com/MetaMask/metamask-mobile/pull/12415): chore: Cherry pick 2506358 (merge in trackEvent work) (#12415) +- [#12238](https://github.com/MetaMask/metamask-mobile/pull/12238): chore: update codeowners (#12238) +- [#12416](https://github.com/MetaMask/metamask-mobile/pull/12416): chore: Chore/update accounts controller messenger code owner (#12416) +- [#12313](https://github.com/MetaMask/metamask-mobile/pull/12313): fix: Remove run all tests section (#12313) +- [#12366](https://github.com/MetaMask/metamask-mobile/pull/12366): chore: #12184 MVP split engine file (#12366) +- [#12362](https://github.com/MetaMask/metamask-mobile/pull/12362): chore: Unit tests for tags approval controller undefined (#12362) +- [#12343](https://github.com/MetaMask/metamask-mobile/pull/12343): chore: Cherry pick f35d583 (#12343) +- [#12332](https://github.com/MetaMask/metamask-mobile/pull/12332): chore: do not show staked eth balance when balance is zero on homepage or asset detail (#12332) +- [#12413](https://github.com/MetaMask/metamask-mobile/pull/12413): chore: simplify cicd rls script (#12413) +- [#12334](https://github.com/MetaMask/metamask-mobile/pull/12334): chore: updating filter icon (#12334) + +### Fixed +- [#12489](https://github.com/MetaMask/metamask-mobile/pull/12489): fix: replace end of navigation init and UIStartup span (#12489) +- [#12331](https://github.com/MetaMask/metamask-mobile/pull/12331): fix: tags pending approvals receiving undefined (#12331) +- [#10486](https://github.com/MetaMask/metamask-mobile/pull/10486): fix: limit ReactNativeWebview message size (#10486) +- [#12478](https://github.com/MetaMask/metamask-mobile/pull/12478): fix: incorrect event source in analytics and connection (#12478) +- [#10786](https://github.com/MetaMask/metamask-mobile/pull/10786): fix: added icon to walletconnect metadata (#10786) +- [#12455](https://github.com/MetaMask/metamask-mobile/pull/12455): fix: gas fee edit from swaps (#12455) +- [#12370](https://github.com/MetaMask/metamask-mobile/pull/12370): "fix: Fix copy of ""Network fee"" on approval (#12370)" +- [#12273](https://github.com/MetaMask/metamask-mobile/pull/12273): fix: Disable confirm button if `transactionMeta` is undefined (#12273) +- [#12367](https://github.com/MetaMask/metamask-mobile/pull/12367): fix: app crashing after send or swap (#12367) +- [#12446](https://github.com/MetaMask/metamask-mobile/pull/12446): fix: update wallet_addEthereumChain.js with correct MetricsEventBuilder (#12446) +- [#12180](https://github.com/MetaMask/metamask-mobile/pull/12180): fix: trackevent enabled is undefined (#12180) +- [#12315](https://github.com/MetaMask/metamask-mobile/pull/12315): fix: e2e: ensure Decrypt button is displayed (#12315) +- [#12402](https://github.com/MetaMask/metamask-mobile/pull/12402): fix: fix missing variable patch (#12402) +- [#12319](https://github.com/MetaMask/metamask-mobile/pull/12319): fix: hide rpc url selector for networks with one rpc (#12319) +- [#12371](https://github.com/MetaMask/metamask-mobile/pull/12371): fix: fix patch missing variable sentry error (#12371) +- [#12375](https://github.com/MetaMask/metamask-mobile/pull/12375): fix: breaking selector due to missing controller state (#12375) + +### Other +- [#12374](https://github.com/MetaMask/metamask-mobile/pull/12374): perf: Remove costly reduce operation for generating Engine context (#12374) +- [#12345](https://github.com/MetaMask/metamask-mobile/pull/12345): chore: bump walletconnect/* deps (#12345) +- [#Daniel](Daniel): "feat: Support returning a txHash asap +- [#EtherWizard33](EtherWizard33): "feat: non-permissioned networks +- [#12474](https://github.com/MetaMask/metamask-mobile/pull/12474): chore: bump `@metamask/signature-controller` to `^22.0.0` (#12474) +- [#12472](https://github.com/MetaMask/metamask-mobile/pull/12472): chore: bump `@metamask/preferences-controller` to `^14.0.0` (#12472) +- [#Michele Esposito](Michele Esposito): "chore(deps): bump `@metamask/{swaps +- [#12003](https://github.com/MetaMask/metamask-mobile/pull/12003): build(deps): bump `@metamask/smart-transaction-controller` to `^14.0.0` (#12003) +- [#12004](https://github.com/MetaMask/metamask-mobile/pull/12004): build(deps): bump `@metamask/selected-network-controller` to `^18.0.2` (#12004) +- [#12471](https://github.com/MetaMask/metamask-mobile/pull/12471): chore(runway): cherry-pick fix: gas fee edit from swaps (#12471) +- [#12453](https://github.com/MetaMask/metamask-mobile/pull/12453): chore(runway): cherry-pick fix: update wallet_addEthereumChain.js with correct MetricsEventBuilder (#12453) +- [#12361](https://github.com/MetaMask/metamask-mobile/pull/12361): chore(runway): cherry-pick fix: tags pending approvals receiving undefined (#12361) +- [#12335](https://github.com/MetaMask/metamask-mobile/pull/12335): chore: cherrypick do not show staked eth balance when balance is zero on homepage or asset detail (#12335) +- [#12414](https://github.com/MetaMask/metamask-mobile/pull/12414): chore(runway): cherry-pick fix: breaking selector due to missing controller state (#12414) +- [#12349](https://github.com/MetaMask/metamask-mobile/pull/12349): fix: Remove duplicate notifications controllers entries in `EngineService` (#12349) + + ## 7.35.1 - Nov 20, 2024 ### Fixed - [#12331](https://github.com/MetaMask/metamask-mobile/pull/12331): fix: tags pending approvals receiving undefined (#12331) From 18aca38b380e0a79367a2d9a97f05c895d859b87 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:50:45 -0700 Subject: [PATCH 03/17] chore(runway): cherry-pick feat: implement remote feature flag controller (#12510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feat: implement remote feature flag controller (#12427) ## **Description** Introduction of `@metamask/remote-feature-flag-controller` library. Remote feature flag controller manages data flow, retry policy, and cache expiry. The controller consumer manages default values, data persistency, and data distribution. See [ADR](https://github.com/MetaMask/decisions/blob/b3094d47a568ac1e076a44fa704c2d29d1b59f35/decisions/wallet-platform/0001-remote-rollout-feature-flags.md) for a in-depth description ## Technical decisions ### Controller init on `Engine.ts` with feature flags fetching only on cold app starts. `@metamask/remote-feature-flag-controller` is only asked to fetch feature flags after its init in `Engine.ts`. Ensures feature flags are only fetched on cold app starts. ### Fallback values Default values are used when remote feature flags are undefined. The fallback mechanism is implemented by each feature flag selector `app/selectors/featureFlagsController/` In this PR we include `minimumAppVersion` selector, which manages the LD feature flag `mobile-minimum-versions` ### One selector per each feature flag [LD feature flags can be boolean, number, strings on JSON objects](https://docs.launchdarkly.com/sdk/concepts/flag-types#understanding-flag-types). We've decided to have each feature flag with its own selector A feature flag selector contains: - state selectors for each feature flag value. - business logic - defaults for when feature flags values are undefined. - TS types. - unit tests and mocked data. This architecture offers a clear separation between each feature flag and the logic behind it, allowing easier manipulation. Code owners are assigned to each feature flag. ### ## **Related issues** Fixes: https://github.com/MetaMask/mobile-planning/issues/2054 Fixes: https://github.com/MetaMask/mobile-planning/issues/1975 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.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. --------- Co-authored-by: Nico MASSART Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com> [a8c0783](https://github.com/MetaMask/metamask-mobile/commit/a8c0783272f18e74e56773498291d5e540de4217) Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> Co-authored-by: Nico MASSART Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com> --- .github/CODEOWNERS | 37 ++--- .../useMinimumVersions.test.ts | 29 +++- .../MinimumVersions/useMinimumVersions.tsx | 14 +- app/core/Engine/Engine.test.ts | 5 +- app/core/Engine/Engine.ts | 43 ++++-- .../RemoteFeatureFlagController/index.ts | 2 + .../RemoteFeatureFlagController/types.ts | 8 + .../RemoteFeatureFlagController/utils.test.ts | 101 ++++++++++++ .../RemoteFeatureFlagController/utils.ts | 61 ++++++++ app/core/Engine/types.ts | 6 + app/core/EngineService/EngineService.test.ts | 1 + app/core/EngineService/EngineService.ts | 4 + .../redux/slices/featureFlags/index.test.ts | 51 ------- app/core/redux/slices/featureFlags/index.ts | 77 ---------- app/reducers/index.ts | 5 - .../featureFlagController/index.test.ts | 29 ++++ app/selectors/featureFlagController/index.ts | 12 ++ .../minimumAppVersion/constants.ts | 15 ++ .../minimumAppVersion/index.test.ts | 144 ++++++++++++++++++ .../minimumAppVersion/index.ts | 41 +++++ .../minimumAppVersion/types.ts | 10 ++ app/selectors/featureFlagController/mocks.ts | 48 ++++++ app/selectors/featureFlagController/types.ts | 9 ++ app/selectors/settings.ts | 7 + app/store/index.ts | 5 - app/store/migrations/061.test.ts | 59 +++++++ app/store/migrations/061.ts | 12 ++ app/store/migrations/index.ts | 4 +- app/store/sagas/index.ts | 42 ----- app/util/test/initial-background-state.json | 4 + app/util/test/initial-root-state.ts | 2 - package.json | 1 + yarn.lock | 9 ++ 33 files changed, 670 insertions(+), 227 deletions(-) create mode 100644 app/core/Engine/controllers/RemoteFeatureFlagController/index.ts create mode 100644 app/core/Engine/controllers/RemoteFeatureFlagController/types.ts create mode 100644 app/core/Engine/controllers/RemoteFeatureFlagController/utils.test.ts create mode 100644 app/core/Engine/controllers/RemoteFeatureFlagController/utils.ts delete mode 100644 app/core/redux/slices/featureFlags/index.test.ts delete mode 100644 app/core/redux/slices/featureFlags/index.ts create mode 100644 app/selectors/featureFlagController/index.test.ts create mode 100644 app/selectors/featureFlagController/index.ts create mode 100644 app/selectors/featureFlagController/minimumAppVersion/constants.ts create mode 100644 app/selectors/featureFlagController/minimumAppVersion/index.test.ts create mode 100644 app/selectors/featureFlagController/minimumAppVersion/index.ts create mode 100644 app/selectors/featureFlagController/minimumAppVersion/types.ts create mode 100644 app/selectors/featureFlagController/mocks.ts create mode 100644 app/selectors/featureFlagController/types.ts create mode 100644 app/store/migrations/061.test.ts create mode 100644 app/store/migrations/061.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 16348bdf720..96b4b4dbbbd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,19 +7,22 @@ app/component-library/ @MetaMask/design-system-engineers # Platform Team -.github/CODEOWNERS @MetaMask/mobile-platform -patches/ @MetaMask/mobile-platform -app/core/Engine/Engine.ts @MetaMask/mobile-platform -app/core/Engine/Engine.test.ts @MetaMask/mobile-platform -app/core/Engine/index.ts @MetaMask/mobile-platform -app/core/Engine/types.ts @MetaMask/mobile-platform -app/core/Analytics/ @MetaMask/mobile-platform -app/util/metrics/ @MetaMask/mobile-platform -app/components/hooks/useMetrics/ @MetaMask/mobile-platform -app/store/migrations/ @MetaMask/mobile-platform -bitrise.yml @MetaMask/mobile-platform -yarn.lock @MetaMask/mobile-platform -ios/Podfile.lock @MetaMask/mobile-platform +.github/CODEOWNERS @MetaMask/mobile-platform +patches/ @MetaMask/mobile-platform +app/core/Engine/Engine.ts @MetaMask/mobile-platform +app/core/Engine/Engine.test.ts @MetaMask/mobile-platform +app/core/Engine/index.ts @MetaMask/mobile-platform +app/core/Engine/types.ts @MetaMask/mobile-platform +app/core/Engine/controllers/RemoteFeatureFlagController/ @MetaMask/mobile-platform +app/core/Analytics/ @MetaMask/mobile-platform +app/util/metrics/ @MetaMask/mobile-platform +app/components/hooks/useMetrics/ @MetaMask/mobile-platform +app/selectors/featureFlagController/* @MetaMask/mobile-platform +app/selectors/featureFlagController/minimumAppVersion/ @MetaMask/mobile-platform +app/store/migrations/ @MetaMask/mobile-platform +bitrise.yml @MetaMask/mobile-platform +yarn.lock @MetaMask/mobile-platform +ios/Podfile.lock @MetaMask/mobile-platform # Ramps Team app/components/UI/Ramp/ @MetaMask/ramp @@ -53,10 +56,10 @@ app/components/UI/Swaps @MetaMask/swaps-engineers app/components/Views/Notifications @MetaMask/notifications app/components/Views/Settings/NotificationsSettings @MetaMask/notifications app/components/UI/Notifications @MetaMask/notifications -app/reducers/notification @MetaMask/notifications -app/actions/notification @MetaMask/notifications -app/selectors/notification @MetaMask/notifications -app/util/notifications @MetaMask/notifications +app/reducers/notification @MetaMask/notifications +app/actions/notification @MetaMask/notifications +app/selectors/notification @MetaMask/notifications +app/util/notifications @MetaMask/notifications app/store/util/notifications @MetaMask/notifications # LavaMoat Team diff --git a/app/components/hooks/MinimumVersions/useMinimumVersions.test.ts b/app/components/hooks/MinimumVersions/useMinimumVersions.test.ts index 529f2d520af..2602c871af8 100644 --- a/app/components/hooks/MinimumVersions/useMinimumVersions.test.ts +++ b/app/components/hooks/MinimumVersions/useMinimumVersions.test.ts @@ -35,12 +35,21 @@ describe('useMinimumVersions', () => { jest.clearAllMocks(); (useNavigation as jest.Mock).mockReturnValue(mockNavigation); }); - it('requires update only if automaticSecurityChecksEnabled', () => { (useSelector as jest.Mock).mockImplementation(() => ({ security: { automaticSecurityChecksEnabled: false }, - featureFlags: { - featureFlags: { mobileMinimumVersions: { appMinimumBuild: 100 } }, + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + mobileMinimumVersions: { + appMinimumBuild: 100, + appleMinimumOS: 100, + androidMinimumAPIVersion: 100, + }, + }, + }, + }, }, })); @@ -54,8 +63,18 @@ describe('useMinimumVersions', () => { it('requires update only if currentBuildNumber is lower than appMinimumBuild', () => { (useSelector as jest.Mock).mockImplementation(() => ({ security: { automaticSecurityChecksEnabled: true }, - featureFlags: { - featureFlags: { mobileMinimumVersions: { appMinimumBuild: 100 } }, + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + mobileMinimumVersions: { + appMinimumBuild: 100, + appleMinimumOS: 100, + androidMinimumAPIVersion: 100, + }, + }, + }, + }, }, })); diff --git a/app/components/hooks/MinimumVersions/useMinimumVersions.tsx b/app/components/hooks/MinimumVersions/useMinimumVersions.tsx index 88831f75112..e74b5f11ff8 100644 --- a/app/components/hooks/MinimumVersions/useMinimumVersions.tsx +++ b/app/components/hooks/MinimumVersions/useMinimumVersions.tsx @@ -4,22 +4,20 @@ import { createUpdateNeededNavDetails } from '../../UI/UpdateNeeded/UpdateNeeded import { useSelector } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; import { InteractionManager } from 'react-native'; -import { FeatureFlagsState } from '../../../core/redux/slices/featureFlags'; -import { SecurityState } from '../../../../app/reducers/security'; -import { RootState } from '../../../../app/reducers'; +import { SecurityState } from '../../../reducers/security'; +import { RootState } from '../../../reducers'; +import { selectAppMinimumBuild } from '../../../selectors/featureFlagController/minimumAppVersion'; const useMinimumVersions = () => { const { automaticSecurityChecksEnabled }: SecurityState = useSelector( (state: RootState) => state.security, ); - const { featureFlags }: FeatureFlagsState = useSelector( - (state: RootState) => state.featureFlags, - ); + + const appMinimumBuild = useSelector((state: RootState) => selectAppMinimumBuild(state)); const currentBuildNumber = Number(getBuildNumber()); const navigation = useNavigation(); const shouldTriggerUpdateFlow = - automaticSecurityChecksEnabled && - featureFlags?.mobileMinimumVersions?.appMinimumBuild > currentBuildNumber; + automaticSecurityChecksEnabled && appMinimumBuild > currentBuildNumber; useEffect(() => { if (shouldTriggerUpdateFlow) { diff --git a/app/core/Engine/Engine.test.ts b/app/core/Engine/Engine.test.ts index b54e6d32fc5..a23b76957ce 100644 --- a/app/core/Engine/Engine.test.ts +++ b/app/core/Engine/Engine.test.ts @@ -21,7 +21,9 @@ jest.mock('../../store', () => ({ jest.mock('../../selectors/smartTransactionsController', () => ({ selectShouldUseSmartTransaction: jest.fn().mockReturnValue(false), })); - +jest.mock('../../selectors/settings', () => ({ + selectBasicFunctionalityEnabled: jest.fn().mockReturnValue(true), +})); describe('Engine', () => { it('should expose an API', () => { const engine = Engine.init({}); @@ -37,6 +39,7 @@ describe('Engine', () => { expect(engine.context).toHaveProperty('NetworkController'); expect(engine.context).toHaveProperty('PhishingController'); expect(engine.context).toHaveProperty('PreferencesController'); + expect(engine.context).toHaveProperty('RemoteFeatureFlagController'); expect(engine.context).toHaveProperty('SignatureController'); expect(engine.context).toHaveProperty('TokenBalancesController'); expect(engine.context).toHaveProperty('TokenRatesController'); diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index 1ae6d2b0c5b..463f8598f05 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-shadow */ import Crypto from 'react-native-quick-crypto'; import { scrypt } from 'react-native-fast-crypto'; + import { AccountTrackerController, AssetsContractController, @@ -153,6 +154,7 @@ import { } from './controllers/accounts/constants'; import { AccountsControllerMessenger } from '@metamask/accounts-controller'; import { createAccountsController } from './controllers/accounts/utils'; +import { createRemoteFeatureFlagController } from './controllers/RemoteFeatureFlagController'; import { captureException } from '@sentry/react-native'; import { lowerCase } from 'lodash'; import { @@ -161,6 +163,7 @@ import { } from '../../core/redux/slices/inpageProvider'; import SmartTransactionsController from '@metamask/smart-transactions-controller'; import { getAllowedSmartTransactionsChainIds } from '../../../app/constants/smartTransactions'; +import { selectBasicFunctionalityEnabled } from '../../selectors/settings'; import { selectShouldUseSmartTransaction } from '../../selectors/smartTransactionsController'; import { selectSwapsChainFeatureFlags } from '../../reducers/swaps'; import { SmartTransactionStatuses, ClientId } from '@metamask/smart-transactions-controller/dist/types'; @@ -264,6 +267,9 @@ export class Engine { ) { this.controllerMessenger = new ExtendedControllerMessenger(); + const isBasicFunctionalityToggleEnabled = () => + selectBasicFunctionalityEnabled(store.getState()); + const approvalController = new ApprovalController({ messenger: this.controllerMessenger.getRestricted({ name: 'ApprovalController', @@ -476,6 +482,16 @@ export class Engine { 'https://gas.api.cx.metamask.io/networks//suggestedGasFees', }); + const remoteFeatureFlagController = createRemoteFeatureFlagController({ + state: initialState.RemoteFeatureFlagController, + messenger: this.controllerMessenger.getRestricted({ + name: 'RemoteFeatureFlagController', + allowedActions: [], + allowedEvents: [], + }), + disabled: !isBasicFunctionalityToggleEnabled(), + }); + const phishingController = new PhishingController({ messenger: this.controllerMessenger.getRestricted({ name: 'PhishingController', @@ -924,8 +940,7 @@ export class Engine { encryptor, getMnemonic: getPrimaryKeyringMnemonic.bind(this), getFeatureFlags: () => ({ - disableSnaps: - store.getState().settings.basicFunctionalityEnabled === false, + disableSnaps: !isBasicFunctionalityToggleEnabled(), }), }); @@ -1126,7 +1141,7 @@ export class Engine { return Boolean( hasProperty(showIncomingTransactions, currentChainId) && - showIncomingTransactions?.[currentHexChainId], + showIncomingTransactions?.[currentHexChainId], ); }, updateTransactions: true, @@ -1383,6 +1398,7 @@ export class Engine { GasFeeController: gasFeeController, ApprovalController: approvalController, PermissionController: permissionController, + RemoteFeatureFlagController: remoteFeatureFlagController, SelectedNetworkController: selectedNetworkController, SignatureController: new SignatureController({ messenger: this.controllerMessenger.getRestricted({ @@ -1484,7 +1500,7 @@ export class Engine { (state: NetworkState) => { if ( state.networksMetadata[state.selectedNetworkClientId].status === - NetworkStatus.Available && + NetworkStatus.Available && networkController.getNetworkClientById( networkController?.state.selectedNetworkClientId, ).configuration.chainId !== currentChainId @@ -1509,10 +1525,9 @@ export class Engine { } catch (error) { console.error( error, - `Network ID not changed, current chainId: ${ - networkController.getNetworkClientById( - networkController?.state.selectedNetworkClientId, - ).configuration.chainId + `Network ID not changed, current chainId: ${networkController.getNetworkClientById( + networkController?.state.selectedNetworkClientId, + ).configuration.chainId }`, ); } @@ -1714,7 +1729,7 @@ export class Engine { const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; if ( accountsByChainId?.[toHexadecimal(chainId)]?.[ - selectSelectedInternalAccountChecksummedAddress + selectSelectedInternalAccountChecksummedAddress ] ) { const balanceBN = hexToBN( @@ -1751,7 +1766,7 @@ export class Engine { const tokenBalances = allTokenBalances?.[selectedInternalAccount.address as Hex]?.[ - chainId + chainId ] ?? {}; tokens.forEach( (item: { address: string; balance?: string; decimals: number }) => { @@ -1762,9 +1777,9 @@ export class Engine { item.balance || (item.address in tokenBalances ? renderFromTokenMinimalUnit( - tokenBalances[item.address as Hex], - item.decimals, - ) + tokenBalances[item.address as Hex], + item.decimals, + ) : undefined); const tokenBalanceFiat = balanceToFiatNumber( // TODO: Fix this by handling or eliminating the undefined case @@ -2057,6 +2072,7 @@ export default { NetworkController, PreferencesController, PhishingController, + RemoteFeatureFlagController, PPOMController, TokenBalancesController, TokenRatesController, @@ -2102,6 +2118,7 @@ export default { KeyringController, NetworkController, PhishingController, + RemoteFeatureFlagController, PPOMController, PreferencesController, TokenBalancesController, diff --git a/app/core/Engine/controllers/RemoteFeatureFlagController/index.ts b/app/core/Engine/controllers/RemoteFeatureFlagController/index.ts new file mode 100644 index 00000000000..54fc847b397 --- /dev/null +++ b/app/core/Engine/controllers/RemoteFeatureFlagController/index.ts @@ -0,0 +1,2 @@ +export { createRemoteFeatureFlagController } from './utils'; + diff --git a/app/core/Engine/controllers/RemoteFeatureFlagController/types.ts b/app/core/Engine/controllers/RemoteFeatureFlagController/types.ts new file mode 100644 index 00000000000..b42163fa235 --- /dev/null +++ b/app/core/Engine/controllers/RemoteFeatureFlagController/types.ts @@ -0,0 +1,8 @@ +import { RemoteFeatureFlagControllerMessenger, RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller'; + +export interface RemoteFeatureFlagInitParamTypes { + state?: RemoteFeatureFlagControllerState; + messenger: RemoteFeatureFlagControllerMessenger, + disabled: boolean +} + diff --git a/app/core/Engine/controllers/RemoteFeatureFlagController/utils.test.ts b/app/core/Engine/controllers/RemoteFeatureFlagController/utils.test.ts new file mode 100644 index 00000000000..21da6091ed9 --- /dev/null +++ b/app/core/Engine/controllers/RemoteFeatureFlagController/utils.test.ts @@ -0,0 +1,101 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import { + RemoteFeatureFlagController, + RemoteFeatureFlagControllerMessenger, +} from '@metamask/remote-feature-flag-controller'; +import { createRemoteFeatureFlagController } from './utils'; + +describe('RemoteFeatureFlagController utils', () => { + let messenger: RemoteFeatureFlagControllerMessenger; + + beforeEach(() => { + messenger = + new ControllerMessenger() as unknown as RemoteFeatureFlagControllerMessenger; + jest.clearAllMocks(); + }); + + describe('createRemoteFeatureFlagController', () => { + it('creates controller with initial undefined state', () => { + + const controller = createRemoteFeatureFlagController({ + state: undefined, + messenger, + disabled: false, + }); + + expect(controller).toBeDefined(); + + // Initializing with am empty object should return an empty obj? + expect(controller.state).toStrictEqual({ + cacheTimestamp: 0, + remoteFeatureFlags: {}, + }); + }); + + it('internal state matches initial state', () => { + const initialState = { + remoteFeatureFlags: { + testFlag: true, + }, + cacheTimestamp: 123, + }; + + const controller = createRemoteFeatureFlagController({ + state: initialState, + messenger, + disabled: false, + }); + + expect(controller.state).toStrictEqual(initialState); + }); + + it('calls updateRemoteFeatureFlags when enabled', () => { + const spy = jest.spyOn( + RemoteFeatureFlagController.prototype, + 'updateRemoteFeatureFlags', + ); + + createRemoteFeatureFlagController({ + state: undefined, + messenger, + disabled: false, + }); + + expect(spy).toHaveBeenCalled(); + }); + + it('does not call updateRemoteFeatureFlagscontroller when controller is disabled', () => { + const spy = jest.spyOn( + RemoteFeatureFlagController.prototype, + 'updateRemoteFeatureFlags', + ); + + createRemoteFeatureFlagController({ + state: undefined, + messenger, + disabled: true, + }); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('controller keeps initial extra data into its state', () => { + const initialState = { + extraData: true, + }; + + const controller = createRemoteFeatureFlagController({ + // @ts-expect-error giving a wrong initial state + state: initialState, + messenger, + disabled: false, + }); + + expect(controller.state).toStrictEqual({ + cacheTimestamp: 0, + extraData: true, + remoteFeatureFlags: {}, + }); + }); + }); +}); diff --git a/app/core/Engine/controllers/RemoteFeatureFlagController/utils.ts b/app/core/Engine/controllers/RemoteFeatureFlagController/utils.ts new file mode 100644 index 00000000000..c45fbe167bf --- /dev/null +++ b/app/core/Engine/controllers/RemoteFeatureFlagController/utils.ts @@ -0,0 +1,61 @@ +import { + RemoteFeatureFlagController, + ClientConfigApiService, + ClientType, + DistributionType, + EnvironmentType, +} from '@metamask/remote-feature-flag-controller'; + +import Logger from '../../../../util/Logger'; + +import { RemoteFeatureFlagInitParamTypes } from './types'; + +const getFeatureFlagAppEnvironment = () => { + const env = process.env.METAMASK_ENVIRONMENT; + switch (env) { + case 'local': return EnvironmentType.Development; + case 'pre-release': return EnvironmentType.ReleaseCandidate; + case 'production': return EnvironmentType.Production; + default: return EnvironmentType.Development; + } +}; + +const getFeatureFlagAppDistribution = () => { + const dist = process.env.METAMASK_BUILD_TYPE; + switch (dist) { + case 'main': return DistributionType.Main; + case 'flask': return DistributionType.Flask; + default: return DistributionType.Main; + } +}; + +export const createRemoteFeatureFlagController = ({ + state, + messenger, + disabled, +}: RemoteFeatureFlagInitParamTypes) => { + + const remoteFeatureFlagController = new RemoteFeatureFlagController({ + messenger, + state, + disabled, + clientConfigApiService: new ClientConfigApiService({ + fetch, + config: { + client: ClientType.Mobile, + environment: getFeatureFlagAppEnvironment(), + distribution: getFeatureFlagAppDistribution(), + }, + }), + }); + + if (disabled) { + Logger.log('Feature flag controller disabled'); + } else { + remoteFeatureFlagController.updateRemoteFeatureFlags().then(() => { + Logger.log('Feature flags updated'); + }); + } + return remoteFeatureFlagController; +}; + diff --git a/app/core/Engine/types.ts b/app/core/Engine/types.ts index 456cfb8759f..61c7b0037d8 100644 --- a/app/core/Engine/types.ts +++ b/app/core/Engine/types.ts @@ -153,6 +153,10 @@ import { } from '@metamask/accounts-controller'; import { BaseState } from '@metamask/base-controller'; import { getPermissionSpecifications } from '../Permissions/specifications.js'; +import { + RemoteFeatureFlagController, + RemoteFeatureFlagControllerState +} from '@metamask/remote-feature-flag-controller'; /** * Controllers that area always instantiated @@ -282,6 +286,7 @@ export interface Controllers { SelectedNetworkController: SelectedNetworkController; PhishingController: PhishingController; PreferencesController: PreferencesController; + RemoteFeatureFlagController: RemoteFeatureFlagController; PPOMController: PPOMController; TokenBalancesController: TokenBalancesController; TokenListController: TokenListController; @@ -320,6 +325,7 @@ export interface EngineState { KeyringController: KeyringControllerState; NetworkController: NetworkState; PreferencesController: PreferencesState; + RemoteFeatureFlagController: RemoteFeatureFlagControllerState; PhishingController: PhishingControllerState; TokenBalancesController: TokenBalancesControllerState; TokenRatesController: TokenRatesControllerState; diff --git a/app/core/EngineService/EngineService.test.ts b/app/core/EngineService/EngineService.test.ts index 9c2d6a457da..2d5d0fcacf4 100644 --- a/app/core/EngineService/EngineService.test.ts +++ b/app/core/EngineService/EngineService.test.ts @@ -51,6 +51,7 @@ jest.mock('../Engine', () => { NetworkController: { subscribe: jest.fn() }, PhishingController: { subscribe: jest.fn() }, PreferencesController: { subscribe: jest.fn() }, + RemoteFeatureFlagController: { subscribe: jest.fn() }, TokenBalancesController: { subscribe: jest.fn() }, TokenRatesController: { subscribe: jest.fn() }, TransactionController: { subscribe: jest.fn() }, diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts index ecbec9b8715..e47ffe5261c 100644 --- a/app/core/EngineService/EngineService.ts +++ b/app/core/EngineService/EngineService.ts @@ -96,6 +96,10 @@ class EngineService { name: 'PreferencesController', key: `${engine.context.PreferencesController.name}:stateChange`, }, + { + name: 'RemoteFeatureFlagController', + key: `${engine.context.RemoteFeatureFlagController.name}:stateChange`, + }, { name: 'SelectedNetworkController', key: `${engine.context.SelectedNetworkController.name}:stateChange`, diff --git a/app/core/redux/slices/featureFlags/index.test.ts b/app/core/redux/slices/featureFlags/index.test.ts deleted file mode 100644 index 3915e1853ba..00000000000 --- a/app/core/redux/slices/featureFlags/index.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import reducer, { - getFeatureFlags, - getFeatureFlagsSuccess, - getFeatureFlagsError, - initialState, -} from './index'; - -describe('featureFlags slice', () => { - it('should handle initial state', () => { - expect(reducer(undefined, { type: 'unknown' })).toEqual(initialState); - }); - - it('should handle getFeatureFlags', () => { - const nextState = reducer(initialState, getFeatureFlags()); - expect(nextState).toEqual({ - ...initialState, - loading: true, - error: null, - }); - }); - - it('should handle getFeatureFlagsSuccess', () => { - const featureFlags = { - mobileMinimumVersions: { - appMinimumBuild: 1243, - appleMinimumOS: 6, - androidMinimumAPIVersion: 21, - }, - }; - const nextState = reducer( - initialState, - getFeatureFlagsSuccess(featureFlags), - ); - expect(nextState).toEqual({ - ...initialState, - featureFlags, - loading: false, - error: null, - }); - }); - - it('should handle getFeatureFlagsError', () => { - const error = 'Failed to fetch feature flags'; - const nextState = reducer(initialState, getFeatureFlagsError(error)); - expect(nextState).toEqual({ - ...initialState, - loading: false, - error, - }); - }); -}); diff --git a/app/core/redux/slices/featureFlags/index.ts b/app/core/redux/slices/featureFlags/index.ts deleted file mode 100644 index 2e27d4e9bb2..00000000000 --- a/app/core/redux/slices/featureFlags/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; - -export interface FeatureFlagsState { - featureFlags: { - mobileMinimumVersions: { - appMinimumBuild: number; - appleMinimumOS: number; - androidMinimumAPIVersion: number; - }; - }; - loading: boolean; - error: string | null; -} - -export const initialState: FeatureFlagsState = { - featureFlags: { - mobileMinimumVersions: { - appMinimumBuild: 1243, - appleMinimumOS: 6, - androidMinimumAPIVersion: 21, - }, - }, - loading: false, - error: null, -}; - -const name = 'featureFlags'; - -const slice = createSlice({ - name, - initialState, - reducers: { - /** - * Initiates the fetching of feature flags. - * @param state - The current state of the featureFlags slice. - */ - getFeatureFlags: (state: FeatureFlagsState) => { - state.loading = true; - state.error = null; - }, - /** - * Handles the successful fetching of feature flags. - * @param state - The current state of the featureFlags slice. - * @param action - An action with the fetched feature flags as payload. - */ - getFeatureFlagsSuccess: ( - state: FeatureFlagsState, - action: PayloadAction, - ) => { - state.featureFlags = action.payload; - state.loading = false; - state.error = null; - }, - /** - * Handles errors that occur during the fetching of feature flags. - * @param state - The current state of the featureFlags slice. - * @param action - An action with the error message as payload. - */ - getFeatureFlagsError: ( - state: FeatureFlagsState, - action: PayloadAction, - ) => { - state.loading = false; - state.error = action.payload; - }, - }, -}); - -const { actions, reducer } = slice; - -export default reducer; - -export const { getFeatureFlags, getFeatureFlagsSuccess, getFeatureFlagsError } = - actions; - -export const FETCH_FEATURE_FLAGS = 'getFeatureFlags'; -export type FETCH_FEATURE_FLAGS = typeof FETCH_FEATURE_FLAGS; diff --git a/app/reducers/index.ts b/app/reducers/index.ts index dc95de44ca3..088040c0620 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -1,9 +1,6 @@ import bookmarksReducer from './bookmarks'; import browserReducer from './browser'; import engineReducer from '../core/redux/slices/engine'; -import featureFlagsReducer, { - FeatureFlagsState, -} from '../core/redux/slices/featureFlags'; import privacyReducer from './privacy'; import modalsReducer from './modals'; import settingsReducer from './settings'; @@ -58,7 +55,6 @@ export interface RootState { // eslint-disable-next-line @typescript-eslint/no-explicit-any collectibles: any; engine: { backgroundState: EngineState }; - featureFlags: FeatureFlagsState; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any privacy: any; @@ -137,7 +133,6 @@ const rootReducer = combineReducers({ // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any engine: engineReducer as any, - featureFlags: featureFlagsReducer, privacy: privacyReducer, bookmarks: bookmarksReducer, browser: browserReducer, diff --git a/app/selectors/featureFlagController/index.test.ts b/app/selectors/featureFlagController/index.test.ts new file mode 100644 index 00000000000..ff76cdbdefa --- /dev/null +++ b/app/selectors/featureFlagController/index.test.ts @@ -0,0 +1,29 @@ +import mockedEngine from '../../core/__mocks__/MockedEngine'; +import { selectRemoteFeatureFlagControllerState } from '.'; +import { mockedEmptyFlagsState, mockedState, mockedUndefinedFlagsState } from './mocks'; + +jest.mock('../../core/Engine', () => ({ + init: () => mockedEngine.init(), +})); + +describe('featureFlagController selector', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns feature flag remote values', () => { + const result = selectRemoteFeatureFlagControllerState(mockedState); + expect(result?.remoteFeatureFlags).toBeDefined(); + }); + + it('returns feature flag empty state', () => { + const result = selectRemoteFeatureFlagControllerState(mockedEmptyFlagsState); + expect(result?.remoteFeatureFlags).toBeDefined(); + }); + + it('returns feature flag undefined state', () => { + const result = selectRemoteFeatureFlagControllerState(mockedUndefinedFlagsState); + expect(result).toBeUndefined(); + }); +}); + diff --git a/app/selectors/featureFlagController/index.ts b/app/selectors/featureFlagController/index.ts new file mode 100644 index 00000000000..01f67f3de0a --- /dev/null +++ b/app/selectors/featureFlagController/index.ts @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; +import { StateWithPartialEngine } from './types'; + +export const selectRemoteFeatureFlagControllerState = (state: StateWithPartialEngine) => + state.engine.backgroundState.RemoteFeatureFlagController; + +export const selectRemoteFeatureFlags = createSelector( + selectRemoteFeatureFlagControllerState, + (remoteFeatureFlagControllerState) => + remoteFeatureFlagControllerState?.remoteFeatureFlags ?? {} +); + diff --git a/app/selectors/featureFlagController/minimumAppVersion/constants.ts b/app/selectors/featureFlagController/minimumAppVersion/constants.ts new file mode 100644 index 00000000000..2738657c58c --- /dev/null +++ b/app/selectors/featureFlagController/minimumAppVersion/constants.ts @@ -0,0 +1,15 @@ +import { FEATURE_FLAG_NAME, MinimumAppVersionType } from './types'; + +export const defaultValues: MinimumAppVersionType = { + appMinimumBuild: 1243, + appleMinimumOS: 6, + androidMinimumAPIVersion: 21, +}; + +export const mockedMinimumAppVersion = { + [FEATURE_FLAG_NAME]: { + appMinimumBuild: 1337, + androidMinimumAPIVersion: 12, + appleMinimumOS: 2, + }, +}; diff --git a/app/selectors/featureFlagController/minimumAppVersion/index.test.ts b/app/selectors/featureFlagController/minimumAppVersion/index.test.ts new file mode 100644 index 00000000000..785cf94a22d --- /dev/null +++ b/app/selectors/featureFlagController/minimumAppVersion/index.test.ts @@ -0,0 +1,144 @@ +import mockedEngine from '../../../core/__mocks__/MockedEngine'; +import { mockedEmptyFlagsState, mockedState, mockedUndefinedFlagsState } from '../mocks'; +import { mockedMinimumAppVersion, defaultValues } from './constants'; +import { MinimumAppVersionType } from './types'; +import { + selectAndroidMinimumAPI, + selectAppMinimumBuild, + selectAppleMinimumOS, + selectMobileMinimumVersions +} from '.'; + +jest.mock('../../../core/Engine', () => ({ + init: () => mockedEngine.init(), +})); + + +describe('minimumAppVersion Feature flag: selectMobileMinimumVersions selector', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + const testFlagValues = (result: unknown, expected: MinimumAppVersionType) => { + const { + appMinimumBuild, + appleMinimumOS, + androidMinimumAPIVersion, + } = result as MinimumAppVersionType; + + const { + appMinimumBuild: mockedAppMinimumBuild, + appleMinimumOS: mockedAppleMinimumOS, + androidMinimumAPIVersion: mockedAndroidMinimumAPIVersion, + } = expected; + + expect(appMinimumBuild).toEqual(mockedAppMinimumBuild); + expect(appleMinimumOS).toEqual(mockedAppleMinimumOS); + expect(androidMinimumAPIVersion).toEqual(mockedAndroidMinimumAPIVersion); + }; + it('returns default values when empty feature flag state', () => { + testFlagValues( + selectMobileMinimumVersions(mockedEmptyFlagsState), + defaultValues); + }); + + it('returns default values when undefined RemoteFeatureFlagController state', () => { + testFlagValues( + selectMobileMinimumVersions(mockedUndefinedFlagsState), + defaultValues + ); + }); + + it('returns remote values', () => { + testFlagValues( + selectMobileMinimumVersions(mockedState), + mockedMinimumAppVersion.mobileMinimumVersions + ); + }); + +}); + +describe('minimumAppVersion Feature flag: appMinimumBuild selector', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns default value when empty feature flag state', () => { + expect( + selectAppMinimumBuild(mockedEmptyFlagsState) + ).toEqual(defaultValues.appMinimumBuild); + }); + + it('returns default value when undefined RemoteFeatureFlagController state', () => { + expect( + selectAppMinimumBuild(mockedUndefinedFlagsState) + ).toEqual(defaultValues.appMinimumBuild); + }); + + it('returns default value when empty feature flag state', () => { + const { appMinimumBuild: mockedValue } = + mockedMinimumAppVersion.mobileMinimumVersions; + + expect( + selectAppMinimumBuild(mockedState) + ).toEqual(mockedValue); + }); +}); + +describe('minimumAppVersion Feature flag: appleMinimumOS selector', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns default value when empty feature flag state', () => { + const { appleMinimumOS: mockedValue } = defaultValues; + expect( + selectAppleMinimumOS(mockedEmptyFlagsState) + ).toEqual(mockedValue); + }); + + it('returns default value when undefined RemoteFeatureFlagController state', () => { + const { appleMinimumOS: mockedValue } = defaultValues; + expect( + selectAppleMinimumOS(mockedUndefinedFlagsState) + ).toEqual(mockedValue); + }); + + it('returns default value when empty feature flag state', () => { + const { appleMinimumOS: mockedValue } = + mockedMinimumAppVersion.mobileMinimumVersions; + + expect( + selectAppleMinimumOS(mockedState) + ).toEqual(mockedValue); + }); +}); + +describe('minimumAppVersion Feature flag: androidMinimumAPIVersion selector', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns default value when empty feature flag state', () => { + const { androidMinimumAPIVersion: mockedValue } = defaultValues; + expect( + selectAndroidMinimumAPI(mockedEmptyFlagsState) + ).toEqual(mockedValue); + }); + + it('returns default value when undefined RemoteFeatureFlagController state', () => { + const { androidMinimumAPIVersion: mockedValue } = defaultValues; + expect( + selectAndroidMinimumAPI(mockedUndefinedFlagsState) + ).toEqual(mockedValue); + }); + + it('returns default value when empty feature flag state', () => { + const { androidMinimumAPIVersion: mockedValue } = + mockedMinimumAppVersion.mobileMinimumVersions; + + expect( + selectAndroidMinimumAPI(mockedState) + ).toEqual(mockedValue); + }); +}); diff --git a/app/selectors/featureFlagController/minimumAppVersion/index.ts b/app/selectors/featureFlagController/minimumAppVersion/index.ts new file mode 100644 index 00000000000..98d79403a9e --- /dev/null +++ b/app/selectors/featureFlagController/minimumAppVersion/index.ts @@ -0,0 +1,41 @@ +import { createSelector } from 'reselect'; +import { selectRemoteFeatureFlags } from '../.'; +import { + FEATURE_FLAG_NAME, + MinimumAppVersionType, +} from './types'; +import { Json, hasProperty, isObject } from '@metamask/utils'; +import { defaultValues } from './constants'; + +const isMinimumAppVersionType = (obj: Json): + obj is MinimumAppVersionType => + isObject(obj) && + hasProperty(obj, 'appMinimumBuild') && + hasProperty(obj, 'appleMinimumOS') && + hasProperty(obj, 'androidMinimumAPIVersion'); + +export const selectMobileMinimumVersions = createSelector( + selectRemoteFeatureFlags, + (remoteFeatureFlags) => { + const remoteFeatureFlag = remoteFeatureFlags[FEATURE_FLAG_NAME]; + + return isMinimumAppVersionType(remoteFeatureFlag) + ? remoteFeatureFlag + : defaultValues; + } +); + +export const selectAppMinimumBuild = createSelector( + selectMobileMinimumVersions, + ({ appMinimumBuild }) => appMinimumBuild, +); + +export const selectAppleMinimumOS = createSelector( + selectMobileMinimumVersions, + ({ appleMinimumOS }) => appleMinimumOS, +); + +export const selectAndroidMinimumAPI = createSelector( + selectMobileMinimumVersions, + ({ androidMinimumAPIVersion }) => androidMinimumAPIVersion, +); diff --git a/app/selectors/featureFlagController/minimumAppVersion/types.ts b/app/selectors/featureFlagController/minimumAppVersion/types.ts new file mode 100644 index 00000000000..8aaa7350176 --- /dev/null +++ b/app/selectors/featureFlagController/minimumAppVersion/types.ts @@ -0,0 +1,10 @@ +export const FEATURE_FLAG_NAME = 'mobileMinimumVersions'; + +// A type predicate's type must be assignable to its parameter's type +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type MinimumAppVersionType = { + appMinimumBuild: number; + appleMinimumOS: number; + androidMinimumAPIVersion: number; +} + diff --git a/app/selectors/featureFlagController/mocks.ts b/app/selectors/featureFlagController/mocks.ts new file mode 100644 index 00000000000..8433d6ef284 --- /dev/null +++ b/app/selectors/featureFlagController/mocks.ts @@ -0,0 +1,48 @@ +import { FeatureFlags } from '@metamask/remote-feature-flag-controller'; +import { mockedMinimumAppVersion } from './minimumAppVersion/constants'; + +export const mockedState = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + ...mockedMinimumAppVersion + }, + cacheTimestamp: 0, + }, + }, + }, +}; + +export const mockedEmptyFlagsState = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: {}, + cacheTimestamp: 0, + }, + }, + }, +}; + +export const mockedUndefinedFlagsState = { + engine: { + backgroundState: { + RemoteFeatureFlagController: undefined, + }, + }, +}; + +export const getInvalidMockedFeatureFlag = (invalidFeatureFlag: FeatureFlags) => ({ + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + ...invalidFeatureFlag, + }, + cacheTimestamp: 0, + }, + }, + }, +}); + diff --git a/app/selectors/featureFlagController/types.ts b/app/selectors/featureFlagController/types.ts new file mode 100644 index 00000000000..7640a1b81a1 --- /dev/null +++ b/app/selectors/featureFlagController/types.ts @@ -0,0 +1,9 @@ +import { EngineState } from '../../core/Engine'; +import { RootState } from '../../reducers'; + +export type StateWithPartialEngine = RootState | { + engine: { + backgroundState: Partial + } +}; + diff --git a/app/selectors/settings.ts b/app/selectors/settings.ts index 758a82cc2ce..4f111a2b8c9 100644 --- a/app/selectors/settings.ts +++ b/app/selectors/settings.ts @@ -16,3 +16,10 @@ export const selectShowCustomNonce = createSelector( selectSettings, (settingsState: Record) => settingsState.showCustomNonce, ); + +export const selectBasicFunctionalityEnabled = createSelector( + selectSettings, + (settingsState: Record) => + settingsState.basicFunctionalityEnabled as boolean, +); + diff --git a/app/store/index.ts b/app/store/index.ts index 01200a0261f..5f74eb552d9 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -87,11 +87,6 @@ const createStoreAndPersistor = async () => { basicFunctionalityEnabled: store.getState().settings.basicFunctionalityEnabled, }); - // Fetch feature flags only if basic functionality is enabled - store.getState().settings.basicFunctionalityEnabled && - store.dispatch({ - type: 'FETCH_FEATURE_FLAGS', - }); EngineService.initalizeEngine(store); diff --git a/app/store/migrations/061.test.ts b/app/store/migrations/061.test.ts new file mode 100644 index 00000000000..c757258dfba --- /dev/null +++ b/app/store/migrations/061.test.ts @@ -0,0 +1,59 @@ +import migrate from './061'; +import { captureException } from '@sentry/react-native'; +import mockedEngine from '../../core/__mocks__/MockedEngine'; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +jest.mock('../../core/Engine', () => ({ + init: () => mockedEngine.init(), +})); + +describe('Migration #61 - remove featureFlags property from redux state', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: null, + errorMessage: "FATAL ERROR: Migration 61: Invalid state error: 'object'", + scenario: 'state is invalid', + }, + ]; + + for (const { errorMessage, scenario, state } of invalidStates) { + it(`captures exception if ${scenario}`, async () => { + const newState = await migrate(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }); + } + + it('removes featureFlags property from redux state', async () => { + const oldState = { + engine: { + backgroundState: {}, + }, + featureFlags: { + minimumAppVersion: 29, + }, + }; + + const expectedState = { + engine: { + backgroundState: {}, + }, + }; + + const migratedState = await migrate(oldState); + expect(migratedState).toStrictEqual(expectedState); + }); +}); diff --git a/app/store/migrations/061.ts b/app/store/migrations/061.ts new file mode 100644 index 00000000000..5194bd2b09c --- /dev/null +++ b/app/store/migrations/061.ts @@ -0,0 +1,12 @@ +import { hasProperty } from '@metamask/utils'; +import { ensureValidState } from './util'; + +export default function migrate(state: unknown) { + if (!ensureValidState(state, 61)) { + return state; + } + if (hasProperty(state, 'featureFlags')) { + delete state.featureFlags; + } + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index a1b929c96c7..5d6408a2959 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -61,6 +61,7 @@ import migration57 from './057'; import migration58 from './058'; import migration59 from './059'; import migration60 from './060'; +import migration61 from './061'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -133,7 +134,8 @@ export const migrationList: MigrationsList = { 57: migration57, 58: migration58, 59: migration59, - 60: migration60 + 60: migration60, + 61: migration61 }; // Enable both synchronous and asynchronous migrations diff --git a/app/store/sagas/index.ts b/app/store/sagas/index.ts index 0985f917d33..32afb2b55c7 100644 --- a/app/store/sagas/index.ts +++ b/app/store/sagas/index.ts @@ -19,14 +19,6 @@ import { restoreXMLHttpRequest, } from './xmlHttpRequestOverride'; -import { - getFeatureFlagsSuccess, - getFeatureFlagsError, - FeatureFlagsState, -} from '../../../app/core/redux/slices/featureFlags'; - -import launchDarklyURL from '../../../app/util/featureFlags'; - export function* appLockStateMachine() { let biometricsListenerTask: Task | undefined; while (true) { @@ -132,42 +124,8 @@ export function* basicFunctionalityToggle() { } } -function arrayToObject(data: []): FeatureFlagsState['featureFlags'] { - return data.reduce((obj, current) => { - Object.assign(obj, current); - return obj; - }, {} as FeatureFlagsState['featureFlags']); -} - -function* fetchFeatureFlags(): Generator { - try { - const response: Response = (yield fetch( - launchDarklyURL( - process.env.METAMASK_BUILD_TYPE, - process.env.METAMASK_ENVIRONMENT, - ), - )) as Response; - const jsonData = (yield response.json()) as { message: string } | []; - - if (!response.ok) { - if (jsonData && typeof jsonData === 'object' && 'message' in jsonData) { - yield put(getFeatureFlagsError(jsonData.message)); - } else { - yield put(getFeatureFlagsError('Unknown error')); - } - return; - } - - yield put(getFeatureFlagsSuccess(arrayToObject(jsonData as []))); - } catch (error) { - Logger.log(error); - yield put(getFeatureFlagsError(error as string)); - } -} - // Main generator function that initializes other sagas in parallel. export function* rootSaga() { yield fork(authStateMachine); yield fork(basicFunctionalityToggle); - yield fork(fetchFeatureFlags); } diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 97eb70731c2..c441087a6f0 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -315,6 +315,10 @@ "PermissionController": { "subjects": {} }, + "RemoteFeatureFlagController": { + "cacheTimestamp": 0, + "remoteFeatureFlags": {} + }, "SelectedNetworkController": { "domains": {} }, diff --git a/app/util/test/initial-root-state.ts b/app/util/test/initial-root-state.ts index d7f50fbe654..d857ff8b585 100644 --- a/app/util/test/initial-root-state.ts +++ b/app/util/test/initial-root-state.ts @@ -5,7 +5,6 @@ import { initialState as initialSecurityState } from '../../reducers/security'; import { initialState as initialInpageProvider } from '../../core/redux/slices/inpageProvider'; import { initialState as transactionMetrics } from '../../core/redux/slices/transactionMetrics'; import { initialState as originThrottling } from '../../core/redux/slices/originThrottling'; -import { initialState as initialFeatureFlagsState } from '../../core/redux/slices/featureFlags'; import initialBackgroundState from './initial-background-state.json'; import { userInitialState } from '../../reducers/user'; import { initialState as initialStakingState } from '../../core/redux/slices/staking'; @@ -22,7 +21,6 @@ const initialRootState: RootState = { privacy: undefined, bookmarks: undefined, browser: undefined, - featureFlags: initialFeatureFlagsState, modals: undefined, settings: undefined, alert: undefined, diff --git a/package.json b/package.json index 5f7a6ebb065..2715417dfb2 100644 --- a/package.json +++ b/package.json @@ -176,6 +176,7 @@ "@metamask/react-native-payments": "^2.0.0", "@metamask/react-native-search-api": "1.0.1", "@metamask/react-native-webview": "^14.0.4", + "@metamask/remote-feature-flag-controller": "^1.0.0", "@metamask/rpc-errors": "^7.0.1", "@metamask/scure-bip39": "^2.1.0", "@metamask/sdk-communication-layer": "0.29.0-wallet", diff --git a/yarn.lock b/yarn.lock index 74ba5243847..7b176e20ab3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4903,6 +4903,15 @@ escape-string-regexp "^4.0.0" invariant "2.2.4" +"@metamask/remote-feature-flag-controller@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@metamask/remote-feature-flag-controller/-/remote-feature-flag-controller-1.0.0.tgz#048162eaa6fa34401cfbabfa0eb33f0255bb2945" + integrity sha512-jrjEQhW/RdHQ/GQbgXH97N6YqDUW7nGA40lEr0TUSIhJVVaHDX0gCiNmJZcQ89yLY4DZ0bisEwjrCu8LycYiQQ== + dependencies: + "@metamask/base-controller" "^7.0.2" + "@metamask/utils" "^10.0.0" + cockatiel "^3.1.2" + "@metamask/rpc-errors@7.0.1", "@metamask/rpc-errors@^6.2.1", "@metamask/rpc-errors@^6.3.1", "@metamask/rpc-errors@^7.0.0", "@metamask/rpc-errors@^7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-7.0.1.tgz#0eb2231a1d5e6bb102df5ac07f365c695bf70055" From e7b65d531db45b43f70cca358570d8fe0d112248 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Mon, 2 Dec 2024 19:29:36 +0000 Subject: [PATCH 04/17] Bump version number to 1508 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6e59998cbf2..1c5194cf9a2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -174,7 +174,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.37.0" - versionCode 1506 + versionCode 1508 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index 33276a0fa8d..6de9effe774 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1687,13 +1687,13 @@ app: VERSION_NAME: 7.37.0 - opts: is_expand: false - VERSION_NUMBER: 1506 + VERSION_NUMBER: 1508 - opts: is_expand: false FLASK_VERSION_NAME: 7.37.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 1506 + FLASK_VERSION_NUMBER: 1508 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 5d23a5ecb2c..d1ea7c8b538 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1506; + CURRENT_PROJECT_VERSION = 1508; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1346,7 +1346,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1506; + CURRENT_PROJECT_VERSION = 1508; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1409,7 +1409,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1506; + CURRENT_PROJECT_VERSION = 1508; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1470,7 +1470,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1506; + CURRENT_PROJECT_VERSION = 1508; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1624,7 +1624,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1506; + CURRENT_PROJECT_VERSION = 1508; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1692,7 +1692,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1506; + CURRENT_PROJECT_VERSION = 1508; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 220c4b9686574fce714a8213150f1b282f27d9d8 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 5 Dec 2024 00:15:24 +0000 Subject: [PATCH 05/17] Bump version number to 1510 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1c5194cf9a2..d51e362f6c9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -174,7 +174,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.37.0" - versionCode 1508 + versionCode 1510 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index 6de9effe774..1d9e52d4c47 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1687,13 +1687,13 @@ app: VERSION_NAME: 7.37.0 - opts: is_expand: false - VERSION_NUMBER: 1508 + VERSION_NUMBER: 1510 - opts: is_expand: false FLASK_VERSION_NAME: 7.37.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 1508 + FLASK_VERSION_NUMBER: 1510 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index d1ea7c8b538..a7c47668fd1 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1508; + CURRENT_PROJECT_VERSION = 1510; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1346,7 +1346,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1508; + CURRENT_PROJECT_VERSION = 1510; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1409,7 +1409,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1508; + CURRENT_PROJECT_VERSION = 1510; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1470,7 +1470,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1508; + CURRENT_PROJECT_VERSION = 1510; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1624,7 +1624,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1508; + CURRENT_PROJECT_VERSION = 1510; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1692,7 +1692,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1508; + CURRENT_PROJECT_VERSION = 1510; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 0df8b7ed1d032aa426272654e3cbc1d858325282 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:32:28 -0700 Subject: [PATCH 06/17] chore(runway): cherry-pick fix: Move `AssetPollingProvider` from Root to Nav/Main/index.js (#12564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: Move `AssetPollingProvider` from Root to Nav/Main/index.js (#12562) ## **Description** Picks `AssetPollingProvider` diff from https://github.com/MetaMask/metamask-mobile/pull/12538/files bug fix to isolate changes. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.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. [a93ef10](https://github.com/MetaMask/metamask-mobile/commit/a93ef1060eee81f93bddffde239804e356e137ff) Co-authored-by: Nick Gambino <35090461+gambinish@users.noreply.github.com> --- app/components/Nav/Main/index.js | 61 ++++++++++++++++-------------- app/components/Views/Root/index.js | 9 ++--- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 8d463aae71b..0ff9ee33f51 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -82,6 +82,7 @@ import { } from '../../../util/transaction-controller'; import isNetworkUiRedesignEnabled from '../../../util/networks/isNetworkUiRedesignEnabled'; import { useConnectionHandler } from '../../../util/navigation/useConnectionHandler'; +import { AssetPollingProvider } from '../../hooks/AssetPolling/AssetPollingProvider'; const Stack = createStackNavigator(); @@ -362,35 +363,37 @@ const Main = (props) => { return ( - - {!forceReload ? ( - - ) : ( - renderLoader() - )} - - - - - - - {renderDeprecatedNetworkAlert( - props.chainId, - props.backUpSeedphraseVisible, - )} - - - - + + + {!forceReload ? ( + + ) : ( + renderLoader() + )} + + + + + + + {renderDeprecatedNetworkAlert( + props.chainId, + props.backUpSeedphraseVisible, + )} + + + + + ); }; diff --git a/app/components/Views/Root/index.js b/app/components/Views/Root/index.js index 05a52cb033b..7ffa81485d3 100644 --- a/app/components/Views/Root/index.js +++ b/app/components/Views/Root/index.js @@ -12,7 +12,6 @@ import { useAppTheme, ThemeContext } from '../../../util/theme'; import { ToastContextWrapper } from '../../../component-library/components/Toast'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { isTest } from '../../../util/test/utils'; -import { AssetPollingProvider } from '../../hooks/AssetPolling/AssetPollingProvider'; /** * Top level of the component hierarchy @@ -86,11 +85,9 @@ const ConnectedRoot = () => { - - - - - + + + From 9d1a470983cd38734a0fae8669fc9bd824bdb531 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:30:54 -0300 Subject: [PATCH 07/17] chore(runway): cherry-pick fix(12527): sdk connection with unknown url causes a bug (#12573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(12527): sdk connection with unknown url causes a bug (#12566) ## **Description** When the user uses SDK to connect to a dapp which does not provide a URL, unknown URL, this causes a bug. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/12527 ## **Manual testing steps** 1. Go to https://unknown-url.vercel.app/ 2. Tap connect 3. choose Metamask Wallet, or scan the QR Code with the MetaMask QR Code reader ## **Screenshots/Recordings** Screenshot 2024-04-18s at 3 56 43 PM ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.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. [43ae0a1](https://github.com/MetaMask/metamask-mobile/commit/43ae0a1bbe9797e84783178936eef0c275c74d89) Co-authored-by: EtherWizard33 <165834542+EtherWizard33@users.noreply.github.com> --- .../Views/AccountConnect/AccountConnect.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx index d7321855075..6f8426b4466 100644 --- a/app/components/Views/AccountConnect/AccountConnect.tsx +++ b/app/components/Views/AccountConnect/AccountConnect.tsx @@ -159,6 +159,8 @@ const AccountConnect = (props: AccountConnectProps) => { const dappIconUrl = sdkConnection?.originatorInfo?.icon; const dappUrl = sdkConnection?.originatorInfo?.url ?? ''; + const [isSdkUrlUnknown, setIsSdkUrlUnknown] = useState(false); + const { domainTitle, hostname } = useMemo(() => { let title = ''; let dappHostname = dappUrl || channelIdOrHostname; @@ -178,6 +180,7 @@ const AccountConnect = (props: AccountConnectProps) => { dappHostname = inappBrowserOrigin; } else { title = strings('sdk.unknown'); + setIsSdkUrlUnknown(true); } return { domainTitle: title, hostname: dappHostname }; @@ -357,7 +360,13 @@ const AccountConnect = (props: AccountConnectProps) => { .build(), ); }, - [accountsLength, channelIdOrHostname, trackEvent, createEventBuilder, eventSource], + [ + accountsLength, + channelIdOrHostname, + trackEvent, + createEventBuilder, + eventSource, + ], ); const navigateToUrlInEthPhishingModal = useCallback( @@ -799,7 +808,9 @@ const AccountConnect = (props: AccountConnectProps) => { const renderConnectScreens = useCallback(() => { switch (screen) { case AccountConnectScreens.SingleConnect: - return isMultichainVersion1Enabled + return isSdkUrlUnknown + ? renderSingleConnectScreen() + : isMultichainVersion1Enabled ? renderPermissionsSummaryScreen() : renderSingleConnectScreen(); case AccountConnectScreens.SingleConnectSelector: @@ -811,6 +822,7 @@ const AccountConnect = (props: AccountConnectProps) => { } }, [ screen, + isSdkUrlUnknown, renderSingleConnectScreen, renderPermissionsSummaryScreen, renderSingleConnectSelectorScreen, From 1930c1f8684436576c8d949a463daff5ea5cb4d2 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 6 Dec 2024 00:20:33 +0000 Subject: [PATCH 08/17] Bump version number to 1511 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d51e362f6c9..9016085b557 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -174,7 +174,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.37.0" - versionCode 1510 + versionCode 1511 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index 1d9e52d4c47..8fe4ed3ca67 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1687,13 +1687,13 @@ app: VERSION_NAME: 7.37.0 - opts: is_expand: false - VERSION_NUMBER: 1510 + VERSION_NUMBER: 1511 - opts: is_expand: false FLASK_VERSION_NAME: 7.37.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 1510 + FLASK_VERSION_NUMBER: 1511 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index a7c47668fd1..a485e10e3f3 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1510; + CURRENT_PROJECT_VERSION = 1511; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1346,7 +1346,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1510; + CURRENT_PROJECT_VERSION = 1511; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1409,7 +1409,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1510; + CURRENT_PROJECT_VERSION = 1511; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1470,7 +1470,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1510; + CURRENT_PROJECT_VERSION = 1511; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1624,7 +1624,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1510; + CURRENT_PROJECT_VERSION = 1511; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1692,7 +1692,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1510; + CURRENT_PROJECT_VERSION = 1511; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 2ff2d53e677214067c8182e4b8fa74c381df58b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:15:03 -0700 Subject: [PATCH 09/17] chore: cherry-pick #12540 (#12600) This PR cherry-picks #12540 Co-authored-by: Salim TOUBAL --- app/store/migrations/062.test.ts | 269 +++++++++++++++++++++++++++ app/store/migrations/062.ts | 128 +++++++++++++ app/store/migrations/index.ts | 4 +- app/util/networks/customNetworks.tsx | 77 +++++++- e2e/resources/networks.e2e.js | 2 +- 5 files changed, 477 insertions(+), 3 deletions(-) create mode 100644 app/store/migrations/062.test.ts create mode 100644 app/store/migrations/062.ts diff --git a/app/store/migrations/062.test.ts b/app/store/migrations/062.test.ts new file mode 100644 index 00000000000..32046cd3cd2 --- /dev/null +++ b/app/store/migrations/062.test.ts @@ -0,0 +1,269 @@ +import migrate from './062'; +import { merge } from 'lodash'; +import { captureException } from '@sentry/react-native'; +import initialRootState from '../../util/test/initial-root-state'; +import { RootState } from '../../reducers'; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +describe('Migration #62 - Replace Base Network RPC URL', () => { + const BASE_CHAIN_ID = '0x2105'; + const OLD_RPC_URL = 'https://mainnet.base.org'; + const NEW_RPC_URL = `https://base-mainnet.infura.io/v3/${ + process.env.MM_INFURA_PROJECT_ID === 'null' + ? '' + : process.env.MM_INFURA_PROJECT_ID + }`; + const LINEA_CHAIN_ID = '0x1234'; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: null, + errorMessage: "FATAL ERROR: Migration 62: Invalid state error: 'object'", + scenario: 'state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: null, + }), + errorMessage: + "FATAL ERROR: Migration 62: Invalid engine state error: 'object'", + scenario: 'engine state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: null, + }, + }), + errorMessage: + "FATAL ERROR: Migration 62: Invalid engine backgroundState error: 'object'", + scenario: 'backgroundState is invalid', + }, + ]; + + for (const { errorMessage, scenario, state } of invalidStates) { + it(`should capture exception if ${scenario}`, async () => { + const newState = await migrate(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }); + } + + it('should replace the first occurrence of the Base network RPC URL', async () => { + const oldState = merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + [BASE_CHAIN_ID]: { + rpcEndpoints: [ + { url: OLD_RPC_URL }, + { url: 'https://another.rpc' }, + ], + }, + }, + }, + }, + }, + }); + + const expectedState = merge({}, oldState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + [BASE_CHAIN_ID]: { + rpcEndpoints: [ + { url: NEW_RPC_URL }, + { url: 'https://another.rpc' }, + ], + }, + }, + }, + }, + }, + }); + + const newState = await migrate(oldState); + expect(newState).toStrictEqual(expectedState); + }); + + it('should do nothing if the Base network configuration is missing', async () => { + const oldState = merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: {}, + }, + }, + }, + }); + + const newState = await migrate(oldState); + expect(newState).toStrictEqual(oldState); + }); + + it('should do nothing if the Base network RPC URL is not present', async () => { + const oldState = merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + [BASE_CHAIN_ID]: { + rpcEndpoints: [ + { url: 'https://another.rpc' }, + { url: 'https://yet.another.rpc' }, + ], + }, + }, + }, + }, + }, + }); + + const newState = await migrate(oldState); + expect(newState).toStrictEqual(oldState); + }); + + it('should handle cases where rpcEndpoints is not an array', async () => { + const oldState = merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + [BASE_CHAIN_ID]: { + rpcEndpoints: null, + }, + }, + }, + }, + }, + }); + + const newState = await migrate(oldState); + expect(newState).toStrictEqual(oldState); + }); + + it('should do nothing if no networks use Infura RPC endpoints', async () => { + const oldState = merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x1': { + rpcEndpoints: [ + { url: 'https://non-infura.rpc' }, + { url: 'https://another-non-infura.rpc' }, + ], + defaultRpcEndpointIndex: 0, + }, + }, + }, + }, + }, + }); + + const newState = await migrate(oldState); + expect(newState).toStrictEqual(oldState); + }); + + it('should proceed with migration if at least one network uses an Infura RPC endpoint', async () => { + const oldState = merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x1': { + rpcEndpoints: [ + { url: 'https://mainnet.infura.io/v3/some-key' }, + { url: 'https://non-infura.rpc' }, + ], + defaultRpcEndpointIndex: 0, + }, + [BASE_CHAIN_ID]: { + rpcEndpoints: [ + { url: OLD_RPC_URL }, + { url: 'https://another.rpc' }, + ], + }, + }, + }, + }, + }, + }); + + const expectedState = merge({}, oldState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + [BASE_CHAIN_ID]: { + rpcEndpoints: [ + { url: NEW_RPC_URL }, + { url: 'https://another.rpc' }, + ], + }, + }, + }, + }, + }, + }); + + const newState = await migrate(oldState); + expect(newState).toStrictEqual(expectedState); + }); + + it('should exclude LINEA_MAINNET from Infura RPC endpoint checks', async () => { + const oldState = merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + [LINEA_CHAIN_ID]: { + rpcEndpoints: [ + { url: 'https://linea.infura.io/v3/some-key' }, + { url: 'https://another-linea.rpc' }, + ], + defaultRpcEndpointIndex: 0, + }, + [BASE_CHAIN_ID]: { + rpcEndpoints: [ + { url: OLD_RPC_URL }, + { url: 'https://another.rpc' }, + ], + }, + }, + }, + }, + }, + }); + + const newState = await migrate(oldState); + + // The LINEA_MAINNET should not trigger migration; only BASE_CHAIN_ID is updated + expect( + (newState as RootState).engine.backgroundState.NetworkController + .networkConfigurationsByChainId[LINEA_CHAIN_ID], + ).toStrictEqual( + oldState.engine.backgroundState.NetworkController + .networkConfigurationsByChainId[LINEA_CHAIN_ID], + ); + expect( + (newState as RootState).engine.backgroundState.NetworkController + .networkConfigurationsByChainId[BASE_CHAIN_ID].rpcEndpoints[0].url, + ).toBe(NEW_RPC_URL); + }); +}); diff --git a/app/store/migrations/062.ts b/app/store/migrations/062.ts new file mode 100644 index 00000000000..42ad85a2671 --- /dev/null +++ b/app/store/migrations/062.ts @@ -0,0 +1,128 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { ensureValidState } from './util'; +import Logger from '../../util/Logger'; +import { RpcEndpointType } from '@metamask/network-controller'; +import { + allowedInfuraHosts, + infuraChainIdsTestNets, +} from '../../util/networks/customNetworks'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; + +const BASE_CHAIN_ID = '0x2105'; +const INFURA_KEY = process.env.MM_INFURA_PROJECT_ID; +const infuraProjectId = INFURA_KEY === 'null' ? '' : INFURA_KEY; + +/** + * Migration to update the Base network configuration by replacing + * "https://mainnet.base.org" with "https://base-mainnet.infura.io/v3/{infuraProjectId}". + * + * This addresses potential compatibility issues caused by deprecated endpoints. + * + * @param state - The current MetaMask extension state. + * @returns The updated state with the revised Infura endpoint for the Base network. + */ +export default function migrate(state: unknown) { + // Ensure the state is valid for migration + if (!ensureValidState(state, 62)) { + return state; + } + + // Locate the Base network configuration in the NetworkController + const { engine } = state; + if ( + hasProperty(engine.backgroundState, 'NetworkController') && + isObject(engine.backgroundState.NetworkController) && + hasProperty( + engine.backgroundState.NetworkController, + 'networkConfigurationsByChainId', + ) && + isObject( + engine.backgroundState.NetworkController.networkConfigurationsByChainId, + ) + ) { + const networkConfigurationsByChainId = + engine.backgroundState.NetworkController.networkConfigurationsByChainId; + + // Check if at least one network uses an Infura RPC endpoint, excluding testnets + const usesInfura = Object.entries(networkConfigurationsByChainId) + .filter( + ([chainId]) => + ![...infuraChainIdsTestNets, CHAIN_IDS.LINEA_MAINNET].includes( + chainId, + ), + ) // Exclude testnet chain IDs + .some(([, networkConfig]) => { + if ( + !isObject(networkConfig) || + !Array.isArray(networkConfig.rpcEndpoints) || + typeof networkConfig.defaultRpcEndpointIndex !== 'number' + ) { + return false; + } + + // Get the default RPC endpoint used by the network + const defaultRpcEndpoint = + networkConfig.rpcEndpoints?.[networkConfig.defaultRpcEndpointIndex]; + + if ( + !isObject(defaultRpcEndpoint) || + typeof defaultRpcEndpoint.url !== 'string' + ) { + return false; + } + + try { + const urlHost = new URL(defaultRpcEndpoint.url).host; + return ( + defaultRpcEndpoint.type === RpcEndpointType.Infura || + allowedInfuraHosts.includes(urlHost) + ); + } catch { + return false; + } + }); + + if (!usesInfura) { + // If no Infura endpoints are used, return the state unchanged + return state; + } + + const baseNetworkConfig = networkConfigurationsByChainId[BASE_CHAIN_ID]; + if ( + isObject(baseNetworkConfig) && + hasProperty(baseNetworkConfig, 'rpcEndpoints') + ) { + const { rpcEndpoints } = baseNetworkConfig; + + if (Array.isArray(rpcEndpoints)) { + const endpointIndex = rpcEndpoints.findIndex( + (endpoint) => + isObject(endpoint) && endpoint.url === 'https://mainnet.base.org', + ); + + if (endpointIndex !== -1) { + Logger.log( + `Migration 135: Updating 'https://mainnet.base.org' to 'https://base-mainnet.infura.io/v3/${infuraProjectId}' in Base network RPC endpoints.`, + ); + + // Update the first occurrence of the deprecated URL + rpcEndpoints[endpointIndex] = { + ...rpcEndpoints[endpointIndex], + url: `https://base-mainnet.infura.io/v3/${infuraProjectId}`, + }; + + // Apply the changes to the Base network configuration + networkConfigurationsByChainId[BASE_CHAIN_ID] = { + ...baseNetworkConfig, + rpcEndpoints, + }; + + engine.backgroundState.NetworkController.networkConfigurationsByChainId = + networkConfigurationsByChainId; + } + } + } + } + + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 5d6408a2959..4a8d11ad47c 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -62,6 +62,7 @@ import migration58 from './058'; import migration59 from './059'; import migration60 from './060'; import migration61 from './061'; +import migration62 from './062'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -135,7 +136,8 @@ export const migrationList: MigrationsList = { 58: migration58, 59: migration59, 60: migration60, - 61: migration61 + 61: migration61, + 62: migration62, }; // Enable both synchronous and asynchronous migrations diff --git a/app/util/networks/customNetworks.tsx b/app/util/networks/customNetworks.tsx index fe301ceae68..83dada0ef22 100644 --- a/app/util/networks/customNetworks.tsx +++ b/app/util/networks/customNetworks.tsx @@ -42,7 +42,7 @@ export const PopularList = [ { chainId: toHex('8453'), nickname: 'Base', - rpcUrl: `https://mainnet.base.org`, + rpcUrl: `https://base-mainnet.infura.io/v3/${infuraProjectId}`, ticker: 'ETH', warning: true, rpcPrefs: { @@ -98,6 +98,81 @@ export const PopularList = [ }, ]; +export const INFURA_TESTNET_CHAIN_IDS = { + GOERLI: '0x5', + LINEA_GOERLI: '0xe704', + SEPOLIA: '0xaa36a7', + HOLESKY: '0x4268', + LINEA_SEPOLIA: '0xe705', + AMOY: '0x13882', + BASE_SEPOLIA: '0x14a34', + OPTIMISM_SEPOLIA: '0xaa37dc', + ARBITRUM_SEPOLIA: '0x66eee', + PALM_TESTNET: '0x2a15c3083', + AVALANCHE_TESTNET: '0xa869', + CELO_TESTNET: '0xaef3', + ZK_SYNC_ERA_TESTNET: '0x12c', + BSC_TESTNET: '0x61', + MANTA_SEPOLIA: '0x138b', + OPBNB_TESTNET: '0x15eb', + SCROLL_SEPOLIA: '0x8274f', + UNICHAIN_SEPOLIA: '0x515', +} as const; + +export const infuraChainIdsTestNets: string[] = [ + INFURA_TESTNET_CHAIN_IDS.GOERLI, + INFURA_TESTNET_CHAIN_IDS.LINEA_GOERLI, + INFURA_TESTNET_CHAIN_IDS.SEPOLIA, + INFURA_TESTNET_CHAIN_IDS.HOLESKY, + INFURA_TESTNET_CHAIN_IDS.LINEA_SEPOLIA, + INFURA_TESTNET_CHAIN_IDS.AMOY, + INFURA_TESTNET_CHAIN_IDS.BASE_SEPOLIA, + INFURA_TESTNET_CHAIN_IDS.OPTIMISM_SEPOLIA, + INFURA_TESTNET_CHAIN_IDS.ARBITRUM_SEPOLIA, + INFURA_TESTNET_CHAIN_IDS.PALM_TESTNET, + INFURA_TESTNET_CHAIN_IDS.AVALANCHE_TESTNET, + INFURA_TESTNET_CHAIN_IDS.CELO_TESTNET, + INFURA_TESTNET_CHAIN_IDS.ZK_SYNC_ERA_TESTNET, + INFURA_TESTNET_CHAIN_IDS.BSC_TESTNET, + INFURA_TESTNET_CHAIN_IDS.MANTA_SEPOLIA, + INFURA_TESTNET_CHAIN_IDS.OPBNB_TESTNET, + INFURA_TESTNET_CHAIN_IDS.SCROLL_SEPOLIA, + INFURA_TESTNET_CHAIN_IDS.UNICHAIN_SEPOLIA, +]; + +export const allowedInfuraHosts = [ + // Ethereum + 'mainnet.infura.io', + // Linea + 'linea-mainnet.infura.io', + // Polygon + 'polygon-mainnet.infura.io', + // Base + 'base-mainnet.infura.io', + // Blast + 'blast-mainnet.infura.io', + // Optimism + 'optimism-mainnet.infura.io', + // Arbitrum + 'arbitrum-mainnet.infura.io', + // Palm + 'palm-mainnet.infura.io', + // Avalanche + 'avalanche-mainnet.infura.io', + // Celo + 'celo-mainnet.infura.io', + // ZKSync + 'zksync-mainnet.infura.io', + // BSC + 'bsc-mainnet.infura.io', + // Mantle + 'mantle-mainnet.infura.io', + // OPBNB + 'opbnb-mainnet.infura.io', + // Scroll + 'scroll-mainnet.infura.io', +]; + /** * List of popularList will change in the future, removing networks from the list will lead to users not * seeing the logo of the network anymore. diff --git a/e2e/resources/networks.e2e.js b/e2e/resources/networks.e2e.js index fd6fc94a4a7..1cd7b4eb927 100644 --- a/e2e/resources/networks.e2e.js +++ b/e2e/resources/networks.e2e.js @@ -36,7 +36,7 @@ const PopularNetworksList = { providerConfig: { type: 'rpc', chainId: toHex('8453'), - rpcUrl: `https://mainnet.base.org`, + rpcUrl: `https://base-mainnet.infura.io/v3/${infuraProjectId}`, nickname: 'Base', ticker: 'ETH', }, From 12ac883dcd692b876d885e8bec6945f468b063e8 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Mon, 9 Dec 2024 17:18:58 +0000 Subject: [PATCH 10/17] Bump version number to 1512 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9016085b557..124ad2b6ac9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -174,7 +174,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.37.0" - versionCode 1511 + versionCode 1512 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index 8fe4ed3ca67..79776789c05 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1687,13 +1687,13 @@ app: VERSION_NAME: 7.37.0 - opts: is_expand: false - VERSION_NUMBER: 1511 + VERSION_NUMBER: 1512 - opts: is_expand: false FLASK_VERSION_NAME: 7.37.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 1511 + FLASK_VERSION_NUMBER: 1512 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index a485e10e3f3..b0ed3471cdf 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1511; + CURRENT_PROJECT_VERSION = 1512; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1346,7 +1346,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1511; + CURRENT_PROJECT_VERSION = 1512; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1409,7 +1409,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1511; + CURRENT_PROJECT_VERSION = 1512; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1470,7 +1470,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1511; + CURRENT_PROJECT_VERSION = 1512; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1624,7 +1624,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1511; + CURRENT_PROJECT_VERSION = 1512; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1692,7 +1692,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1511; + CURRENT_PROJECT_VERSION = 1512; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 0f7453072fe29b00f61e4ad7ceaec8e784859f4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:18:49 -0700 Subject: [PATCH 11/17] feat: 7.37.1 (#12662) This is the hot fix release candidate for version 7.37.1. # Team sign-off checklist - [x] team-confirmations --------- Co-authored-by: metamaskbot Co-authored-by: runway-github[bot] <73448015+runway-github[bot]@users.noreply.github.com> Co-authored-by: Daniel <80175477+dan437@users.noreply.github.com> Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com> Co-authored-by: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> --- android/app/build.gradle | 4 +- app/lib/ppom/ppom-util.test.ts | 7 +- app/store/migrations/063.test.ts | 328 +++++++++++++++++++++++++ app/store/migrations/063.ts | 105 ++++++++ app/store/migrations/index.ts | 2 + bitrise.yml | 8 +- ios/MetaMask.xcodeproj/project.pbxproj | 24 +- package.json | 20 +- yarn.lock | 250 +++++++++++-------- 9 files changed, 609 insertions(+), 139 deletions(-) create mode 100644 app/store/migrations/063.test.ts create mode 100644 app/store/migrations/063.ts diff --git a/android/app/build.gradle b/android/app/build.gradle index 124ad2b6ac9..5e1076971a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -173,8 +173,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.37.0" - versionCode 1512 + versionName "7.37.1" + versionCode 1520 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/lib/ppom/ppom-util.test.ts b/app/lib/ppom/ppom-util.test.ts index 95241217c88..a0d14c8d73b 100644 --- a/app/lib/ppom/ppom-util.test.ts +++ b/app/lib/ppom/ppom-util.test.ts @@ -151,6 +151,7 @@ describe('PPOM Utils', () => { url: 'https://mainnet.infura.io/v3', }, ], + lastUpdatedAt: Date.now(), }, }, networksMetadata: {}, @@ -176,8 +177,10 @@ describe('PPOM Utils', () => { MockEngine.context.PreferencesController.state.securityAlertsEnabled = false; await PPOMUtil.validateRequest(mockRequest, CHAIN_ID_MOCK); - expect(MockEngine.context.PPOMController?.usePPOM).toBeCalledTimes(0); - expect(spyTransactionAction).toBeCalledTimes(0); + expect(MockEngine.context.PPOMController?.usePPOM).toHaveBeenCalledTimes( + 0, + ); + expect(spyTransactionAction).toHaveBeenCalledTimes(0); }); it('should not validate if request is send to users own account ', async () => { diff --git a/app/store/migrations/063.test.ts b/app/store/migrations/063.test.ts new file mode 100644 index 00000000000..c57e1163b1d --- /dev/null +++ b/app/store/migrations/063.test.ts @@ -0,0 +1,328 @@ +import migrate from './063'; +import { merge } from 'lodash'; +import { captureException } from '@sentry/react-native'; +import initialRootState from '../../util/test/initial-root-state'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { TransactionStatus, CHAIN_IDS } from '@metamask/transaction-controller'; + +const expectedState = { + engine: { + backgroundState: { + TransactionController: { + transactions: [ + { + chainId: CHAIN_IDS.MAINNET, + id: '1', + origin: 'test.com', + status: TransactionStatus.confirmed, + time: 1631714312, + txParams: { + from: '0x1', + }, + hash: '0x2', + rawTx: '0x3', + }, + { + chainId: CHAIN_IDS.LINEA_MAINNET, + id: '2', + origin: 'test.com', + status: TransactionStatus.confirmed, + time: 1631714312, + txParams: { + from: '0x1', + }, + hash: '0x3', + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '3', + origin: 'test2.com', + status: TransactionStatus.failed, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x4', + rawTx: '0x5', + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '4', + origin: 'test2.com', + status: TransactionStatus.failed, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x5', + rawTx: '0x6', + error: { + name: 'SmartTransactionCancelled', + message: 'Smart transaction cancelled. Previous status: submitted', + }, + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '5', + origin: 'test2.com', + status: TransactionStatus.failed, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x6', + rawTx: '0x7', + error: { + name: 'SmartTransactionCancelled', + message: 'Smart transaction cancelled. Previous status: signed', + }, + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '6', + origin: 'test2.com', + status: TransactionStatus.failed, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x7', + rawTx: '0x8', + error: { + name: 'SmartTransactionCancelled', + message: 'Smart transaction cancelled. Previous status: signed', + }, + }, + ], + }, + SmartTransactionsController: { + smartTransactionsState: { + smartTransactions: { + [CHAIN_IDS.MAINNET]: [ + { + txHash: '0x2', + status: SmartTransactionStatuses.SUCCESS, + }, + { + txHash: '0x4', + status: SmartTransactionStatuses.CANCELLED, + }, + { + txHash: '0x5', + status: SmartTransactionStatuses.CANCELLED, + }, + { + txHash: '0x6', + status: SmartTransactionStatuses.UNKNOWN, + }, + { + txHash: '0x7', + status: SmartTransactionStatuses.RESOLVED, + }, + ], + }, + }, + }, + }, + }, +}; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +describe('Migration #63', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: null, + errorMessage: "FATAL ERROR: Migration 63: Invalid state error: 'object'", + scenario: 'state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: null, + }), + errorMessage: + "FATAL ERROR: Migration 63: Invalid engine state error: 'object'", + scenario: 'engine state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: null, + }, + }), + errorMessage: + "FATAL ERROR: Migration 63: Invalid engine backgroundState error: 'object'", + scenario: 'backgroundState is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { TransactionController: null }, + }, + }), + errorMessage: "Migration 63: Invalid TransactionController state: 'null'", + scenario: 'transactionController is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { SmartTransactionsController: null }, + }, + }), + errorMessage: + "Migration 63: Invalid SmartTransactionsController state: 'null'", + scenario: 'smartTransactionsController is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { + SmartTransactionsController: { + smartTransactionsState: { smartTransactions: null }, + }, + }, + }, + }), + errorMessage: + "Migration 63: Missing smart transactions property from SmartTransactionsController: 'object'", + scenario: + 'smartTransactionsController.smartTransactionsState.smartTransactions is invalid', + }, + ]; + it.each(invalidStates)( + 'captures exception if $scenario', + ({ errorMessage, state }) => { + const newState = migrate(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }, + ); + + it('applies migration, changes transaction status to failed if a smart transaction was cancelled or unknown', () => { + const oldState = { + engine: { + backgroundState: { + TransactionController: { + transactions: [ + { + chainId: CHAIN_IDS.MAINNET, + id: '1', + origin: 'test.com', + status: TransactionStatus.confirmed, + time: 1631714312, + txParams: { + from: '0x1', + }, + hash: '0x2', + rawTx: '0x3', + }, + { + chainId: CHAIN_IDS.LINEA_MAINNET, + id: '2', + origin: 'test.com', + status: TransactionStatus.confirmed, + time: 1631714312, + txParams: { + from: '0x1', + }, + hash: '0x3', + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '3', + origin: 'test2.com', + status: TransactionStatus.failed, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x4', + rawTx: '0x5', + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '4', + origin: 'test2.com', + status: TransactionStatus.submitted, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x5', + rawTx: '0x6', + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '5', + origin: 'test2.com', + status: TransactionStatus.signed, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x6', + rawTx: '0x7', + }, + { + chainId: CHAIN_IDS.MAINNET, + id: '6', + origin: 'test2.com', + status: TransactionStatus.signed, + time: 1631714313, + txParams: { + from: '0x6', + }, + hash: '0x7', + rawTx: '0x8', + }, + ], + }, + SmartTransactionsController: { + smartTransactionsState: { + smartTransactions: { + [CHAIN_IDS.MAINNET]: [ + { + txHash: '0x2', + status: SmartTransactionStatuses.SUCCESS, + }, + { + txHash: '0x4', + status: SmartTransactionStatuses.CANCELLED, + }, + { + txHash: '0x5', + status: SmartTransactionStatuses.CANCELLED, + }, + { + txHash: '0x6', + status: SmartTransactionStatuses.UNKNOWN, + }, + { + txHash: '0x7', + status: SmartTransactionStatuses.RESOLVED, + }, + ], + }, + }, + }, + }, + }, + }; + + const newState = migrate(oldState); + + expect(newState).toStrictEqual(expectedState); + }); +}); diff --git a/app/store/migrations/063.ts b/app/store/migrations/063.ts new file mode 100644 index 00000000000..5d453db3cc2 --- /dev/null +++ b/app/store/migrations/063.ts @@ -0,0 +1,105 @@ +import { isObject } from '@metamask/utils'; +import { captureException } from '@sentry/react-native'; +import { ensureValidState } from './util'; +import { SmartTransactionStatuses, type SmartTransaction } from '@metamask/smart-transactions-controller/dist/types'; +import { TransactionStatus, CHAIN_IDS } from '@metamask/transaction-controller'; + +const migrationVersion = 63; + +interface SmartTransactionsState { + smartTransactions: { + [chainId: string]: SmartTransaction[]; + }; +} + +export default function migrate(state: unknown) { + if (!ensureValidState(state, migrationVersion)) { + return state; + } + + const transactionControllerState = state.engine.backgroundState.TransactionController; + const smartTransactionsControllerState = state.engine.backgroundState.SmartTransactionsController; + + if (!isObject(transactionControllerState)) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid TransactionController state: '${transactionControllerState}'`, + ), + ); + return state; + } + + if (!isObject(smartTransactionsControllerState)) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid SmartTransactionsController state: '${smartTransactionsControllerState}'`, + ), + ); + return state; + } + + if (!Array.isArray(transactionControllerState.transactions)) { + captureException( + new Error( + `Migration ${migrationVersion}: Missing transactions property from TransactionController: '${typeof state + .engine.backgroundState.TransactionController}'`, + ), + ); + return state; + } + const smartTransactions = (smartTransactionsControllerState?.smartTransactionsState as SmartTransactionsState)?.smartTransactions; + if (!isObject(smartTransactions)) { + captureException( + new Error( + `Migration ${migrationVersion}: Missing smart transactions property from SmartTransactionsController: '${typeof smartTransactionsControllerState?.smartTransactionsState}'`, + ), + ); + return state; + } + + const ethereumMainnetSmartTransactions = smartTransactions[CHAIN_IDS.MAINNET]; + + // If there are no smart transactions, we can skip this migration. + if ( + !Array.isArray(ethereumMainnetSmartTransactions) || + ethereumMainnetSmartTransactions.length === 0 + ) { + return state; + } + + const smartTransactionStatusesForUpdate: SmartTransactionStatuses[] = [ + SmartTransactionStatuses.CANCELLED, + SmartTransactionStatuses.UNKNOWN, + SmartTransactionStatuses.RESOLVED, + ]; + + // Create a Set of transaction hashes for quick lookup. + const smartTransactionTxHashesForUpdate = new Set( + ethereumMainnetSmartTransactions + .filter( + (smartTransaction) => + smartTransaction.txHash && + smartTransaction.status && + smartTransactionStatusesForUpdate.includes(smartTransaction.status as SmartTransactionStatuses), + ) + .map((smartTransaction) => smartTransaction.txHash?.toLowerCase()), + ); + + // Update transactions based on the Set. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transactionControllerState.transactions.forEach((transaction: any) => { + if (!transaction.hash || transaction.status === TransactionStatus.failed) { + return; + } + const previousStatus = transaction.status; + if (smartTransactionTxHashesForUpdate.has(transaction.hash.toLowerCase())) { + transaction.status = TransactionStatus.failed; + transaction.error = { + name: 'SmartTransactionCancelled', + message: `Smart transaction cancelled. Previous status: ${previousStatus}`, + }; + } + }); + + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 4a8d11ad47c..06f50319276 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -63,6 +63,7 @@ import migration59 from './059'; import migration60 from './060'; import migration61 from './061'; import migration62 from './062'; +import migration63 from './063'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -138,6 +139,7 @@ export const migrationList: MigrationsList = { 60: migration60, 61: migration61, 62: migration62, + 63: migration63, }; // Enable both synchronous and asynchronous migrations diff --git a/bitrise.yml b/bitrise.yml index 79776789c05..3bd4134bb7e 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1684,16 +1684,16 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.37.0 + VERSION_NAME: 7.37.1 - opts: is_expand: false - VERSION_NUMBER: 1512 + VERSION_NUMBER: 1520 - opts: is_expand: false - FLASK_VERSION_NAME: 7.37.0 + FLASK_VERSION_NAME: 7.37.1 - opts: is_expand: false - FLASK_VERSION_NUMBER: 1512 + FLASK_VERSION_NUMBER: 1520 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index b0ed3471cdf..e6e0e7b7324 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1512; + CURRENT_PROJECT_VERSION = 1520; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1318,7 +1318,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.37.0; + MARKETING_VERSION = 7.37.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1346,7 +1346,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1512; + CURRENT_PROJECT_VERSION = 1520; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1381,7 +1381,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.37.0; + MARKETING_VERSION = 7.37.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1409,7 +1409,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1512; + CURRENT_PROJECT_VERSION = 1520; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1442,7 +1442,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; LLVM_LTO = YES; - MARKETING_VERSION = 7.37.0; + MARKETING_VERSION = 7.37.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1470,7 +1470,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1512; + CURRENT_PROJECT_VERSION = 1520; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1501,7 +1501,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; LLVM_LTO = YES; - MARKETING_VERSION = 7.37.0; + MARKETING_VERSION = 7.37.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1624,7 +1624,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1512; + CURRENT_PROJECT_VERSION = 1520; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1661,7 +1661,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.37.0; + MARKETING_VERSION = 7.37.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1692,7 +1692,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1512; + CURRENT_PROJECT_VERSION = 1520; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1727,7 +1727,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.37.0; + MARKETING_VERSION = 7.37.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index 2715417dfb2..6b15b8b25d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.37.0", + "version": "7.37.1", "private": true, "scripts": { "audit:ci": "./scripts/yarn-audit.sh", @@ -156,21 +156,21 @@ "@metamask/ethjs-contract": "^0.4.1", "@metamask/ethjs-query": "^0.7.1", "@metamask/ethjs-unit": "^0.3.0", - "@metamask/gas-fee-controller": "^21.0.0", + "@metamask/gas-fee-controller": "^22.0.2", "@metamask/json-rpc-middleware-stream": "^8.0.2", "@metamask/key-tree": "^9.0.0", "@metamask/keyring-api": "^8.1.0", "@metamask/keyring-controller": "^18.0.0", "@metamask/logging-controller": "^6.0.1", "@metamask/message-signing-snap": "^0.3.3", - "@metamask/network-controller": "^21.0.0", - "@metamask/notification-services-controller": "^0.11.0", + "@metamask/network-controller": "^22.1.0", + "@metamask/notification-services-controller": "^0.14.0", "@metamask/permission-controller": "^11.0.0", "@metamask/phishing-controller": "^12.0.3", "@metamask/post-message-stream": "^8.0.0", - "@metamask/ppom-validator": "0.35.1", - "@metamask/preferences-controller": "^14.0.0", - "@metamask/profile-sync-controller": "^0.9.7", + "@metamask/ppom-validator": "0.36.0", + "@metamask/preferences-controller": "^15.0.1", + "@metamask/profile-sync-controller": "^2.0.0", "@metamask/react-native-actionsheet": "2.4.2", "@metamask/react-native-button": "^3.0.0", "@metamask/react-native-payments": "^2.0.0", @@ -180,9 +180,9 @@ "@metamask/rpc-errors": "^7.0.1", "@metamask/scure-bip39": "^2.1.0", "@metamask/sdk-communication-layer": "0.29.0-wallet", - "@metamask/selected-network-controller": "^18.0.2", - "@metamask/signature-controller": "^22.0.0", - "@metamask/slip44": "3.1.0", + "@metamask/selected-network-controller": "^19.0.0", + "@metamask/signature-controller": "^23.1.0", + "@metamask/slip44": "^4.1.0", "@metamask/smart-transactions-controller": "^15.0.0", "@metamask/snaps-controllers": "^9.13.0", "@metamask/snaps-execution-environments": "^6.10.0", diff --git a/yarn.lock b/yarn.lock index 7b176e20ab3..7467daad58a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4213,6 +4213,22 @@ eth-ens-namehash "^2.0.8" fast-deep-equal "^3.1.3" +"@metamask/controller-utils@^11.4.4": + version "11.4.4" + resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.4.4.tgz#6e43e4cf53d34dad225bab8aaf4e7efcb1fe7623" + integrity sha512-0/gKC6jxlj8KRzi0RjGDQnml6l4b46Da/AIqnGJMOC59zl4qD5UN1GM+mq7L5duw/m8sSHa7VbL1hL0l7Cw1pg== + dependencies: + "@ethereumjs/util" "^8.1.0" + "@metamask/eth-query" "^4.0.0" + "@metamask/ethjs-unit" "^0.3.0" + "@metamask/utils" "^10.0.0" + "@spruceid/siwe-parser" "2.1.0" + "@types/bn.js" "^5.1.5" + bignumber.js "^9.1.2" + bn.js "^5.2.1" + eth-ens-namehash "^2.0.8" + fast-deep-equal "^3.1.3" + "@metamask/design-tokens@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@metamask/design-tokens/-/design-tokens-4.0.0.tgz#3aad7e4da21c279374668e179c0b055d93aa0552" @@ -4471,17 +4487,17 @@ is-hex-prefixed "1.0.0" strip-hex-prefix "1.0.0" -"@metamask/gas-fee-controller@^21.0.0": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@metamask/gas-fee-controller/-/gas-fee-controller-21.0.0.tgz#1cd21f3d33b097adb9c2d9bea17ac5ee72b491e6" - integrity sha512-34/AORJFpaHgr1jtzIk1/SzGSw7sYZL49kww7x7tW9wZcxV7r8rPZ5K25TveORzZtdfEsz1CVZjO4iEM8B4Lwg== +"@metamask/gas-fee-controller@^22.0.2": + version "22.0.2" + resolved "https://registry.yarnpkg.com/@metamask/gas-fee-controller/-/gas-fee-controller-22.0.2.tgz#6e092c3579d3b041ea252364a81190d8968cec5e" + integrity sha512-VaJHNWHL2mdswOIRrz5zes/recHRiXUG5sa5Co1eMFxpHQoX57ciFQ4J8rWsBP4P7H6a8MOB0TNFkn2fYCvpRg== dependencies: - "@metamask/base-controller" "^7.0.1" - "@metamask/controller-utils" "^11.3.0" + "@metamask/base-controller" "^7.0.2" + "@metamask/controller-utils" "^11.4.4" "@metamask/eth-query" "^4.0.0" "@metamask/ethjs-unit" "^0.3.0" - "@metamask/polling-controller" "^11.0.0" - "@metamask/utils" "^9.1.0" + "@metamask/polling-controller" "^12.0.2" + "@metamask/utils" "^10.0.0" "@types/bn.js" "^5.1.5" "@types/uuid" "^8.3.0" bn.js "^5.2.1" @@ -4554,6 +4570,19 @@ "@noble/hashes" "^1.3.2" "@scure/base" "^1.0.0" +"@metamask/keyring-api@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-api/-/keyring-api-10.1.0.tgz#22bb96886b9dbf9a6d6f48e5975cb16499209959" + integrity sha512-T1/4oQKJ4+V8gsXusVF515B+kpHSMiSbCpxZunJDjvnXEheT2NjbdvjtoIK5AT+Fe6JBWDzeZGpt6gv2vHX2SA== + dependencies: + "@metamask/snaps-sdk" "^6.7.0" + "@metamask/superstruct" "^3.1.0" + "@metamask/utils" "^9.2.1" + "@types/uuid" "^9.0.8" + bech32 "^2.0.0" + uuid "^9.0.1" + webextension-polyfill "^0.12.0" + "@metamask/keyring-api@^8.1.0", "@metamask/keyring-api@^8.1.3": version "8.1.3" resolved "https://registry.yarnpkg.com/@metamask/keyring-api/-/keyring-api-8.1.3.tgz#53e6a68236b88592db5bd43cf7e0d7e97dfad818" @@ -4566,10 +4595,10 @@ bech32 "^2.0.0" uuid "^9.0.1" -"@metamask/keyring-controller@^17.2.2": - version "17.3.1" - resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-17.3.1.tgz#1a498dd165df5b908761e62fc9e194b8a4f9a074" - integrity sha512-+R4tD0KtXjjAts5xOo+CKETPQVa+RJDC98L2qU2iGHyFKN05gFYt4M8HMcK4gq2GhGxm+0r6SYOUw2jK/wjD5g== +"@metamask/keyring-controller@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-18.0.0.tgz#48d5a441cd798cef840c5bf6334d53b6e37dc938" + integrity sha512-SIvjDUvt9U+fqge3ao0qZYfWrlBVf/NI/nFOwp9Q8IFpPsZYHKpbLahLfviunR/KItBul7vTzWBIN/FtQo/eLg== dependencies: "@ethereumjs/util" "^8.1.0" "@keystonehq/metamask-airgapped-keyring" "^0.14.1" @@ -4585,10 +4614,10 @@ ethereumjs-wallet "^1.0.1" immer "^9.0.6" -"@metamask/keyring-controller@^18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-18.0.0.tgz#48d5a441cd798cef840c5bf6334d53b6e37dc938" - integrity sha512-SIvjDUvt9U+fqge3ao0qZYfWrlBVf/NI/nFOwp9Q8IFpPsZYHKpbLahLfviunR/KItBul7vTzWBIN/FtQo/eLg== +"@metamask/keyring-controller@^19.0.0": + version "19.0.1" + resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-19.0.1.tgz#6fee40a46a780a720f4c864ea779673569be06a7" + integrity sha512-6dNQBaJanAKEg7V0ksnWyqxHY1r3YCe910OF6DpnN97NZIPx3tba2zy32jbQWGPG6XaybfTG1xdUBtdYdeTpQA== dependencies: "@ethereumjs/util" "^8.1.0" "@keystonehq/metamask-airgapped-keyring" "^0.14.1" @@ -4597,8 +4626,8 @@ "@metamask/eth-hd-keyring" "^7.0.4" "@metamask/eth-sig-util" "^8.0.0" "@metamask/eth-simple-keyring" "^6.0.5" - "@metamask/keyring-api" "^8.1.3" - "@metamask/message-manager" "^11.0.1" + "@metamask/keyring-api" "^10.1.0" + "@metamask/message-manager" "^11.0.2" "@metamask/utils" "^10.0.0" async-mutex "^0.5.0" ethereumjs-wallet "^1.0.1" @@ -4614,12 +4643,25 @@ uuid "^8.3.2" "@metamask/message-manager@^11.0.1": - version "11.0.1" - resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-11.0.1.tgz#7ffa6ea5a0daebb0ccb78dbd75774bf3aa0b9d69" - integrity sha512-dPkx6v14MyBPqdnKSlBPR97/BCx8KLuGudK9u0U3CmqI5dpO3mXvwXNspu5lnBrnjAoYqQVb+/e4vqkOx4/DlQ== + version "11.0.3" + resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-11.0.3.tgz#7fc1a41566d7611dc99124b66dcbc29a0dc58efc" + integrity sha512-y3AnzPQvzAE6GWytmdfftp5vr1LepA3L02iFS4PFj0Xk+t1d42IjPAdd7XC/wWFJ+tGasrSDW6T0PKXoItCqEA== + dependencies: + "@metamask/base-controller" "^7.0.2" + "@metamask/controller-utils" "^11.4.4" + "@metamask/eth-sig-util" "^8.0.0" + "@metamask/utils" "^10.0.0" + "@types/uuid" "^8.3.0" + jsonschema "^1.4.1" + uuid "^8.3.2" + +"@metamask/message-manager@^11.0.2": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-11.0.2.tgz#805ac8d2184c56d973a787bdc7c9571024348ce6" + integrity sha512-j8DgWn7ACfwZtLFqeAYx4U/TquBEtcPYa81LjhoZYK39W/MQVr4ZKAmGEKxKRruMLz50GX7ciOEZU9SBtPBAog== dependencies: "@metamask/base-controller" "^7.0.2" - "@metamask/controller-utils" "^11.4.2" + "@metamask/controller-utils" "^11.4.4" "@metamask/eth-sig-util" "^8.0.0" "@metamask/utils" "^10.0.0" "@types/uuid" "^8.3.0" @@ -4649,23 +4691,24 @@ resolved "https://registry.yarnpkg.com/@metamask/mobile-provider/-/mobile-provider-3.0.0.tgz#8a6a5a0874c8cbe4b468f63dfc57117d207f9595" integrity sha512-XwFJk0rd9lAZR5xS3VC7ypEhD7DvZR2gi2Ch6PHnODIqeS9Te3OdVKK5+jHI4his8v/zs6LWdFdlRtx5/jL96w== -"@metamask/network-controller@^21.0.0": - version "21.1.0" - resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-21.1.0.tgz#766598a7e854a86bcb5f94af6c7bbc7f67820d02" - integrity sha512-AyGumRyUtuQAkYJ/7f/KSPqwc+vndUP20BE0NbGo2hVfMS/IOdvUj2Dz+c8+LnqkO6JfyuQ7BVJiMy5JnMucJw== +"@metamask/network-controller@^22.0.2", "@metamask/network-controller@^22.1.0": + version "22.1.0" + resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-22.1.0.tgz#b4c81a31fc52147d12131bfb16c77c1c1cfbe43c" + integrity sha512-wvP2HUBQCWvlvBkuhqMNtd6+D/cJa343ABFLfCLNrQsk8QjdHPFw/QDIqC+QXTPxyrPWw9GL5akaLb//rmO1NA== dependencies: - "@metamask/base-controller" "^7.0.1" - "@metamask/controller-utils" "^11.3.0" + "@metamask/base-controller" "^7.0.2" + "@metamask/controller-utils" "^11.4.4" "@metamask/eth-block-tracker" "^11.0.2" "@metamask/eth-json-rpc-infura" "^10.0.0" "@metamask/eth-json-rpc-middleware" "^15.0.0" - "@metamask/eth-json-rpc-provider" "^4.1.5" + "@metamask/eth-json-rpc-provider" "^4.1.6" "@metamask/eth-query" "^4.0.0" - "@metamask/json-rpc-engine" "^10.0.0" - "@metamask/rpc-errors" "^7.0.0" + "@metamask/json-rpc-engine" "^10.0.1" + "@metamask/rpc-errors" "^7.0.1" "@metamask/swappable-obj-proxy" "^2.2.0" - "@metamask/utils" "^9.1.0" + "@metamask/utils" "^10.0.0" async-mutex "^0.5.0" + fast-deep-equal "^3.1.3" immer "^9.0.6" loglevel "^1.8.1" reselect "^5.1.1" @@ -4680,16 +4723,16 @@ "@ethersproject/providers" "^5.7.2" async-mutex "^0.3.1" -"@metamask/notification-services-controller@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@metamask/notification-services-controller/-/notification-services-controller-0.11.0.tgz#6f1f8553e73bbaa5e3631c772efc34640264ff81" - integrity sha512-2QplJt1gqdPkhnHN6+C1E8MrcdZh1JgoDf9YnL2WsuCmMFlpujYD7mW7lRInBnCrPbKliHgF96oOT0D5gFigoA== +"@metamask/notification-services-controller@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@metamask/notification-services-controller/-/notification-services-controller-0.14.0.tgz#c2699db0c9e3329c2654a4a39d14176009963eaa" + integrity sha512-/OJW4j8PY66Gil+I/sJxstqXlR8ug+enOn1mCu0eqTDUwlrY+Qb3jOzww5mTWwN2q8D9IhkOHZm7HkM9dSWGSw== dependencies: "@contentful/rich-text-html-renderer" "^16.5.2" - "@metamask/base-controller" "^7.0.1" - "@metamask/controller-utils" "^11.3.0" - "@metamask/utils" "^9.1.0" - bignumber.js "^4.1.0" + "@metamask/base-controller" "^7.0.2" + "@metamask/controller-utils" "^11.4.3" + "@metamask/utils" "^10.0.0" + bignumber.js "^9.1.2" firebase "^10.11.0" loglevel "^1.8.1" uuid "^8.3.2" @@ -4764,25 +4807,13 @@ fastest-levenshtein "^1.0.16" punycode "^2.1.1" -"@metamask/polling-controller@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@metamask/polling-controller/-/polling-controller-11.0.0.tgz#3de28c27a9be5d85879cf9bd35d03a62dbf8932c" - integrity sha512-fXG60Ft7lYgqC8lV8m4FL18hU4K2NnsPzoWTm1Lhen2cysLbgAzqxxGdeIFEMaDiCzcvEtLuEffY8FDKCM0ewQ== - dependencies: - "@metamask/base-controller" "^7.0.1" - "@metamask/controller-utils" "^11.3.0" - "@metamask/utils" "^9.1.0" - "@types/uuid" "^8.3.0" - fast-json-stable-stringify "^2.1.0" - uuid "^8.3.2" - -"@metamask/polling-controller@^12.0.0", "@metamask/polling-controller@^12.0.1": - version "12.0.1" - resolved "https://registry.yarnpkg.com/@metamask/polling-controller/-/polling-controller-12.0.1.tgz#89593c892d7fbbc93ccb6fe45dd2d820839f3d93" - integrity sha512-ZCF7UBuyxk9utdmOAh9/VBT780OxRPN5pD0wJaRwsFnFM3FFOFmvmojqnw8Kz4lSglyETtOBBMvzyQsnqoXQEQ== +"@metamask/polling-controller@^12.0.0", "@metamask/polling-controller@^12.0.1", "@metamask/polling-controller@^12.0.2": + version "12.0.2" + resolved "https://registry.yarnpkg.com/@metamask/polling-controller/-/polling-controller-12.0.2.tgz#de37ff5f4f997d57d4da34b0c6574d2eba3184e0" + integrity sha512-TYplREQMrhPcsJn+b7WS2oZqMwt4jKcyL/tDmygVv90RcvESBBhXxz7D0Brm5UAirnkE3TPRq8fnVWwVSQAVcA== dependencies: "@metamask/base-controller" "^7.0.2" - "@metamask/controller-utils" "^11.4.2" + "@metamask/controller-utils" "^11.4.4" "@metamask/utils" "^10.0.0" "@types/uuid" "^8.3.0" fast-json-stable-stringify "^2.1.0" @@ -4796,10 +4827,10 @@ "@metamask/utils" "^9.0.0" readable-stream "3.6.2" -"@metamask/ppom-validator@0.35.1": - version "0.35.1" - resolved "https://registry.yarnpkg.com/@metamask/ppom-validator/-/ppom-validator-0.35.1.tgz#9ccece28f9f8947caef575849ef41f2ca15baa13" - integrity sha512-iaJRIFUXNBeuQwy+6B10mkPJDAmhvSyu5PAehSNSj+J2Q7Vy3AJe9GWI5t62j3IeDnwuny+Av+x/T+iI5D/b8w== +"@metamask/ppom-validator@0.36.0": + version "0.36.0" + resolved "https://registry.yarnpkg.com/@metamask/ppom-validator/-/ppom-validator-0.36.0.tgz#cc8ace84ead3521c1b079650fa4169d1020bc070" + integrity sha512-9PN+QZpQCq0ctu0b7LeHFWeZQ5phKavVQ7t0tp2ZYtea6ql7zayjDhZQitoFUEHN3R6BuHxn5ORfDGFJgNDL9Q== dependencies: "@metamask/base-controller" "^7.0.1" "@metamask/controller-utils" "^11.3.0" @@ -4810,24 +4841,25 @@ eslint-plugin-n "^16.6.2" json-rpc-random-id "^1.0.1" -"@metamask/preferences-controller@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@metamask/preferences-controller/-/preferences-controller-14.0.0.tgz#1ec10cf7d2091962345dfc99fa050339a997e467" - integrity sha512-rh0kWWIDCa1V/zAfC3pA+0I3JSiQp23kpi838uMqCDMBIB0ifFbWX+qjnZlxZp77R1bvJiSqtHGlYLl60/Y7FA== +"@metamask/preferences-controller@^15.0.1": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@metamask/preferences-controller/-/preferences-controller-15.0.1.tgz#4306099e4659591636304d81ed0954afb160ab81" + integrity sha512-y2rGKMr9fY2LCwEjvX7QKxOqxy5Tz6vA+QK8YEXuczeHd3n2jrh9DNBeM+BZzzOO2cXJgmbn0Jeotl09kDD94g== dependencies: "@metamask/base-controller" "^7.0.2" - "@metamask/controller-utils" "^11.4.3" + "@metamask/controller-utils" "^11.4.4" -"@metamask/profile-sync-controller@^0.9.7": - version "0.9.7" - resolved "https://registry.yarnpkg.com/@metamask/profile-sync-controller/-/profile-sync-controller-0.9.7.tgz#d5e78cb8004f0dcb8637410bb8b54911e8f2c0a7" - integrity sha512-1R4P1/9VdGEHGPb68gc2oNM9/95xc84hNqIlZDL/OISSRgvW3wJguXwEVLfW6GE91gHmzHtMe4MxDM3pXQWc9w== +"@metamask/profile-sync-controller@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/profile-sync-controller/-/profile-sync-controller-2.0.0.tgz#140297d4608373501b8dbe6fd86cbe3e63cdcc41" + integrity sha512-HdMlIz3Iun9wESUVcaH3y1pKNcnH+DJD0J0OKVaUxk4oKpS+u0QauEaMbvNps1ZxAY23x9gXTzeE3MMLbXYLgw== dependencies: - "@metamask/base-controller" "^7.0.1" - "@metamask/keyring-api" "^8.1.3" - "@metamask/keyring-controller" "^17.2.2" - "@metamask/snaps-sdk" "^6.5.0" - "@metamask/snaps-utils" "^8.1.1" + "@metamask/base-controller" "^7.0.2" + "@metamask/keyring-api" "^10.1.0" + "@metamask/keyring-controller" "^19.0.0" + "@metamask/network-controller" "^22.0.2" + "@metamask/snaps-sdk" "^6.7.0" + "@metamask/snaps-utils" "^8.3.0" "@noble/ciphers" "^0.5.2" "@noble/hashes" "^1.4.0" immer "^9.0.6" @@ -4949,39 +4981,39 @@ utf-8-validate "^5.0.2" uuid "^8.3.2" -"@metamask/selected-network-controller@^18.0.2": - version "18.0.2" - resolved "https://registry.yarnpkg.com/@metamask/selected-network-controller/-/selected-network-controller-18.0.2.tgz#a6bd7916c47307999cada50d8e5d3d839a29246c" - integrity sha512-0a0uAW3EH56zWuDtCw5al6PcwWjDEsy4ydoUV77+Ko8h46WsS8gbV4VpoKAN+MJpihty0LVM8J6whT3Z5qP7iQ== +"@metamask/selected-network-controller@^19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@metamask/selected-network-controller/-/selected-network-controller-19.0.0.tgz#7c950e051d6848ac5cdcf980f90196a0772425ef" + integrity sha512-ijCFHwZN+73VskJU+X0AoSlCTycgLveEjwUoycUeajmUuZyLV5KLLVYPKTk0Pd/aQAaO2tL9yZi0/geazNGV7A== dependencies: "@metamask/base-controller" "^7.0.1" "@metamask/json-rpc-engine" "^10.0.0" "@metamask/swappable-obj-proxy" "^2.2.0" - "@metamask/utils" "^9.1.0" + "@metamask/utils" "^10.0.0" -"@metamask/signature-controller@^22.0.0": - version "22.0.0" - resolved "https://registry.yarnpkg.com/@metamask/signature-controller/-/signature-controller-22.0.0.tgz#a52c79881aae4b47fecfc2c2b1f2d0e1e649e240" - integrity sha512-k4Kvq4tdFDWLiFiNijUeOGVQj10PfNp5R6DCPXwMgQnJqmUBXM14i/kMN4lU7rRsieYhQdXRVOQZ3/5r3wCstg== +"@metamask/signature-controller@^23.1.0": + version "23.1.0" + resolved "https://registry.yarnpkg.com/@metamask/signature-controller/-/signature-controller-23.1.0.tgz#45b3b545e5a4e890ff41a737b6526cb08fd9f1b3" + integrity sha512-HPUDjVjsZ/HU5QZlmllh1yN2Z1+VhqjTPmzxqXBqD28iKYwUU0YEMN+Jahyh9Ukl7BH/UgzGaaHytGXJ1Xf2Xw== dependencies: "@metamask/base-controller" "^7.0.2" - "@metamask/controller-utils" "^11.4.3" + "@metamask/controller-utils" "^11.4.4" "@metamask/eth-sig-util" "^8.0.0" "@metamask/utils" "^10.0.0" - jsonschema "^1.2.4" + jsonschema "^1.4.1" lodash "^4.17.21" uuid "^8.3.2" -"@metamask/slip44@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-3.1.0.tgz#f8067796f89fbbd6eaa594660bd6a1fbd6837a51" - integrity sha512-bFlJ8jhTYJ4iQ0zgh2WMO2615UJ4Ne5J831EjsqKYaZs3qd6UTw/cy76hAmSxhnBluNAH5S6zZzxESLrTitCmQ== - "@metamask/slip44@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-4.0.0.tgz#690a52d2ba74ea677d9bd0e827a90c8809fbecd3" integrity sha512-MQMocMvFmp1MWownjKMuxevivwYeNQPSpNyIg9K7nmxKuoatp5NUc9L8EJ3Bh//rOfl6fBfXn9byfS0t+NE02Q== +"@metamask/slip44@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-4.1.0.tgz#6f2702de7ba64dad3ab6586ea3ac4e5647804b0a" + integrity sha512-RQ2MJO0X3QLnJo0rFlb83h2tNAkqqx/VNOPLc3/S2CvY3/cXy3UAEw/xRM/475BeAAkWI93yiIn/FoGUy3E0Ig== + "@metamask/smart-transactions-controller@^15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-15.0.0.tgz#d9a3c2b3e3b1c5d9ddf68c03c0a537d348119fab" @@ -5086,30 +5118,30 @@ superstruct "^1.0.3" "@metamask/snaps-sdk@^6.11.0", "@metamask/snaps-sdk@^6.12.0", "@metamask/snaps-sdk@^6.5.0", "@metamask/snaps-sdk@^6.5.1", "@metamask/snaps-sdk@^6.7.0": - version "6.12.0" - resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-6.12.0.tgz#e609dd98b14bb33b55c6327fa94d9d1e768fe40d" - integrity sha512-6YHLKJFDaRLrnBMaFsv9xEEoZlibmBTyxV4S6z7U6zxfxknxOW2SI5tAiuz3e5TjD/p3F5siJYNsrRgnOEgxAA== + version "6.13.0" + resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-6.13.0.tgz#cbfef71253264efd4e0ef4606f89e93f83885816" + integrity sha512-WXNt0XZSnmgbwgETL0RiRvl0CMa78ZA1zLS0olK8QR/+9zcPCSrh68v1lVAa+LcctvzpRJ8NiRAar2fRBthqyw== dependencies: - "@metamask/key-tree" "^9.1.2" + "@metamask/key-tree" "^10.0.1" "@metamask/providers" "^18.1.1" "@metamask/rpc-errors" "^7.0.1" "@metamask/superstruct" "^3.1.0" "@metamask/utils" "^10.0.0" "@metamask/snaps-utils@^8.1.1", "@metamask/snaps-utils@^8.3.0", "@metamask/snaps-utils@^8.6.0": - version "8.6.0" - resolved "https://registry.yarnpkg.com/@metamask/snaps-utils/-/snaps-utils-8.6.0.tgz#96e4b3fbca8109d5335502ad1d71e8b6ec06765a" - integrity sha512-Q+CmJ5pbSj3USeDTsPnJBJ27qSWAYGlYAC6M2NTe/0OmGUFPx4V54kT2HCc/97AOG6fChSeLqfF3KpTtFuaiAQ== + version "8.6.1" + resolved "https://registry.yarnpkg.com/@metamask/snaps-utils/-/snaps-utils-8.6.1.tgz#9f3b52f9b00a93ce9e3f22e79066fea0d6df458a" + integrity sha512-R6Gj6Im5gV09kjkSH9vDKrvsqNGaFGbSAl95fhlQBW3QevLaMbvAF4bCfhA3YCfsEYZUWsgplYpCjX78q2jvBA== dependencies: "@babel/core" "^7.23.2" "@babel/types" "^7.23.0" "@metamask/base-controller" "^7.0.2" - "@metamask/key-tree" "^9.1.2" + "@metamask/key-tree" "^10.0.1" "@metamask/permission-controller" "^11.0.3" "@metamask/rpc-errors" "^7.0.1" "@metamask/slip44" "^4.0.0" "@metamask/snaps-registry" "^3.2.2" - "@metamask/snaps-sdk" "^6.11.0" + "@metamask/snaps-sdk" "^6.13.0" "@metamask/superstruct" "^3.1.0" "@metamask/utils" "^10.0.0" "@noble/hashes" "^1.3.1" @@ -12320,11 +12352,6 @@ big-integer@1.6.x: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== -bignumber.js@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" - integrity sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA== - bignumber.js@^7.2.1: version "7.2.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" @@ -19761,10 +19788,10 @@ jsonpath-plus@^7.2.0: resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz#7ad94e147b3ed42f7939c315d2b9ce490c5a3899" integrity sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA== -jsonschema@^1.2.4: - version "1.4.0" - resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2" - integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw== +jsonschema@^1.2.4, jsonschema@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" + integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== jsprim@^1.2.2: version "1.4.1" @@ -27584,6 +27611,11 @@ webdriverio@~7.16.13: resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== +webextension-polyfill@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69" + integrity sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 3dd4902d67f9984c344bc01136ad4ff49f6ebd34 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:45:58 -0700 Subject: [PATCH 12/17] feat: 7.38.0 (#12671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the release candidate for version 7.38.0. The changelog will be found in another PR chore/7.38.0-Changelog. # Team sign-off checklist - [ ] Accounts - [ ] Assets - [ ] Confirmations - [ ] Dev-Ops - [ ] Hardware Wallets - [ ] Identity - [x] Mobile Platform - [x] Notifications - [ ] QA - [ ] Snaps Platform - [x] Stake - [ ] Swaps - [ ] Tiger - [x] Transactions - [ ] Wallet Framework - [ ] Wallet UX # Reference - Testing plan sheet - https://docs.google.com/spreadsheets/d/1tsoodlAlyvEUpkkcNcbZ4PM9HuC9cEM80RZeoVv5OCQ/edit?gid=404070372#gid=404070372 --------- Co-authored-by: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Co-authored-by: Jongsun Suh Co-authored-by: Vince Howard Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> Co-authored-by: Nico MASSART Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com> Co-authored-by: Owen Craston Co-authored-by: Curtis David Co-authored-by: Xiaoming Wang <7315988+dawnseeker8@users.noreply.github.com> Co-authored-by: Prithpal Sooriya Co-authored-by: Salim TOUBAL Co-authored-by: sahar-fehri Co-authored-by: SamuelSalas Co-authored-by: jake-perkins <128608287+jake-perkins@users.noreply.github.com> Co-authored-by: Cal Leung Co-authored-by: Charly Chevalier Co-authored-by: Aslau Mario-Daniel Co-authored-by: Mathieu Artu Co-authored-by: OGPoyraz Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com> Co-authored-by: metamaskbot Co-authored-by: runway-github[bot] <73448015+runway-github[bot]@users.noreply.github.com> Co-authored-by: Nicholas Smith Co-authored-by: Kylan Hurt Co-authored-by: Frank von Hoven <141057783+frankvonhoven@users.noreply.github.com> Co-authored-by: Frank von Hoven Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Co-authored-by: Matthew Walsh Co-authored-by: Nick Gambino <35090461+gambinish@users.noreply.github.com> Co-authored-by: Frederik Bolding Co-authored-by: Amitabh Aggarwal Co-authored-by: EtherWizard33 <165834542+EtherWizard33@users.noreply.github.com> Co-authored-by: Mark Stacey Co-authored-by: CW Co-authored-by: Andre Pimenta Co-authored-by: sethkfman Co-authored-by: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Co-authored-by: Salah-Eddine Saakoun Co-authored-by: jvbriones <1674192+jvbriones@users.noreply.github.com> Co-authored-by: Nicholas Gambino Co-authored-by: Daniel <80175477+dan437@users.noreply.github.com> Co-authored-by: Bryan Fullam Co-authored-by: Sébastien Van Eyck Co-authored-by: Jyoti Puri Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Co-authored-by: Pedro Pablo Aste Kompen Co-authored-by: George Weiler Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: tommasini Co-authored-by: kylanhurt Co-authored-by: Nicolas MASSART Co-authored-by: Kylan Hurt <6249205+smilingkylan@users.noreply.github.com> --- .depcheckrc.yml | 7 + .e2e.env.example | 2 + .eslintrc.js | 2 +- .github/CODEOWNERS | 8 +- .github/ISSUE_TEMPLATE/bug-report.yml | 3 +- .github/guidelines/LABELING_GUIDELINES.md | 3 +- .../scripts/bitrise/run-bitrise-e2e-check.ts | 112 +- .../scripts/check-template-and-add-labels.ts | 20 +- .github/scripts/tsconfig.json | 60 +- .github/workflows/auto-draft-prs.yml | 39 + .github/workflows/ci.yml | 10 +- .github/workflows/docker.yml | 2 +- .github/workflows/run-bitrise-e2e-check.yml | 20 +- .gitignore | 7 +- .iyarc | 2 + .js.env.example | 15 +- CHANGELOG.md | 168 +- README.md | 73 +- android/app/build.gradle | 9 +- android/app/src/main/AndroidManifest.xml | 7 + .../main/java/io/metamask/MainActivity.java | 5 +- .../java/io/metamask/MainApplication.java | 16 +- .../src/main/res/xml/react_native_config.xml | 2 +- android/settings.gradle | 3 + app.config.js | 23 + app.json | 4 - app/actions/identity/constants/errors.ts | 8 + app/actions/identity/index.test.ts | 51 + app/actions/identity/index.ts | 53 + app/actions/multichain/state.ts | 4 + app/actions/navigation/index.ts | 30 +- app/actions/navigation/types.ts | 31 + app/actions/notification/constants/index.ts | 4 - .../notification/helpers/index.test.tsx | 51 +- app/actions/notification/helpers/index.ts | 66 +- app/actions/user/index.js | 134 - app/actions/user/index.ts | 161 + app/actions/user/types.ts | 111 + .../CellSelectWithMenu.test.tsx | 4 +- .../CellSelectWithMenu/CellSelectWithMenu.tsx | 10 +- .../AggregatedPercentage.tsx | 2 +- .../AggregatedPercentageCrossChains.test.tsx | 431 + .../AggregatedPercentageCrossChains.tsx | 171 + ...regatedPercentageCrossChains.test.tsx.snap | 45 + .../Price/AggregatedPercentage/utils.ts | 32 + .../components/Cells/Cell/Cell.test.tsx | 20 +- .../components/Cells/Cell/Cell.tsx | 10 +- .../foundation/CellBase/CellBase.test.tsx | 4 +- .../Cell/foundation/CellBase/CellBase.tsx | 6 +- .../variants/CellDisplay/CellDisplay.test.tsx | 4 +- .../Cell/variants/CellDisplay/CellDisplay.tsx | 4 +- .../CellMultiSelect/CellMultiSelect.test.tsx | 4 +- .../CellMultiSelect/CellMultiSelect.tsx | 4 +- .../variants/CellSelect/CellSelect.test.tsx | 4 +- .../Cell/variants/CellSelect/CellSelect.tsx | 4 +- .../Modals/ModalMandatory/ModalMandatory.tsx | 2 +- .../Pickers/PickerNetwork/PickerNetwork.tsx | 8 +- .../PickerNetwork/PickerNetwork.types.ts | 4 + .../__snapshots__/PickerNetwork.test.tsx.snap | 1 + .../components/Toast/Toast.tsx | 2 +- app/components/Nav/App/index.js | 7 + app/components/Nav/Main/RootRPCMethodsUI.js | 32 +- app/components/Nav/Main/index.js | 25 +- app/components/UI/AccountApproval/index.js | 4 +- .../UI/AccountApproval/index.test.tsx | 7 + .../AccountFromToInfoCard.test.tsx | 8 +- app/components/UI/AccountOverview/index.js | 4 +- .../UI/AccountRightButton/index.tsx | 2 +- .../AccountSelector.test.tsx | 25 +- .../AccountSelectorList.tsx | 7 +- .../UI/AddCustomCollectible/index.tsx | 4 +- .../AddToAddressBookWrapper.test.tsx | 2 +- .../AddToAddressBookWrapper.tsx | 2 +- app/components/UI/AddressInputs/index.js | 2 +- .../UI/AddressInputs/index.test.jsx | 2 +- .../UI/AssetOverview/AssetOverview.test.tsx | 88 +- .../UI/AssetOverview/AssetOverview.tsx | 241 +- .../UI/AssetOverview/Balance/Balance.tsx | 113 +- .../UI/AssetOverview/Balance/index.test.tsx | 110 +- .../UI/AssetOverview/Price/Price.tsx | 7 +- .../TokenDetails/TokenDetails.test.tsx | 107 +- .../TokenDetails/TokenDetails.tsx | 44 +- .../TokenDetailsList.test.tsx | 1 + .../__snapshots__/AssetOverview.test.tsx.snap | 1152 ++ app/components/UI/AssetSearch/index.tsx | 3 +- app/components/UI/BackupAlert/BackupAlert.tsx | 2 +- .../BasicFunctionalityModal.tsx | 6 +- .../UI/CollectibleContractElement/index.js | 4 +- .../UI/CollectibleContractOverview/index.js | 4 +- .../UI/CollectibleContracts/index.js | 4 +- .../UI/CollectibleOverview/index.js | 4 +- .../EnableAutomaticSecurityChecksModal.tsx | 2 +- .../LoginOptionsSwitch/LoginOptionsSwitch.tsx | 4 +- .../__snapshots__/ManageNetworks.test.js.snap | 1 + app/components/UI/Navbar/index.js | 87 +- app/components/UI/NavbarTitle/index.js | 22 +- .../__snapshots__/index.test.tsx.snap | 3 + .../UI/NetworkAssetLogo/index.test.tsx | 73 + app/components/UI/NetworkAssetLogo/index.tsx | 44 + app/components/UI/NetworkCell/NetworkCell.tsx | 10 +- .../__snapshots__/index.test.tsx.snap | 30 +- app/components/UI/NetworkModal/index.test.tsx | 74 +- app/components/UI/NetworkModal/index.tsx | 56 +- .../NetworkVerificationInfo.tsx | 2 +- .../UI/Notification/BaseNotification/index.js | 4 +- app/components/UI/Notification/List/index.tsx | 2 +- .../TransactionNotification/index.js | 4 +- .../UI/OnboardingWizard/Coachmark/index.js | 2 +- .../UI/OnboardingWizard/Step1/index.tsx | 2 +- .../UI/OnboardingWizard/Step2/index.tsx | 2 +- .../UI/OnboardingWizard/Step3/index.tsx | 2 +- .../UI/OnboardingWizard/Step4/index.tsx | 2 +- .../UI/OnboardingWizard/Step5/index.tsx | 2 +- .../UI/OnboardingWizard/Step6/index.tsx | 2 +- .../UI/OnboardingWizard/Step7/index.tsx | 2 +- .../UI/PaymentRequest/AssetList/index.tsx | 3 +- app/components/UI/PaymentRequest/index.js | 4 +- .../UI/PaymentRequest/index.test.tsx | 7 +- .../PermissionsSummary/PermissionsSummary.tsx | 19 +- .../UI/ProfileSyncing/ProfileSyncing.tsx | 2 +- .../ProfileSyncingModal.tsx | 8 +- .../UI/ProtectYourWalletModal/index.js | 2 +- .../__snapshots__/BuildQuote.test.tsx.snap | 28 +- .../__snapshots__/GetStarted.test.tsx.snap | 8 +- .../NetworkSwitcher.test.tsx.snap | 18 +- .../__snapshots__/OrderDetails.test.tsx.snap | 22 +- .../PaymentMethods.test.tsx.snap | 20 +- .../Quotes/__snapshots__/Quotes.test.tsx.snap | 14 +- .../__snapshots__/Regions.test.tsx.snap | 18 +- .../SendTransaction/SendTransaction.test.tsx | 2 + .../Views/SendTransaction/SendTransaction.tsx | 6 + .../SendTransaction.test.tsx.snap | 6 +- app/components/UI/Ramp/components/Account.tsx | 4 +- .../UI/Ramp/components/PaymentMethodIcon.tsx | 12 +- app/components/UI/Ramp/hooks/useBalance.ts | 4 +- app/components/UI/ReceiveRequest/index.js | 4 +- .../UI/SkipAccountSecurityModal/index.js | 2 +- .../StakeInputView/StakeInputView.test.tsx | 18 + .../UnstakeInputView.test.tsx | 16 + app/components/UI/Stake/__mocks__/mockData.ts | 26 +- .../StakeButton/StakeButton.test.tsx | 17 +- .../UI/Stake/components/StakeButton/index.tsx | 13 +- .../StakingBalance/StakingBalance.test.tsx | 47 + .../StakingBalance/StakingBalance.tsx | 19 +- .../StakingBalance.test.tsx.snap | 478 +- .../StakingEarnings/StakingEarnings.test.tsx | 21 +- .../components/StakingEarnings/index.tsx | 20 +- .../UI/Stake/hooks/useBalance.test.tsx | 32 +- app/components/UI/Stake/hooks/useBalance.ts | 35 +- .../UI/Stake/hooks/useInputHandler.ts | 7 +- .../Stake/hooks/usePoolStakedClaim/index.ts | 25 +- .../usePoolStakedClaim.test.tsx | 3 + .../Stake/hooks/usePoolStakedDeposit/index.ts | 19 +- .../usePoolStakedDeposit.test.tsx | 3 + .../Stake/hooks/usePoolStakedUnstake/index.ts | 44 +- .../usePoolStakedUnstake.test.tsx | 28 +- .../UI/Stake/hooks/usePooledStakes.ts | 4 +- .../UI/Stake/hooks/useStakingChain.test.tsx | 41 +- .../UI/Stake/hooks/useStakingChain.ts | 9 + .../UI/Stake/hooks/useStakingEligibility.ts | 4 +- .../UI/Stake/hooks/useStakingGasFee.ts | 4 +- .../UI/Stake/sdk/stakeSdkProvider.tsx | 11 +- app/components/UI/Swaps/QuotesView.js | 357 +- app/components/UI/Swaps/QuotesView.test.ts | 303 + .../__snapshots__/QuotesView.test.ts.snap | 2131 +++ .../UI/Swaps/components/TokenSelectModal.js | 4 +- app/components/UI/Swaps/index.js | 6 +- app/components/UI/Swaps/utils/gas.test.ts | 76 + app/components/UI/Swaps/utils/gas.ts | 28 + .../TokenList/PortfolioBalance/index.test.tsx | 40 +- .../TokenList/PortfolioBalance/index.tsx | 73 +- .../__snapshots__/index.test.tsx.snap | 141 + .../TokenList/TokenListFooter/index.test.tsx | 165 + .../TokenList/TokenListFooter/index.tsx | 56 +- .../Tokens/TokenList/TokenListItem/index.tsx | 237 +- .../TokenFilterBottomSheet.test.tsx | 63 +- .../TokenFilterBottomSheet.tsx | 36 +- .../Tokens/__snapshots__/index.test.tsx.snap | 1792 +- app/components/UI/Tokens/constants.ts | 1 + app/components/UI/Tokens/index.test.tsx | 552 +- app/components/UI/Tokens/index.tsx | 209 +- app/components/UI/Tokens/styles.ts | 10 + app/components/UI/Tokens/types.ts | 5 +- ...eriveBalanceFromAssetMarketDetails.test.ts | 37 +- .../deriveBalanceFromAssetMarketDetails.ts | 31 +- .../util/enableAllNetworksFilter.test.ts | 164 + .../UI/Tokens/util/enableAllNetworksFilter.ts | 18 + .../UI/Tokens/util/filterAssets.test.ts | 183 + app/components/UI/Tokens/util/filterAssets.ts | 91 + .../TransactionDetails/index.js | 7 +- app/components/UI/TransactionHeader/index.js | 2 +- .../UI/TransactionHeader/index.test.tsx | 2 +- app/components/UI/Transactions/index.js | 31 +- .../TurnOffRememberMeModal.tsx | 2 - .../UI/WhatsNewModal/WhatsNewModal.tsx | 2 +- .../AccountActions/AccountActions.test.tsx | 20 +- .../Views/AccountActions/AccountActions.tsx | 14 +- .../Views/AccountConnect/AccountConnect.tsx | 4 +- .../AccountConnectMultiSelector.tsx | 4 +- .../NetworkPermissionsConnected.tsx | 6 +- .../AccountPermissions.test.tsx.snap | 1 + .../AccountSelector/AccountSelector.test.tsx | 6 +- .../Views/AccountSelector/AccountSelector.tsx | 6 +- app/components/Views/ActivityView/index.js | 4 +- .../AddAccountActions/AddAccountActions.tsx | 84 +- .../Views/AddAsset/AddAsset.test.tsx | 2 +- app/components/Views/AddAsset/AddAsset.tsx | 2 +- app/components/Views/AddressQRCode/index.js | 4 +- .../AesCryptoTestForm.test.tsx | 5 + .../AesCryptoTestForm/AesCryptoTestForm.tsx | 22 +- .../AesCryptoTestForm.test.tsx.snap | 38 + .../Asset/__snapshots__/index.test.js.snap | 39 + app/components/Views/Asset/index.js | 83 +- app/components/Views/Asset/index.test.js | 63 +- .../AssetDetailsActions.test.tsx | 2 +- .../AssetDetailsActions.tsx | 2 +- .../Views/AssetDetails/AssetsDetails.test.tsx | 261 + .../__snapshots__/AssetsDetails.test.tsx.snap | 318 + .../__snapshots__/index.test.tsx.snap | 40 - .../Views/AssetDetails/index.test.tsx | 44 - app/components/Views/AssetDetails/index.tsx | 110 +- .../Views/AssetOptions/AssetOptions.test.tsx | 356 + .../Views/AssetOptions/AssetOptions.tsx | 60 +- .../__snapshots__/AssetOptions.test.tsx.snap | 273 + app/components/Views/BrowserTab/index.js | 4 +- .../Views/BrowserUrlModal/BrowserUrlModal.tsx | 6 +- .../BrowserUrlModal.test.tsx.snap | 1 - app/components/Views/ChoosePassword/index.js | 2 +- .../Views/ConnectQRHardware/index.test.tsx | 16 +- .../Views/DataCollectionModal/index.test.tsx | 4 +- .../Views/DataCollectionModal/index.tsx | 6 +- .../__snapshots__/index.test.tsx.snap | 888 + .../DetectedTokens/components/Token.test.tsx | 125 + .../Views/DetectedTokens/components/Token.tsx | 85 +- .../__snapshots__/Token.test.tsx.snap | 814 + .../Views/DetectedTokens/index.test.tsx | 160 + app/components/Views/DetectedTokens/index.tsx | 144 +- .../EditAccountName/EditAccountName.test.tsx | 4 +- .../Views/EditAccountName/EditAccountName.tsx | 2 +- .../EditAccountName.test.tsx.snap | 2 +- .../ExperienceEnhancerModal/index.test.tsx | 8 +- .../Views/ExperienceEnhancerModal/index.tsx | 24 +- .../ImportFromSecretRecoveryPhrase/index.js | 2 +- .../Views/ImportPrivateKeySuccess/index.js | 7 +- .../Views/LedgerSelectAccount/index.tsx | 20 +- app/components/Views/Login/index.js | 2 +- .../Views/MultiRpcModal/MultiRpcModal.tsx | 2 +- .../NFTAutoDetectionModal.tsx | 2 +- .../NetworkConnectMultiSelector.tsx | 12 +- .../NetworkSelector/NetworkSelector.test.tsx | 26 + .../Views/NetworkSelector/NetworkSelector.tsx | 38 +- .../Views/NftOptions/NftOptions.tsx | 4 +- .../Views/Notifications/OptIn/index.tsx | 6 +- app/components/Views/Notifications/index.tsx | 2 +- .../Views/OnboardingCarousel/index.test.tsx | 4 +- .../Views/OnboardingCarousel/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 606 - .../__snapshots__/index.test.tsx.snap | 1 + .../OnboardingGeneralSettings/index.tsx | 4 +- .../QRTabSwitcher/QRTabSwitcher.test.tsx | 17 + app/components/Views/ResetPassword/index.js | 4 +- .../RevealPrivateCredential.tsx | 4 +- .../Views/Settings/AdvancedSettings/index.js | 2 +- .../Views/Settings/GeneralSettings/index.js | 4 +- .../__snapshots__/index.test.tsx.snap | 34 - .../IncomingTransactionsSettings/index.tsx | 24 +- .../index.test.tsx | 4 +- .../NetworkDetailsCheckSettings/index.tsx | 2 +- .../NetworksSettings/NetworkSettings/index.js | 62 +- .../Views/Settings/NetworksSettings/index.js | 5 +- .../Settings/NotificationsSettings/index.tsx | 2 +- .../SecuritySettings/SecuritySettings.tsx | 8 +- .../SecuritySettings.test.tsx.snap | 606 - .../Views/TransactionsView/index.js | 2 +- .../Wallet/__snapshots__/index.test.tsx.snap | 1 + app/components/Views/Wallet/index.test.tsx | 5 +- app/components/Views/Wallet/index.tsx | 62 +- .../WalletActions/WalletActions.test.tsx | 36 +- .../Views/WalletActions/WalletActions.tsx | 14 +- .../components/TransactionEditor/index.js | 4 +- .../Views/confirmations/Approval/index.js | 5 +- .../ApproveView/Approve/index.js | 24 +- .../ApproveView/Approve/index.test.tsx | 51 + .../Send/__snapshots__/index.test.tsx.snap | 2 +- .../Views/confirmations/Send/index.js | 23 +- .../SendFlow/AddressList/AddressList.tsx | 2 +- .../SendFlow/AddressTo/AddressTo.tsx | 2 +- .../confirmations/SendFlow/Amount/index.js | 23 +- .../SendFlow/Amount/index.test.tsx | 234 +- .../Confirm/__snapshots__/index.test.tsx.snap | 2 +- .../confirmations/SendFlow/Confirm/index.js | 45 +- .../confirmations/SendFlow/SendTo/index.js | 12 +- .../AddNickname/index.tsx | 8 +- .../VerifyContractDetails.test.tsx | 23 +- .../ApproveTransactionReview/index.js | 2 +- .../ApproveTransactionReview/index.test.tsx | 22 +- .../BlockaidBanner/BlockaidBanner.tsx | 3 + .../BlockaidBanner.test.tsx.snap | 10 +- .../components/Confirm/Info/Info.tsx | 18 +- .../Info/PersonalSign/PersonalSign.tsx | 14 +- .../InfoRowOrigin/InfoRowOrigin.test.tsx | 15 + .../Shared/InfoRowOrigin/InfoRowOrigin.tsx | 28 + .../Info/Shared/InfoRowOrigin/index.ts | 1 + .../Info/TypedSignV1/TypedSignV1.test.tsx | 2 +- .../Confirm/Info/TypedSignV1/TypedSignV1.tsx | 16 +- .../Confirm/Info/TypedSignV3V4/Message.tsx | 30 + .../Info/TypedSignV3V4/TypedSignV3V4.test.tsx | 15 + .../Info/TypedSignV3V4/TypedSignV3V4.tsx | 23 + .../Confirm/Info/TypedSignV3V4/index.ts | 1 + .../components/EditGasFee1559Update/index.tsx | 2 +- .../EditGasFeeLegacyUpdate/index.tsx | 2 +- .../components/SignatureRequest/index.js | 4 +- .../TransactionBlockaidBanner.test.tsx.snap | 6 +- .../TransactionReviewEIP1559Update/index.tsx | 2 +- .../TransactionReviewInformation/index.js | 32 +- .../components/TransactionReview/index.js | 6 +- .../components/UpdateEIP1559Tx/index.tsx | 4 +- .../hooks/useConfirmationRedesignEnabled.ts | 11 +- .../AssetPollingProvider.test.tsx | 5 +- .../AssetPolling/AssetPollingProvider.tsx | 2 + .../useAccountTrackerPolling.test.ts | 202 + .../AssetPolling/useAccountTrackerPolling.ts | 61 + .../useCurrencyRatePolling.test.ts | 66 +- .../AssetPolling/useCurrencyRatePolling.ts | 33 +- .../useTokenBalancesPolling.test.ts | 160 +- .../AssetPolling/useTokenBalancesPolling.ts | 38 +- .../useTokenDetectionPolling.test.ts | 294 +- .../AssetPolling/useTokenDetectionPolling.ts | 50 +- .../AssetPolling/useTokenListPolling.test.ts | 129 +- .../hooks/AssetPolling/useTokenListPolling.ts | 36 +- .../AssetPolling/useTokenRatesPolling.test.ts | 64 +- .../AssetPolling/useTokenRatesPolling.ts | 22 +- .../DeleteWallet/useDeleteWallet.test.tsx | 6 + .../hooks/useAccounts/useAccounts.test.ts | 43 +- .../hooks/useAccounts/useAccounts.ts | 180 +- .../hooks/useAccounts/useAccounts.types.ts | 3 +- app/components/hooks/useAccounts/utils.ts | 62 + .../useAddressBalance/useAddressBalance.ts | 4 +- .../useGetFormattedTokensPerChain.test.ts | 198 + .../hooks/useGetFormattedTokensPerChain.tsx | 172 + .../useGetTotalFiatBalanceCrossChains.test.ts | 151 + .../useGetTotalFiatBalanceCrossChains.tsx | 145 + app/core/AppStateEventListener.test.ts | 50 +- app/core/AppStateEventListener.ts | 35 +- .../Authentication/Authentication.test.ts | 28 +- app/core/Authentication/Authentication.ts | 49 +- app/core/BackgroundBridge/BackgroundBridge.js | 14 +- .../BackgroundBridge/WalletConnectPort.ts | 4 +- .../Handlers/switchNetwork.test.ts | 4 +- .../DeeplinkManager/Handlers/switchNetwork.ts | 2 +- .../approveTransaction.test.ts | 5 +- .../TransactionManager/approveTransaction.ts | 7 + app/core/Encryptor/index.ts | 3 +- app/core/Encryptor/pbkdf2.test.ts | 76 + app/core/Encryptor/pbkdf2.ts | 33 + app/core/Engine/Engine.test.ts | 35 +- app/core/Engine/Engine.ts | 227 +- app/core/Engine/constants.ts | 45 + .../constants.ts | 0 .../utils.test.ts | 0 .../{accounts => AccountsController}/utils.ts | 0 .../RemoteFeatureFlagController/index.ts | 1 - .../RemoteFeatureFlagController/types.ts | 5 +- .../RemoteFeatureFlagController/utils.test.ts | 1 - .../RemoteFeatureFlagController/utils.ts | 2 - .../Engine/controllers/accounts/README.md | 3 - app/core/Engine/types.ts | 125 +- app/core/EngineService/EngineService.test.ts | 112 +- app/core/EngineService/EngineService.ts | 91 +- app/core/EngineService/index.ts | 3 +- app/core/Ledger/Ledger.test.ts | 40 +- app/core/Ledger/Ledger.ts | 40 +- app/core/LockManagerService/index.test.ts | 234 +- app/core/LockManagerService/index.ts | 90 +- app/core/Multichain/constants.ts | 4 + app/core/Multichain/test/utils.test.ts | 169 + app/core/Multichain/utils.ts | 80 + app/core/NotificationManager.js | 89 +- .../RPCMethods/RPCMethodMiddleware.test.ts | 32 +- app/core/RPCMethods/RPCMethodMiddleware.ts | 2 +- .../index.test.ts | 10 +- .../index.ts | 8 +- .../RPCMethods/eth_sendTransaction.test.ts | 41 +- app/core/RPCMethods/eth_sendTransaction.ts | 32 +- app/core/RPCMethods/utils.ts | 7 +- .../RPCMethods/wallet_switchEthereumChain.js | 4 +- app/core/RPCMethods/wallet_watchAsset.ts | 10 +- app/core/SanitizationMiddleware.ts | 21 +- app/core/SnapKeyring/BitcoinWalletSnap.ts | 30 + app/core/Snaps/SnapBridge.ts | 19 +- app/core/WalletConnect/WalletConnect.js | 8 +- .../WalletConnect/WalletConnect2Session.ts | 6 +- .../WalletConnect/extractApprovedAccounts.ts | 2 +- .../createTracingMiddleware/index.test.ts | 8 +- app/core/createTracingMiddleware/index.ts | 6 +- app/core/redux/ReduxService.test.ts | 67 + app/core/redux/ReduxService.ts | 49 + app/core/redux/index.ts | 3 + app/core/redux/types.ts | 8 + app/images/bitcoin-logo.png | Bin 0 -> 100284 bytes app/lib/ppom/ppom-util.test.ts | 51 + app/lib/ppom/ppom-util.ts | 23 +- app/lib/snaps/SnapsExecutionWebView.tsx | 2 +- app/lib/snaps/preinstalled-snaps.ts | 6 + app/reducers/fiatOrders/index.ts | 4 +- app/reducers/index.ts | 22 +- app/reducers/multichain/index.ts | 10 + app/reducers/navigation/index.ts | 38 +- app/reducers/navigation/selectors.ts | 23 + app/reducers/navigation/types.ts | 8 + app/reducers/swaps/index.js | 63 +- app/reducers/transaction/index.js | 2 + app/reducers/user/index.ts | 78 +- app/reducers/user/selectors.ts | 6 + app/reducers/user/types.ts | 19 + app/selectors/accountTrackerController.ts | 5 +- ...accountTrackerControllerReRenders.test.tsx | 4 +- app/selectors/accountsController.test.ts | 74 +- app/selectors/accountsController.ts | 42 +- app/selectors/currencyRateController.test.ts | 100 + app/selectors/currencyRateController.ts | 18 +- .../featureFlagController/index.test.ts | 1 - app/selectors/featureFlagController/index.ts | 1 - .../minimumAppVersion/index.test.ts | 2 +- .../minimumAppVersion/types.ts | 1 - app/selectors/featureFlagController/mocks.ts | 1 - app/selectors/featureFlagController/types.ts | 1 - app/selectors/identity/index.test.ts | 53 + app/selectors/identity/index.tsx | 41 + app/selectors/multichain.test.ts | 188 + app/selectors/multichain.ts | 241 + app/selectors/networkController.test.ts | 165 + app/selectors/networkController.ts | 74 +- app/selectors/notifications/index.test.ts | 36 +- app/selectors/notifications/index.tsx | 38 - app/selectors/notifications/testUtils.ts | 13 - app/selectors/preferenceesController.test.ts | 177 + app/selectors/preferencesController.ts | 20 + app/selectors/settings.ts | 1 - app/selectors/smartTransactionsController.ts | 8 +- app/selectors/tokenBalancesController.test.ts | 140 + app/selectors/tokenBalancesController.ts | 13 +- app/selectors/tokenRatesController.ts | 5 + app/selectors/tokensController.test.ts | 335 + app/selectors/tokensController.ts | 116 +- app/selectors/types.ts | 6 - app/store/index.ts | 37 +- app/store/persistConfig.ts | 6 +- app/store/sagas/index.ts | 61 +- app/store/sagas/sagas.test.ts | 78 +- app/util/address/index.test.ts | 113 +- app/util/address/index.ts | 50 +- app/util/blockaid/index.test.ts | 2 + app/util/bytes.test.ts | 10 + app/util/bytes.ts | 10 + app/util/custom-gas/index.js | 8 +- app/util/dappTransactions/index.ts | 5 +- .../hooks/useAccountSyncing.test.tsx | 2 +- .../hooks/useAccountSyncing.ts | 2 +- .../hooks/useCreateSession.test.tsx | 13 +- .../hooks/useCreateSession.ts | 16 +- .../hooks/useProfileSyncing.test.tsx | 6 +- .../hooks/useProfileSyncing.ts | 7 +- app/util/importAdditionalAccounts.test.ts | 5 +- app/util/importAdditionalAccounts.ts | 3 +- app/util/networks/customNetworks.tsx | 31 +- app/util/networks/engineNetworkUtils.ts | 86 + app/util/networks/global-network.test.ts | 123 + app/util/networks/global-network.ts | 35 + app/util/networks/handleNetworkSwitch.test.ts | 2 +- app/util/networks/handleNetworkSwitch.ts | 2 +- app/util/networks/index.js | 108 +- app/util/networks/index.test.ts | 48 +- app/util/notifications/hooks/types.ts | 12 - app/util/regex/index.test.ts | 8 +- app/util/regex/index.ts | 4 +- .../sentry/__snapshots__/utils.test.ts.snap | 2 - app/util/sentry/utils.js | 4 - app/util/sentry/utils.test.ts | 2 - app/util/smart-transactions/index.test.ts | 39 +- app/util/smart-transactions/index.ts | 8 +- .../smart-publish-hook.test.ts | 64 + .../smart-transactions/smart-publish-hook.ts | 13 +- app/util/termsOfUse/termsOfUse.ts | 2 +- app/util/test/accountsControllerTestUtils.ts | 10 +- app/util/test/confirm-data-helpers.ts | 36 + app/util/test/initial-background-state.json | 10 +- app/util/test/initial-root-state.ts | 9 +- app/util/transaction-controller/index.test.ts | 85 +- app/util/transaction-controller/index.ts | 35 +- app/util/transactions/index.js | 24 +- app/util/transactions/index.test.ts | 52 +- attribution.txt | 14418 ++++++++++++---- babel.config.js | 15 +- bitrise.yml | 155 +- docs/readme/expo-environment.md | 45 + docs/readme/testing.md | 27 + e2e/api-mocking/mock-config/mock-events.js | 45 + e2e/api-mocking/mock-server.js | 9 +- e2e/api-specs/json-rpc-coverage.js | 2 +- e2e/fixtures/fixture-builder.js | 85 +- e2e/fixtures/fixture-helper.js | 21 +- e2e/fixtures/utils.js | 2 +- e2e/helpers.js | 40 +- e2e/pages/Browser/BrowserView.js | 3 +- .../Browser/NetworkConnectMultiSelector.js | 27 + .../Browser/PermissionSummaryBottomSheet.js | 6 +- e2e/pages/Confirmation/ConfirmationView.js | 15 + e2e/pages/EnableDeviceNotificationsAlert.js | 36 - e2e/pages/Network/NetworkListModal.js | 2 + .../Network/NetworkNonPemittedBottomSheet.js | 81 + .../EnableAutomaticSecurityChecksView.js | 2 +- .../EnableDeviceNotificationsAlert.js | 24 + .../ExperienceEnhancerBottomSheet.js | 23 + .../OnboardingWizardModal.js | 2 +- .../ProtectYourWalletModal.js | 2 +- .../SkipAccountSecurityModal.js | 7 +- .../{modals => Onboarding}/TermsOfUseModal.js | 2 +- .../{modals => Onboarding}/WhatsNewModal.js | 2 +- e2e/pages/{modals => Send}/AddAddressModal.js | 2 +- e2e/pages/Send/SendView.js | 2 +- e2e/pages/Send/TransactionConfirmView.js | 19 +- e2e/pages/Settings/AesCryptoTestForm.js | 8 +- e2e/pages/Transactions/ActivitiesView.js | 12 +- e2e/pages/modals/ExperienceEnhancerModal.js | 33 - e2e/pages/swaps/QuoteView.js | 8 + .../AccountActionsBottomSheet.js} | 18 +- .../AccountListBottomSheet.js} | 50 +- .../AddAccountBottomSheet.js} | 10 +- e2e/pages/{ => wallet}/EditAccountNameView.js | 6 +- e2e/pages/{ => wallet}/LoginView.js | 6 +- .../{modals => wallet}/NftDetectionModal.js | 2 +- e2e/pages/{ => wallet}/TabBarComponent.js | 6 +- e2e/pages/{modals => wallet}/ToastModal.js | 2 +- e2e/pages/{ => wallet}/TokenOverview.js | 6 +- .../WalletActionsBottomSheet.js} | 16 +- e2e/pages/wallet/WalletView.js | 36 + e2e/selectors/AddTokenView.selectors.js | 3 - .../AccountOverview.selectors.js | 0 .../NetworkConnectMultiSelector.selectors.js | 5 + e2e/selectors/ConfirmImportToken.selectors.js | 6 - .../ConfirmationView.selectors.js | 4 + .../ContractNickNameView.selectors.js | 5 - ...ImportPrivateKeySuccessScreen.selectors.js | 4 - e2e/selectors/LoginOptionsSwitch.selectors.js | 3 - .../Modals/BrowserUrlModal.selectors.js | 3 - .../Modals/ConnectNetworkModal.selectors.js | 5 - .../TurnOffRememberMeModal.selectors.js | 3 - ...NetworkNonPemittedBottomSheet.selectors.js | 16 + e2e/selectors/NotificationsView.selectors.js | 11 - ...EnableAutomaticSecurityChecks.selectors.js | 0 ...nableDeviceNotificationsAlert.selectors.js | 4 + .../ExperienceEnhancerModal.selectors.js | 2 +- .../OnboardingCarousel.selectors.js | 1 + .../OnboardingWizardModal.selectors.js | 0 .../ProtectWalletModal.selectors.js | 0 .../SkipAccountSecurityModal.selectors.js | 0 .../TermsOfUseModal.selectors.js | 0 .../WhatsNewModal.selectors.js | 0 .../PerformanceRegression.selectors.js | 4 - .../AddAddressModal.selectors.js | 0 .../{ => SendFlow}/EditGasView.selectors.js | 2 +- .../{ => SendFlow}/SendView.selectors.js | 0 .../TransactionConfirmView.selectors.js | 4 +- .../TransactionReview.selectors.js | 0 .../{ => Settings}/AesCrypto.selectors.js | 3 +- .../DataCollectionBottomSheet.selectors.js} | 2 +- .../Transactions/ActivitiesView.selectors.js | 4 + e2e/selectors/swaps/QuoteView.selectors.js | 1 + e2e/selectors/swaps/SwapsView.selectors.js | 1 + .../AccountActionsBottomSheet.selectors.js} | 4 +- .../AccountListBottomSheet.selectors.js} | 7 +- .../AddAccountBottomSheet.selectors.js} | 2 +- .../{ => wallet}/AddAssetView.selectors.js | 0 .../CellComponent.selectors.js} | 2 +- .../{ => wallet}/EditAccountName.selectors.js | 4 +- .../wallet/ImportTokenView.selectors.js | 1 + .../{ => wallet}/LoginView.selectors.js | 0 .../NftDetectionModal.selectors.js | 0 .../wallet/NotificationsView.selectors.js | 4 + .../{ => wallet}/TabBar.selectors.js | 0 .../ToastModal.selectors.js | 0 .../{ => wallet}/TokenOverview.selectors.js | 2 +- .../WalletActionsBottomSheet.selectors.js} | 2 +- e2e/selectors/wallet/WalletView.selectors.js | 3 + .../accounts/aes/encryption-with-key.spec.js | 10 +- .../aes/encryption-with-password.spec.js | 4 +- .../accounts/aes/salt-generation.spec.js | 4 +- e2e/specs/accounts/auto-lock.spec.js | 10 +- .../accounts/change-account-name.spec.js | 24 +- .../accounts/import-wallet-account.spec.js | 15 +- ...imported-account-remove-and-import.spec.js | 22 +- e2e/specs/accounts/reveal-private-key.spec.js | 16 +- .../reveal-secret-recovery-phrase.spec.js | 8 +- e2e/specs/assets/import-tokens.spec.js | 3 +- e2e/specs/assets/nft-details.spec.js | 7 +- e2e/specs/assets/nft-detection-modal.spec.js | 4 +- e2e/specs/browser/browser-tests.spec.js | 5 +- .../advanced-gas-fees.mock.spec.js | 32 +- .../approve-custom-erc20.spec.js | 2 +- .../approve-default-erc20.spec.js | 2 +- .../increase-allowance-erc20.spec.js | 2 +- .../security-alert-send-eth.mock.spec.js | 120 + .../send-erc20-with-dapp.spec.js | 2 +- e2e/specs/confirmations/send-erc721.spec.js | 2 +- e2e/specs/confirmations/send-eth.spec.js | 8 +- .../set-approve-for-all-erc721.spec.js | 2 +- .../signatures/personal-sign.spec.js | 2 +- .../security-alert-signatures.mock.spec.js | 136 + .../signatures/typed-sign-v3.spec.js | 2 +- .../signatures/typed-sign-v4.spec.js | 2 +- .../signatures/typed-sign.spec.js | 2 +- ...spec.js => suggested-gas-api.mock.spec.js} | 26 +- .../account-syncing/mockData.js | 0 ...c-after-adding-custom-name-account.spec.js | 66 +- .../sync-after-onboarding.spec.js | 42 +- e2e/specs/identity/utils/constants.js | 7 + e2e/specs/identity/utils/helpers.js | 10 + e2e/specs/identity/utils/mocks.js | 67 + .../userStorageMockttpController.js | 111 +- .../userStorageMockttpController.test.js | 427 + e2e/specs/multichain/asset-list.spec.js | 131 + ...tem-connect-to-non-permitted-chain.spec.js | 241 + ...iscard-change-to-chain-permissions.spec.js | 59 + ...ns-grant-one-account-and-one-chain.spec.js | 91 + .../permission-system-remove-network.spec.js | 6 +- ...on-system-revoke-multiple-accounts.spec.js | 20 +- ...ssion-system-revoke-single-account.spec.js | 6 +- ...ssion-system-revoke-single-network.spec.js | 13 +- ...on-system-update-chain-permissions.spec.js | 113 + e2e/specs/networks/add-custom-rpc.spec.js | 8 +- .../networks/connect-test-network.spec.js | 2 +- e2e/specs/networks/networks-search.spec.js | 9 +- e2e/specs/notifications/utils/mocks.js | 19 +- .../userStorageMockttpController.test.js | 305 - .../onboarding-wizard-opt-in.spec.js | 20 +- e2e/specs/onboarding/term-of-use.spec.js | 4 +- .../permission-system-delete-wallet.spec.js | 10 +- ...-system-revoking-multiple-accounts.spec.js | 16 +- .../create-wallet-account.failing.js | 15 +- e2e/specs/quarantine/deeplinks.failing.js | 4 +- e2e/specs/quarantine/import-nft.failing.js | 62 - ...ystem-removing-imported-account.failing.js | 14 +- .../quarantine/send-to-contact.failing.js | 77 - .../quarantine/swap-token-chart.failing.js | 6 +- e2e/specs/ramps/offramp.spec.js | 8 +- e2e/specs/ramps/onramp.spec.js | 10 +- e2e/specs/settings/addressbook-tests.spec.js | 12 +- e2e/specs/settings/contact-us.spec.js | 4 +- e2e/specs/settings/delete-wallet.spec.js | 4 +- e2e/specs/settings/fiat-on-testnets.spec.js | 3 +- .../swaps/swap-action-regression.spec.js | 8 +- e2e/specs/swaps/swap-action-smoke.spec.js | 8 +- e2e/specs/swaps/token-details.spec.js | 6 +- .../wallet/incoming-transactions.spec.js | 217 + .../portfolio-connect-account.spec.js} | 12 +- e2e/specs/wallet/request-token-flow.spec.js | 10 +- e2e/specs/wallet/send-ERC-token.spec.js | 4 +- e2e/specs/wallet/start-exploring.spec.js | 8 +- e2e/tags.js | 10 +- e2e/utils/Assertions.js | 4 +- e2e/utils/Matchers.js | 2 +- e2e/viewHelper.js | 20 +- index.js | 2 +- ios/MetaMask-Bridging-Header.h | 1 + ios/MetaMask.xcodeproj/project.pbxproj | 177 +- ios/MetaMask/AppDelegate.h | 3 +- ios/MetaMask/AppDelegate.m | 25 +- ios/MetaMask/Info.plist | 1 + .../IosExportOptionsMetaMaskDevelopment.plist | 17 + .../NativeModules/RCTMinimizer/RCTMinimizer.m | 4 +- ios/Podfile | 14 +- ios/Podfile.lock | 145 +- jest.config.js | 3 + locales/languages/de.json | 173 +- locales/languages/el.json | 173 +- locales/languages/en.json | 10 +- locales/languages/es.json | 173 +- locales/languages/fr.json | 173 +- locales/languages/hi.json | 173 +- locales/languages/id.json | 173 +- locales/languages/ja.json | 173 +- locales/languages/ko.json | 179 +- locales/languages/pt.json | 173 +- locales/languages/ru.json | 173 +- locales/languages/tl.json | 171 +- locales/languages/tr.json | 171 +- locales/languages/vi.json | 173 +- locales/languages/zh.json | 173 +- metro.config.js | 3 +- package.json | 79 +- ...@metamask+assets-controllers+45.1.1.patch} | 110 +- .../@metamask+keyring-controller+19.0.1.patch | 14 + ...amask+preferences-controller+15.0.1.patch} | 0 scripts/build.sh | 74 +- scripts/setup.mjs | 73 +- shim.js | 4 +- .../ColdStartLoginToWalletScreen.feature | 2 +- .../WarmStartLoginToWalletScreen.feature | 2 +- wdio/screen-objects/AccountListComponent.js | 10 +- wdio/screen-objects/CommonScreen.js | 5 +- .../EnableSecurityChecksScreen.js | 5 +- wdio/screen-objects/LoginScreen.js | 2 +- wdio/screen-objects/Modals/AddAccountModal.js | 9 +- .../screen-objects/Modals/AddressBookModal.js | 2 +- .../Modals/ExperienceEnhancerModal.js | 6 +- .../screen-objects/Modals/NetworkListModal.js | 4 +- .../Modals/NotificationModal.js | 4 +- .../Modals/OnboardingWizardModal.js | 2 +- .../Modals/SkipAccountSecurityModal.js | 7 +- wdio/screen-objects/Modals/TabBarModal.js | 2 +- wdio/screen-objects/Modals/TermOfUseScreen.js | 18 +- .../Modals/WalletActionModal.js | 6 +- wdio/screen-objects/Modals/WhatsNewModal.js | 2 +- .../Onboarding/OnboardingCarousel.js | 4 +- wdio/screen-objects/SendScreen.js | 2 +- wdio/screen-objects/TokenOverviewScreen.js | 2 +- .../TransactionConfirmScreen.js | 4 +- wdio/screen-objects/WalletMainScreen.js | 27 +- .../Components/AddAccountModal.testIds.js | 2 - .../Components/Notification.testIds.js | 2 - .../SkipAccountSecurityModalTestIds.js | 5 - .../testIDs/Components/TermsOfUse.testIds.js | 10 - ...eDeviceNotificationsChecksAlert.testIds.js | 6 - .../Screens/TransactionConfirm.testIds.js | 2 +- .../testIDs/Screens/WalletView.testIds.js | 1 - yarn.lock | 4217 +++-- 727 files changed, 41291 insertions(+), 11411 deletions(-) create mode 100644 .github/workflows/auto-draft-prs.yml create mode 100644 app.config.js delete mode 100644 app.json create mode 100644 app/actions/identity/constants/errors.ts create mode 100644 app/actions/identity/index.test.ts create mode 100644 app/actions/identity/index.ts create mode 100644 app/actions/multichain/state.ts create mode 100644 app/actions/navigation/types.ts delete mode 100644 app/actions/user/index.js create mode 100644 app/actions/user/index.ts create mode 100644 app/actions/user/types.ts create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.test.tsx create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.tsx create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentageCrossChains.test.tsx.snap create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/utils.ts create mode 100644 app/components/UI/NetworkAssetLogo/__snapshots__/index.test.tsx.snap create mode 100644 app/components/UI/NetworkAssetLogo/index.test.tsx create mode 100644 app/components/UI/NetworkAssetLogo/index.tsx create mode 100644 app/components/UI/Swaps/QuotesView.test.ts create mode 100644 app/components/UI/Swaps/__snapshots__/QuotesView.test.ts.snap create mode 100644 app/components/UI/Swaps/utils/gas.test.ts create mode 100644 app/components/UI/Swaps/utils/gas.ts create mode 100644 app/components/UI/Tokens/TokenList/TokenListFooter/__snapshots__/index.test.tsx.snap create mode 100644 app/components/UI/Tokens/TokenList/TokenListFooter/index.test.tsx create mode 100644 app/components/UI/Tokens/util/enableAllNetworksFilter.test.ts create mode 100644 app/components/UI/Tokens/util/enableAllNetworksFilter.ts create mode 100644 app/components/UI/Tokens/util/filterAssets.test.ts create mode 100644 app/components/UI/Tokens/util/filterAssets.ts create mode 100644 app/components/Views/AssetDetails/AssetsDetails.test.tsx create mode 100644 app/components/Views/AssetDetails/__snapshots__/AssetsDetails.test.tsx.snap delete mode 100644 app/components/Views/AssetDetails/__snapshots__/index.test.tsx.snap delete mode 100644 app/components/Views/AssetDetails/index.test.tsx create mode 100644 app/components/Views/AssetOptions/AssetOptions.test.tsx create mode 100644 app/components/Views/AssetOptions/__snapshots__/AssetOptions.test.tsx.snap create mode 100644 app/components/Views/DetectedTokens/__snapshots__/index.test.tsx.snap create mode 100644 app/components/Views/DetectedTokens/components/Token.test.tsx create mode 100644 app/components/Views/DetectedTokens/components/__snapshots__/Token.test.tsx.snap create mode 100644 app/components/Views/DetectedTokens/index.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/Shared/InfoRowOrigin/InfoRowOrigin.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/Shared/InfoRowOrigin/InfoRowOrigin.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/Shared/InfoRowOrigin/index.ts create mode 100644 app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Message.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx create mode 100644 app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/index.ts create mode 100644 app/components/hooks/AssetPolling/useAccountTrackerPolling.test.ts create mode 100644 app/components/hooks/AssetPolling/useAccountTrackerPolling.ts create mode 100644 app/components/hooks/useAccounts/utils.ts create mode 100644 app/components/hooks/useGetFormattedTokensPerChain.test.ts create mode 100644 app/components/hooks/useGetFormattedTokensPerChain.tsx create mode 100644 app/components/hooks/useGetTotalFiatBalanceCrossChains.test.ts create mode 100644 app/components/hooks/useGetTotalFiatBalanceCrossChains.tsx create mode 100644 app/core/Encryptor/pbkdf2.test.ts create mode 100644 app/core/Encryptor/pbkdf2.ts create mode 100644 app/core/Engine/constants.ts rename app/core/Engine/controllers/{accounts => AccountsController}/constants.ts (100%) rename app/core/Engine/controllers/{accounts => AccountsController}/utils.test.ts (100%) rename app/core/Engine/controllers/{accounts => AccountsController}/utils.ts (100%) delete mode 100644 app/core/Engine/controllers/accounts/README.md create mode 100644 app/core/Multichain/constants.ts create mode 100644 app/core/Multichain/test/utils.test.ts create mode 100644 app/core/Multichain/utils.ts create mode 100644 app/core/SnapKeyring/BitcoinWalletSnap.ts create mode 100644 app/core/redux/ReduxService.test.ts create mode 100644 app/core/redux/ReduxService.ts create mode 100644 app/core/redux/index.ts create mode 100644 app/core/redux/types.ts create mode 100644 app/images/bitcoin-logo.png create mode 100644 app/reducers/multichain/index.ts create mode 100644 app/reducers/navigation/selectors.ts create mode 100644 app/reducers/navigation/types.ts create mode 100644 app/reducers/user/selectors.ts create mode 100644 app/reducers/user/types.ts create mode 100644 app/selectors/currencyRateController.test.ts create mode 100644 app/selectors/identity/index.test.ts create mode 100644 app/selectors/identity/index.tsx create mode 100644 app/selectors/multichain.test.ts create mode 100644 app/selectors/multichain.ts create mode 100644 app/selectors/networkController.test.ts create mode 100644 app/selectors/preferenceesController.test.ts create mode 100644 app/selectors/tokenBalancesController.test.ts create mode 100644 app/selectors/tokensController.test.ts create mode 100644 app/util/bytes.test.ts rename app/util/{notifications => identity}/hooks/useAccountSyncing.test.tsx (97%) rename app/util/{notifications => identity}/hooks/useAccountSyncing.ts (92%) rename app/util/{notifications => identity}/hooks/useCreateSession.test.tsx (92%) rename app/util/{notifications => identity}/hooks/useCreateSession.ts (83%) rename app/util/{notifications => identity}/hooks/useProfileSyncing.test.tsx (97%) rename app/util/{notifications => identity}/hooks/useProfileSyncing.ts (89%) create mode 100644 app/util/networks/engineNetworkUtils.ts create mode 100644 app/util/networks/global-network.test.ts create mode 100644 app/util/networks/global-network.ts create mode 100644 docs/readme/expo-environment.md create mode 100644 e2e/pages/Browser/NetworkConnectMultiSelector.js create mode 100644 e2e/pages/Confirmation/ConfirmationView.js delete mode 100644 e2e/pages/EnableDeviceNotificationsAlert.js create mode 100644 e2e/pages/Network/NetworkNonPemittedBottomSheet.js rename e2e/pages/{modals => Onboarding}/EnableAutomaticSecurityChecksView.js (91%) create mode 100644 e2e/pages/Onboarding/EnableDeviceNotificationsAlert.js create mode 100644 e2e/pages/Onboarding/ExperienceEnhancerBottomSheet.js rename e2e/pages/{modals => Onboarding}/OnboardingWizardModal.js (97%) rename e2e/pages/{modals => Onboarding}/ProtectYourWalletModal.js (94%) rename e2e/pages/{modals => Onboarding}/SkipAccountSecurityModal.js (85%) rename e2e/pages/{modals => Onboarding}/TermsOfUseModal.js (89%) rename e2e/pages/{modals => Onboarding}/WhatsNewModal.js (81%) rename e2e/pages/{modals => Send}/AddAddressModal.js (90%) delete mode 100644 e2e/pages/modals/ExperienceEnhancerModal.js rename e2e/pages/{modals/AccountActionsModal.js => wallet/AccountActionsBottomSheet.js} (53%) rename e2e/pages/{AccountListView.js => wallet/AccountListBottomSheet.js} (58%) rename e2e/pages/{modals/AddAccountModal.js => wallet/AddAccountBottomSheet.js} (58%) rename e2e/pages/{ => wallet}/EditAccountNameView.js (66%) rename e2e/pages/{ => wallet}/LoginView.js (81%) rename e2e/pages/{modals => wallet}/NftDetectionModal.js (86%) rename e2e/pages/{ => wallet}/TabBarComponent.js (85%) rename e2e/pages/{modals => wallet}/ToastModal.js (91%) rename e2e/pages/{ => wallet}/TokenOverview.js (93%) rename e2e/pages/{modals/WalletActionsModal.js => wallet/WalletActionsBottomSheet.js} (52%) delete mode 100644 e2e/selectors/AddTokenView.selectors.js rename e2e/selectors/{ => Browser}/AccountOverview.selectors.js (100%) create mode 100644 e2e/selectors/Browser/NetworkConnectMultiSelector.selectors.js delete mode 100644 e2e/selectors/ConfirmImportToken.selectors.js create mode 100644 e2e/selectors/Confirmation/ConfirmationView.selectors.js delete mode 100644 e2e/selectors/ContractNickNameView.selectors.js delete mode 100644 e2e/selectors/ImportPrivateKeySuccessScreen.selectors.js delete mode 100644 e2e/selectors/LoginOptionsSwitch.selectors.js delete mode 100644 e2e/selectors/Modals/BrowserUrlModal.selectors.js delete mode 100644 e2e/selectors/Modals/ConnectNetworkModal.selectors.js delete mode 100644 e2e/selectors/Modals/TurnOffRememberMeModal.selectors.js create mode 100644 e2e/selectors/Network/NetworkNonPemittedBottomSheet.selectors.js delete mode 100644 e2e/selectors/NotificationsView.selectors.js rename e2e/selectors/{Modals => Onboarding}/EnableAutomaticSecurityChecks.selectors.js (100%) create mode 100644 e2e/selectors/Onboarding/EnableDeviceNotificationsAlert.selectors.js rename e2e/selectors/{Modals => Onboarding}/ExperienceEnhancerModal.selectors.js (90%) rename e2e/selectors/{Modals => Onboarding}/OnboardingWizardModal.selectors.js (100%) rename e2e/selectors/{Modals => Onboarding}/ProtectWalletModal.selectors.js (100%) rename e2e/selectors/{Modals => Onboarding}/SkipAccountSecurityModal.selectors.js (100%) rename e2e/selectors/{Modals => Onboarding}/TermsOfUseModal.selectors.js (100%) rename e2e/selectors/{Modals => Onboarding}/WhatsNewModal.selectors.js (100%) delete mode 100644 e2e/selectors/PerformanceRegression.selectors.js rename e2e/selectors/{Modals => SendFlow}/AddAddressModal.selectors.js (100%) rename e2e/selectors/{ => SendFlow}/EditGasView.selectors.js (90%) rename e2e/selectors/{ => SendFlow}/SendView.selectors.js (100%) rename e2e/selectors/{ => SendFlow}/TransactionConfirmView.selectors.js (77%) rename e2e/selectors/{ => SendFlow}/TransactionReview.selectors.js (100%) rename e2e/selectors/{ => Settings}/AesCrypto.selectors.js (96%) rename e2e/selectors/{Modals/DataCollectionModal.selectors.js => Settings/SecurityAndPrivacy/DataCollectionBottomSheet.selectors.js} (67%) rename e2e/selectors/{Modals/AccountActionsModal.selectors.js => wallet/AccountActionsBottomSheet.selectors.js} (70%) rename e2e/selectors/{AccountListView.selectors.js => wallet/AccountListBottomSheet.selectors.js} (66%) rename e2e/selectors/{Modals/AddAccountModal.selectors.js => wallet/AddAccountBottomSheet.selectors.js} (67%) rename e2e/selectors/{ => wallet}/AddAssetView.selectors.js (100%) rename e2e/selectors/{Modals/CellModal.selectors.js => wallet/CellComponent.selectors.js} (84%) rename e2e/selectors/{ => wallet}/EditAccountName.selectors.js (56%) rename e2e/selectors/{ => wallet}/LoginView.selectors.js (100%) rename e2e/selectors/{Modals => wallet}/NftDetectionModal.selectors.js (100%) create mode 100644 e2e/selectors/wallet/NotificationsView.selectors.js rename e2e/selectors/{ => wallet}/TabBar.selectors.js (100%) rename e2e/selectors/{Modals => wallet}/ToastModal.selectors.js (100%) rename e2e/selectors/{ => wallet}/TokenOverview.selectors.js (93%) rename e2e/selectors/{Modals/WalletActionsModal.selectors.js => wallet/WalletActionsBottomSheet.selectors.js} (81%) create mode 100644 e2e/specs/confirmations/security-alert-send-eth.mock.spec.js create mode 100644 e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.js rename e2e/specs/confirmations/{suggestedGasApi.mock.spec.js => suggested-gas-api.mock.spec.js} (77%) rename e2e/specs/{notifications => identity}/account-syncing/mockData.js (100%) rename e2e/specs/{notifications => identity}/account-syncing/sync-after-adding-custom-name-account.spec.js (51%) rename e2e/specs/{notifications => identity}/account-syncing/sync-after-onboarding.spec.js (58%) create mode 100644 e2e/specs/identity/utils/constants.js create mode 100644 e2e/specs/identity/utils/helpers.js create mode 100644 e2e/specs/identity/utils/mocks.js rename e2e/specs/{notifications => identity}/utils/user-storage/userStorageMockttpController.js (56%) create mode 100644 e2e/specs/identity/utils/user-storage/userStorageMockttpController.test.js create mode 100644 e2e/specs/multichain/asset-list.spec.js create mode 100644 e2e/specs/multichain/permission-system-connect-to-non-permitted-chain.spec.js create mode 100644 e2e/specs/multichain/permission-system-discard-change-to-chain-permissions.spec.js create mode 100644 e2e/specs/multichain/permission-system-initial-permissions-grant-one-account-and-one-chain.spec.js create mode 100644 e2e/specs/multichain/permission-system-update-chain-permissions.spec.js delete mode 100644 e2e/specs/notifications/utils/user-storage/userStorageMockttpController.test.js delete mode 100644 e2e/specs/quarantine/import-nft.failing.js delete mode 100644 e2e/specs/quarantine/send-to-contact.failing.js create mode 100644 e2e/specs/wallet/incoming-transactions.spec.js rename e2e/specs/{quarantine/portfolio-connect-account.failing.js => wallet/portfolio-connect-account.spec.js} (89%) create mode 100644 ios/MetaMask/IosExportOptionsMetaMaskDevelopment.plist rename patches/{@metamask+assets-controllers+44.1.0.patch => @metamask+assets-controllers+45.1.1.patch} (61%) create mode 100644 patches/@metamask+keyring-controller+19.0.1.patch rename patches/{@metamask+preferences-controller+14.0.0.patch => @metamask+preferences-controller+15.0.1.patch} (100%) delete mode 100644 wdio/screen-objects/testIDs/Components/AddAccountModal.testIds.js delete mode 100644 wdio/screen-objects/testIDs/Components/Notification.testIds.js delete mode 100644 wdio/screen-objects/testIDs/Components/SkipAccountSecurityModalTestIds.js delete mode 100644 wdio/screen-objects/testIDs/Components/TermsOfUse.testIds.js delete mode 100644 wdio/screen-objects/testIDs/Screens/EnableDeviceNotificationsChecksAlert.testIds.js diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 36e2bc04bb1..babe74bcd3a 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -84,3 +84,10 @@ ignores: - 'app' - 'i18n-js' - 'images' + + ## Expo + - '@config-plugins/detox' + - 'cross-spawn' + - 'expo-build-properties' + - 'expo-dev-client' + diff --git a/.e2e.env.example b/.e2e.env.example index fd46ecba336..29071a958ef 100644 --- a/.e2e.env.example +++ b/.e2e.env.example @@ -3,3 +3,5 @@ export MM_TEST_ACCOUNT_SRP='word1 word... word12' export MM_TEST_ACCOUNT_ADDRESS='0x...' export MM_TEST_ACCOUNT_PRIVATE_KEY='' export IS_TEST="true" +# Temporary mechanism to enable security alerts API prior to release. +export MM_SECURITY_ALERTS_API_ENABLED="true" diff --git a/.eslintrc.js b/.eslintrc.js index 567466baec8..21a0949d4ea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,7 +60,7 @@ module.exports = { }, }, { - files: ['scripts/**/*.js'], + files: ['scripts/**/*.js', 'app.config.js'], rules: { 'no-console': 0, 'import/no-commonjs': 0, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 96b4b4dbbbd..9576fde1dd0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -47,7 +47,7 @@ app/util/walletconnect.js @MetaMask/sdk-devs # Accounts Team app/core/Encryptor/ @MetaMask/accounts-engineers -app/core/Engine/controllers/accounts @MetaMask/accounts-engineers +app/core/Engine/controllers/AccountsController @MetaMask/accounts-engineers # Swaps Team app/components/UI/Swaps @MetaMask/swaps-engineers @@ -62,6 +62,12 @@ app/selectors/notification @MetaMask/notifications app/util/notifications @MetaMask/notifications app/store/util/notifications @MetaMask/notifications +# Identity Team +app/actions/identity @MetaMask/identity +app/util/identity @MetaMask/identity +app/components/UI/ProfileSyncing @MetaMask/identity +e2e/specs/identity @MetaMask/identity + # LavaMoat Team ses.cjs @MetaMask/supply-chain patches/react-native+0.*.patch @MetaMask/supply-chain diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 31ee91b2191..4214943e066 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -57,7 +57,8 @@ body: - In production (default) - In beta - During release testing - - On the development branch + - On main branch + - On a feature branch validations: required: true - type: input diff --git a/.github/guidelines/LABELING_GUIDELINES.md b/.github/guidelines/LABELING_GUIDELINES.md index ee49fad0639..4b36fc4275e 100644 --- a/.github/guidelines/LABELING_GUIDELINES.md +++ b/.github/guidelines/LABELING_GUIDELINES.md @@ -25,7 +25,8 @@ To merge your PR one of the following QA labels are required: - **Run E2E Smoke**: This label will kick-off E2E testing and trigger a check to make sure the E2E tests pass. ### Optional labels: -- **regression-develop**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on the development branch, i.e., `main`, but is not yet released in production. +- **regression-main**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on the development branch, i.e., `main`, but is not yet released in production. +- **feature-branch-bug**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on a feature branch, i.e., before merging to `main`. ### Labels prohibited when PR needs to be merged: Any PR that includes one of the following labels can not be merged: diff --git a/.github/scripts/bitrise/run-bitrise-e2e-check.ts b/.github/scripts/bitrise/run-bitrise-e2e-check.ts index f25b5a9e7ce..62935ca966d 100644 --- a/.github/scripts/bitrise/run-bitrise-e2e-check.ts +++ b/.github/scripts/bitrise/run-bitrise-e2e-check.ts @@ -13,9 +13,33 @@ main().catch((error: Error): void => { process.exit(1); }); +// Determine whether E2E should run and provide the associated reason +function shouldRunBitriseE2E(antiLabel: boolean, hasSmokeTestLabel: boolean, isDocs: boolean, isFork: boolean, isMergeQueue: boolean): [boolean, string] { + + const conditions = [ + {condition: hasSmokeTestLabel, message: "The smoke test label is present.", shouldRun: true}, + {condition: isFork, message: "The pull request is from a fork.", shouldRun: false}, + {condition: isDocs, message: "The pull request is documentation related.", shouldRun: false}, + {condition: isMergeQueue, message: "The pull request is part of a merge queue.", shouldRun: false}, + {condition: antiLabel, message: "The pull request has the anti-label.", shouldRun: false} + ]; + + // Iterate through conditions to determine action + for (const {condition, message, shouldRun} of conditions) { + if (condition) { + return [shouldRun, message]; + } + } + + // Default case if no conditions met + return [false, "Unexpected scenario or no relevant labels found."]; +} + + async function main(): Promise { const githubToken = process.env.GITHUB_TOKEN; const e2eLabel = process.env.E2E_LABEL; + const antiLabel = process.env.NO_E2E_LABEL; const e2ePipeline = process.env.E2E_PIPELINE; const workflowName = process.env.WORKFLOW_NAME; const triggerAction = context.payload.action as PullRequestTriggerType; @@ -42,6 +66,20 @@ async function main(): Promise { process.exit(1); } + if (!antiLabel) { + core.setFailed('NO_E2E_LABEL not found'); + process.exit(1); + } + + // Logging for Pipeline debugging + console.log(`Trigger action: ${triggerAction}`); + console.log(`event: ${context.eventName}`); + console.log(`pullRequestNumber: ${pullRequestNumber}`); + + const mergeQueue = (context.eventName === 'merge_group') + const mqCommitHash = context.payload?.merge_group?.head_sha; + + const octokit: InstanceType = getOctokit(githubToken); const { data: prData } = await octokit.rest.pulls.get({ @@ -51,17 +89,67 @@ async function main(): Promise { }); // Get the latest commit hash - const latestCommitHash = prData.head.sha; + const prCommitHash = prData?.head?.sha; + // Determine the latest commit hash depending if it's a PR or MQ + const latestCommitHash = mergeQueue ? mqCommitHash : prCommitHash; - // Check if the e2e smoke label is applied - const labels = prData.labels; + // Grab flags & labels + const labels = prData?.labels ?? []; const hasSmokeTestLabel = labels.some((label) => label.name === e2eLabel); + const hasAntiLabel = labels.some((label) => label.name === antiLabel); + const fork = context.payload.pull_request?.head.repo.fork || false; + const docs = mergeQueue ? false : prData.title.startsWith("docs:"); + + + console.log(`Docs: ${docs}`); + console.log(`Fork: ${fork}`); + console.log(`Merge Queue: ${mergeQueue}`); + console.log(`Has smoke test label: ${hasSmokeTestLabel}`); + console.log(`Anti label: ${hasAntiLabel}`); + + const [shouldRun, reason] = shouldRunBitriseE2E(hasAntiLabel, hasSmokeTestLabel, docs, fork, mergeQueue); + console.log(`Should run: ${shouldRun}, Reason: ${reason}`); + + // One of these two labels must exist for pull_request type + if (!mergeQueue && !hasSmokeTestLabel && !hasAntiLabel) { + + // Fail Status due to missing labels + const createStatusCheckResponse = await octokit.rest.checks.create({ + owner, + repo, + name: statusCheckName, + head_sha: latestCommitHash, + status: StatusCheckStatusType.Completed, + conclusion: CompletedConclusionType.Failure, + started_at: new Date().toISOString(), + output: { + title: statusCheckTitle, + summary: `Failed due to missing labels. Please apply either ${e2eLabel} or ${antiLabel}.`, + }, + }); - // Pass check since e2e smoke label is not applied - if (!hasSmokeTestLabel) { + if (createStatusCheckResponse.status === 201) { + console.log( + `Created '${statusCheckName}' check with failed status for commit ${latestCommitHash}`, + ); + } else { + core.setFailed( + `Failed to create '${statusCheckName}' check with failed status for commit ${latestCommitHash} with status code ${createStatusCheckResponse.status}`, + ); + process.exit(1); + } + core.setFailed( + `At least 1 E2E Label must be Applied either ${e2eLabel} or ${antiLabel}`, + ); + process.exit(1); + } + + if (!shouldRun) { console.log( - `"${e2eLabel}" label not applied. Skipping Bitrise status check.`, + `Skipping Bitrise status check. due to the following reason: ${reason}`, ); + + // Post success status (skipped) const createStatusCheckResponse = await octokit.rest.checks.create({ owner, @@ -69,11 +157,11 @@ async function main(): Promise { name: statusCheckName, head_sha: latestCommitHash, status: StatusCheckStatusType.Completed, - conclusion: CompletedConclusionType.Success, + conclusion: CompletedConclusionType.Success, started_at: new Date().toISOString(), output: { title: statusCheckTitle, - summary: 'Skip run since no E2E smoke label is applied', + summary: `Skip run since ${reason}`, }, }); @@ -95,6 +183,8 @@ async function main(): Promise { triggerAction === PullRequestTriggerType.Labeled && context.payload?.label?.name === e2eLabel ) { + + console.log(`Starting Bitrise build for commit ${latestCommitHash}`); // Configure Bitrise configuration for API call const data = { build_params: { @@ -222,6 +312,8 @@ async function main(): Promise { } // Post pending status + console.log(`Posting pending status for commit ${latestCommitHash}`); + const createStatusCheckResponse = await octokit.rest.checks.create({ owner, repo, @@ -256,6 +348,8 @@ async function main(): Promise { const lastCommentPage = Math.ceil( numberOfTotalComments / numberOfCommentsToCheck, ); + + const { data: latestCommentBatch } = await octokit.rest.issues.listComments({ owner, repo, @@ -287,6 +381,8 @@ async function main(): Promise { // Bitrise comment doesn't exist, post fail status if (!bitriseComment) { + + console.log(`Bitrise comment not detected for commit ${latestCommitHash}`); // Post fail status const createStatusCheckResponse = await octokit.rest.checks.create({ owner, diff --git a/.github/scripts/check-template-and-add-labels.ts b/.github/scripts/check-template-and-add-labels.ts index e0a59e21d8e..fef8a5585d1 100644 --- a/.github/scripts/check-template-and-add-labels.ts +++ b/.github/scripts/check-template-and-add-labels.ts @@ -20,7 +20,8 @@ import { TemplateType, templates } from './shared/template'; import { retrievePullRequest } from './shared/pull-request'; enum RegressionStage { - Development, + DevelopmentFeature, + DevelopmentMain, Testing, Beta, Production @@ -202,8 +203,10 @@ function extractRegressionStageFromBugReportIssueBody( const extractedAnswer = match ? match[1].trim() : undefined; switch (extractedAnswer) { - case 'On the development branch': - return RegressionStage.Development; + case 'On a feature branch': + return RegressionStage.DevelopmentFeature; + case 'On main branch': + return RegressionStage.DevelopmentMain; case 'During release testing': return RegressionStage.Testing; case 'In beta': @@ -317,11 +320,18 @@ async function userBelongsToMetaMaskOrg( // This function crafts appropriate label, corresponding to regression stage and release version. function craftRegressionLabel(regressionStage: RegressionStage | undefined, releaseVersion: string | undefined): Label { switch (regressionStage) { - case RegressionStage.Development: + case RegressionStage.DevelopmentFeature: + return { + name: `feature-branch-bug`, + color: '5319E7', // violet + description: `bug that was found on a feature branch, but not yet merged in main branch`, + }; + + case RegressionStage.DevelopmentMain: return { name: `regression-develop`, color: '5319E7', // violet - description: `Regression bug that was found on development branch, but not yet present in production`, + description: `Regression bug that was found on main branch, but not yet present in production`, }; case RegressionStage.Testing: diff --git a/.github/scripts/tsconfig.json b/.github/scripts/tsconfig.json index 4082f16a5d9..4bb976aefe1 100644 --- a/.github/scripts/tsconfig.json +++ b/.github/scripts/tsconfig.json @@ -1,3 +1,61 @@ { - "extends": "../../tsconfig.json" + "compilerOptions": { + /* Basic Options */ + "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "lib": [ + "es2017", + ] /* Specify library files to be included in the compilation. */, + "allowJs": true /* Allow javascript files to be compiled. */, + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "react-native" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true /* Do not emit outputs. */, + // "incremental": true, /* Enable incremental compilation */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "resolveJsonModule": true /* Allows importing JSON files */, + "baseUrl": "." /* Base directory to resolve non-absolute module names. */, + /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + "skipLibCheck": true /* Skip type checking of declaration files. */ + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "exclude": [ + "node_modules", + "jest.config.js" + ] } diff --git a/.github/workflows/auto-draft-prs.yml b/.github/workflows/auto-draft-prs.yml new file mode 100644 index 00000000000..994e6f0a488 --- /dev/null +++ b/.github/workflows/auto-draft-prs.yml @@ -0,0 +1,39 @@ +name: Auto Draft + +on: + pull_request: + types: [opened] + branches: + - main + +permissions: + pull-requests: write + contents: read + issues: write + +jobs: + process_pr: + runs-on: ubuntu-latest + steps: + - name: Convert PR to Draft and Add Label + uses: actions/github-script@v6 + with: + script: | + // Convert PR to draft + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + draft: true + }); + + // Check if the PR title includes "docs:" or if it's from a fork + if (context.payload.pull_request.title.includes('docs:') || context.payload.pull_request.head.repo.fork) { + // Add label "No E2E Smoke Needed" + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['No E2E Smoke Needed'] + }); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f13d4efd5a..f2e1766f1bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: shell: bash run: | mv ./tests/coverage/coverage-final.json ./tests/coverage/coverage-${{ matrix.shard }}.json - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.shard }} path: ./tests/coverage/coverage-${{ matrix.shard }}.json @@ -120,7 +120,7 @@ jobs: node-version-file: '.nvmrc' cache: yarn - run: yarn setup - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: tests/coverage/ - name: Gather partial coverage reports into one directory @@ -129,7 +129,7 @@ jobs: mv ./tests/coverage/coverage-*/* ./tests/coverage - run: yarn test:merge-coverage - run: yarn test:validate-coverage - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: coverage path: ./tests/merged-coverage/lcov.info @@ -196,7 +196,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version-file: '.nvmrc' - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: coverage path: coverage/ @@ -323,4 +323,4 @@ jobs: else echo "All jobs passed step skipped. Block PR." exit 1 - fi + fi \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a95001cb1c6..6208030fb13 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -39,6 +39,6 @@ jobs: --rm \ -v "$(pwd):/app" -w /app \ metamask-mobile-builder:latest \ - bash -c 'yarn && yarn setup --build-ios' + bash -c 'yarn && yarn setup --build-ios --no-install-pods' # restore ownership for cleanup sudo chown -R "$(id -u):$(id -g)" . diff --git a/.github/workflows/run-bitrise-e2e-check.yml b/.github/workflows/run-bitrise-e2e-check.yml index ac1016e9d86..f30bc78962a 100644 --- a/.github/workflows/run-bitrise-e2e-check.yml +++ b/.github/workflows/run-bitrise-e2e-check.yml @@ -5,32 +5,18 @@ on: types: [edited, deleted] pull_request: types: [opened, reopened, labeled, unlabeled, synchronize] + merge_group: # Trigger on merge queue events to satisfy the branch protection rules + types: [checks_requested] env: E2E_LABEL: 'Run Smoke E2E' + NO_E2E_LABEL: 'No E2E Smoke Needed' E2E_PIPELINE: 'pr_smoke_e2e_pipeline' WORKFLOW_NAME: 'run-bitrise-e2e-check' jobs: - is-fork-pull-request: - name: Determine pull request source - if: ${{ github.event.issue.pull_request || github.event_name == 'pull_request' }} - runs-on: ubuntu-latest - outputs: - IS_FORK: ${{ steps.is-fork.outputs.IS_FORK }} - steps: - - uses: actions/checkout@v3 - - name: Determine whether this PR is from a fork - id: is-fork - run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${PR_NUMBER}" )" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.number || github.event.issue.number }} - run-bitrise-e2e-check: - needs: is-fork-pull-request runs-on: ubuntu-latest - if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} permissions: pull-requests: write contents: write diff --git a/.gitignore b/.gitignore index f25f95d5af6..092fc633fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ DerivedData project.xcworkspace ios/debug.xcconfig ios/release.xcconfig +.xcode.env.local # android / intellij build/ @@ -130,4 +131,8 @@ docs/assets/termsOfUse.html android/app/src/main/assets/modules.json # Google firebase base64 derived configs -**/GoogleService-Info.plist \ No newline at end of file +**/GoogleService-Info.plist +# Expo +.expo +dist/ +web-build/ diff --git a/.iyarc b/.iyarc index e69de29bb2d..e4f2229aec5 100644 --- a/.iyarc +++ b/.iyarc @@ -0,0 +1,2 @@ +# Added Temp to Pass Ci +GHSA-3xgq-45jj-v275 diff --git a/.js.env.example b/.js.env.example index 1c11f591536..56c7c1bb865 100644 --- a/.js.env.example +++ b/.js.env.example @@ -1,16 +1,15 @@ # Sign up and generate your own keys at pubnub.com # Then rename this file to ".js.env" and rebuild the app -# +# # In order for this feature to work properly, you need to # build metamask-extension from source (https://github.com/MetaMask/metamask-extension) # and set your the same values there. -# +# # For more info take a look at https://github.com/MetaMask/metamask-extension/pull/5955 export MM_PUBNUB_SUB_KEY="" export MM_PUBNUB_PUB_KEY="" export MM_OPENSEA_KEY="" -export MM_ETHERSCAN_KEY="" export MM_FOX_CODE="EXAMPLE_FOX_CODE" # NOTE: Non-MetaMask only, will need to create an account and generate @@ -70,6 +69,10 @@ export SEGMENT_FLUSH_EVENT_LIMIT="1" # URL of security alerts API used to validate dApp requests. export SECURITY_ALERTS_API_URL="https://security-alerts.api.cx.metamask.io" +# Enable Portfolio View +export PORTFOLIO_VIEW="true" + + # Temporary mechanism to enable security alerts API prior to release. export MM_SECURITY_ALERTS_API_ENABLED="true" # Firebase @@ -81,7 +84,7 @@ export FCM_CONFIG_MESSAGING_SENDER_ID="" export FCM_CONFIG_APP_ID="" export GOOGLE_SERVICES_B64_ANDROID="" export GOOGLE_SERVICES_B64_IOS="" -#Notifications Feature Announcements +# Notifications Feature Announcements export FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= export FEATURES_ANNOUNCEMENTS_SPACE_ID= @@ -96,8 +99,8 @@ export MM_PER_DAPP_SELECTED_NETWORK="" export MM_CHAIN_PERMISSIONS="" -#Multichain feature flag specific to UI changes +# Multichain feature flag specific to UI changes export MM_MULTICHAIN_V1_ENABLED="" -#Permissions Settings feature flag specific to UI changes +# Permissions Settings feature flag specific to UI changes export MM_PERMISSIONS_SETTINGS_V1_ENABLED="" diff --git a/CHANGELOG.md b/CHANGELOG.md index 02597c48457..8a753df77c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,84 @@ ## Current Main Branch -## 7.37.0 - Nov 28, 2024 +## 7.38.0 - Jan 16, 2024 +### Added +- [#12427](https://github.com/MetaMask/metamask-mobile/pull/12427): feat: implement remote feature flag controller (#12427) +- [#12507](https://github.com/MetaMask/metamask-mobile/pull/12507): feat: activate portfolio view (#12507) +- [#12540](https://github.com/MetaMask/metamask-mobile/pull/12540): feat: migrate Base network RPC from https://mainnet.base.org to base-… (#12540) +- [#12505](https://github.com/MetaMask/metamask-mobile/pull/12505): feat: add aggregated portfolio balance cross chains (#12505) +- [#12417](https://github.com/MetaMask/metamask-mobile/pull/12417): feat: multichain detect tokens feat (#12417) +- [#12419](https://github.com/MetaMask/metamask-mobile/pull/12419): feat: upgrade transaction controller to get incoming transactions using accounts API (#12419) +- [#12537](https://github.com/MetaMask/metamask-mobile/pull/12537): feat: enable ledger clear signing feature (#12537) +- [#12622](https://github.com/MetaMask/metamask-mobile/pull/12622): feat: Hide the smart transaction status page if we return a txHash asap (#12622) +- [#12244](https://github.com/MetaMask/metamask-mobile/pull/12244): feat(ci): Expo (#12244) +- [#12459](https://github.com/MetaMask/metamask-mobile/pull/12459): feat: upgrade profile-sync-controller to 1.0.0 (#12459) +- [#12294](https://github.com/MetaMask/metamask-mobile/pull/12294): feat: Add Bitcoin accounts (Flask Only) (#12294) +- [#12243](https://github.com/MetaMask/metamask-mobile/pull/12243): feat: cicd e2e label requirements + pr automation (#12243) +- [#12495](https://github.com/MetaMask/metamask-mobile/pull/12495): feat: Support gas fee flows in swaps (#12495) +- [#12431](https://github.com/MetaMask/metamask-mobile/pull/12431): feat: multi chain asset list (#12431) + +### Changed +- [#12623](https://github.com/MetaMask/metamask-mobile/pull/12623): chore: update bug template to include feature branches (#12623) +- [#12538](https://github.com/MetaMask/metamask-mobile/pull/12538): chore: Chore/12435 mvp handle engine does not exist (#12538) +- [#12617](https://github.com/MetaMask/metamask-mobile/pull/12617): docs: Update README.md with new expo instructions (#12617) +- [#12559](https://github.com/MetaMask/metamask-mobile/pull/12559): test: move remaining modal pages and selectors to their respective folders (#12559) +- [#12556](https://github.com/MetaMask/metamask-mobile/pull/12556): test: remove redundent tests in quarantine folder (#12556) +- [#12558](https://github.com/MetaMask/metamask-mobile/pull/12558): test: Create e2e tag for multi chain (#12558) +- [#12531](https://github.com/MetaMask/metamask-mobile/pull/12531): test: Move files to Wallet folder (#12531) +- [#12511](https://github.com/MetaMask/metamask-mobile/pull/12511): test: Move files to Onboarding folder (#12511) +- [#12512](https://github.com/MetaMask/metamask-mobile/pull/12512): test: address regression pipeline slow down (#12512) +- [#12513](https://github.com/MetaMask/metamask-mobile/pull/12513): ci: disable security e2e tests (#12513) +- [#12602](https://github.com/MetaMask/metamask-mobile/pull/12602): chore: Additional e2e test to support `PortfolioView` (#12602) +- [#12321](https://github.com/MetaMask/metamask-mobile/pull/12321): refactor: remove global network from transaction controller (#12321) +- [#12536](https://github.com/MetaMask/metamask-mobile/pull/12536): test: fix mock server (#12536) +- [#12288](https://github.com/MetaMask/metamask-mobile/pull/12288): test: add e2e test for security alert api (#12288) +- [#12597](https://github.com/MetaMask/metamask-mobile/pull/12597): test(3615): additional e2e scenarios editing permissions and non permitted networks (#12597) +- [#12488](https://github.com/MetaMask/metamask-mobile/pull/12488): test(3615): add new e2e test for initial dapp connection and non permitted flow (#12488) +- [#12532](https://github.com/MetaMask/metamask-mobile/pull/12532): refactor: de-anonymize insensitive properties of swaps events (#12532) +- [#12485](https://github.com/MetaMask/metamask-mobile/pull/12485): chore: Stop suppressing pod install failures (#12485) +- [#12574](https://github.com/MetaMask/metamask-mobile/pull/12574): chore: Add option to skip pod install setup step (#12574) +- [#12609](https://github.com/MetaMask/metamask-mobile/pull/12609): chore: update user storage E2E framework (#12609) +- [#12569](https://github.com/MetaMask/metamask-mobile/pull/12569): chore: transfer ownership of auth & profile sync E2E from notifications to identity (#12569) +- [#12534](https://github.com/MetaMask/metamask-mobile/pull/12534): chore: change ownership of profile sync from notifications to identity (#12534) +- [#12543](https://github.com/MetaMask/metamask-mobile/pull/12543): chore: Decrease hot and cold start app to wallet view time (#12543) +- [#12428](https://github.com/MetaMask/metamask-mobile/pull/12428): chore: Add eth hd keyring and key tree to decrease unlock time (#12428) +- [#12555](https://github.com/MetaMask/metamask-mobile/pull/12555): chore: Update accounts packages (#12555) +- [#12563](https://github.com/MetaMask/metamask-mobile/pull/12563): chore: cicd e2e hardening (#12563) +- [#12554](https://github.com/MetaMask/metamask-mobile/pull/12554): chore: fail status when on no labels for retro-label changes (#12554) +- [#12295](https://github.com/MetaMask/metamask-mobile/pull/12295): chore: use getShares contract method from stake-sdk for unstake all flow (#12295) +- [#12551](https://github.com/MetaMask/metamask-mobile/pull/12551): chore: Bump Snaps packages (#12551) + +### Fixed +- [#12650](https://github.com/MetaMask/metamask-mobile/pull/12650): fix: fix swaps button on asset overview page for multichain feature (#12650) +- [#12659](https://github.com/MetaMask/metamask-mobile/pull/12659): fix: fix token details navigation (#12659) +- [#12624](https://github.com/MetaMask/metamask-mobile/pull/12624): fix: add new translations (#12624) +- [#12373](https://github.com/MetaMask/metamask-mobile/pull/12373): fix: circular dependencies engine-network-handleNetworkSwitch (#12373) +- [#12663](https://github.com/MetaMask/metamask-mobile/pull/12663): fix: disable flaky tests on incoming-transactions.spec (#12663) +- [#12598](https://github.com/MetaMask/metamask-mobile/pull/12598): fix: disable mock poc test (#12598) +- [#12230](https://github.com/MetaMask/metamask-mobile/pull/12230): fix: Jest timer error in unit test (#12230) +- [#12626](https://github.com/MetaMask/metamask-mobile/pull/12626): fix: fix flaky test (#12626) +- [#12372](https://github.com/MetaMask/metamask-mobile/pull/12372): fix: abstract out circular dependencies between engine and networks util (#12372) +- [#12641](https://github.com/MetaMask/metamask-mobile/pull/12641): fix: fix network selector (#12641) +- [#12637](https://github.com/MetaMask/metamask-mobile/pull/12637): fix: fix native tokens filter when all networks is selected (#12637) +- [#12529](https://github.com/MetaMask/metamask-mobile/pull/12529): fix: fix NFTs disappearing after killing app (#12529) +- [#12562](https://github.com/MetaMask/metamask-mobile/pull/12562): fix: Move `AssetPollingProvider` from Root to Nav/Main/index.js (#12562) +- [#12607](https://github.com/MetaMask/metamask-mobile/pull/12607): fix: e2e regression gas api (#12607) +- [#12460](https://github.com/MetaMask/metamask-mobile/pull/12460): fix: add source when local PPOM fails (#12460) +- [#12199](https://github.com/MetaMask/metamask-mobile/pull/12199): fix: 10967 User able to add Ledger account with existing account name (#12199) +- [#12566](https://github.com/MetaMask/metamask-mobile/pull/12566): fix(12527): sdk connection with unknown url causes a bug (#12566) +- [#12405](https://github.com/MetaMask/metamask-mobile/pull/12405): fix(431-2): active network icon has too much margin and adding optional prop (#12405) +- [#12591](https://github.com/MetaMask/metamask-mobile/pull/12591): fix: add resolution for express to fix failing audit on path-to-regexp (#12591) +- [#12567](https://github.com/MetaMask/metamask-mobile/pull/12567): fix: update input handling in useInputHandler to support BACK key functionality (#12567) +- [#12630](https://github.com/MetaMask/metamask-mobile/pull/12630): fix: hide tokens without balance for multichain (#12630) + +## 7.37.1 - Dec 16, 2024 +### Fixed +- [#12577](https://github.com/MetaMask/metamask-mobile/pull/12577): chore: bump {gas-fee,network,selected-network,notification-services,profile-sync,signature}-controller (#12577) +- [#12694](https://github.com/MetaMask/metamask-mobile/pull/12694): fix: small refactoring of the latest migration script + add a new migration case (#12694) +- [#12664](https://github.com/MetaMask/metamask-mobile/pull/12664): fix: mark transactions as failed for cancelled / unknown smart transactions (#12664) +## 7.37.0 - Nov 28, 2024 ### Added - [#12091](https://github.com/MetaMask/metamask-mobile/pull/12091): feat: 2020 Add a performance test for iOS in Bitrise (#12091) - [#12148](https://github.com/MetaMask/metamask-mobile/pull/12148): feat: Enable smart transactions for new users (#12148) @@ -15,16 +91,14 @@ - [#12393](https://github.com/MetaMask/metamask-mobile/pull/12393): feat: Creating data tree for signed type V1 signatures (#12393) - [#12160](https://github.com/MetaMask/metamask-mobile/pull/12160): feat: Integrate NFT api to display image & names in simulations includes `erc721`s (#12160) - [#12324](https://github.com/MetaMask/metamask-mobile/pull/12324): feat: confirmation re-designs add basic page for types sign V1 signature request (#12324) -- [#12452](https://github.com/MetaMask/metamask-mobile/pull/12452): [chore] Merge in feat: updated staking events to use withMetaMetrics helper (#12337) (#12452) - [#11424](https://github.com/MetaMask/metamask-mobile/pull/11424): feat: add workflow for updating automated test results in TestRail (#11424) -- [#12359](https://github.com/MetaMask/metamask-mobile/pull/12359): feat: v7.35.1 (#12359) -- [#12167](https://github.com/MetaMask/metamask-mobile/pull/12167): feat: v7.35.0 (#12167) - [#12337](https://github.com/MetaMask/metamask-mobile/pull/12337): feat: updated staking events to use withMetaMetrics helper (#12337) - [#12363](https://github.com/MetaMask/metamask-mobile/pull/12363): feat: add PooledStaking slice for managing staking state (#12363) - [#12398](https://github.com/MetaMask/metamask-mobile/pull/12398): feat: limit input digits to 12 in useInputHandler (#12398) - [#12344](https://github.com/MetaMask/metamask-mobile/pull/12344): feat: upgrade assets controllers to v44 (#12344) - [#12340](https://github.com/MetaMask/metamask-mobile/pull/12340): feat: upgrade assets controllers to version 43 (#12340) - [#12270](https://github.com/MetaMask/metamask-mobile/pull/12270): feat: upgrade assets controllers to 42 with multichain token rates (#12270) +- [#12452](https://github.com/MetaMask/metamask-mobile/pull/12452): feat: updated staking events to use withMetaMetrics helper (#12337) (#12452) ### Changed - [#12356](https://github.com/MetaMask/metamask-mobile/pull/12356): chore: Remove unnecessary event prop (#12356) @@ -41,7 +115,6 @@ - [#12415](https://github.com/MetaMask/metamask-mobile/pull/12415): chore: Cherry pick 2506358 (merge in trackEvent work) (#12415) - [#12238](https://github.com/MetaMask/metamask-mobile/pull/12238): chore: update codeowners (#12238) - [#12416](https://github.com/MetaMask/metamask-mobile/pull/12416): chore: Chore/update accounts controller messenger code owner (#12416) -- [#12313](https://github.com/MetaMask/metamask-mobile/pull/12313): fix: Remove run all tests section (#12313) - [#12366](https://github.com/MetaMask/metamask-mobile/pull/12366): chore: #12184 MVP split engine file (#12366) - [#12362](https://github.com/MetaMask/metamask-mobile/pull/12362): chore: Unit tests for tags approval controller undefined (#12362) - [#12343](https://github.com/MetaMask/metamask-mobile/pull/12343): chore: Cherry pick f35d583 (#12343) @@ -50,13 +123,14 @@ - [#12334](https://github.com/MetaMask/metamask-mobile/pull/12334): chore: updating filter icon (#12334) ### Fixed +- [#12313](https://github.com/MetaMask/metamask-mobile/pull/12313): fix: Remove run all tests section (#12313) - [#12489](https://github.com/MetaMask/metamask-mobile/pull/12489): fix: replace end of navigation init and UIStartup span (#12489) - [#12331](https://github.com/MetaMask/metamask-mobile/pull/12331): fix: tags pending approvals receiving undefined (#12331) - [#10486](https://github.com/MetaMask/metamask-mobile/pull/10486): fix: limit ReactNativeWebview message size (#10486) - [#12478](https://github.com/MetaMask/metamask-mobile/pull/12478): fix: incorrect event source in analytics and connection (#12478) - [#10786](https://github.com/MetaMask/metamask-mobile/pull/10786): fix: added icon to walletconnect metadata (#10786) - [#12455](https://github.com/MetaMask/metamask-mobile/pull/12455): fix: gas fee edit from swaps (#12455) -- [#12370](https://github.com/MetaMask/metamask-mobile/pull/12370): "fix: Fix copy of ""Network fee"" on approval (#12370)" +- [#12370](https://github.com/MetaMask/metamask-mobile/pull/12370): fix: Fix copy of ""Network fee"" on approval (#12370) - [#12273](https://github.com/MetaMask/metamask-mobile/pull/12273): fix: Disable confirm button if `transactionMeta` is undefined (#12273) - [#12367](https://github.com/MetaMask/metamask-mobile/pull/12367): fix: app crashing after send or swap (#12367) - [#12446](https://github.com/MetaMask/metamask-mobile/pull/12446): fix: update wallet_addEthereumChain.js with correct MetricsEventBuilder (#12446) @@ -67,23 +141,73 @@ - [#12371](https://github.com/MetaMask/metamask-mobile/pull/12371): fix: fix patch missing variable sentry error (#12371) - [#12375](https://github.com/MetaMask/metamask-mobile/pull/12375): fix: breaking selector due to missing controller state (#12375) -### Other -- [#12374](https://github.com/MetaMask/metamask-mobile/pull/12374): perf: Remove costly reduce operation for generating Engine context (#12374) -- [#12345](https://github.com/MetaMask/metamask-mobile/pull/12345): chore: bump walletconnect/* deps (#12345) -- [#Daniel](Daniel): "feat: Support returning a txHash asap -- [#EtherWizard33](EtherWizard33): "feat: non-permissioned networks -- [#12474](https://github.com/MetaMask/metamask-mobile/pull/12474): chore: bump `@metamask/signature-controller` to `^22.0.0` (#12474) -- [#12472](https://github.com/MetaMask/metamask-mobile/pull/12472): chore: bump `@metamask/preferences-controller` to `^14.0.0` (#12472) -- [#Michele Esposito](Michele Esposito): "chore(deps): bump `@metamask/{swaps -- [#12003](https://github.com/MetaMask/metamask-mobile/pull/12003): build(deps): bump `@metamask/smart-transaction-controller` to `^14.0.0` (#12003) -- [#12004](https://github.com/MetaMask/metamask-mobile/pull/12004): build(deps): bump `@metamask/selected-network-controller` to `^18.0.2` (#12004) -- [#12471](https://github.com/MetaMask/metamask-mobile/pull/12471): chore(runway): cherry-pick fix: gas fee edit from swaps (#12471) -- [#12453](https://github.com/MetaMask/metamask-mobile/pull/12453): chore(runway): cherry-pick fix: update wallet_addEthereumChain.js with correct MetricsEventBuilder (#12453) -- [#12361](https://github.com/MetaMask/metamask-mobile/pull/12361): chore(runway): cherry-pick fix: tags pending approvals receiving undefined (#12361) -- [#12335](https://github.com/MetaMask/metamask-mobile/pull/12335): chore: cherrypick do not show staked eth balance when balance is zero on homepage or asset detail (#12335) -- [#12414](https://github.com/MetaMask/metamask-mobile/pull/12414): chore(runway): cherry-pick fix: breaking selector due to missing controller state (#12414) -- [#12349](https://github.com/MetaMask/metamask-mobile/pull/12349): fix: Remove duplicate notifications controllers entries in `EngineService` (#12349) +## 7.36.0 - Nov 15, 2024 +### Added +- [#12015](https://github.com/MetaMask/metamask-mobile/pull/12015): feat: 1957 crash screen redesign (#12015) +- [#12186](https://github.com/MetaMask/metamask-mobile/pull/12186): feat (cherry-pick): display staking transaction methods (#12110) (#12186) +- [#12110](https://github.com/MetaMask/metamask-mobile/pull/12110): feat: display staking transaction methods (#12110) +- [#12290](https://github.com/MetaMask/metamask-mobile/pull/12290): feat: STAKE-827: track additional pooled staking events (#12290) +- [#12280](https://github.com/MetaMask/metamask-mobile/pull/12280): feat: add loading skeleton for staking banners (#12280) +- [#12245](https://github.com/MetaMask/metamask-mobile/pull/12245): feat: add gas impact modal to stake confirmation input view (#12245) +- [#12263](https://github.com/MetaMask/metamask-mobile/pull/12263): feat: conditionally display stake/earn text based on pooled staking feature flag (#12261) (#12263) +- [#12146](https://github.com/MetaMask/metamask-mobile/pull/12146): feat: add staked ETH to metamask mobile homepage and account list menu (#12146) +- [#12261](https://github.com/MetaMask/metamask-mobile/pull/12261): feat: conditionally display stake/earn text based on pooled staking feature flag (#12261) +- [#12247](https://github.com/MetaMask/metamask-mobile/pull/12247): feat: update input colors and text formatting (#12247) +- [#12210](https://github.com/MetaMask/metamask-mobile/pull/12210): chore: disable pooled staking feature flag (#12210) +- [#12144](https://github.com/MetaMask/metamask-mobile/pull/12144): feat: add staking events (#12144) +- [#12268](https://github.com/MetaMask/metamask-mobile/pull/12268): feat: multichain currency rate polling (#12268) +- [#11808](https://github.com/MetaMask/metamask-mobile/pull/11808): feat: Token Network Filter UI [Mobile] (#11808) +- [#12171](https://github.com/MetaMask/metamask-mobile/pull/12171): feat: multichain polling hook (#12171) +- [#12168](https://github.com/MetaMask/metamask-mobile/pull/12168): feat(2808): improvements-and-small-features-and-small-fixes-that-still-needed-to-be-added-to-edit-permissions (#12168) +- [#11590](https://github.com/MetaMask/metamask-mobile/pull/11590): feat(2796): permission settings replace some of the mock data by real data (#11590) +- [#11511](https://github.com/MetaMask/metamask-mobile/pull/11511): feat: display snap name (#11511) +- [#12145](https://github.com/MetaMask/metamask-mobile/pull/12145): feat: disable wallet buttons for accounts that cannot sign transactions (#12145) +- [#12057](https://github.com/MetaMask/metamask-mobile/pull/12057): feat: team-label-token (#12057) +- [#11836](https://github.com/MetaMask/metamask-mobile/pull/11836): feat: upgrade @metamask/eth-ledger-bridge-keyring (#11836) +### Changed +- [#11898](https://github.com/MetaMask/metamask-mobile/pull/11898): chore: New Crowdin translations by Github Action (#11898) +- [#12292](https://github.com/MetaMask/metamask-mobile/pull/12292): chore: Allow for higher versions of ruby (#12292) +- [#12291](https://github.com/MetaMask/metamask-mobile/pull/12291): chore: Remove notifications logic from wallet view (#12276) (#12291) +- [#12271](https://github.com/MetaMask/metamask-mobile/pull/12271): chore: Cache node installed via nvm on Bitrise (#12271) +- [#12121](https://github.com/MetaMask/metamask-mobile/pull/12121): chore: udpate LSMinimumSystemVersion (#12121) +- [#11658](https://github.com/MetaMask/metamask-mobile/pull/11658): chore: 8618 reduce enzyme usage in unit test by 25 (#11658) +- [#12257](https://github.com/MetaMask/metamask-mobile/pull/12257): refactor: remove global network usage from petnames (#12257) +- [#11996](https://github.com/MetaMask/metamask-mobile/pull/11996): chore: upgrade signature controller to remove global network (#11996) +- [#12274](https://github.com/MetaMask/metamask-mobile/pull/12274): chore: Update naming for returning a txHash asap for smart transactions (#12274) +- [#12287](https://github.com/MetaMask/metamask-mobile/pull/12287): docs: update onboarding readme (#12287) +- [#12234](https://github.com/MetaMask/metamask-mobile/pull/12234): chore: add unit test for native currency validation (#12234) +- [#12237](https://github.com/MetaMask/metamask-mobile/pull/12237): chore: Remove GoogleService files from git cache (#12237) +- [#12178](https://github.com/MetaMask/metamask-mobile/pull/12178): chore: upgrade assets-controllers to v41 (#12178) +- [#12209](https://github.com/MetaMask/metamask-mobile/pull/12209): chore: Modify gitignore to include generated ios/plist files (#12209) +- [#12286](https://github.com/MetaMask/metamask-mobile/pull/12286): chore: Add tags to UI Startup sentry transaction (#12286) +- [#12276](https://github.com/MetaMask/metamask-mobile/pull/12276): chore: Remove notifications logic from wallet view (#12276) +- [#12174](https://github.com/MetaMask/metamask-mobile/pull/12174): chore: Remove navigation instrumentation (#12174) +- [#12211](https://github.com/MetaMask/metamask-mobile/pull/12211): chore: disable pooled staking release for v7.35.0 (#12211) +- [#12194](https://github.com/MetaMask/metamask-mobile/pull/12194): chore: cicd error handling (#12194) +- [#12192](https://github.com/MetaMask/metamask-mobile/pull/12192): chore: fix release pr fixes (#12192) +- [#12175](https://github.com/MetaMask/metamask-mobile/pull/12175): chore: cicd - propagate changes to release pr from scripts (#12175) +- [#12225](https://github.com/MetaMask/metamask-mobile/pull/12225): chore: bump `@metamask/ppom-validator` to `0.35.1` (#12225) + +### Fixed +- [#12166](https://github.com/MetaMask/metamask-mobile/pull/12166): fix: remove SmokeNotifications tests for android on smoke tests pipeline (#12166) +- [#12217](https://github.com/MetaMask/metamask-mobile/pull/12217): fix: e2e: use different wallet SRP for non accounts tests (#12217) +- [#12197](https://github.com/MetaMask/metamask-mobile/pull/12197): fix: E2E: quarantine import-wallet-account tests (#12197) +- [#12250](https://github.com/MetaMask/metamask-mobile/pull/12250): fix: Add migration to fix NotificationServicesController bug (#12219) (#12250) +- [#12232](https://github.com/MetaMask/metamask-mobile/pull/12232): fix: e2e re-enable notifications android workflow (#12232) +- [#12219](https://github.com/MetaMask/metamask-mobile/pull/12219): fix: Add migration to fix NotificationServicesController bug (#12219) +- [#12120](https://github.com/MetaMask/metamask-mobile/pull/12120): fix: Onboarding failing biometrics locks screen for user instead of disabling biometrics and continuing with the onboarding (#12120) +- [#12177](https://github.com/MetaMask/metamask-mobile/pull/12177): fix: Create migration 59 to fix undefined selectedAccount (#12177) +- [#12311](https://github.com/MetaMask/metamask-mobile/pull/12311): fix: transaction reject crash (#12311) +- [#12228](https://github.com/MetaMask/metamask-mobile/pull/12228): fix: Update `transaction-controller` version (#12228) +- [#12100](https://github.com/MetaMask/metamask-mobile/pull/12100): fix: hide internal transaction origins in confirmation views (#12100) +- [#12283](https://github.com/MetaMask/metamask-mobile/pull/12283): fix: ensure unstake max will unstake all user shares (#12283) +- [#12231](https://github.com/MetaMask/metamask-mobile/pull/12231): fix: added ScrollView to stake confirmation review screen (#12231) +- [#12255](https://github.com/MetaMask/metamask-mobile/pull/12255): fix: fix displayed selected rpc for linea (#12255) +- [#11693](https://github.com/MetaMask/metamask-mobile/pull/11693): fix: relax network symbol length validation (#11693) +- [#12205](https://github.com/MetaMask/metamask-mobile/pull/12205): fix: add contractBalances as dependency (#12205) +- [#12235](https://github.com/MetaMask/metamask-mobile/pull/12235): fix: privacy mode is enabled in account selector by params (#12235) +- [#12282](https://github.com/MetaMask/metamask-mobile/pull/12282): fix: Lock ruby version to 3.1.6 and bump pod to 1.16.2 (#12282) ## 7.35.1 - Nov 20, 2024 ### Fixed diff --git a/README.md b/README.md index 92c2ef0d35a..2f47d76233d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ To learn how to contribute to the MetaMask codebase, visit our [Contributor Docs ## Documentation - [Architecture](./docs/readme/architecture.md) -- [Development Environment Setup](./docs/readme/environment.md) +- [Expo Development Environment Setup](./docs/readme/expo-environment.md) +- [Native Development Environment Setup](./docs/readme/environment.md) - [Build Troubleshooting](./docs/readme/troubleshooting.md) - [Testing](./docs/readme/testing.md) - [Debugging](./docs/readme/debugging.md) @@ -24,13 +25,67 @@ To learn how to contribute to the MetaMask codebase, visit our [Contributor Docs ## Getting started -### Environment setup +### Using Expo (recommended) -Before running the app, make sure your development environment has all the required tools. Several of these tools (ie Node and Ruby) may require specific versions in order to successfully build the app. +Expo is the fastest way to start developing. With the Expo framework, developers don't need to compile the native side of the application as before, hence no need for any native enviornment setup, developers only need to download a precompiled develpoment build and run the javascript bundler. The development build will then connect with the bundler to load the javascript code. + +#### Expo Environment Setup + +[Install node, yarn and watchman.](./docs/readme/expo-environment.md) + +#### Clone the project + +```bash +git clone git@github.com:MetaMask/metamask-mobile.git && \ +cd metamask-mobile +``` + +#### Install dependencies + +```bash +yarn setup:expo +``` + +#### Run the bundler + +```bash +yarn watch +``` + +#### Download and install the development build + +#### For internal developers +- Access Runway via Okta and go to the Expo bucket either on the iOS or Android section. From there you will see the available development builds (android-expo-dev-build.apk or ios-expo-dev-build.ipa). +- For Android: + - Install the .apk on your Android device or simulator. +- For iOS: + - Device: you need to have your iPhone registered with our Apple dev account. If you have it, you can install the .ipa on your device. + - Simulator: please follow the [native development section](https://github.com/MetaMask/metamask-mobile?tab=readme-ov-file#native-development) and run `yarn setup` and `yarn start:ios` as the .ipa will not work for now, we are working on having an .app that works on simulators. + +##### [SOON] For external developers (we are testing the new dev builds and will make them publicly available soon after) + +#### Load the app + +If on a simulator: +- use the initial expo screen that appears when starting the development to choose the bundler url +- OR press "a" for Android or "i" for iOS on the terminal where the bundler is running + +If on a physical device: +- Use the camera app to scan the QR code presented by the bundler running on the terminal + +That's it! This will work for any javascript development, if you need to develop or modify native code please see the next section. + +### Native Development + +If developing or modifying native code or installing any library that introduces or uses native code, it is not possible to use an Expo precompiled development build as you need to compile the native side of the application again. To do so, please follow the steps stated in this section. + +#### Native Environment setup + +Before running the app for native development, make sure your development environment has all the required tools. Several of these tools (ie Node and Ruby) may require specific versions in order to successfully build the app. [Setup your development environment](./docs/readme/environment.md) -### Building the app +#### Building the app **Clone the project** @@ -39,16 +94,16 @@ git clone git@github.com:MetaMask/metamask-mobile.git && \ cd metamask-mobile ``` -#### Firebase Messaging Setup +##### Firebase Messaging Setup MetaMask uses Firebase Cloud Messaging (FCM) to enable app communications. To integrate FCM, you’ll need configuration files for both iOS and Android platforms. -##### Internal Contributor instructions +###### Internal Contributor instructions 1. Grab the `.js.env` file from 1Password, ask around for the correct vault. This file contains the `GOOGLE_SERVICES_B64_ANDROID` and `GOOGLE_SERVICES_B64_IOS` secrets that will be used to generate the relevant configuration files for IOS/Android. 2. [Install](./README.md#install-dependencies) and [run & start](./README.md#running-the-app) the application as documented below. -##### External Contributor instructions +###### External Contributor instructions As an external contributor, you need to provide your own Firebase project configuration files: - **`GoogleService-Info.plist`** (iOS) @@ -77,7 +132,7 @@ export GOOGLE_SERVICES_B64_IOS="$(base64 -w0 -i ./ios/GoogleServices/GoogleServi In case of any doubt, please follow the instructions in the link below to get your Firebase project config file. [Firebase Project Quickstart](https://firebaseopensource.com/projects/firebase/quickstart-js/messaging/readme/#getting_started) -#### Install dependencies +##### Install dependencies ```bash yarn setup @@ -85,7 +140,7 @@ yarn setup _Not the usual install command, this will run scripts and a lengthy postinstall flow_ -### Running the app +#### Running the app for native development **Run Metro bundler** diff --git a/android/app/build.gradle b/android/app/build.gradle index 5e1076971a0..d934382fa02 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,6 +47,11 @@ react { // // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" // hermesFlags = ["-O", "-output-source-map"] + // + // Added by install-expo-modules + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", rootDir.getAbsoluteFile().getParentFile().getAbsolutePath(), "android", "absolute"].execute(null, rootDir).text.trim()) + cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" } // Override default React Native to generate source maps for Hermes @@ -173,8 +178,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.37.1" - versionCode 1520 + versionName "7.38.0" + versionCode 1528 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bd03aa9f0cf..2770038a620 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -76,6 +76,7 @@ + @@ -114,6 +115,12 @@ + + + + + + diff --git a/android/app/src/main/java/io/metamask/MainActivity.java b/android/app/src/main/java/io/metamask/MainActivity.java index 1e1832d74cd..7bc4f9a2b65 100644 --- a/android/app/src/main/java/io/metamask/MainActivity.java +++ b/android/app/src/main/java/io/metamask/MainActivity.java @@ -1,4 +1,5 @@ package io.metamask; +import expo.modules.ReactActivityDelegateWrapper; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; @@ -62,7 +63,7 @@ public void onNewIntent(Intent intent) { */ @Override protected ReactActivityDelegate createReactActivityDelegate() { - return new DefaultReactActivityDelegate(this, getMainComponentName(), DefaultNewArchitectureEntryPoint.getFabricEnabled()) { + return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate(this, getMainComponentName(), DefaultNewArchitectureEntryPoint.getFabricEnabled()) { @Override protected Bundle getLaunchOptions() { Bundle initialProperties = new Bundle(); @@ -73,6 +74,6 @@ protected Bundle getLaunchOptions() { } return initialProperties; } - }; + }); } } diff --git a/android/app/src/main/java/io/metamask/MainApplication.java b/android/app/src/main/java/io/metamask/MainApplication.java index 42be05c5b37..d3359c371db 100644 --- a/android/app/src/main/java/io/metamask/MainApplication.java +++ b/android/app/src/main/java/io/metamask/MainApplication.java @@ -1,4 +1,7 @@ package io.metamask; +import android.content.res.Configuration; +import expo.modules.ApplicationLifecycleDispatcher; +import expo.modules.ReactNativeHostWrapper; import android.app.Application; import com.facebook.react.ReactApplication; @@ -37,7 +40,7 @@ public String getFileProviderAuthority() { return BuildConfig.APPLICATION_ID + ".provider"; } - private final ReactNativeHost mReactNativeHost = new DefaultReactNativeHost(this) { + private final ReactNativeHost mReactNativeHost = new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) { @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; @@ -68,9 +71,9 @@ protected Boolean isHermesEnabled() { @Override protected String getJSMainModuleName() { - return "index"; + return ".expo/.virtual-metro-entry"; } - }; + }); @Override public ReactNativeHost getReactNativeHost() { @@ -112,5 +115,12 @@ public void onCreate() { } ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + ApplicationLifecycleDispatcher.onApplicationCreate(this); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); } } diff --git a/android/app/src/main/res/xml/react_native_config.xml b/android/app/src/main/res/xml/react_native_config.xml index 313da0eedad..aa2ca91bf79 100644 --- a/android/app/src/main/res/xml/react_native_config.xml +++ b/android/app/src/main/res/xml/react_native_config.xml @@ -6,5 +6,5 @@ 10.0.2.2 10.0.3.2 - + diff --git a/android/settings.gradle b/android/settings.gradle index 8e99b8ad9ae..1ba3878ea62 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -20,3 +20,6 @@ include ':react-native-gesture-handler' project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') include ':react-native-video' project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer') + +apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") +useExpoModules() \ No newline at end of file diff --git a/app.config.js b/app.config.js new file mode 100644 index 00000000000..f1f87b695a4 --- /dev/null +++ b/app.config.js @@ -0,0 +1,23 @@ +module.exports = { + name: 'MetaMask', + displayName: 'MetaMask', + plugins: [ + [ + 'expo-build-properties', + { + android: { + extraMavenRepos: [ + '../../node_modules/@notifee/react-native/android/libs' + ] + }, + ios: {} + } + ], + [ + '@config-plugins/detox', + { + subdomains: '*' + } + ] + ] +}; diff --git a/app.json b/app.json deleted file mode 100644 index 213fa9da6b0..00000000000 --- a/app.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "MetaMask", - "displayName": "MetaMask" -} diff --git a/app/actions/identity/constants/errors.ts b/app/actions/identity/constants/errors.ts new file mode 100644 index 00000000000..b4b7537dfad --- /dev/null +++ b/app/actions/identity/constants/errors.ts @@ -0,0 +1,8 @@ +export enum identityErrors { + PERFORM_SIGN_IN = 'Error while trying to sign in', + PERFORM_SIGN_OUT = 'Error while trying to sign out', + ENABLE_PROFILE_SYNCING = 'Error while trying to enable profile syncing', + DISABLE_PROFILE_SYNCING = 'Error while trying to disable profile syncing', +} + +export default identityErrors; diff --git a/app/actions/identity/index.test.ts b/app/actions/identity/index.test.ts new file mode 100644 index 00000000000..53bd04ac1bc --- /dev/null +++ b/app/actions/identity/index.test.ts @@ -0,0 +1,51 @@ +import { performSignIn, performSignOut } from '.'; +import Engine from '../../core/Engine'; + +jest.mock('../../core/Engine', () => ({ + resetState: jest.fn(), + context: { + AuthenticationController: { + performSignIn: jest.fn(), + performSignOut: jest.fn(), + getSessionProfile: jest.fn(), + }, + }, +})); + +describe('Identity actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('signs in successfully and obtain profile', async () => { + ( + Engine.context.AuthenticationController.performSignIn as jest.Mock + ).mockResolvedValue('valid-access-token'); + ( + Engine.context.AuthenticationController.getSessionProfile as jest.Mock + ).mockResolvedValue('valid-profile'); + + const result = await performSignIn(); + + expect( + Engine.context.AuthenticationController.performSignIn, + ).toHaveBeenCalled(); + expect( + Engine.context.AuthenticationController.getSessionProfile, + ).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('signs out successfully', async () => { + ( + Engine.context.AuthenticationController.performSignOut as jest.Mock + ).mockResolvedValue(undefined); + + const result = await performSignOut(); + + expect( + Engine.context.AuthenticationController.performSignOut, + ).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); +}); diff --git a/app/actions/identity/index.ts b/app/actions/identity/index.ts new file mode 100644 index 00000000000..3a4605975ba --- /dev/null +++ b/app/actions/identity/index.ts @@ -0,0 +1,53 @@ +import { getErrorMessage } from '@metamask/utils'; +import Engine from '../../core/Engine'; +import identityErrors from './constants/errors'; + +export const performSignIn = async () => { + try { + const accessToken = + await Engine.context.AuthenticationController.performSignIn(); + if (!accessToken) { + return getErrorMessage(identityErrors.PERFORM_SIGN_IN); + } + + const profile = + await Engine.context.AuthenticationController.getSessionProfile(); + if (!profile) { + return getErrorMessage(identityErrors.PERFORM_SIGN_IN); + } + } catch (error) { + return getErrorMessage(error); + } +}; + +export const performSignOut = async () => { + try { + await Engine.context.AuthenticationController.performSignOut(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const enableProfileSyncing = async () => { + try { + await Engine.context.UserStorageController.enableProfileSyncing(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const disableProfileSyncing = async () => { + try { + await Engine.context.UserStorageController.disableProfileSyncing(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const syncInternalAccountsWithUserStorage = async () => { + try { + await Engine.context.UserStorageController.syncInternalAccountsWithUserStorage(); + } catch (error) { + return getErrorMessage(error); + } +}; diff --git a/app/actions/multichain/state.ts b/app/actions/multichain/state.ts new file mode 100644 index 00000000000..643638e0e91 --- /dev/null +++ b/app/actions/multichain/state.ts @@ -0,0 +1,4 @@ +export interface MultichainSettingsState { + bitcoinSupportEnabled: boolean; + bitcoinTestnetSupportEnabled: boolean; +} diff --git a/app/actions/navigation/index.ts b/app/actions/navigation/index.ts index 7a6fac7e9a0..b4c82b9a98e 100644 --- a/app/actions/navigation/index.ts +++ b/app/actions/navigation/index.ts @@ -1,18 +1,28 @@ /* eslint-disable import/prefer-default-export */ import { - SET_CURRENT_ROUTE, - SET_CURRENT_BOTTOM_NAV_ROUTE, -} from '../../reducers/navigation'; + type OnNavigationReadyAction, + type SetCurrentRouteAction, + type SetCurrentBottomNavRouteAction, + NavigationActionType, +} from './types'; -/** - * Action Creators - */ -export const setCurrentRoute = (route: string) => ({ - type: SET_CURRENT_ROUTE, +export * from './types'; + +export const setCurrentRoute = (route: string): SetCurrentRouteAction => ({ + type: NavigationActionType.SET_CURRENT_ROUTE, payload: { route }, }); -export const setCurrentBottomNavRoute = (route: string) => ({ - type: SET_CURRENT_BOTTOM_NAV_ROUTE, +export const setCurrentBottomNavRoute = ( + route: string, +): SetCurrentBottomNavRouteAction => ({ + type: NavigationActionType.SET_CURRENT_BOTTOM_NAV_ROUTE, payload: { route }, }); + +/** + * Action that is called when navigation is ready + */ +export const onNavigationReady = (): OnNavigationReadyAction => ({ + type: NavigationActionType.ON_NAVIGATION_READY, +}); diff --git a/app/actions/navigation/types.ts b/app/actions/navigation/types.ts new file mode 100644 index 00000000000..c57beba69da --- /dev/null +++ b/app/actions/navigation/types.ts @@ -0,0 +1,31 @@ +import { type Action } from 'redux'; + +/** + * Navigation action type enum + */ +export enum NavigationActionType { + ON_NAVIGATION_READY = 'ON_NAVIGATION_READY', + SET_CURRENT_ROUTE = 'SET_CURRENT_ROUTE', + SET_CURRENT_BOTTOM_NAV_ROUTE = 'SET_CURRENT_BOTTOM_NAV_ROUTE', +} + +export type OnNavigationReadyAction = + Action; + +export type SetCurrentRouteAction = + Action & { + payload: { route: string }; + }; + +export type SetCurrentBottomNavRouteAction = + Action & { + payload: { route: string }; + }; + +/** + * Navigation action + */ +export type NavigationAction = + | OnNavigationReadyAction + | SetCurrentRouteAction + | SetCurrentBottomNavRouteAction; diff --git a/app/actions/notification/constants/index.ts b/app/actions/notification/constants/index.ts index ee8c63fdffa..5c009930692 100644 --- a/app/actions/notification/constants/index.ts +++ b/app/actions/notification/constants/index.ts @@ -1,8 +1,4 @@ export enum notificationsErrors { - PERFORM_SIGN_IN = 'Error while trying to sign in', - PERFORM_SIGN_OUT = 'Error while trying to sign out', - ENABLE_PROFILE_SYNCING = 'Error while trying to enable profile syncing', - DISABLE_PROFILE_SYNCING = 'Error while trying to disable profile syncing', ENABLE_PUSH_NOTIFICATIONS = 'Error while trying to enable push notifications', DISABLE_PUSH_NOTIFICATIONS = 'Error while trying to disable push notifications', CHECK_ACCOUNTS_PRESENCE = 'Error while trying to check accounts presence', diff --git a/app/actions/notification/helpers/index.test.tsx b/app/actions/notification/helpers/index.test.tsx index 8fd7a7ea805..ddba30413d7 100644 --- a/app/actions/notification/helpers/index.test.tsx +++ b/app/actions/notification/helpers/index.test.tsx @@ -1,20 +1,15 @@ // Import necessary libraries and modules -import { signIn, signOut, enableNotificationServices, disableNotificationServices } from '.'; +import { enableNotificationServices, disableNotificationServices } from '.'; import Engine from '../../../core/Engine'; jest.mock('../../../core/Engine', () => ({ resetState: jest.fn(), context: { - AuthenticationController: { - performSignIn: jest.fn(), - performSignOut: jest.fn(), - getSessionProfile: jest.fn(), - }, NotificationServicesController: { - enableMetamaskNotifications:jest.fn(), - disableNotificationServices:jest.fn(), + enableMetamaskNotifications: jest.fn(), + disableNotificationServices: jest.fn(), checkAccountsPresence: jest.fn(), - } + }, }, })); @@ -23,41 +18,31 @@ describe('Notification Helpers', () => { jest.clearAllMocks(); }); - it('signs in successfully and obtain profile', async () => { - (Engine.context.AuthenticationController.performSignIn as jest.Mock).mockResolvedValue('valid-access-token'); - (Engine.context.AuthenticationController.getSessionProfile as jest.Mock).mockResolvedValue('valid-profile'); - - const result = await signIn(); - - expect(Engine.context.AuthenticationController.performSignIn).toHaveBeenCalled(); - expect(Engine.context.AuthenticationController.getSessionProfile).toHaveBeenCalled(); - expect(result).toBeUndefined(); - }); - - it('signs out successfully', async () => { - (Engine.context.AuthenticationController.performSignOut as jest.Mock).mockResolvedValue(undefined); - - const result = await signOut(); - - expect(Engine.context.AuthenticationController.performSignOut).toHaveBeenCalled(); - expect(result).toBeUndefined(); - }); - it('enables notification services successfully', async () => { - (Engine.context.NotificationServicesController.enableMetamaskNotifications as jest.Mock).mockResolvedValue(undefined); + ( + Engine.context.NotificationServicesController + .enableMetamaskNotifications as jest.Mock + ).mockResolvedValue(undefined); const result = await enableNotificationServices(); - expect(Engine.context.NotificationServicesController.enableMetamaskNotifications).toHaveBeenCalled(); + expect( + Engine.context.NotificationServicesController.enableMetamaskNotifications, + ).toHaveBeenCalled(); expect(result).toBeUndefined(); }); it('disables notification services successfully', async () => { - (Engine.context.NotificationServicesController.disableNotificationServices as jest.Mock).mockResolvedValue(undefined); + ( + Engine.context.NotificationServicesController + .disableNotificationServices as jest.Mock + ).mockResolvedValue(undefined); const result = await disableNotificationServices(); - expect(Engine.context.NotificationServicesController.disableNotificationServices).toHaveBeenCalled(); + expect( + Engine.context.NotificationServicesController.disableNotificationServices, + ).toHaveBeenCalled(); expect(result).toBeUndefined(); }); }); diff --git a/app/actions/notification/helpers/index.ts b/app/actions/notification/helpers/index.ts index a52e524009a..7038cb02841 100644 --- a/app/actions/notification/helpers/index.ts +++ b/app/actions/notification/helpers/index.ts @@ -2,7 +2,11 @@ import { getErrorMessage } from '@metamask/utils'; import { notificationsErrors } from '../constants'; import Engine from '../../../core/Engine'; -import { Notification, mmStorage, getAllUUIDs } from '../../../util/notifications'; +import { + Notification, + mmStorage, + getAllUUIDs, +} from '../../../util/notifications'; import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs'; export type MarkAsReadNotificationsParam = Pick< @@ -10,48 +14,6 @@ export type MarkAsReadNotificationsParam = Pick< 'id' | 'type' | 'isRead' >[]; -export const signIn = async () => { - try { - const accessToken = - await Engine.context.AuthenticationController.performSignIn(); - if (!accessToken) { - return getErrorMessage(notificationsErrors.PERFORM_SIGN_IN); - } - - const profile = - await Engine.context.AuthenticationController.getSessionProfile(); - if (!profile) { - return getErrorMessage(notificationsErrors.PERFORM_SIGN_IN); - } - } catch (error) { - return getErrorMessage(error); - } -}; - -export const signOut = async () => { - try { - await Engine.context.AuthenticationController.performSignOut(); - } catch (error) { - return getErrorMessage(error); - } -}; - -export const enableProfileSyncing = async () => { - try { - await Engine.context.UserStorageController.enableProfileSyncing(); - } catch (error) { - return getErrorMessage(error); - } -}; - -export const disableProfileSyncing = async () => { - try { - await Engine.context.UserStorageController.disableProfileSyncing(); - } catch (error) { - return getErrorMessage(error); - } -}; - export const enableNotificationServices = async () => { try { await Engine.context.NotificationServicesController.enableMetamaskNotifications(); @@ -176,14 +138,6 @@ export const markMetamaskNotificationsAsRead = async ( } }; -export const syncInternalAccountsWithUserStorage = async () => { - try { - await Engine.context.UserStorageController.syncInternalAccountsWithUserStorage(); - } catch (error) { - return getErrorMessage(error); - } -}; - /** * Perform the deletion of the notifications storage key and the creation of on chain triggers to reset the notifications. * @@ -201,7 +155,11 @@ export const performDeleteStorage = async (): Promise => { return getErrorMessage(error); } }; -export const enablePushNotifications = async (userStorage: UserStorage, fcmToken?: string) => { + +export const enablePushNotifications = async ( + userStorage: UserStorage, + fcmToken?: string, +) => { try { const uuids = getAllUUIDs(userStorage); await Engine.context.NotificationServicesPushController.enablePushNotifications( @@ -224,7 +182,9 @@ export const disablePushNotifications = async (userStorage: UserStorage) => { } }; -export const updateTriggerPushNotifications = async (userStorage: UserStorage) => { +export const updateTriggerPushNotifications = async ( + userStorage: UserStorage, +) => { try { const uuids = getAllUUIDs(userStorage); await Engine.context.NotificationServicesPushController.updateTriggerPushNotifications( diff --git a/app/actions/user/index.js b/app/actions/user/index.js deleted file mode 100644 index fd996b8707f..00000000000 --- a/app/actions/user/index.js +++ /dev/null @@ -1,134 +0,0 @@ -// Constants -export const LOCKED_APP = 'LOCKED_APP'; -export const AUTH_SUCCESS = 'AUTH_SUCCESS'; -export const AUTH_ERROR = 'AUTH_ERROR'; -export const INTERRUPT_BIOMETRICS = 'INTERRUPT_BIOMETRICS'; -export const LOGIN = 'LOGIN'; -export const LOGOUT = 'LOGOUT'; - -export function interruptBiometrics() { - return { - type: INTERRUPT_BIOMETRICS, - }; -} - -export function lockApp() { - return { - type: LOCKED_APP, - }; -} - -export function authSuccess(bioStateMachineId) { - return { - type: AUTH_SUCCESS, - payload: { bioStateMachineId }, - }; -} - -export function authError(bioStateMachineId) { - return { - type: AUTH_ERROR, - payload: { bioStateMachineId }, - }; -} - -export function passwordSet() { - return { - type: 'PASSWORD_SET', - }; -} - -export function passwordUnset() { - return { - type: 'PASSWORD_UNSET', - }; -} - -export function seedphraseBackedUp() { - return { - type: 'SEEDPHRASE_BACKED_UP', - }; -} - -export function seedphraseNotBackedUp() { - return { - type: 'SEEDPHRASE_NOT_BACKED_UP', - }; -} - -export function backUpSeedphraseAlertVisible() { - return { - type: 'BACK_UP_SEEDPHRASE_VISIBLE', - }; -} - -export function backUpSeedphraseAlertNotVisible() { - return { - type: 'BACK_UP_SEEDPHRASE_NOT_VISIBLE', - }; -} - -export function protectWalletModalVisible() { - return { - type: 'PROTECT_MODAL_VISIBLE', - }; -} - -export function protectWalletModalNotVisible() { - return { - type: 'PROTECT_MODAL_NOT_VISIBLE', - }; -} - -export function loadingSet(loadingMsg) { - return { - type: 'LOADING_SET', - loadingMsg, - }; -} - -export function loadingUnset() { - return { - type: 'LOADING_UNSET', - }; -} - -export function setGasEducationCarouselSeen() { - return { - type: 'SET_GAS_EDUCATION_CAROUSEL_SEEN', - }; -} - -export function logIn() { - return { - type: LOGIN, - }; -} - -export function logOut() { - return { - type: LOGOUT, - }; -} - -export function setAppTheme(theme) { - return { - type: 'SET_APP_THEME', - payload: { theme }, - }; -} - -/** - * Temporary action to control auth flow - * - * @param {string} initialScreen - "login" or "onboarding" - * @returns - void - */ -export function checkedAuth(initialScreen) { - return { - type: 'CHECKED_AUTH', - payload: { - initialScreen, - }, - }; -} diff --git a/app/actions/user/index.ts b/app/actions/user/index.ts new file mode 100644 index 00000000000..9071fcffd50 --- /dev/null +++ b/app/actions/user/index.ts @@ -0,0 +1,161 @@ +import { type AppThemeKey } from '../../util/theme/models'; +import { + type InterruptBiometricsAction, + type LockAppAction, + type AuthSuccessAction, + type AuthErrorAction, + type PasswordSetAction, + type PasswordUnsetAction, + type SeedphraseBackedUpAction, + type SeedphraseNotBackedUpAction, + type BackUpSeedphraseVisibleAction, + type BackUpSeedphraseNotVisibleAction, + type ProtectModalVisibleAction, + type ProtectModalNotVisibleAction, + type LoadingSetAction, + type LoadingUnsetAction, + type SetGasEducationCarouselSeenAction, + type LoginAction, + type LogoutAction, + type SetAppThemeAction, + type CheckedAuthAction, + type PersistedDataLoadedAction, + UserActionType, +} from './types'; + +export * from './types'; + +export function interruptBiometrics(): InterruptBiometricsAction { + return { + type: UserActionType.INTERRUPT_BIOMETRICS, + }; +} + +export function lockApp(): LockAppAction { + return { + type: UserActionType.LOCKED_APP, + }; +} + +export function authSuccess(bioStateMachineId?: string): AuthSuccessAction { + return { + type: UserActionType.AUTH_SUCCESS, + payload: { bioStateMachineId }, + }; +} + +export function authError(bioStateMachineId?: string): AuthErrorAction { + return { + type: UserActionType.AUTH_ERROR, + payload: { bioStateMachineId }, + }; +} + +export function passwordSet(): PasswordSetAction { + return { + type: UserActionType.PASSWORD_SET, + }; +} + +export function passwordUnset(): PasswordUnsetAction { + return { + type: UserActionType.PASSWORD_UNSET, + }; +} + +export function seedphraseBackedUp(): SeedphraseBackedUpAction { + return { + type: UserActionType.SEEDPHRASE_BACKED_UP, + }; +} + +export function seedphraseNotBackedUp(): SeedphraseNotBackedUpAction { + return { + type: UserActionType.SEEDPHRASE_NOT_BACKED_UP, + }; +} + +export function backUpSeedphraseAlertVisible(): BackUpSeedphraseVisibleAction { + return { + type: UserActionType.BACK_UP_SEEDPHRASE_VISIBLE, + }; +} + +export function backUpSeedphraseAlertNotVisible(): BackUpSeedphraseNotVisibleAction { + return { + type: UserActionType.BACK_UP_SEEDPHRASE_NOT_VISIBLE, + }; +} + +export function protectWalletModalVisible(): ProtectModalVisibleAction { + return { + type: UserActionType.PROTECT_MODAL_VISIBLE, + }; +} + +export function protectWalletModalNotVisible(): ProtectModalNotVisibleAction { + return { + type: UserActionType.PROTECT_MODAL_NOT_VISIBLE, + }; +} + +export function loadingSet(loadingMsg: string): LoadingSetAction { + return { + type: UserActionType.LOADING_SET, + loadingMsg, + }; +} + +export function loadingUnset(): LoadingUnsetAction { + return { + type: UserActionType.LOADING_UNSET, + }; +} + +export function setGasEducationCarouselSeen(): SetGasEducationCarouselSeenAction { + return { + type: UserActionType.SET_GAS_EDUCATION_CAROUSEL_SEEN, + }; +} + +export function logIn(): LoginAction { + return { + type: UserActionType.LOGIN, + }; +} + +export function logOut(): LogoutAction { + return { + type: UserActionType.LOGOUT, + }; +} + +export function setAppTheme(theme: AppThemeKey): SetAppThemeAction { + return { + type: UserActionType.SET_APP_THEME, + payload: { theme }, + }; +} + +/** + * Temporary action to control auth flow + * + * @param initialScreen - "login" or "onboarding" + */ +export function checkedAuth(initialScreen: string): CheckedAuthAction { + return { + type: UserActionType.CHECKED_AUTH, + payload: { + initialScreen, + }, + }; +} + +/** + * Action to signal that persisted data has been loaded + */ +export function onPersistedDataLoaded(): PersistedDataLoadedAction { + return { + type: UserActionType.ON_PERSISTED_DATA_LOADED, + }; +} diff --git a/app/actions/user/types.ts b/app/actions/user/types.ts new file mode 100644 index 00000000000..704aee6092d --- /dev/null +++ b/app/actions/user/types.ts @@ -0,0 +1,111 @@ +import { type AppThemeKey } from '../../util/theme/models'; +import { type Action } from 'redux'; + +// Action type enum +export enum UserActionType { + LOCKED_APP = 'LOCKED_APP', + AUTH_SUCCESS = 'AUTH_SUCCESS', + AUTH_ERROR = 'AUTH_ERROR', + INTERRUPT_BIOMETRICS = 'INTERRUPT_BIOMETRICS', + LOGIN = 'LOGIN', + LOGOUT = 'LOGOUT', + ON_PERSISTED_DATA_LOADED = 'ON_PERSISTED_DATA_LOADED', + PASSWORD_SET = 'PASSWORD_SET', + PASSWORD_UNSET = 'PASSWORD_UNSET', + SEEDPHRASE_BACKED_UP = 'SEEDPHRASE_BACKED_UP', + SEEDPHRASE_NOT_BACKED_UP = 'SEEDPHRASE_NOT_BACKED_UP', + BACK_UP_SEEDPHRASE_VISIBLE = 'BACK_UP_SEEDPHRASE_VISIBLE', + BACK_UP_SEEDPHRASE_NOT_VISIBLE = 'BACK_UP_SEEDPHRASE_NOT_VISIBLE', + PROTECT_MODAL_VISIBLE = 'PROTECT_MODAL_VISIBLE', + PROTECT_MODAL_NOT_VISIBLE = 'PROTECT_MODAL_NOT_VISIBLE', + LOADING_SET = 'LOADING_SET', + LOADING_UNSET = 'LOADING_UNSET', + SET_GAS_EDUCATION_CAROUSEL_SEEN = 'SET_GAS_EDUCATION_CAROUSEL_SEEN', + SET_APP_THEME = 'SET_APP_THEME', + CHECKED_AUTH = 'CHECKED_AUTH', +} + +// User actions +export type LockAppAction = Action; + +export type AuthSuccessAction = Action & { + payload: { bioStateMachineId?: string }; +}; + +export type AuthErrorAction = Action & { + payload: { bioStateMachineId?: string }; +}; + +export type InterruptBiometricsAction = + Action; + +export type LoginAction = Action; + +export type LogoutAction = Action; + +export type PersistedDataLoadedAction = + Action; + +export type PasswordSetAction = Action; + +export type PasswordUnsetAction = Action; + +export type SeedphraseBackedUpAction = + Action; + +export type SeedphraseNotBackedUpAction = + Action; + +export type BackUpSeedphraseVisibleAction = + Action; + +export type BackUpSeedphraseNotVisibleAction = + Action; + +export type ProtectModalVisibleAction = + Action; + +export type ProtectModalNotVisibleAction = + Action; + +export type LoadingSetAction = Action & { + loadingMsg: string; +}; + +export type LoadingUnsetAction = Action; + +export type SetGasEducationCarouselSeenAction = + Action; + +export type SetAppThemeAction = Action & { + payload: { theme: AppThemeKey }; +}; + +export type CheckedAuthAction = Action & { + payload: { initialScreen: string }; +}; + +/** + * User actions union type + */ +export type UserAction = + | LockAppAction + | AuthSuccessAction + | AuthErrorAction + | InterruptBiometricsAction + | LoginAction + | LogoutAction + | PersistedDataLoadedAction + | PasswordSetAction + | PasswordUnsetAction + | SeedphraseBackedUpAction + | SeedphraseNotBackedUpAction + | BackUpSeedphraseVisibleAction + | BackUpSeedphraseNotVisibleAction + | ProtectModalVisibleAction + | ProtectModalNotVisibleAction + | LoadingSetAction + | LoadingUnsetAction + | SetGasEducationCarouselSeenAction + | SetAppThemeAction + | CheckedAuthAction; diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx index 280df813ba8..0e2a604be97 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react-native'; import CellSelectWithMenu from './CellSelectWithMenu'; -import { CellModalSelectorsIDs } from '../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../e2e/selectors/wallet/CellComponent.selectors'; import { SAMPLE_CELLSELECT_WITH_BUTTON_PROPS } from './CellSelectWithMenu.constants'; @@ -19,6 +19,6 @@ describe('CellSelectWithMenu', () => { , ); // Adjust the testID to match the one used in CellSelectWithMenu, if different - expect(queryByTestId(CellModalSelectorsIDs.MULTISELECT)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx index 144f2408e20..3327adff85b 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx @@ -11,7 +11,7 @@ import Tag from '../../../component-library/components/Tags/Tag'; // Internal dependencies. import styleSheet from './CellSelectWithMenu.styles'; import { CellSelectWithMenuProps } from './CellSelectWithMenu.types'; -import { CellModalSelectorsIDs } from '../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../e2e/selectors/wallet/CellComponent.selectors'; import ListItemMultiSelectButton from '../ListItemMultiSelectButton/ListItemMultiSelectButton'; import Avatar from '../../../component-library/components/Avatars/Avatar'; import Text from '../../../component-library/components/Texts/Text'; @@ -44,7 +44,7 @@ const CellSelectWithMenu = ({ @@ -52,7 +52,7 @@ const CellSelectWithMenu = ({ {withAvatar ? ( @@ -62,7 +62,7 @@ const CellSelectWithMenu = ({ {title} @@ -91,7 +91,7 @@ const CellSelectWithMenu = ({ )} {!!tagLabel && ( ({ + selectTokenMarketData: jest.fn(), +})); + +const mockSelectTokenMarketData = selectTokenMarketData as unknown as jest.Mock; + +jest.mock('../../../../selectors/currencyRateController', () => ({ + selectCurrentCurrency: jest.fn(() => 'USD'), +})); + +jest.mock('ethereumjs-util', () => ({ + toChecksumAddress: jest.fn((address) => address), + zeroAddress: jest.fn(() => '0x0000000000000000000000000000000000000000'), +})); + +describe('AggregatedPercentageCrossChains', () => { + const mockStore = configureStore([]); + let store: Store; + + beforeEach(() => { + store = mockStore({ + tokenRatesController: { + marketData: { + '1': { + '0xTokenAddress': { pricePercentChange1d: 5 }, + '0x0000000000000000000000000000000000000000': { + pricePercentChange1d: 3, + }, + }, + }, + }, + currencyRateController: { + currentCurrency: 'USD', + }, + }); + }); + + const testPositiveMarketData = { + '0x1': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: 0.26612196896783435, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + allTimeHigh: 0.00033787994095450243, + allTimeLow: 0.00024425950223297784, + circulatingSupply: 3590801926.36846, + currency: 'ETH', + dilutedMarketCap: 994523.5027585331, + high1d: 0.00027833552513055324, + id: 'dai', + low1d: 0.0002760612053968497, + marketCap: 994523.5027585331, + marketCapPercentChange1d: 1.94598, + price: 0.0002768602083719757, + priceChange1d: 0.00026184, + pricePercentChange14d: 0.06084239990548266, + pricePercentChange1d: 0.026199760027318986, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + allTimeHigh: 0.00032403240239079335, + allTimeLow: 0.00024306501355647231, + circulatingSupply: 39913626551.4682, + currency: 'ETH', + dilutedMarketCap: 11055935.156778876, + high1d: 0.0002780585743592791, + id: 'usd-coin', + low1d: 0.00027576902233315543, + marketCap: 11051818.92055066, + marketCapPercentChange1d: 0.05978, + price: 0.000276807033823891, + priceChange1d: -0.001160693663459944, + pricePercentChange14d: -0.05092221365479972, + pricePercentChange1d: 0.11599496417519209, + }, + }, + '0x89': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.9754341688014825, + allTimeLow: 0.43865817360906284, + circulatingSupply: 8081058250.420915, + currency: 'POL', + dilutedMarketCap: 10243600815.359032, + high1d: 0.9992817966819728, + id: 'polygon-ecosystem-token', + low1d: 0.8791508977097724, + marketCap: 8032270011.436278, + marketCapPercentChange1d: 6.17765, + price: 0.9962956752640171, + priceChange1d: 0.03843712, + pricePercentChange14d: 42.73351669766473, + pricePercentChange1d: 6.278884861668764, + }, + '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed': { + allTimeHigh: 1.88355350978746, + allTimeLow: 1.3144508332318727, + circulatingSupply: 0, + currency: 'POL', + dilutedMarketCap: 90736605.45696501, + high1d: 1.539001038484876, + id: 'axlusdc', + low1d: 1.522920391813105, + marketCap: 0, + marketCapPercentChange1d: 0, + price: 1.531344316900374, + priceChange1d: -0.00066261854319527, + pricePercentChange14d: 0.09924058418049628, + pricePercentChange1d: 0.0661881094544663, + }, + }, + '0xe708': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: 0.26612196896783435, + }, + }, + }; + + const testNegativeMarketData = { + '0x1': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: -0.26612196896783435, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + allTimeHigh: 0.00033787994095450243, + allTimeLow: 0.00024425950223297784, + circulatingSupply: 3590801926.36846, + currency: 'ETH', + dilutedMarketCap: 994523.5027585331, + high1d: 0.00027833552513055324, + id: 'dai', + low1d: 0.0002760612053968497, + marketCap: 994523.5027585331, + marketCapPercentChange1d: 1.94598, + price: 0.0002768602083719757, + priceChange1d: 0.00026184, + pricePercentChange14d: 0.06084239990548266, + pricePercentChange1d: -0.026199760027318986, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + allTimeHigh: 0.00032403240239079335, + allTimeLow: 0.00024306501355647231, + circulatingSupply: 39913626551.4682, + currency: 'ETH', + dilutedMarketCap: 11055935.156778876, + high1d: 0.0002780585743592791, + id: 'usd-coin', + low1d: 0.00027576902233315543, + marketCap: 11051818.92055066, + marketCapPercentChange1d: 0.05978, + price: 0.000276807033823891, + priceChange1d: -0.001160693663459944, + pricePercentChange14d: -0.05092221365479972, + pricePercentChange1d: -0.11599496417519209, + }, + }, + '0x89': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.9754341688014825, + allTimeLow: 0.43865817360906284, + circulatingSupply: 8081058250.420915, + currency: 'POL', + dilutedMarketCap: 10243600815.359032, + high1d: 0.9992817966819728, + id: 'polygon-ecosystem-token', + low1d: 0.8791508977097724, + marketCap: 8032270011.436278, + marketCapPercentChange1d: 6.17765, + price: 0.9962956752640171, + priceChange1d: 0.03843712, + pricePercentChange14d: 42.73351669766473, + pricePercentChange1d: -6.278884861668764, + }, + '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed': { + allTimeHigh: 1.88355350978746, + allTimeLow: 1.3144508332318727, + circulatingSupply: 0, + currency: 'POL', + dilutedMarketCap: 90736605.45696501, + high1d: 1.539001038484876, + id: 'axlusdc', + low1d: 1.522920391813105, + marketCap: 0, + marketCapPercentChange1d: 0, + price: 1.531344316900374, + priceChange1d: -0.00066261854319527, + pricePercentChange14d: 0.09924058418049628, + pricePercentChange1d: -0.0661881094544663, + }, + }, + '0xe708': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: -0.26612196896783435, + }, + }, + }; + + it('should match snapshot', () => { + const { toJSON } = render( + + + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('should return positive amount change if market data is all positive', () => { + mockSelectTokenMarketData.mockReturnValue(testPositiveMarketData); + const testTokenFiatBalancesCrossChains: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }[] = [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + balance: '3.08657', + tokenBalanceFiat: 3.11, + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + decimals: 18, + balance: '4.00229', + tokenBalanceFiat: 4.03, + }, + ], + tokenFiatBalances: [3.11, 4.03], + nativeFiatValue: 79.49, + }, + { + chainId: '0xe708', + tokensWithBalances: [], + tokenFiatBalances: [], + nativeFiatValue: 0, + }, + { + chainId: '0x89', + tokensWithBalances: [ + { + address: '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed', + symbol: 'AXLUSDC', + decimals: 6, + balance: '12.87735', + tokenBalanceFiat: 12.8, + }, + ], + tokenFiatBalances: [12.8], + nativeFiatValue: 9.28, + }, + ]; + const { getByTestId } = render( + + + , + ); + + const formattedValuePriceElement = getByTestId( + FORMATTED_VALUE_PRICE_TEST_ID, + ); + const formattedValuePercentageElement = getByTestId( + FORMATTED_PERCENTAGE_TEST_ID, + ); + + expect(formattedValuePriceElement).toBeDefined(); + expect(formattedValuePercentageElement).toBeDefined(); + expect(formattedValuePriceElement.props.children).toBe('+0.77 USD '); + expect(formattedValuePercentageElement.props.children).toBe('(+0.72%)'); + }); + + it('should return negative amount change if market data is all negative', () => { + mockSelectTokenMarketData.mockReturnValue(testNegativeMarketData); + const testTokenFiatBalancesCrossChains: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }[] = [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + balance: '3.08657', + tokenBalanceFiat: 3.11, + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + decimals: 18, + balance: '4.00229', + tokenBalanceFiat: 4.03, + }, + ], + tokenFiatBalances: [3.11, 4.03], + nativeFiatValue: 79.49, + }, + { + chainId: '0xe708', + tokensWithBalances: [], + tokenFiatBalances: [], + nativeFiatValue: 0, + }, + { + chainId: '0x89', + tokensWithBalances: [ + { + address: '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed', + symbol: 'AXLUSDC', + decimals: 6, + balance: '12.87735', + tokenBalanceFiat: 12.8, + }, + ], + tokenFiatBalances: [12.8], + nativeFiatValue: 9.28, + }, + ]; + const { getByTestId } = render( + + + , + ); + + const formattedValuePriceElement = getByTestId( + FORMATTED_VALUE_PRICE_TEST_ID, + ); + const formattedValuePercentageElement = getByTestId( + FORMATTED_PERCENTAGE_TEST_ID, + ); + + expect(formattedValuePriceElement).toBeDefined(); + expect(formattedValuePercentageElement).toBeDefined(); + expect(formattedValuePriceElement.props.children).toBe('-0.85 USD '); + expect(formattedValuePercentageElement.props.children).toBe('(-0.77%)'); + }); +}); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.tsx new file mode 100644 index 00000000000..8c6da659c23 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.tsx @@ -0,0 +1,171 @@ +import React, { useMemo } from 'react'; +import { TextVariant } from '../../../../component-library/components/Texts/Text'; +import SensitiveText from '../../../../component-library/components/Texts/SensitiveText'; +import { View } from 'react-native'; +import { useSelector } from 'react-redux'; +import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; +import styleSheet from './AggregatedPercentage.styles'; +import { useStyles } from '../../../hooks'; +import { + FORMATTED_VALUE_PRICE_TEST_ID, + FORMATTED_PERCENTAGE_TEST_ID, +} from './AggregatedPercentage.constants'; +import { toChecksumAddress, zeroAddress } from 'ethereumjs-util'; +import { selectTokenMarketData } from '../../../../selectors/tokenRatesController'; +import { + MarketDataMapping, + TokensWithBalances, +} from '../../../../components/hooks/useGetFormattedTokensPerChain'; +import { getFormattedAmountChange, getPercentageTextColor } from './utils'; + +export interface AggregatedPercentageProps { + ethFiat: number; + tokenFiat: number; + tokenFiat1dAgo: number; + ethFiat1dAgo: number; +} + +export const getCalculatedTokenAmount1dAgo = ( + tokenFiatBalance: number, + tokenPricePercentChange1dAgo: number, +) => + tokenPricePercentChange1dAgo !== undefined && tokenFiatBalance + ? tokenFiatBalance / (1 + tokenPricePercentChange1dAgo / 100) + : tokenFiatBalance ?? 0; + +const isValidAmount = (amount: number | null | undefined): boolean => + amount !== null && amount !== undefined && !Number.isNaN(amount); + +const AggregatedPercentageCrossChains = ({ + privacyMode = false, + totalFiatCrossChains, + tokenFiatBalancesCrossChains, +}: { + privacyMode?: boolean; + totalFiatCrossChains: number; + tokenFiatBalancesCrossChains: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }[]; +}) => { + const crossChainMarketData: MarketDataMapping = useSelector( + selectTokenMarketData, + ); + + const totalFiat1dAgoCrossChains = useMemo(() => { + const getPerChainTotalFiat1dAgo = ( + chainId: string, + tokenFiatBalances: number[], + tokensWithBalances: TokensWithBalances[], + ) => { + const totalPerChain1dAgoERC20 = tokensWithBalances.reduce( + (total1dAgo: number, item: { address: string }, idx: number) => { + const found = + crossChainMarketData?.[chainId]?.[toChecksumAddress(item.address)]; + + const tokenFiat1dAgo = getCalculatedTokenAmount1dAgo( + tokenFiatBalances[idx], + found?.pricePercentChange1d, + ); + return total1dAgo + Number(tokenFiat1dAgo); + }, + 0, + ); + + return totalPerChain1dAgoERC20; + }; + return tokenFiatBalancesCrossChains.reduce( + ( + total1dAgoCrossChains: number, + item: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }, + ) => { + const perChainERC20Total = getPerChainTotalFiat1dAgo( + item.chainId, + item.tokenFiatBalances, + item.tokensWithBalances, + ); + + const nativePricePercentChange1d = + crossChainMarketData?.[item.chainId]?.[zeroAddress()] + ?.pricePercentChange1d; + + const nativeFiat1dAgo = getCalculatedTokenAmount1dAgo( + item.nativeFiatValue, + nativePricePercentChange1d, + ); + return ( + total1dAgoCrossChains + perChainERC20Total + Number(nativeFiat1dAgo) + ); + }, + 0, + ); + }, [tokenFiatBalancesCrossChains, crossChainMarketData]); + + const totalCrossChainBalance: number = Number(totalFiatCrossChains); + const crossChainTotalBalance1dAgo = totalFiat1dAgoCrossChains; + + const amountChangeCrossChains = + totalCrossChainBalance - crossChainTotalBalance1dAgo; + + const percentageChangeCrossChains = + (amountChangeCrossChains / crossChainTotalBalance1dAgo) * 100 || 0; + + const validFormattedPercentChange = `(${ + percentageChangeCrossChains >= 0 ? '+' : '' + }${percentageChangeCrossChains.toFixed(2)}%)`; + + const formattedPercentChangeCrossChains = isValidAmount( + percentageChangeCrossChains, + ) + ? validFormattedPercentChange + : ''; + const currentCurrency = useSelector(selectCurrentCurrency); + + const validFormattedAmountChange = getFormattedAmountChange( + amountChangeCrossChains, + currentCurrency, + ); + const formattedAmountChangeCrossChains = isValidAmount( + amountChangeCrossChains, + ) + ? validFormattedAmountChange + : ''; + + const percentageTextColor = getPercentageTextColor( + privacyMode, + percentageChangeCrossChains, + ); + const { styles } = useStyles(styleSheet, {}); + + return ( + + + {formattedAmountChangeCrossChains} + + + {formattedPercentChangeCrossChains} + + + ); +}; + +export default AggregatedPercentageCrossChains; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentageCrossChains.test.tsx.snap b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentageCrossChains.test.tsx.snap new file mode 100644 index 00000000000..bb203e9582e --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentageCrossChains.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggregatedPercentageCrossChains should match snapshot 1`] = ` + + + +0 USD + + + (+0.00%) + + +`; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts b/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts new file mode 100644 index 00000000000..77aea4b2ac2 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts @@ -0,0 +1,32 @@ +import { DECIMALS_TO_SHOW } from '../../../../components/UI/Tokens/constants'; +import { renderFiat } from '../../../../util/number'; +import { TextColor } from '../../../components/Texts/Text'; + +export const getFormattedAmountChange = ( + input: number, + currentCurrency: string, +) => + `${input >= 0 ? '+' : ''}${renderFiat( + input, + currentCurrency, + DECIMALS_TO_SHOW, + )} `; + +export const getPercentageTextColor = ( + privacyMode: boolean, + percentageChangeCrossChains: number, +) => { + let percentageTextColor; + if (!privacyMode) { + if (percentageChangeCrossChains === 0) { + percentageTextColor = TextColor.Default; + } else if (percentageChangeCrossChains > 0) { + percentageTextColor = TextColor.Success; + } else { + percentageTextColor = TextColor.Error; + } + } else { + percentageTextColor = TextColor.Alternative; + } + return percentageTextColor; +}; diff --git a/app/component-library/components/Cells/Cell/Cell.test.tsx b/app/component-library/components/Cells/Cell/Cell.test.tsx index 1006e0cee03..7bf02ce7d96 100644 --- a/app/component-library/components/Cells/Cell/Cell.test.tsx +++ b/app/component-library/components/Cells/Cell/Cell.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { render } from '@testing-library/react-native'; // External dependencies. -import { CellModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import Cell from './Cell'; @@ -16,28 +16,28 @@ describe('Cell', () => { , ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.DISPLAY)).not.toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.MULTISELECT)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.SELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.DISPLAY)).not.toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.SELECT)).toBe(null); }); it('should render CellMultiSelect given the type MultiSelect', () => { const wrapper = render( , ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.DISPLAY)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.MULTISELECT)).not.toBe( + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.DISPLAY)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).not.toBe( null, ); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.SELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.SELECT)).toBe(null); }); it('should render CellSelect given the type Select', () => { const wrapper = render( , ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.DISPLAY)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.MULTISELECT)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.SELECT)).not.toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.DISPLAY)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.SELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/Cell.tsx b/app/component-library/components/Cells/Cell/Cell.tsx index 354219dfb25..7ea7c925f89 100644 --- a/app/component-library/components/Cells/Cell/Cell.tsx +++ b/app/component-library/components/Cells/Cell/Cell.tsx @@ -6,7 +6,7 @@ import CellDisplay from './variants/CellDisplay'; import CellMultiSelect from './variants/CellMultiSelect'; import CellSelect from './variants/CellSelect'; import CellSelectWithMenu from '../../../components-temp/CellSelectWithMenu'; -import { CellModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import { CellProps, CellVariant } from './Cell.types'; @@ -14,20 +14,20 @@ import { CellProps, CellVariant } from './Cell.types'; const Cell = ({ variant, hitSlop, ...props }: CellProps) => { switch (variant) { case CellVariant.Display: - return ; + return ; case CellVariant.MultiSelect: return ( ); case CellVariant.Select: - return ; + return ; case CellVariant.SelectWithMenu: return ( ); diff --git a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx index 9be4ccb0612..ff18c2df818 100644 --- a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx +++ b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx @@ -11,7 +11,7 @@ import { SAMPLE_CELLBASE_TERTIARY_TEXT, SAMPLE_CELLBASE_TAGLABEL, } from './CellBase.constants'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; describe('CellBase', () => { it('should render default settings correctly', () => { @@ -33,7 +33,7 @@ describe('CellBase', () => { title={SAMPLE_CELLBASE_TITLE} />, ); - expect(queryByTestId(CellModalSelectorsIDs.BASE_AVATAR)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.BASE_AVATAR)).not.toBe(null); }); it('should render the given title', async () => { diff --git a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx index 65c647c108d..8b48669c957 100644 --- a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx +++ b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx @@ -19,7 +19,7 @@ import { } from './CellBase.constants'; import styleSheet from './CellBase.styles'; import { CellBaseProps } from './CellBase.types'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; const CellBase = ({ style, @@ -37,7 +37,7 @@ const CellBase = ({ {/* DEV Note: Account Avatar should be replaced with Avatar with Badge whenever available */} @@ -45,7 +45,7 @@ const CellBase = ({ {title} diff --git a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx index b0af59591dd..7addd5903ec 100644 --- a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { render } from '@testing-library/react-native'; //External dependencies -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import CellDisplay from './CellDisplay'; @@ -18,6 +18,6 @@ describe('CellDisplay', () => { const { queryByTestId } = render( , ); - expect(queryByTestId(CellModalSelectorsIDs.DISPLAY)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.DISPLAY)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx index f69a63194fb..6b0c12bd24b 100644 --- a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { useStyles } from '../../../../../hooks'; import CellBase from '../../foundation/CellBase'; import Card from '../../../../Cards/Card'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import styleSheet from './CellDisplay.styles'; @@ -17,7 +17,7 @@ const CellDisplay = ({ style, ...props }: CellDisplayProps) => { const { styles } = useStyles(styleSheet, { style }); return ( - + ); diff --git a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx index df20af9fcde..ceb42effede 100644 --- a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx @@ -5,7 +5,7 @@ import { render } from '@testing-library/react-native'; // Internal dependencies. import CellMultiSelect from './CellMultiSelect'; import { SAMPLE_CELLMULTISELECT_PROPS } from './CellMultiSelect.constants'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; describe('CellMultiSelect', () => { it('should render default settings correctly', () => { @@ -18,6 +18,6 @@ describe('CellMultiSelect', () => { const { queryByTestId } = render( , ); - expect(queryByTestId(CellModalSelectorsIDs.MULTISELECT)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx index 3870d5fda28..c129e03884c 100644 --- a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { useStyles } from '../../../../../hooks'; import ListItemMultiSelect from '../../../../List/ListItemMultiSelect'; import CellBase from '../../foundation/CellBase'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import styleSheet from './CellMultiSelect.styles'; @@ -30,7 +30,7 @@ const CellMultiSelect = ({ { it('should render default settings correctly', () => { @@ -16,6 +16,6 @@ describe('CellSelect', () => { const { queryByTestId } = render( , ); - expect(queryByTestId(CellModalSelectorsIDs.SELECT)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.SELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx b/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx index 1b9e4b7e583..d131c40ade6 100644 --- a/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx @@ -11,7 +11,7 @@ import CellBase from '../../foundation/CellBase'; // Internal dependencies. import styleSheet from './CellSelect.styles'; import { CellSelectProps } from './CellSelect.types'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; const CellSelect = ({ style, @@ -30,7 +30,7 @@ const CellSelect = ({ { const { colors } = useTheme(); diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx index 29c48def333..1dc1dc5ca43 100644 --- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx +++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx @@ -22,12 +22,18 @@ const PickerNetwork = ({ label, imageSource, hideNetworkName, + isDisabled = false, ...props }: PickerNetworkProps) => { const { styles } = useStyles(stylesheet, { style }); return ( - + { } }; + /** + * Triggers when the navigation is ready + */ + const onNavigationReadyHandler = () => dispatch(onNavigationReady()); + return supressRender ? null : ( <> { @@ -905,6 +911,7 @@ const App = (props) => { const currentRoute = findRouteNameFromNavigatorState(state.routes); triggerSetCurrentRoute(currentRoute); }} + onReady={onNavigationReadyHandler} > { ({ id }) => id === approvalTransactionMetaId, ); - const ethQuery = Engine.getGlobalEthQuery(); + const ethQuery = getGlobalEthQuery(); const ethBalance = await query(ethQuery, 'getBalance', [ props.selectedAddress, @@ -233,20 +234,34 @@ const RootRPCMethodsUI = (props) => { ); const parameters = { - ...analyticsParams, time_to_mine: timeToMine, estimated_vs_used_gasRatio: estimatedVsUsedGasRatio, quote_vs_executionRatio: quoteVsExecutionRatio, token_to_amount_received: tokenToAmountReceived.toString(), is_smart_transaction: props.shouldUseSmartTransaction, ...smartTransactionMetricsProperties, + available_quotes: analyticsParams.available_quotes, + best_quote_source: analyticsParams.best_quote_source, + chain_id: analyticsParams.chain_id, + custom_slippage: analyticsParams.custom_slippage, + network_fees_USD: analyticsParams.network_fees_USD, + other_quote_selected: analyticsParams.other_quote_selected, + request_type: analyticsParams.request_type, + token_from: analyticsParams.token_from, + token_to: analyticsParams.token_to, + }; + const sensitiveParameters = { + token_from_amount: analyticsParams.token_from_amount, + token_to_amount: analyticsParams.token_to_amount, + network_fees_ETH: analyticsParams.network_fees_ETH, }; Logger.log('Swaps', 'Sending metrics event', event); trackEvent( createEventBuilder(event) - .addSensitiveProperties({ ...parameters }) + .addProperties({ ...parameters }) + .addSensitiveProperties({ ...sensitiveParameters }) .build(), ); } catch (e) { @@ -320,7 +335,7 @@ const RootRPCMethodsUI = (props) => { transactionId: transactionMeta.id, deviceId, // eslint-disable-next-line no-empty-function - onConfirmationComplete: () => {}, + onConfirmationComplete: () => { }, type: 'signTransaction', }), ); @@ -377,6 +392,7 @@ const RootRPCMethodsUI = (props) => { autoSign(transactionMeta); } else { const { + networkClientId, txParams: { value, gas, gasPrice, data }, } = transactionMeta; const { AssetsContractController } = Engine.context; @@ -388,7 +404,7 @@ const RootRPCMethodsUI = (props) => { data && data !== '0x' && to && - (await getMethodData(data)).name === TOKEN_METHOD_TRANSFER + (await getMethodData(data, networkClientId)).name === TOKEN_METHOD_TRANSFER ) { let asset = props.tokens.find(({ address }) => toLowerCaseEquals(address, to), @@ -431,6 +447,7 @@ const RootRPCMethodsUI = (props) => { id: transactionMeta.id, origin: transactionMeta.origin, securityAlertResponse: transactionMeta.securityAlertResponse, + networkClientId, ...transactionMeta.txParams, }); } else { @@ -443,6 +460,7 @@ const RootRPCMethodsUI = (props) => { id: transactionMeta.id, origin: transactionMeta.origin, securityAlertResponse: transactionMeta.securityAlertResponse, + networkClientId, ...transactionMeta.txParams, }); } @@ -559,7 +577,7 @@ RootRPCMethodsUI.propTypes = { }; const mapStateToProps = (state) => ({ - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), chainId: selectChainId(state), tokens: selectTokens(state), providerType: selectProviderType(state), diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 0ff9ee33f51..b8ba760da0c 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -58,6 +58,7 @@ import { useMinimumVersions } from '../../hooks/MinimumVersions'; import navigateTermsOfUse from '../../../util/termsOfUse/termsOfUse'; import { selectChainId, + selectNetworkClientId, selectNetworkConfigurations, selectProviderConfig, selectProviderType, @@ -83,6 +84,7 @@ import { import isNetworkUiRedesignEnabled from '../../../util/networks/isNetworkUiRedesignEnabled'; import { useConnectionHandler } from '../../../util/navigation/useConnectionHandler'; import { AssetPollingProvider } from '../../hooks/AssetPolling/AssetPollingProvider'; +import { getGlobalEthQuery } from '../../../util/networks/global-network'; const Stack = createStackNavigator(); @@ -117,6 +119,8 @@ const Main = (props) => { useEnableAutomaticSecurityChecks(); useMinimumVersions(); + const { chainId, networkClientId, showIncomingTransactionsNetworks } = props; + useEffect(() => { if (DEPRECATED_NETWORKS.includes(props.chainId)) { setShowDeprecatedAlert(true); @@ -126,19 +130,17 @@ const Main = (props) => { }, [props.chainId]); useEffect(() => { - const chainId = props.chainId; + stopIncomingTransactionPolling(); - if (props.showIncomingTransactionsNetworks[chainId]) { - startIncomingTransactionPolling(); - } else { - stopIncomingTransactionPolling(); + if (showIncomingTransactionsNetworks[chainId]) { + startIncomingTransactionPolling([chainId]); } - }, [props.showIncomingTransactionsNetworks, props.chainId]); + }, [chainId, networkClientId, showIncomingTransactionsNetworks]); const checkInfuraAvailability = useCallback(async () => { if (props.providerType !== 'rpc') { try { - const ethQuery = Engine.getGlobalEthQuery(); + const ethQuery = getGlobalEthQuery(); await query(ethQuery, 'blockNumber', []); props.setInfuraAvailabilityNotBlocked(); } catch (e) { @@ -176,11 +178,11 @@ const Main = (props) => { removeNotVisibleNotifications(); BackgroundTimer.runBackgroundTimer(async () => { - await updateIncomingTransactions(); + await updateIncomingTransactions([props.chainId]); }, AppConstants.TX_CHECK_BACKGROUND_FREQUENCY); } }, - [backgroundMode, removeNotVisibleNotifications], + [backgroundMode, removeNotVisibleNotifications, props.chainId], ); const initForceReload = () => { @@ -450,6 +452,10 @@ Main.propTypes = { * backup seed phrase modal visible */ backUpSeedphraseVisible: PropTypes.bool, + /** + * ID of the global network client + */ + networkClientId: PropTypes.string, }; const mapStateToProps = (state) => ({ @@ -457,6 +463,7 @@ const mapStateToProps = (state) => ({ selectShowIncomingTransactionNetworks(state), providerType: selectProviderType(state), chainId: selectChainId(state), + networkClientId: selectNetworkClientId(state), backUpSeedphraseVisible: state.user.backUpSeedphraseVisible, }); diff --git a/app/components/UI/AccountApproval/index.js b/app/components/UI/AccountApproval/index.js index bce1c643fa3..e9e26c8682b 100644 --- a/app/components/UI/AccountApproval/index.js +++ b/app/components/UI/AccountApproval/index.js @@ -22,7 +22,7 @@ import Routes from '../../../constants/navigation/Routes'; import Engine from '../../../core/Engine'; import SDKConnect from '../../../core/SDKConnect/SDKConnect'; import { selectAccountsLength } from '../../../selectors/accountTrackerController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; import { selectChainId, selectProviderType, @@ -411,7 +411,7 @@ class AccountApproval extends PureComponent { const mapStateToProps = (state) => ({ accountsLength: selectAccountsLength(state), tokensLength: selectTokensLength(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), networkType: selectProviderType(state), chainId: selectChainId(state), }); diff --git a/app/components/UI/AccountApproval/index.test.tsx b/app/components/UI/AccountApproval/index.test.tsx index 0afe79dd39e..29e438ead16 100644 --- a/app/components/UI/AccountApproval/index.test.tsx +++ b/app/components/UI/AccountApproval/index.test.tsx @@ -48,6 +48,13 @@ const mockInitialState = { }, }, }, + TokensController: { + allTokens: { + '0x1': { + '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756': [], + }, + }, + }, }, }, }; diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx index 68db9cb046b..c650e8640f5 100644 --- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx +++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx @@ -38,7 +38,13 @@ const mockInitialState: DeepPartial = { }, }, TokenBalancesController: { - tokenBalances: { }, + tokenBalances: { + '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': { + '0x5': { + '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x2b46', + }, + }, + }, }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index c812e65eee2..fa806567139 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -36,7 +36,7 @@ import { selectChainId } from '../../../selectors/networkController'; import { selectCurrentCurrency } from '../../../selectors/currencyRateController'; import { selectInternalAccounts, - selectSelectedInternalAccountChecksummedAddress, + selectSelectedInternalAccountFormattedAddress, } from '../../../selectors/accountsController'; import { createAccountSelectorNavDetails } from '../../Views/AccountSelector'; import Text, { @@ -450,7 +450,7 @@ class AccountOverview extends PureComponent { } const mapStateToProps = (state) => ({ - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), internalAccounts: selectInternalAccounts(state), currentCurrency: selectCurrentCurrency(state), chainId: selectChainId(state), diff --git a/app/components/UI/AccountRightButton/index.tsx b/app/components/UI/AccountRightButton/index.tsx index c50304d9dfc..b144242f5f2 100644 --- a/app/components/UI/AccountRightButton/index.tsx +++ b/app/components/UI/AccountRightButton/index.tsx @@ -26,7 +26,7 @@ import BadgeWrapper from '../../../component-library/components/Badges/BadgeWrap import { selectProviderConfig } from '../../../selectors/networkController'; import Routes from '../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { AccountOverviewSelectorsIDs } from '../../../../e2e/selectors/AccountOverview.selectors'; +import { AccountOverviewSelectorsIDs } from '../../../../e2e/selectors/Browser/AccountOverview.selectors'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { useNetworkInfo } from '../../../selectors/selectedNetworkController'; import UrlParser from 'url-parse'; diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 506d7e6e37d..ad88315241f 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -5,7 +5,7 @@ import renderWithProvider from '../../../util/test/renderWithProvider'; import AccountSelectorList from './AccountSelectorList'; import { useAccounts } from '../../../components/hooks/useAccounts'; import { View } from 'react-native'; -import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; +import { AccountListBottomSheetSelectorsIDs } from '../../../../e2e/selectors/wallet/AccountListBottomSheet.selectors'; import { backgroundState } from '../../../util/test/initial-root-state'; import { regex } from '../../../../app/util/regex'; import { @@ -18,6 +18,9 @@ import { mockNetworkState } from '../../../util/test/network'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { AccountSelectorListProps } from './AccountSelectorList.types'; +// eslint-disable-next-line import/no-namespace +import * as Utils from '../../hooks/useAccounts/utils'; + const BUSINESS_ACCOUNT = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; const PERSONAL_ACCOUNT = '0xd018538C87232FF95acbCe4870629b75640a78E7'; @@ -125,6 +128,16 @@ const renderComponent = ( describe('AccountSelectorList', () => { beforeEach(() => { + jest.spyOn(Utils, 'getAccountBalances').mockReturnValueOnce({ + balanceETH: '1', + balanceFiat: '$3200.00', + balanceWeiHex: '', + }); + jest.spyOn(Utils, 'getAccountBalances').mockReturnValueOnce({ + balanceETH: '2', + balanceFiat: '$6400.00', + balanceWeiHex: '', + }); onSelectAccount.mockClear(); onRemoveImportedAccount.mockClear(); }); @@ -140,10 +153,10 @@ describe('AccountSelectorList', () => { await waitFor(async () => { const businessAccountItem = await queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); const personalAccountItem = await queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -183,7 +196,7 @@ describe('AccountSelectorList', () => { expect(accounts.length).toBe(1); const businessAccountItem = await queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -251,7 +264,7 @@ describe('AccountSelectorList', () => { await waitFor(() => { const businessAccountItem = queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -272,7 +285,7 @@ describe('AccountSelectorList', () => { await waitFor(() => { const businessAccountItem = queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); expect(within(businessAccountItem).queryByText(regex.eth(1))).toBeNull(); diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index 30b8241836f..499a3eacb96 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -5,7 +5,6 @@ import { FlatList } from 'react-native-gesture-handler'; import { useSelector } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { Hex } from '@metamask/utils'; // External dependencies. import { selectInternalAccounts } from '../../../selectors/accountsController'; @@ -36,7 +35,7 @@ import Routes from '../../../constants/navigation/Routes'; // Internal dependencies. import { AccountSelectorListProps } from './AccountSelectorList.types'; import styleSheet from './AccountSelectorList.styles'; -import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; +import { AccountListBottomSheetSelectorsIDs } from '../../../../e2e/selectors/wallet/AccountListBottomSheet.selectors'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; const AccountSelectorList = ({ @@ -82,7 +81,7 @@ const AccountSelectorList = ({ return ( { const actualNav = jest.requireActual('@react-navigation/native'); return { @@ -72,9 +95,21 @@ jest.mock('../../hooks/useStyles', () => ({ }), })); +jest.mock('../../../core/Engine', () => ({ + context: { + NetworkController: { + getNetworkConfigurationByChainId: jest + .fn() + .mockReturnValue(mockNetworkConfiguration), + setActiveNetwork: jest.fn().mockResolvedValue(undefined), + }, + }, +})); + const asset = { balance: '400', balanceFiat: '1500', + chainId: MOCK_CHAIN_ID, logo: 'https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg', symbol: 'ETH', name: 'Ethereum', @@ -87,6 +122,10 @@ const asset = { }; describe('AssetOverview', () => { + beforeEach(() => { + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(false); + }); + it('should render correctly', async () => { const container = renderWithProvider( , @@ -95,6 +134,16 @@ describe('AssetOverview', () => { expect(container).toMatchSnapshot(); }); + it('should render correctly when portfolio view is enabled', async () => { + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true); + + const container = renderWithProvider( + , + { state: mockInitialState }, + ); + expect(container).toMatchSnapshot(); + }); + it('should handle buy button press', async () => { const { getByTestId } = renderWithProvider( , @@ -133,13 +182,34 @@ describe('AssetOverview', () => { const swapButton = getByTestId('token-swap-button'); fireEvent.press(swapButton); - expect(navigate).toHaveBeenCalledWith('Swaps', { - params: { - sourcePage: 'MainView', - sourceToken: asset.address, - }, - screen: 'SwapsAmountView', - }); + if (isPortfolioViewEnabled()) { + expect(navigate).toHaveBeenCalledTimes(3); + expect(navigate).toHaveBeenNthCalledWith(1, 'RampBuy', { + screen: 'GetStarted', + params: { + address: asset.address, + chainId: getDecimalChainId(MOCK_CHAIN_ID), + }, + }); + expect(navigate).toHaveBeenNthCalledWith(2, 'SendFlowView', {}); + expect(navigate).toHaveBeenNthCalledWith(3, 'Swaps', { + screen: 'SwapsAmountView', + params: { + sourcePage: 'MainView', + address: asset.address, + chainId: MOCK_CHAIN_ID, + }, + }); + } else { + expect(navigate).toHaveBeenCalledWith('Swaps', { + screen: 'SwapsAmountView', + params: { + sourcePage: 'MainView', + sourceToken: asset.address, + chainId: '0x1', + }, + }); + } }); it('should not render swap button if displaySwapsButton is false', async () => { diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx index 32df4d6527a..9e1b944bdb6 100644 --- a/app/components/UI/AssetOverview/AssetOverview.tsx +++ b/app/components/UI/AssetOverview/AssetOverview.tsx @@ -1,25 +1,37 @@ -import { zeroAddress } from 'ethereumjs-util'; import React, { useCallback, useEffect } from 'react'; import { TouchableOpacity, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { useDispatch, useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; +import { getNativeTokenAddress } from '@metamask/assets-controllers'; import { strings } from '../../../../locales/i18n'; -import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/TokenOverview.selectors'; +import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/wallet/TokenOverview.selectors'; import { newAssetTransaction } from '../../../actions/transaction'; import AppConstants from '../../../core/AppConstants'; import Engine from '../../../core/Engine'; import { selectChainId, selectTicker, + selectNativeCurrencyByChainId, } from '../../../selectors/networkController'; import { selectConversionRate, selectCurrentCurrency, + selectCurrencyRates, } from '../../../selectors/currencyRateController'; -import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; +import { + selectContractExchangeRates, + selectTokenMarketData, +} from '../../../selectors/tokenRatesController'; import { selectAccountsByChainId } from '../../../selectors/accountTrackerController'; -import { selectContractBalances } from '../../../selectors/tokenBalancesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { + selectContractBalances, + selectTokensBalances, +} from '../../../selectors/tokenBalancesController'; +import { + selectSelectedInternalAccountAddress, + selectSelectedInternalAccountFormattedAddress, +} from '../../../selectors/accountsController'; import Logger from '../../../util/Logger'; import { safeToChecksumAddress } from '../../../util/address'; import { @@ -46,9 +58,12 @@ import Routes from '../../../constants/navigation/Routes'; import TokenDetails from './TokenDetails'; import { RootState } from '../../../reducers'; import useGoToBridge from '../Bridge/utils/useGoToBridge'; -import SwapsController from '@metamask/swaps-controller'; +import SwapsController, { swapsUtils } from '@metamask/swaps-controller'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { getDecimalChainId } from '../../../util/networks'; +import { + getDecimalChainId, + isPortfolioViewEnabled, +} from '../../../util/networks'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { createBuyNavigationDetails } from '../Ramp/routes/utils'; import { TokenI } from '../Tokens/types'; @@ -67,24 +82,51 @@ const AssetOverview: React.FC = ({ }: AssetOverviewProps) => { const navigation = useNavigation(); const [timePeriod, setTimePeriod] = React.useState('1d'); - const currentCurrency = useSelector(selectCurrentCurrency); + const selectedInternalAccountAddress = useSelector( + selectSelectedInternalAccountAddress, + ); const conversionRate = useSelector(selectConversionRate); + const conversionRateByTicker = useSelector(selectCurrencyRates); + const currentCurrency = useSelector(selectCurrentCurrency); const accountsByChainId = useSelector(selectAccountsByChainId); const primaryCurrency = useSelector( (state: RootState) => state.settings.primaryCurrency, ); const goToBridge = useGoToBridge('TokenDetails'); const selectedAddress = useSelector( - selectSelectedInternalAccountChecksummedAddress, + selectSelectedInternalAccountFormattedAddress, ); const { trackEvent, createEventBuilder } = useMetrics(); const tokenExchangeRates = useSelector(selectContractExchangeRates); + const allTokenMarketData = useSelector(selectTokenMarketData); const tokenBalances = useSelector(selectContractBalances); - const chainId = useSelector((state: RootState) => selectChainId(state)); - const ticker = useSelector((state: RootState) => selectTicker(state)); + const selectedChainId = useSelector((state: RootState) => + selectChainId(state), + ); + const selectedTicker = useSelector((state: RootState) => selectTicker(state)); + + const nativeCurrency = useSelector((state: RootState) => + selectNativeCurrencyByChainId(state, asset.chainId as Hex), + ); + + const multiChainTokenBalance = useSelector(selectTokensBalances); + const chainId = isPortfolioViewEnabled() + ? (asset.chainId as Hex) + : selectedChainId; + const ticker = isPortfolioViewEnabled() ? nativeCurrency : selectedTicker; + + let currentAddress: Hex; + + if (isPortfolioViewEnabled()) { + currentAddress = asset.address as Hex; + } else { + currentAddress = asset.isETH + ? getNativeTokenAddress(chainId as Hex) + : (asset.address as Hex); + } const { data: prices = [], isLoading } = useTokenHistoricalPrices({ - address: asset.isETH ? zeroAddress() : asset.address, + address: currentAddress, chainId, timePeriod, vsCurrency: currentCurrency, @@ -119,7 +161,41 @@ const AssetOverview: React.FC = ({ }); }; + const handleSwapNavigation = useCallback(() => { + navigation.navigate('Swaps', { + screen: 'SwapsAmountView', + params: { + sourceToken: asset.address ?? swapsUtils.NATIVE_SWAPS_TOKEN_ADDRESS, + sourcePage: 'MainView', + chainId: asset.chainId, + }, + }); + }, [navigation, asset.address, asset.chainId]); + const onSend = async () => { + if (isPortfolioViewEnabled()) { + navigation.navigate(Routes.WALLET.HOME, { + screen: Routes.WALLET.TAB_STACK_FLOW, + params: { + screen: Routes.WALLET_VIEW, + }, + }); + + if (asset.chainId !== selectedChainId) { + const { NetworkController } = Engine.context; + const networkConfiguration = + NetworkController.getNetworkConfigurationByChainId( + asset.chainId as Hex, + ); + + const networkClientId = + networkConfiguration?.rpcEndpoints?.[ + networkConfiguration.defaultRpcEndpointIndex + ]?.networkClientId; + + await NetworkController.setActiveNetwork(networkClientId as string); + } + } if (asset.isETH && ticker) { dispatch(newAssetTransaction(getEther(ticker))); } else { @@ -128,25 +204,58 @@ const AssetOverview: React.FC = ({ navigation.navigate('SendFlowView', {}); }; - const goToSwaps = () => { - navigation.navigate('Swaps', { - screen: 'SwapsAmountView', - params: { - sourceToken: asset.address, - sourcePage: 'MainView', - }, - }); - trackEvent( - createEventBuilder(MetaMetricsEvents.SWAP_BUTTON_CLICKED) - .addProperties({ - text: 'Swap', - tokenSymbol: '', - location: 'TokenDetails', - chain_id: getDecimalChainId(chainId), - }) - .build(), - ); - }; + const goToSwaps = useCallback(() => { + if (isPortfolioViewEnabled()) { + navigation.navigate(Routes.WALLET.HOME, { + screen: Routes.WALLET.TAB_STACK_FLOW, + params: { + screen: Routes.WALLET_VIEW, + }, + }); + if (asset.chainId !== selectedChainId) { + const { NetworkController } = Engine.context; + const networkConfiguration = + NetworkController.getNetworkConfigurationByChainId( + asset.chainId as Hex, + ); + + const networkClientId = + networkConfiguration?.rpcEndpoints?.[ + networkConfiguration.defaultRpcEndpointIndex + ]?.networkClientId; + + NetworkController.setActiveNetwork(networkClientId as string).then( + () => { + setTimeout(() => { + handleSwapNavigation(); + }, 500); + }, + ); + } else { + handleSwapNavigation(); + } + } else { + handleSwapNavigation(); + trackEvent( + createEventBuilder(MetaMetricsEvents.SWAP_BUTTON_CLICKED) + .addProperties({ + text: 'Swap', + tokenSymbol: '', + location: 'TokenDetails', + chain_id: getDecimalChainId(asset.chainId), + }) + .build(), + ); + } + }, [ + navigation, + asset.chainId, + selectedChainId, + trackEvent, + createEventBuilder, + handleSwapNavigation, + ]); + const onBuy = () => { navigation.navigate( ...createBuyNavigationDetails({ @@ -209,14 +318,21 @@ const AssetOverview: React.FC = ({ )), [handleSelectTimePeriod, timePeriod], ); - const itemAddress = safeToChecksumAddress(asset.address); - const exchangeRate = itemAddress - ? tokenExchangeRates?.[itemAddress]?.price - : undefined; + + let exchangeRate: number | undefined; + if (!isPortfolioViewEnabled()) { + exchangeRate = itemAddress + ? tokenExchangeRates?.[itemAddress as Hex]?.price + : undefined; + } else { + const currentChainId = chainId as Hex; + exchangeRate = + allTokenMarketData?.[currentChainId]?.[itemAddress as Hex]?.price; + } let balance, balanceFiat; - if (asset.isETH) { + if (asset.isETH || asset.isNative) { balance = renderFromWei( //@ts-expect-error - This should be fixed at the accountsController selector level, ongoing discussion accountsByChainId[toHexadecimal(chainId)][selectedAddress]?.balance, @@ -230,9 +346,22 @@ const AssetOverview: React.FC = ({ currentCurrency, ); } else { + const multiChainTokenBalanceHex = + itemAddress && + multiChainTokenBalance?.[selectedInternalAccountAddress as Hex]?.[ + chainId as Hex + ]?.[itemAddress as Hex]; + + const selectedTokenBalanceHex = + itemAddress && tokenBalances?.[itemAddress as Hex]; + + const tokenBalanceHex = isPortfolioViewEnabled() + ? multiChainTokenBalanceHex + : selectedTokenBalanceHex; + balance = - itemAddress && tokenBalances?.[itemAddress] - ? renderFromTokenMinimalUnit(tokenBalances[itemAddress], asset.decimals) + itemAddress && tokenBalanceHex + ? renderFromTokenMinimalUnit(tokenBalanceHex, asset.decimals) : 0; balanceFiat = balanceToFiat( balance, @@ -243,23 +372,37 @@ const AssetOverview: React.FC = ({ } let mainBalance, secondaryBalance; - if (primaryCurrency === 'ETH') { - mainBalance = `${balance} ${asset.symbol}`; - secondaryBalance = balanceFiat; + if (!isPortfolioViewEnabled()) { + if (primaryCurrency === 'ETH') { + mainBalance = `${balance} ${asset.symbol}`; + secondaryBalance = balanceFiat; + } else { + mainBalance = !balanceFiat ? `${balance} ${asset.symbol}` : balanceFiat; + secondaryBalance = !balanceFiat + ? balanceFiat + : `${balance} ${asset.symbol}`; + } } else { - mainBalance = !balanceFiat ? `${balance} ${asset.symbol}` : balanceFiat; - secondaryBalance = !balanceFiat - ? balanceFiat - : `${balance} ${asset.symbol}`; + mainBalance = `${balance} ${asset.isETH ? asset.ticker : asset.symbol}`; + secondaryBalance = asset.balanceFiat || ''; } let currentPrice = 0; let priceDiff = 0; - if (asset.isETH) { - currentPrice = conversionRate || 0; - } else if (exchangeRate && conversionRate) { - currentPrice = exchangeRate * conversionRate; + if (!isPortfolioViewEnabled()) { + if (asset.isETH) { + currentPrice = conversionRate || 0; + } else if (exchangeRate && conversionRate) { + currentPrice = exchangeRate * conversionRate; + } + } else { + const tickerConversionRate = + conversionRateByTicker?.[nativeCurrency]?.conversionRate ?? 0; + currentPrice = + exchangeRate && tickerConversionRate + ? exchangeRate * tickerConversionRate + : 0; } const comparePrice = prices[0]?.[1] || 0; diff --git a/app/components/UI/AssetOverview/Balance/Balance.tsx b/app/components/UI/AssetOverview/Balance/Balance.tsx index fed53bd539a..82662417e44 100644 --- a/app/components/UI/AssetOverview/Balance/Balance.tsx +++ b/app/components/UI/AssetOverview/Balance/Balance.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { View } from 'react-native'; +import { Hex } from '@metamask/utils'; import { strings } from '../../../../../locales/i18n'; import { useStyles } from '../../../../component-library/hooks'; import styleSheet from './Balance.styles'; @@ -9,9 +10,11 @@ import { selectNetworkName } from '../../../../selectors/networkInfos'; import { selectChainId } from '../../../../selectors/networkController'; import { getTestNetImageByChainId, + getDefaultNetworkByChainId, isLineaMainnetByChainId, isMainnetByChainId, isTestNet, + isPortfolioViewEnabled, } from '../../../../util/networks'; import images from '../../../../images/image-icons'; import BadgeWrapper from '../../../../component-library/components/Badges/BadgeWrapper'; @@ -20,6 +23,7 @@ import Badge from '../../../../component-library/components/Badges/Badge/Badge'; import NetworkMainAssetLogo from '../../NetworkMainAssetLogo'; import AvatarToken from '../../../../component-library/components/Avatars/Avatar/variants/AvatarToken'; import { AvatarSize } from '../../../../component-library/components/Avatars/Avatar'; +import NetworkAssetLogo from '../../NetworkAssetLogo'; import Text, { TextVariant, } from '../../../../component-library/components/Texts/Text'; @@ -27,6 +31,11 @@ import { TokenI } from '../../Tokens/types'; import { useNavigation } from '@react-navigation/native'; import { isPooledStakingFeatureEnabled } from '../../Stake/constants'; import StakingBalance from '../../Stake/components/StakingBalance/StakingBalance'; +import { + PopularList, + UnpopularNetworkList, + CustomNetworkImgMapping, +} from '../../../../util/networks/customNetworks'; interface BalanceProps { asset: TokenI; @@ -34,17 +43,50 @@ interface BalanceProps { secondaryBalance?: string; } -export const NetworkBadgeSource = (chainId: string, ticker: string) => { +export const NetworkBadgeSource = (chainId: Hex, ticker: string) => { const isMainnet = isMainnetByChainId(chainId); const isLineaMainnet = isLineaMainnetByChainId(chainId); + if (!isPortfolioViewEnabled()) { + if (isTestNet(chainId)) return getTestNetImageByChainId(chainId); + if (isMainnet) return images.ETHEREUM; + + if (isLineaMainnet) return images['LINEA-MAINNET']; + + if (CustomNetworkImgMapping[chainId]) { + return CustomNetworkImgMapping[chainId]; + } + + return ticker ? images[ticker as keyof typeof images] : undefined; + } if (isTestNet(chainId)) return getTestNetImageByChainId(chainId); + const defaultNetwork = getDefaultNetworkByChainId(chainId) as + | { + imageSource: string; + } + | undefined; + + if (defaultNetwork) { + return defaultNetwork.imageSource; + } + + const unpopularNetwork = UnpopularNetworkList.find( + (networkConfig) => networkConfig.chainId === chainId, + ); - if (isMainnet) return images.ETHEREUM; + const customNetworkImg = CustomNetworkImgMapping[chainId]; - if (isLineaMainnet) return images['LINEA-MAINNET']; + const popularNetwork = PopularList.find( + (networkConfig) => networkConfig.chainId === chainId, + ); - return ticker ? images[ticker as keyof typeof images] : undefined; + const network = unpopularNetwork || popularNetwork; + if (network) { + return network.rpcPrefs.imageSource; + } + if (customNetworkImg) { + return customNetworkImg; + } }; const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { @@ -53,6 +95,44 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { const networkName = useSelector(selectNetworkName); const chainId = useSelector(selectChainId); + const tokenChainId = isPortfolioViewEnabled() ? asset.chainId : chainId; + + const ticker = asset.symbol; + + const renderNetworkAvatar = useCallback(() => { + if (!isPortfolioViewEnabled() && asset.isETH) { + return ; + } + + if (isPortfolioViewEnabled() && asset.isNative) { + return ( + + ); + } + + return ( + + ); + }, [ + asset.isETH, + asset.image, + asset.symbol, + asset.isNative, + asset.chainId, + styles.ethLogo, + ]); + return ( @@ -62,27 +142,26 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { asset={asset} mainBalance={mainBalance} balance={secondaryBalance} - onPress={() => !asset.isETH && navigation.navigate('AssetDetails')} + onPress={() => + !asset.isETH && + !asset.isNative && + navigation.navigate('AssetDetails', { + chainId: asset.chainId, + address: asset.address, + }) + } > } > - {asset.isETH ? ( - - ) : ( - - )} + {renderNetworkAvatar()} {asset.name || asset.symbol} diff --git a/app/components/UI/AssetOverview/Balance/index.test.tsx b/app/components/UI/AssetOverview/Balance/index.test.tsx index 5607f71900a..ba5dd5f2fc3 100644 --- a/app/components/UI/AssetOverview/Balance/index.test.tsx +++ b/app/components/UI/AssetOverview/Balance/index.test.tsx @@ -7,6 +7,8 @@ import { selectChainId } from '../../../../selectors/networkController'; import { Provider, useSelector } from 'react-redux'; import configureMockStore from 'redux-mock-store'; import { backgroundState } from '../../../../util/test/initial-root-state'; +import { NetworkBadgeSource } from './Balance'; +import { isPortfolioViewEnabled } from '../../../../util/networks'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -35,6 +37,8 @@ const mockDAI = { symbol: 'DAI', isETH: false, logo: 'image-path', + chainId: '0x1', + isNative: false, }; const mockETH = { @@ -50,6 +54,8 @@ const mockETH = { symbol: 'ETH', isETH: true, logo: 'image-path', + chainId: '0x1', + isNative: true, }; const mockInitialState = { @@ -58,6 +64,16 @@ const mockInitialState = { }, }; +jest.mock('../../../../util/networks', () => ({ + ...jest.requireActual('../../../../util/networks'), + getTestNetImageByChainId: jest.fn((chainId) => `testnet-image-${chainId}`), +})); + +jest.mock('../../../../util/networks', () => ({ + ...jest.requireActual('../../../../util/networks'), + isPortfolioViewEnabled: jest.fn(), +})); + describe('Balance', () => { const mockStore = configureMockStore(); const store = mockStore(mockInitialState); @@ -83,23 +99,27 @@ describe('Balance', () => { jest.clearAllMocks(); }); - it('should render correctly with a fiat balance', () => { - const wrapper = render( - , - ); - expect(wrapper).toMatchSnapshot(); - }); + if (!isPortfolioViewEnabled()) { + it('should render correctly with a fiat balance', () => { + const wrapper = render( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + } - it('should render correctly without a fiat balance', () => { - const wrapper = render( - , - ); - expect(wrapper).toMatchSnapshot(); - }); + if (!isPortfolioViewEnabled()) { + it('should render correctly without a fiat balance', () => { + const wrapper = render( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + } it('should fire navigation event for non native tokens', () => { const { queryByTestId } = render( @@ -120,4 +140,62 @@ describe('Balance', () => { fireEvent.press(assetElement); expect(mockNavigate).toHaveBeenCalledTimes(0); }); + + describe('NetworkBadgeSource', () => { + it('returns testnet image for a testnet chainId', () => { + const result = NetworkBadgeSource('0xaa36a7', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns mainnet Ethereum image for mainnet chainId', () => { + const result = NetworkBadgeSource('0x1', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId', () => { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + }); + + it('returns undefined if no image is found', () => { + const result = NetworkBadgeSource('0x999', 'UNKNOWN'); + expect(result).toBeUndefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId isPortfolioViewEnabled is true', () => { + if (isPortfolioViewEnabled()) { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + } + }); + }); +}); + +describe('NetworkBadgeSource', () => { + it('returns testnet image for a testnet chainId', () => { + const result = NetworkBadgeSource('0xaa36a7', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns mainnet Ethereum image for mainnet chainId', () => { + const result = NetworkBadgeSource('0x1', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId', () => { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + }); + + it('returns undefined if no image is found', () => { + const result = NetworkBadgeSource('0x999', 'UNKNOWN'); + expect(result).toBeUndefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId isPortfolioViewEnabled is true', () => { + if (isPortfolioViewEnabled()) { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + } + }); }); diff --git a/app/components/UI/AssetOverview/Price/Price.tsx b/app/components/UI/AssetOverview/Price/Price.tsx index 9d95b9e17b5..9e65e259d4c 100644 --- a/app/components/UI/AssetOverview/Price/Price.tsx +++ b/app/components/UI/AssetOverview/Price/Price.tsx @@ -17,7 +17,7 @@ import Text, { import PriceChart from '../PriceChart/PriceChart'; import { distributeDataPoints } from '../PriceChart/utils'; import styleSheet from './Price.styles'; -import { TokenOverviewSelectorsIDs } from '../../../../../e2e/selectors/TokenOverview.selectors'; +import { TokenOverviewSelectorsIDs } from '../../../../../e2e/selectors/wallet/TokenOverview.selectors'; import { TokenI } from '../../Tokens/types'; interface PriceProps { @@ -90,7 +90,10 @@ const Price = ({ {asset.symbol} )} {!isNaN(price) && ( - + {isLoading ? ( diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx index 7e8d341492d..881977207bb 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import { Hex } from '@metamask/utils'; +import { MarketDataDetails } from '@metamask/assets-controllers'; import renderWithProvider from '../../../../util/test/renderWithProvider'; import { backgroundState } from '../../../../util/test/initial-root-state'; import TokenDetails from './'; @@ -8,9 +10,12 @@ import { selectConversionRate, selectCurrentCurrency, } from '../../../../selectors/currencyRateController'; +import { + selectProviderConfig, + selectTicker, +} from '../../../../selectors/networkController'; // eslint-disable-next-line import/no-namespace import * as reactRedux from 'react-redux'; - jest.mock('../../../../core/Engine', () => ({ getTotalFiatAccountBalance: jest.fn(), context: { @@ -80,14 +85,60 @@ const mockContractExchangeRates = { }, }; +const mockTokenMarketDataByChainId: Record< + Hex, + Record +> = { + '0x1': { + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + allTimeHigh: 0.00045049491236145674, + allTimeLow: 0.00032567089582484455, + circulatingSupply: 5210102796.32321, + currency: 'ETH', + dilutedMarketCap: 1923097.9291743594, + high1d: 0.0003703658992610993, + low1d: 0.00036798603064620616, + marketCap: 1923097.9291743594, + marketCapPercentChange1d: -0.03026, + price: 0.00036902069191213795, + priceChange1d: 0.00134711, + pricePercentChange14d: -0.01961306580879152, + pricePercentChange1d: 0.13497913251736524, + pricePercentChange1h: -0.15571963819527113, + pricePercentChange1y: -0.01608509228365429, + pricePercentChange200d: -0.0287692372426721, + pricePercentChange30d: -0.08401729203937018, + pricePercentChange7d: 0.019578202262256407, + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + totalVolume: 54440.464606773865, + }, + }, +}; + describe('TokenDetails', () => { beforeAll(() => { jest.resetAllMocks(); }); it('should render correctly', () => { const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector'); - useSelectorSpy.mockImplementation((selector) => { - switch (selector) { + useSelectorSpy.mockImplementation((selectorOrCallback) => { + const SELECTOR_MOCKS = { + selectTokenMarketDataByChainId: mockTokenMarketDataByChainId['0x1'], + selectConversionRateBySymbol: mockExchangeRate, + selectNativeCurrencyByChainId: 'ETH', + } as const; + + if (typeof selectorOrCallback === 'function') { + const selectorString = selectorOrCallback.toString(); + const matchedSelector = Object.keys(SELECTOR_MOCKS).find((key) => + selectorString.includes(key), + ); + if (matchedSelector) { + return SELECTOR_MOCKS[matchedSelector as keyof typeof SELECTOR_MOCKS]; + } + } + + switch (selectorOrCallback) { case selectTokenList: return mockAssets; case selectContractExchangeRates: @@ -133,10 +184,26 @@ describe('TokenDetails', () => { expect(toJSON()).toMatchSnapshot(); }); - it('should render TokenDetils without MarketDetails when marketData is null', () => { + it('should render Token Details without Market Details when marketData is null', () => { const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector'); - useSelectorSpy.mockImplementation((selector) => { - switch (selector) { + const SELECTOR_MOCKS = { + selectTokenMarketDataByChainId: {}, + selectConversionRateBySymbol: mockExchangeRate, + selectNativeCurrencyByChainId: 'ETH', + } as const; + + useSelectorSpy.mockImplementation((selectorOrCallback) => { + if (typeof selectorOrCallback === 'function') { + const selectorString = selectorOrCallback.toString(); + const matchedSelector = Object.keys(SELECTOR_MOCKS).find((key) => + selectorString.includes(key), + ); + if (matchedSelector) { + return SELECTOR_MOCKS[matchedSelector as keyof typeof SELECTOR_MOCKS]; + } + } + + switch (selectorOrCallback) { case selectTokenList: return mockAssets; case selectContractExchangeRates: @@ -145,6 +212,10 @@ describe('TokenDetails', () => { return mockExchangeRate; case selectCurrentCurrency: return mockCurrentCurrency; + case selectProviderConfig: + return { ticker: 'ETH' }; + case selectTicker: + return 'ETH'; default: return undefined; } @@ -162,8 +233,24 @@ describe('TokenDetails', () => { it('should render MarketDetails without TokenDetails when tokenList is null', () => { const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector'); - useSelectorSpy.mockImplementation((selector) => { - switch (selector) { + useSelectorSpy.mockImplementation((selectorOrCallback) => { + const SELECTOR_MOCKS = { + selectTokenMarketDataByChainId: mockTokenMarketDataByChainId['0x1'], + selectConversionRateBySymbol: mockExchangeRate, + selectNativeCurrencyByChainId: 'ETH', + } as const; + + if (typeof selectorOrCallback === 'function') { + const selectorString = selectorOrCallback.toString(); + const matchedSelector = Object.keys(SELECTOR_MOCKS).find((key) => + selectorString.includes(key), + ); + if (matchedSelector) { + return SELECTOR_MOCKS[matchedSelector as keyof typeof SELECTOR_MOCKS]; + } + } + + switch (selectorOrCallback) { case selectTokenList: return {}; case selectContractExchangeRates: @@ -179,9 +266,7 @@ describe('TokenDetails', () => { const { getByText, queryByText } = renderWithProvider( , - { - state: initialState, - }, + { state: initialState }, ); expect(queryByText('Token details')).toBeNull(); expect(getByText('Market details')).toBeDefined(); diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx index 368e2352d23..a56469bfff6 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx @@ -1,4 +1,6 @@ import { zeroAddress } from 'ethereumjs-util'; +import { Hex } from '@metamask/utils'; +import { RootState } from '../../../../reducers'; import React from 'react'; import { View } from 'react-native'; import { useSelector } from 'react-redux'; @@ -7,11 +9,16 @@ import { useStyles } from '../../../../component-library/hooks'; import styleSheet from './TokenDetails.styles'; import { safeToChecksumAddress } from '../../../../util/address'; import { selectTokenList } from '../../../../selectors/tokenListController'; -import { selectContractExchangeRates } from '../../../../selectors/tokenRatesController'; import { - selectConversionRate, + selectTokenMarketDataByChainId, + selectContractExchangeRates, +} from '../../../../selectors/tokenRatesController'; +import { + selectConversionRateBySymbol, selectCurrentCurrency, + selectConversionRate, } from '../../../../selectors/currencyRateController'; +import { selectNativeCurrencyByChainId } from '../../../../selectors/networkController'; import { convertDecimalToPercentage, localizeLargeNumber, @@ -23,6 +30,7 @@ import MarketDetailsList from './MarketDetailsList'; import { TokenI } from '../../Tokens/types'; import { isPooledStakingFeatureEnabled } from '../../Stake/constants'; import StakingEarnings from '../../Stake/components/StakingEarnings'; +import { isPortfolioViewEnabled } from '../../../../util/networks'; export interface TokenDetails { contractAddress: string | null; @@ -46,20 +54,36 @@ interface TokenDetailsProps { const TokenDetails: React.FC = ({ asset }) => { const { styles } = useStyles(styleSheet, {}); - const tokenList = useSelector(selectTokenList); - const tokenExchangeRates = useSelector(selectContractExchangeRates); - const conversionRate = useSelector(selectConversionRate); + const tokenExchangeRatesByChainId = useSelector((state: RootState) => + selectTokenMarketDataByChainId(state, asset.chainId as Hex), + ); + const nativeCurrency = useSelector((state: RootState) => + selectNativeCurrencyByChainId(state, asset.chainId as Hex), + ); + const tokenExchangeRatesLegacy = useSelector(selectContractExchangeRates); + const conversionRateLegacy = useSelector(selectConversionRate); + const conversionRateBySymbol = useSelector((state: RootState) => + selectConversionRateBySymbol(state, nativeCurrency), + ); const currentCurrency = useSelector(selectCurrentCurrency); const tokenContractAddress = safeToChecksumAddress(asset.address); + const tokenList = useSelector(selectTokenList); + + const conversionRate = isPortfolioViewEnabled() + ? conversionRateBySymbol + : conversionRateLegacy; + const tokenExchangeRates = isPortfolioViewEnabled() + ? tokenExchangeRatesByChainId + : tokenExchangeRatesLegacy; let tokenMetadata; let marketData; if (asset.isETH) { - marketData = tokenExchangeRates?.[zeroAddress() as `0x${string}`]; - } else if (!asset.isETH && tokenContractAddress) { + marketData = tokenExchangeRates?.[zeroAddress() as Hex]; + } else if (tokenContractAddress) { tokenMetadata = tokenList?.[tokenContractAddress.toLowerCase()]; - marketData = tokenExchangeRates?.[tokenContractAddress]; + marketData = tokenExchangeRates?.[tokenContractAddress as Hex]; } else { Logger.log('cannot find contract address'); return null; @@ -123,7 +147,9 @@ const TokenDetails: React.FC = ({ asset }) => { return ( - {asset.isETH && isPooledStakingFeatureEnabled() && } + {asset.isETH && isPooledStakingFeatureEnabled() && ( + + )} {(asset.isETH || tokenMetadata) && ( )} diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx index 1b0c37923d0..64f1e8da9d0 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx @@ -19,6 +19,7 @@ describe('TokenDetails', () => { beforeAll(() => { jest.resetAllMocks(); }); + it('should render correctly', () => { const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch'); useDispatchSpy.mockImplementation(() => jest.fn()); diff --git a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap index cc3cbd21a25..1b441f7cea3 100644 --- a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap +++ b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap @@ -1133,3 +1133,1155 @@ exports[`AssetOverview should render correctly 1`] = ` `; + +exports[`AssetOverview should render correctly when portfolio view is enabled 1`] = ` + + + + + Ethereum + ( + ETH + ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1D + + + + + 1W + + + + + 1M + + + + + 3M + + + + + 1Y + + + + + 3Y + + + + + + + + + + + + + + + + Buy + + + + + + + + + + + + + + Swap + + + + + + + + + + + + + + Bridge + + + + + + + + + + + + + + Send + + + + + + + + + + + + + + Receive + + + + + + Your balance + + + + + + + + + + + + + + + + + + Ethereum + + + + 1500 + + + 0 ETH + + + + + + + +`; diff --git a/app/components/UI/AssetSearch/index.tsx b/app/components/UI/AssetSearch/index.tsx index b1cfd4be105..3931f5c7c0e 100644 --- a/app/components/UI/AssetSearch/index.tsx +++ b/app/components/UI/AssetSearch/index.tsx @@ -14,7 +14,6 @@ import { useSelector } from 'react-redux'; import { TokenListToken } from '@metamask/assets-controllers'; import { useTheme } from '../../../util/theme'; import { ImportTokenViewSelectorsIDs } from '../../../../e2e/selectors/wallet/ImportTokenView.selectors'; -import { TokenViewSelectors } from '../../../../e2e/selectors/AddTokenView.selectors'; import { selectTokenListArray } from '../../../selectors/tokenListController'; import Icon, { IconName, @@ -140,7 +139,7 @@ const AssetSearch = memo(({ onSearch, onFocus, onBlur }: Props) => { return ( diff --git a/app/components/UI/BackupAlert/BackupAlert.tsx b/app/components/UI/BackupAlert/BackupAlert.tsx index d3d0175adc8..134de7ca2df 100644 --- a/app/components/UI/BackupAlert/BackupAlert.tsx +++ b/app/components/UI/BackupAlert/BackupAlert.tsx @@ -8,7 +8,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { backUpSeedphraseAlertNotVisible } from '../../../actions/user'; import { findRouteNameFromNavigatorState } from '../../../util/general'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { ProtectWalletModalSelectorsIDs } from '../../../../e2e/selectors/Modals/ProtectWalletModal.selectors'; +import { ProtectWalletModalSelectorsIDs } from '../../../../e2e/selectors/Onboarding/ProtectWalletModal.selectors'; import styleSheet from './BackupAlert.styles'; import { useStyles } from '../../../component-library/hooks'; import { BackupAlertI } from './BackupAlert.types'; diff --git a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx index df79dac4e0e..b488c94a26b 100644 --- a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx +++ b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx @@ -30,10 +30,8 @@ import NotificationsService from '../../../../util/notifications/services/Notifi import { MetaMetricsEvents } from '../../../../core/Analytics'; import { useEnableNotifications } from '../../../../util/notifications/hooks/useNotifications'; import { useMetrics } from '../../../hooks/useMetrics'; -import { - selectIsProfileSyncingEnabled, - selectIsMetamaskNotificationsEnabled, -} from '../../../../selectors/notifications'; +import { selectIsMetamaskNotificationsEnabled } from '../../../../selectors/notifications'; +import { selectIsProfileSyncingEnabled } from '../../../../selectors/identity'; interface Props { route: { diff --git a/app/components/UI/CollectibleContractElement/index.js b/app/components/UI/CollectibleContractElement/index.js index eec785149e6..25eddc35f18 100644 --- a/app/components/UI/CollectibleContractElement/index.js +++ b/app/components/UI/CollectibleContractElement/index.js @@ -13,7 +13,7 @@ import { removeFavoriteCollectible } from '../../../actions/collectibles'; import { collectibleContractsSelector } from '../../../reducers/collectibles'; import { useTheme } from '../../../util/theme'; import { selectChainId } from '../../../selectors/networkController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; import Icon, { IconName, IconColor, @@ -320,7 +320,7 @@ CollectibleContractElement.propTypes = { const mapStateToProps = (state) => ({ collectibleContracts: collectibleContractsSelector(state), chainId: selectChainId(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/app/components/UI/CollectibleContractOverview/index.js b/app/components/UI/CollectibleContractOverview/index.js index ce41eacbb26..94942c48f33 100644 --- a/app/components/UI/CollectibleContractOverview/index.js +++ b/app/components/UI/CollectibleContractOverview/index.js @@ -13,7 +13,7 @@ import { newAssetTransaction } from '../../../actions/transaction'; import { toLowerCaseEquals } from '../../../util/general'; import { collectiblesSelector } from '../../../reducers/collectibles'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import { SEND_BUTTON_ID } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; +import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/wallet/TokenOverview.selectors'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; const createStyles = (colors) => @@ -139,7 +139,7 @@ class CollectibleContractOverview extends PureComponent { icon="send" onPress={this.onSend} label={leftActionButtonText} - testID={SEND_BUTTON_ID} + testID={TokenOverviewSelectorsIDs.SEND_BUTTON} /> ({ networkType: selectProviderType(state), chainId: selectChainId(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), useNftDetection: selectUseNftDetection(state), collectibleContracts: collectibleContractsSelector(state), collectibles: collectiblesSelector(state), diff --git a/app/components/UI/CollectibleOverview/index.js b/app/components/UI/CollectibleOverview/index.js index a2015051686..33999cfb2dc 100644 --- a/app/components/UI/CollectibleOverview/index.js +++ b/app/components/UI/CollectibleOverview/index.js @@ -48,7 +48,7 @@ import { selectDisplayNftMedia, selectIsIpfsGatewayEnabled, } from '../../../selectors/preferencesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; const ANIMATION_VELOCITY = 250; const HAS_NOTCH = Device.hasNotch(); @@ -546,7 +546,7 @@ CollectibleOverview.propTypes = { const mapStateToProps = (state, props) => ({ chainId: selectChainId(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), isInFavorites: isCollectibleInFavoritesSelector(state, props.collectible), }); diff --git a/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx b/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx index a0123631d78..267717fe297 100644 --- a/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx +++ b/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx @@ -23,7 +23,7 @@ import { import { MetaMetricsEvents } from '../../../core/Analytics'; import { ScrollView } from 'react-native-gesture-handler'; -import { EnableAutomaticSecurityChecksIDs } from '../../../../e2e/selectors/Modals/EnableAutomaticSecurityChecks.selectors'; +import { EnableAutomaticSecurityChecksIDs } from '../../../../e2e/selectors/Onboarding/EnableAutomaticSecurityChecks.selectors'; import generateDeviceAnalyticsMetaData from '../../../util/metrics'; import { useMetrics } from '../../../components/hooks/useMetrics'; diff --git a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx index 3deb0c47837..69012308952 100644 --- a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx +++ b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx @@ -3,9 +3,8 @@ import { Switch, Text, View } from 'react-native'; import { strings } from '../../../../locales/i18n'; import { BIOMETRY_TYPE } from 'react-native-keychain'; import { createStyles } from './styles'; -import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selectors'; +import { LoginViewSelectors } from '../../../../e2e/selectors/wallet/LoginView.selectors'; import { useSelector } from 'react-redux'; -import { LoginOptionsSwitchSelectorsIDs } from '../../../../e2e/selectors/LoginOptionsSwitch.selectors'; import { useTheme } from '../../../util/theme'; interface Props { @@ -69,7 +68,6 @@ const LoginOptionsSwitch = ({ }} thumbColor={theme.brandColors.white} ios_backgroundColor={colors.border.muted} - testID={LoginOptionsSwitchSelectorsIDs.BIOMETRICS_SWITCH} /> ); diff --git a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap index 3aef46b9799..c5e91ce27bf 100644 --- a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap +++ b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap @@ -68,6 +68,7 @@ exports[`ManageNetworks should render correctly 1`] = ` to learn more about how Infura handles data. { +const trackEvent = (event, params = {}) => { MetaMetrics.getInstance().trackEvent(event); }; @@ -918,7 +922,7 @@ export function getOfflineModalNavbar() { * Function that returns the navigation options for the wallet screen. * * @param {Object} accountActionsRef - The ref object for the account actions - * @param {string} selectedAddress - The currently selected Ethereum address + * @param {Object} selectedInternalAccount - The currently selected internal account * @param {string} accountName - The name of the currently selected account * @param {string} accountAvatarType - The type of avatar for the currently selected account * @param {string} networkName - The name of the current network @@ -934,7 +938,7 @@ export function getOfflineModalNavbar() { */ export function getWalletNavbarOptions( accountActionsRef, - selectedAddress, + selectedInternalAccount, accountName, accountAvatarType, networkName, @@ -963,6 +967,15 @@ export function getWalletNavbarOptions( }, }); + let formattedAddress = toChecksumHexAddress(selectedInternalAccount.address); + + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + if (isBtcAccount(selectedInternalAccount)) { + // BTC addresses are not checksummed + formattedAddress = selectedInternalAccount.address; + } + ///: END:ONLY_INCLUDE_IF + const onScanSuccess = (data, content) => { if (data.private_key) { Alert.alert( @@ -1046,35 +1059,55 @@ export function getWalletNavbarOptions( } } + const renderNetworkPicker = () => { + let networkPicker = ( + + ); + + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + if (isBtcAccount(selectedInternalAccount)) { + networkPicker = ( + + ); + } + ///: END:ONLY_INCLUDE_IF + + return {networkPicker}; + }; + return { headerTitle: () => ( { navigation.navigate(...createAccountSelectorNavDetails({})); }} - accountTypeLabel={getLabelTextByAddress(selectedAddress) || undefined} + accountTypeLabel={ + getLabelTextByAddress(formattedAddress) || undefined + } showAddress cellAccountContainerStyle={styles.account} testID={WalletViewSelectorsIDs.ACCOUNT_ICON} /> ), - headerLeft: () => ( - - - - ), + headerLeft: () => renderNetworkPicker(), headerRight: () => ( ), headerLeft: () => ( @@ -1705,9 +1740,16 @@ export function getSwapsQuotesNavbar(navigation, route, themeColors) { MetaMetricsEvents.QUOTES_REQUEST_CANCELLED, ) .addProperties({ - ...trade, + token_from: trade.token_from, + token_to: trade.token_to, + request_type: trade.request_type, + custom_slippage: trade.custom_slippage, + chain_id: trade.chain_id, responseTime: new Date().getTime() - quoteBegin, }) + .addSensitiveProperties({ + token_from_amount: trade.token_from_amount, + }) .build(), ); } @@ -1724,9 +1766,16 @@ export function getSwapsQuotesNavbar(navigation, route, themeColors) { MetaMetricsEvents.QUOTES_REQUEST_CANCELLED, ) .addProperties({ - ...trade, + token_from: trade.token_from, + token_to: trade.token_to, + request_type: trade.request_type, + custom_slippage: trade.custom_slippage, + chain_id: trade.chain_id, responseTime: new Date().getTime() - quoteBegin, }) + .addSensitiveProperties({ + token_from_amount: trade.token_from_amount, + }) .build(), ); } diff --git a/app/components/UI/NavbarTitle/index.js b/app/components/UI/NavbarTitle/index.js index d4e7932f52f..d1ce5408209 100644 --- a/app/components/UI/NavbarTitle/index.js +++ b/app/components/UI/NavbarTitle/index.js @@ -6,7 +6,6 @@ import { TouchableOpacity, View, StyleSheet } from 'react-native'; import { fontStyles, colors as importedColors } from '../../../styles/common'; import Networks, { getDecimalChainId } from '../../../util/networks'; import { strings } from '../../../../locales/i18n'; -import Device from '../../../util/device'; import { ThemeContext, mockTheme } from '../../../util/theme'; import Routes from '../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../core/Analytics'; @@ -26,6 +25,7 @@ const createStyles = (colors) => }, network: { flexDirection: 'row', + alignItems: 'center', }, networkName: { fontSize: 11, @@ -37,7 +37,6 @@ const createStyles = (colors) => height: 5, borderRadius: 100, marginRight: 5, - marginTop: Device.isIos() ? 4 : 5, }, title: { fontSize: scale(14), @@ -90,6 +89,10 @@ class NavbarTitle extends PureComponent { * Boolean that specifies if the network selected is displayed */ showSelectedNetwork: PropTypes.bool, + /** + * Name of the network to display + */ + networkName: PropTypes.string, /** * Content to display inside text element */ @@ -127,8 +130,14 @@ class NavbarTitle extends PureComponent { }; render = () => { - const { providerConfig, title, translate, showSelectedNetwork, children } = - this.props; + const { + providerConfig, + title, + translate, + showSelectedNetwork, + children, + networkName, + } = this.props; let name = null; const color = (Networks[providerConfig.type] && Networks[providerConfig.type].color) || @@ -136,7 +145,9 @@ class NavbarTitle extends PureComponent { const colors = this.context.colors || mockTheme.colors; const styles = createStyles(colors); - if (providerConfig.nickname) { + if (networkName) { + name = networkName; + } else if (providerConfig.nickname) { name = providerConfig.nickname; } else { name = @@ -145,7 +156,6 @@ class NavbarTitle extends PureComponent { } const realTitle = translate ? strings(title) : title; - return ( jest.fn(() => null)); + +describe('NetworkAssetLogo Component', () => { + it('matches the snapshot for non-mainnet', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders TokenIcon with ETH for mainnet chainId', () => { + const props = { + chainId: ChainId.mainnet, + ticker: 'TEST', + style: { width: 50, height: 50 }, + big: true, + biggest: false, + testID: 'network-asset-logo', + }; + + render(); + + expect(TokenIcon).toHaveBeenCalledWith( + { + big: props.big, + biggest: props.biggest, + symbol: 'ETH', + style: props.style, + testID: props.testID, + }, + {}, + ); + }); + + it('renders TokenIcon with ticker for non-mainnet chainId', () => { + const props = { + chainId: '0x38', // Binance Smart Chain + ticker: 'BNB', + style: { width: 40, height: 40 }, + big: false, + biggest: true, + testID: 'network-asset-logo', + }; + + render(); + + expect(TokenIcon).toHaveBeenCalledWith( + { + big: props.big, + biggest: props.biggest, + symbol: props.ticker, + style: props.style, + testID: props.testID, + }, + {}, + ); + }); +}); diff --git a/app/components/UI/NetworkAssetLogo/index.tsx b/app/components/UI/NetworkAssetLogo/index.tsx new file mode 100644 index 00000000000..90629c5f871 --- /dev/null +++ b/app/components/UI/NetworkAssetLogo/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { ChainId } from '@metamask/controller-utils'; +import TokenIcon from '../Swaps/components/TokenIcon'; + +interface NetworkAssetLogoProps { + chainId: string; + ticker: string; + style: object; + big: boolean; + biggest: boolean; + testID: string; +} + +function NetworkAssetLogo({ + chainId, + ticker, + style, + big, + biggest, + testID, +}: NetworkAssetLogoProps) { + if (chainId === ChainId.mainnet) { + return ( + + ); + } + return ( + + ); +} + +export default NetworkAssetLogo; diff --git a/app/components/UI/NetworkCell/NetworkCell.tsx b/app/components/UI/NetworkCell/NetworkCell.tsx index 58ea3d26c5d..a9816e6c86a 100644 --- a/app/components/UI/NetworkCell/NetworkCell.tsx +++ b/app/components/UI/NetworkCell/NetworkCell.tsx @@ -1,23 +1,21 @@ import React from 'react'; import { Switch, ImageSourcePropType } from 'react-native'; -import { ETHERSCAN_SUPPORTED_NETWORKS } from '@metamask/transaction-controller'; import { useStyles } from '../../../component-library/hooks'; import Cell from '../../../component-library/components/Cells/Cell/Cell'; import { CellVariant } from '../../../component-library/components/Cells/Cell'; import { AvatarVariant } from '../../../component-library/components/Avatars/Avatar/Avatar.types'; import { useTheme } from '../../../util/theme'; -import { EtherscanSupportedHexChainId } from '@metamask/preferences-controller'; import styleSheet from './NetworkCell.styles'; +import { Hex } from '@metamask/utils'; -const supportedNetworks = ETHERSCAN_SUPPORTED_NETWORKS; interface NetworkCellProps { name: string; - chainId: EtherscanSupportedHexChainId | keyof typeof supportedNetworks; + chainId: Hex; imageSource: ImageSourcePropType; - secondaryText: string; + secondaryText?: string; showIncomingTransactionsNetworks: Record; toggleEnableIncomingTransactions: ( - chainId: EtherscanSupportedHexChainId, + chainId: Hex, value: boolean, ) => void; testID?: string; diff --git a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap index fa6b7d2bf5b..fb2c30e33a1 100644 --- a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap @@ -237,9 +237,8 @@ exports[`NetworkDetails renders correctly 1`] = ` style={ { "alignItems": "center", - "backgroundColor": "#f2f4f6", + "backgroundColor": "#ffffff", "borderRadius": 8, - "borderWidth": 1, "height": 16, "justifyContent": "center", "overflow": "hidden", @@ -248,21 +247,24 @@ exports[`NetworkDetails renders correctly 1`] = ` } testID="network-avatar-picker" > - - T - + testID="network-avatar-image" + /> ({ + context: { + PreferencesController: { + setTokenNetworkFilter: jest.fn(), + }, + NetworkController: { + updateNetwork: jest.fn(), + addNetwork: jest.fn(), + setActiveNetwork: jest.fn(), + }, + }, +})); + interface NetworkProps { isVisible: boolean; onClose: () => void; @@ -18,27 +36,46 @@ interface NetworkProps { showPopularNetworkModal: boolean; } +const mockDispatch = jest.fn(); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), - useDispatch: jest.fn(), + useDispatch: () => mockDispatch, useSelector: jest.fn(), })); + describe('NetworkDetails', () => { const props: NetworkProps = { isVisible: true, - onClose: () => ({}), + onClose: jest.fn(), networkConfiguration: { - chainId: '1', + chainId: '0x1', nickname: 'Test Network', - ticker: 'Test', + ticker: 'TEST', rpcUrl: 'https://localhost:8545', formattedRpcUrl: 'https://localhost:8545', rpcPrefs: { blockExplorerUrl: 'https://test.com', imageUrl: 'image' }, }, - navigation: 'navigation', + navigation: { navigate: jest.fn(), goBack: jest.fn() }, shouldNetworkSwitchPopToWallet: true, showPopularNetworkModal: true, }; + + beforeEach(() => { + jest.clearAllMocks(); + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === selectNetworkName) return 'Ethereum Main Network'; + if (selector === selectUseSafeChainsListValidation) return true; + return {}; + }); + }); + + const renderWithTheme = (component: React.ReactNode) => + render( + + {component} + , + ); + it('renders correctly', () => { (useSelector as jest.MockedFn).mockImplementation( (selector) => { @@ -46,8 +83,31 @@ describe('NetworkDetails', () => { if (selector === selectUseSafeChainsListValidation) return true; }, ); - const { toJSON } = render(); + const { toJSON } = renderWithTheme(); expect(toJSON()).toMatchSnapshot(); }); + + it('should call setTokenNetworkFilter when switching networks', async () => { + const { getByTestId } = renderWithTheme(); + + const approveButton = getByTestId( + NetworkApprovalBottomSheetSelectorsIDs.APPROVE_BUTTON, + ); + fireEvent.press(approveButton); + + const switchButton = getByTestId( + NetworkAddedBottomSheetSelectorsIDs.SWITCH_NETWORK_BUTTON, + ); + await act(async () => { + fireEvent.press(switchButton); + }); + + expect( + Engine.context.PreferencesController.setTokenNetworkFilter, + ).toHaveBeenCalledWith({ + [props.networkConfiguration.chainId]: true, + }); + expect(mockDispatch).toHaveBeenCalled(); + }); }); diff --git a/app/components/UI/NetworkModal/index.tsx b/app/components/UI/NetworkModal/index.tsx index 3c58495c9f5..30d7d4e1efd 100644 --- a/app/components/UI/NetworkModal/index.tsx +++ b/app/components/UI/NetworkModal/index.tsx @@ -6,10 +6,8 @@ import Text from '../../Base/Text'; import NetworkDetails from './NetworkDetails'; import NetworkAdded from './NetworkAdded'; import Engine from '../../../core/Engine'; -import { - isPrivateConnection, - toggleUseSafeChainsListValidation, -} from '../../../util/networks'; +import { isPrivateConnection } from '../../../util/networks'; +import { toggleUseSafeChainsListValidation } from '../../../util/networks/engineNetworkUtils'; import getDecimalChainId from '../../../util/networks/getDecimalChainId'; import URLPARSE from 'url-parse'; import { isWebUri } from 'valid-url'; @@ -24,7 +22,10 @@ import { import { useTheme } from '../../../util/theme'; import { networkSwitched } from '../../../actions/onboardNetwork'; import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkApprovalBottomSheet.selectors'; -import { selectUseSafeChainsListValidation } from '../../../selectors/preferencesController'; +import { + selectTokenNetworkFilter, + selectUseSafeChainsListValidation, +} from '../../../selectors/preferencesController'; import BottomSheetFooter, { ButtonsAlignment, } from '../../../component-library/components/BottomSheets/BottomSheetFooter'; @@ -36,7 +37,10 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; import { toHex } from '@metamask/controller-utils'; import { rpcIdentifierUtility } from '../../../components/hooks/useSafeChains'; import Logger from '../../../util/Logger'; -import { selectNetworkConfigurations } from '../../../selectors/networkController'; +import { + selectNetworkConfigurations, + selectIsAllNetworks, +} from '../../../selectors/networkController'; import { NetworkConfiguration, RpcEndpointType, @@ -87,6 +91,7 @@ const NetworkModals = (props: NetworkProps) => { const [showDetails, setShowDetails] = React.useState(false); const [networkAdded, setNetworkAdded] = React.useState(false); const [showCheckNetwork, setShowCheckNetwork] = React.useState(false); + const tokenNetworkFilter = useSelector(selectTokenNetworkFilter); const [alerts, setAlerts] = React.useState< { alertError: string; @@ -98,6 +103,7 @@ const NetworkModals = (props: NetworkProps) => { const isCustomNetwork = true; const showDetailsModal = () => setShowDetails(!showDetails); const showCheckNetworkModal = () => setShowCheckNetwork(!showCheckNetwork); + const isAllNetworks = useSelector(selectIsAllNetworks); const { colors } = useTheme(); const styles = createNetworkModalStyles(colors); @@ -109,6 +115,30 @@ const NetworkModals = (props: NetworkProps) => { return true; }; + const customNetworkInformation = { + chainId, + blockExplorerUrl, + chainName: nickname, + rpcUrl, + icon: imageUrl, + ticker, + alerts, + }; + + const onUpdateNetworkFilter = useCallback(() => { + const { PreferencesController } = Engine.context; + if (!isAllNetworks) { + PreferencesController.setTokenNetworkFilter({ + [customNetworkInformation.chainId]: true, + }); + } else { + PreferencesController.setTokenNetworkFilter({ + ...tokenNetworkFilter, + [customNetworkInformation.chainId]: true, + }); + } + }, [customNetworkInformation.chainId, isAllNetworks, tokenNetworkFilter]); + const addNetwork = async () => { const isValidUrl = validateRpcUrl(rpcUrl); if (showPopularNetworkModal) { @@ -172,16 +202,6 @@ const NetworkModals = (props: NetworkProps) => { selectNetworkConfigurations, ); - const customNetworkInformation = { - chainId, - blockExplorerUrl, - chainName: nickname, - rpcUrl, - icon: imageUrl, - ticker, - alerts, - }; - const checkNetwork = useCallback(async () => { if (useSafeChainsListValidation) { const alertsNetwork = await checkSafeNetwork( @@ -245,6 +265,7 @@ const NetworkModals = (props: NetworkProps) => { } if (networkClientId) { + onUpdateNetworkFilter(); await NetworkController.setActiveNetwork(networkClientId); } @@ -270,7 +291,7 @@ const NetworkModals = (props: NetworkProps) => { const { networkClientId } = updatedNetwork?.rpcEndpoints?.[updatedNetwork.defaultRpcEndpointIndex] ?? {}; - + onUpdateNetworkFilter(); await NetworkController.setActiveNetwork(networkClientId); }; @@ -339,6 +360,7 @@ const NetworkModals = (props: NetworkProps) => { addedNetwork?.rpcEndpoints?.[addedNetwork.defaultRpcEndpointIndex] ?? {}; + onUpdateNetworkFilter(); NetworkController.setActiveNetwork(networkClientId); } onClose(); diff --git a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx index 96761b37689..7595f71cb85 100644 --- a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx +++ b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx @@ -37,9 +37,9 @@ import BottomSheetFooter, { import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader'; import { getNetworkImageSource, - toggleUseSafeChainsListValidation, isMultichainVersion1Enabled, } from '../../../util/networks'; +import { toggleUseSafeChainsListValidation } from '../../../util/networks/engineNetworkUtils'; import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkApprovalBottomSheet.selectors'; import hideKeyFromUrl from '../../../util/hideKeyFromUrl'; import { convertHexToDecimal } from '@metamask/controller-utils'; diff --git a/app/components/UI/Notification/BaseNotification/index.js b/app/components/UI/Notification/BaseNotification/index.js index 79b1a7dfc2e..8c137a39168 100644 --- a/app/components/UI/Notification/BaseNotification/index.js +++ b/app/components/UI/Notification/BaseNotification/index.js @@ -9,7 +9,7 @@ import IonicIcon from 'react-native-vector-icons/Ionicons'; import AntIcon from 'react-native-vector-icons/AntDesign'; import Text from '../../../Base/Text'; import { useTheme } from '../../../../util/theme'; -import { ToastSelectorsIDs } from '../../../../../e2e/selectors/Modals/ToastModal.selectors'; +import { ToastSelectorsIDs } from '../../../../../e2e/selectors/wallet/ToastModal.selectors'; const createStyles = (colors) => StyleSheet.create({ @@ -154,7 +154,7 @@ const getTitle = (status, { nonce, amount, assetType }) => { }; export const getDescription = (status, { amount = null, type = null }) => { - if (amount && typeof amount !== 'object') { + if (amount && typeof amount !== 'object' && type) { return strings(`notifications.${type}_${status}_message`, { amount }); } return strings(`notifications.${status}_message`); diff --git a/app/components/UI/Notification/List/index.tsx b/app/components/UI/Notification/List/index.tsx index 035977563e7..68b97d9d73c 100644 --- a/app/components/UI/Notification/List/index.tsx +++ b/app/components/UI/Notification/List/index.tsx @@ -7,7 +7,7 @@ import ScrollableTabView, { DefaultTabBarProps, TabBarProps, } from 'react-native-scrollable-tab-view'; -import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/NotificationsView.selectors'; +import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/NotificationsView.selectors'; import { strings } from '../../../../../locales/i18n'; import { hasNotificationComponents, diff --git a/app/components/UI/Notification/TransactionNotification/index.js b/app/components/UI/Notification/TransactionNotification/index.js index e04e2dddacf..23c3c853855 100644 --- a/app/components/UI/Notification/TransactionNotification/index.js +++ b/app/components/UI/Notification/TransactionNotification/index.js @@ -35,7 +35,7 @@ import { selectTokensByAddress } from '../../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../../selectors/tokenRatesController'; import { selectAccounts } from '../../../../selectors/accountTrackerController'; import { speedUpTransaction } from '../../../../util/transaction-controller'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../../selectors/accountsController'; const WINDOW_WIDTH = Dimensions.get('window').width; const ACTION_CANCEL = 'cancel'; @@ -447,7 +447,7 @@ const mapStateToProps = (state) => { return { accounts: selectAccounts(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), transactions: TransactionController.transactions, ticker: selectTicker(state), chainId, diff --git a/app/components/UI/OnboardingWizard/Coachmark/index.js b/app/components/UI/OnboardingWizard/Coachmark/index.js index f84de8cf959..8be92c62e09 100644 --- a/app/components/UI/OnboardingWizard/Coachmark/index.js +++ b/app/components/UI/OnboardingWizard/Coachmark/index.js @@ -22,7 +22,7 @@ import { ButtonWidthTypes, } from '../../../../component-library/components/Buttons/Button'; import Button from '../../../../component-library/components/Buttons/Button/Button'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; const createStyles = (colors) => StyleSheet.create({ diff --git a/app/components/UI/OnboardingWizard/Step1/index.tsx b/app/components/UI/OnboardingWizard/Step1/index.tsx index 71e8ffe5932..34c99d7c5b8 100644 --- a/app/components/UI/OnboardingWizard/Step1/index.tsx +++ b/app/components/UI/OnboardingWizard/Step1/index.tsx @@ -12,7 +12,7 @@ import { ONBOARDING_WIZARD_STEP_DESCRIPTION, } from '../../../../core/Analytics'; import { ThemeContext, mockTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../../components/hooks/useMetrics'; const styles = StyleSheet.create({ diff --git a/app/components/UI/OnboardingWizard/Step2/index.tsx b/app/components/UI/OnboardingWizard/Step2/index.tsx index cafcf535bb7..38d980f3207 100644 --- a/app/components/UI/OnboardingWizard/Step2/index.tsx +++ b/app/components/UI/OnboardingWizard/Step2/index.tsx @@ -12,7 +12,7 @@ import { ONBOARDING_WIZARD_STEP_DESCRIPTION, } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../hooks/useMetrics'; import useHandleLayout from '../useHandleLayout'; diff --git a/app/components/UI/OnboardingWizard/Step3/index.tsx b/app/components/UI/OnboardingWizard/Step3/index.tsx index f21941c5cfb..254884a0d92 100644 --- a/app/components/UI/OnboardingWizard/Step3/index.tsx +++ b/app/components/UI/OnboardingWizard/Step3/index.tsx @@ -12,7 +12,7 @@ import { } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; import { useMetrics } from '../../../hooks/useMetrics'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import useHandleLayout from '../useHandleLayout'; const styles = StyleSheet.create({ diff --git a/app/components/UI/OnboardingWizard/Step4/index.tsx b/app/components/UI/OnboardingWizard/Step4/index.tsx index f7a5b2c1b1d..3b61ed84cc4 100644 --- a/app/components/UI/OnboardingWizard/Step4/index.tsx +++ b/app/components/UI/OnboardingWizard/Step4/index.tsx @@ -13,7 +13,7 @@ import { ONBOARDING_WIZARD_STEP_DESCRIPTION, } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../hooks/useMetrics'; diff --git a/app/components/UI/OnboardingWizard/Step5/index.tsx b/app/components/UI/OnboardingWizard/Step5/index.tsx index 583b68468aa..df08abd3209 100644 --- a/app/components/UI/OnboardingWizard/Step5/index.tsx +++ b/app/components/UI/OnboardingWizard/Step5/index.tsx @@ -13,7 +13,7 @@ import { import { useTheme } from '../../../../util/theme'; import { useMetrics } from '../../../hooks/useMetrics'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; const styles = StyleSheet.create({ main: { diff --git a/app/components/UI/OnboardingWizard/Step6/index.tsx b/app/components/UI/OnboardingWizard/Step6/index.tsx index 58f08290442..944dca5bc90 100644 --- a/app/components/UI/OnboardingWizard/Step6/index.tsx +++ b/app/components/UI/OnboardingWizard/Step6/index.tsx @@ -13,7 +13,7 @@ import { ONBOARDING_WIZARD_STEP_DESCRIPTION, } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../hooks/useMetrics'; const styles = StyleSheet.create({ diff --git a/app/components/UI/OnboardingWizard/Step7/index.tsx b/app/components/UI/OnboardingWizard/Step7/index.tsx index cc0e539cfcd..c71c6414ea8 100644 --- a/app/components/UI/OnboardingWizard/Step7/index.tsx +++ b/app/components/UI/OnboardingWizard/Step7/index.tsx @@ -14,7 +14,7 @@ import { } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../hooks/useMetrics'; const styles = StyleSheet.create({ diff --git a/app/components/UI/PaymentRequest/AssetList/index.tsx b/app/components/UI/PaymentRequest/AssetList/index.tsx index d3e5975e2a3..499817c6065 100644 --- a/app/components/UI/PaymentRequest/AssetList/index.tsx +++ b/app/components/UI/PaymentRequest/AssetList/index.tsx @@ -9,6 +9,7 @@ import { useSelector } from 'react-redux'; import { toChecksumAddress } from 'ethereumjs-util'; import { useTheme } from '../../../../util/theme'; import { selectTokenList } from '../../../../selectors/tokenListController'; +import { ImportTokenViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/ImportTokenView.selectors'; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -111,7 +112,7 @@ const AssetList = ({ ); return ( - + { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/app/components/UI/PaymentRequest/index.js b/app/components/UI/PaymentRequest/index.js index bdbb3bb8f90..5391e0b8816 100644 --- a/app/components/UI/PaymentRequest/index.js +++ b/app/components/UI/PaymentRequest/index.js @@ -58,7 +58,7 @@ import { import { selectTokenListArray } from '../../../selectors/tokenListController'; import { selectTokens } from '../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; import { RequestPaymentViewSelectors } from '../../../../e2e/selectors/Receive/RequestPaymentView.selectors'; @@ -896,7 +896,7 @@ const mapStateToProps = (state) => ({ contractExchangeRates: selectContractExchangeRates(state), searchEngine: state.settings.searchEngine, tokens: selectTokens(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), primaryCurrency: state.settings.primaryCurrency, ticker: selectTicker(state), chainId: selectChainId(state), diff --git a/app/components/UI/PaymentRequest/index.test.tsx b/app/components/UI/PaymentRequest/index.test.tsx index 391fe7e3b89..a01c0814d2f 100644 --- a/app/components/UI/PaymentRequest/index.test.tsx +++ b/app/components/UI/PaymentRequest/index.test.tsx @@ -39,6 +39,11 @@ const initialState = { }, }, tokens: [], + allTokens: { + '0x1': { + '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272': [], + }, + }, }, NetworkController: { provider: { @@ -50,7 +55,7 @@ const initialState = { ...MOCK_ACCOUNTS_CONTROLLER_STATE, internalAccounts: { ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts, - selectedAccount: {}, + selectedAccount: '30786334-3935-4563-b064-363339643939', }, }, TokenListController: { diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx index 526884a8516..cf998ed53a2 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx @@ -47,6 +47,7 @@ import { selectProviderConfig } from '../../../selectors/networkController'; import { useNetworkInfo } from '../../../selectors/selectedNetworkController'; import { ConnectedAccountsSelectorsIDs } from '../../../../e2e/selectors/Browser/ConnectedAccountModal.selectors'; import { PermissionSummaryBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Browser/PermissionSummaryBottomSheet.selectors'; +import { NetworkNonPemittedBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkNonPemittedBottomSheet.selectors'; const PermissionsSummary = ({ currentPageInformation, @@ -272,7 +273,10 @@ const PermissionsSummary = ({ function renderAccountPermissionsRequestInfoCard() { return ( - + {renderHeader()} - + {isNonDappNetworkSwitch ? strings('permissions.title_add_network_permission') @@ -482,6 +491,9 @@ const PermissionsSummary = ({