From 8a7f0928013474fd8574bfea99d99d4233f649ef Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 28 Nov 2024 23:18:06 +0000 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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;