From d4f2f3f29df1443a68497479e23e2d0f459aa628 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Tue, 14 Jan 2025 14:01:32 +0000 Subject: [PATCH 01/20] feat(27255): allow local modification for remote feature flags --- app/scripts/lib/manifestFlags.ts | 12 + development/build/manifest.js | 5 +- .../utils/plugins/ManifestPlugin/helpers.ts | 13 + lavamoat/browserify/beta/policy.json | 2 +- lavamoat/browserify/flask/policy.json | 2 +- lavamoat/browserify/main/policy.json | 2 +- lavamoat/browserify/mmi/policy.json | 2 +- lavamoat/build-system/policy.json | 354 ++++++++++++++---- manifest-flags.json | 8 + test/e2e/constants.ts | 18 + .../pages/developer-options-page.ts | 17 + .../remote-feature-flag.spec.ts | 39 +- .../developer-options-tab.test.tsx.snap | 39 ++ .../developer-options-tab.test.tsx | 17 +- .../developer-options-tab.tsx | 45 ++- .../settings/info-tab/info-tab.component.js | 12 - .../settings/info-tab/info-tab.stories.js | 2 +- ui/pages/settings/info-tab/info-tab.test.tsx | 4 +- ui/pages/settings/settings.component.js | 9 +- ui/pages/settings/settings.container.js | 4 - ui/pages/settings/settings.stories.js | 1 - ui/pages/settings/settings.test.js | 1 - ui/selectors/selectors.js | 17 + ui/selectors/selectors.test.js | 62 ++- 24 files changed, 564 insertions(+), 123 deletions(-) create mode 100644 manifest-flags.json diff --git a/app/scripts/lib/manifestFlags.ts b/app/scripts/lib/manifestFlags.ts index 574099d0cb94..8e6848be7ade 100644 --- a/app/scripts/lib/manifestFlags.ts +++ b/app/scripts/lib/manifestFlags.ts @@ -60,6 +60,18 @@ export type ManifestFlags = { */ forceEnable?: boolean; }; + /** + * Feature flags to control business logic behavior + */ + remoteFeatureFlags?: { + /** + * A test remote featureflag for threshold + */ + testFlagForThreshold: { + name: string; + value: string; + }; + }; }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- you can't extend a type, we want this to be an interface diff --git a/development/build/manifest.js b/development/build/manifest.js index bc5325b372eb..17f6168df7dc 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -11,6 +11,7 @@ const baradDurManifest = isManifestV3 ? require('../../app/manifest/v3/_barad_dur.json') : require('../../app/manifest/v2/_barad_dur.json'); const { loadBuildTypesConfig } = require('../lib/build-type'); +const manifestFlags = require('../../manifest-flags.json'); const { TASKS, ENVIRONMENT } = require('./constants'); const { createTask, composeSeries } = require('./task'); @@ -47,8 +48,10 @@ function createManifestTasks({ browserVersionMap[platform], await getBuildModifications(buildType, platform), customArrayMerge, + { + _flags: manifestFlags, + }, ); - modifyNameAndDescForNonProd(result); const dir = path.join('.', 'dist', platform); diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts index 82efa9acf253..1db54118996e 100644 --- a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -1,3 +1,5 @@ +import manifestFlags from '../../../../../manifest-flags.json'; + /** * Returns a function that will transform a manifest JSON object based on the * given build args. @@ -24,6 +26,17 @@ export function transformManifest(args: { lockdown: boolean; test: boolean }) { } } + /** + * This function sets predefined flags in the manifest's _flags property. + * + * @param browserManifest - The Chrome extension manifest object to modify + */ + function addManifestFlags(browserManifest: chrome.runtime.Manifest) { + browserManifest._flags = manifestFlags; + } + + transforms.push(addManifestFlags); + if (!args.lockdown) { // remove lockdown scripts from content_scripts transforms.push(removeLockdown); diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index c4985af54a1d..dc35b61fdd86 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -5775,4 +5775,4 @@ } } } -} \ No newline at end of file +} diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index c4985af54a1d..dc35b61fdd86 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -5775,4 +5775,4 @@ } } } -} \ No newline at end of file +} diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index c4985af54a1d..dc35b61fdd86 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -5775,4 +5775,4 @@ } } } -} \ No newline at end of file +} diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index bc5be17e5a64..c5d67dc34329 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -5867,4 +5867,4 @@ } } } -} \ No newline at end of file +} diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 545922814d1f..7fc9728c7c90 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -5,7 +5,7 @@ "define": true }, "packages": { - "terser>@jridgewell/source-map>@jridgewell/gen-mapping": true, + "@babel/core>@ampproject/remapping>@jridgewell/gen-mapping": true, "terser-webpack-plugin>@jridgewell/trace-mapping": true } }, @@ -60,7 +60,7 @@ "@babel/preset-react": true, "@babel/preset-typescript": true, "@babel/core>@babel/template": true, - "depcheck>@babel/traverse": true, + "@babel/core>@babel/traverse": true, "@babel/core>@babel/types": true, "@babel/core>convert-source-map": true, "nock>debug": true, @@ -147,7 +147,7 @@ "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": true, "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": true, - "depcheck>@babel/traverse": true, + "@babel/core>@babel/traverse": true, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin>semver": true } }, @@ -175,7 +175,7 @@ "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>babel-plugin-polyfill-corejs2>@babel/helper-define-polyfill-provider>lodash.debounce": true, - "depcheck>resolve": true + "browserify>resolve": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": { @@ -200,9 +200,8 @@ "packages": { "@babel/core": true, "@babel/core>@babel/helper-module-transforms>@babel/helper-module-imports": true, - "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": true, "@babel/code-frame>@babel/helper-validator-identifier": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": { @@ -215,7 +214,7 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": { @@ -223,10 +222,10 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, - "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": { + "@babel/preset-env>@babel/plugin-transform-modules-commonjs>@babel/helper-simple-access": { "packages": { "@babel/core>@babel/types": true } @@ -239,7 +238,7 @@ "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": { "packages": { "@babel/core>@babel/template": true, - "depcheck>@babel/traverse": true, + "@babel/core>@babel/traverse": true, "@babel/core>@babel/types": true } }, @@ -252,7 +251,7 @@ "@babel/preset-env>@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-bugfix-safari-class-field-initializer-scope": { @@ -278,7 +277,7 @@ "packages": { "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-syntax-import-assertions": { @@ -317,7 +316,7 @@ "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-async-to-generator": { @@ -359,8 +358,8 @@ "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": true, - "depcheck>@babel/traverse": true, - "@babel/preset-env>@babel/plugin-transform-classes>globals": true + "@babel/core>@babel/traverse": true, + "depcheck>@babel/traverse>globals": true } }, "@babel/preset-env>@babel/plugin-transform-computed-properties": { @@ -423,7 +422,7 @@ "packages": { "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-json-strings": { @@ -461,7 +460,7 @@ "@babel/core": true, "@babel/core>@babel/helper-module-transforms": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": true + "@babel/preset-env>@babel/plugin-transform-modules-commonjs>@babel/helper-simple-access": true } }, "@babel/preset-env>@babel/plugin-transform-modules-systemjs": { @@ -473,7 +472,7 @@ "@babel/core>@babel/helper-module-transforms": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/code-frame>@babel/helper-validator-identifier": true, - "depcheck>@babel/traverse": true + "@babel/core>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-modules-umd": { @@ -780,7 +779,7 @@ "@babel/core>@babel/types": true } }, - "depcheck>@babel/traverse": { + "@babel/core>@babel/traverse": { "globals": { "console.log": true }, @@ -790,7 +789,25 @@ "@babel/core>@babel/parser": true, "@babel/core>@babel/template": true, "@babel/core>@babel/types": true, - "babel/preset-env>b@babel/types": true, + "nock>debug": true, + "depcheck>@babel/traverse>globals": true + } + }, + "depcheck>@babel/traverse": { + "packages": { + "babel/preset-env>b@babel/types": true + } + }, + "lavamoat>lavamoat-tofu>@babel/traverse": { + "globals": { + "console.log": true + }, + "packages": { + "@babel/code-frame": true, + "@babel/core>@babel/generator": true, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/parser": true, + "@babel/core>@babel/template": true, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/types": true, "nock>debug": true, "depcheck>@babel/traverse>globals": true } @@ -805,6 +822,16 @@ "@babel/code-frame>@babel/helper-validator-identifier": true } }, + "lavamoat>lavamoat-tofu>@babel/traverse>@babel/types": { + "globals": { + "console.warn": true, + "process.env": true + }, + "packages": { + "@babel/core>@babel/types>@babel/helper-string-parser": true, + "@babel/code-frame>@babel/helper-validator-identifier": true + } + }, "sass-embedded>@bufbuild/protobuf": { "globals": { "TextDecoder": true, @@ -881,7 +908,7 @@ "eslint-plugin-prettier": true, "eslint-plugin-react": true, "eslint-plugin-react-hooks": true, - "eslint>globals": true, + "eslint>@eslint/eslintrc>globals": true, "eslint>ignore": true, "eslint>minimatch": true, "mocha>strip-json-comments": true @@ -914,6 +941,16 @@ "eslint>minimatch": true } }, + "@babel/core>@ampproject/remapping>@jridgewell/gen-mapping": { + "globals": { + "define": true + }, + "packages": { + "terser>@jridgewell/source-map>@jridgewell/gen-mapping>@jridgewell/set-array": true, + "terser-webpack-plugin>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true, + "terser-webpack-plugin>@jridgewell/trace-mapping": true + } + }, "terser>@jridgewell/source-map>@jridgewell/gen-mapping": { "globals": { "define": true @@ -975,7 +1012,7 @@ "node:path.relative": true }, "packages": { - "depcheck>resolve": true + "lavamoat>@lavamoat/aa>resolve": true } }, "@lavamoat/lavapack": { @@ -1376,7 +1413,7 @@ "string.prototype.matchall>call-bind": true, "string.prototype.matchall>define-properties": true, "string.prototype.matchall>es-abstract": true, - "string.prototype.matchall>get-intrinsic": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, "eslint-plugin-react>array-includes>is-string": true } }, @@ -1452,7 +1489,7 @@ }, "packages": { "browserslist": true, - "autoprefixer>caniuse-lite": true, + "browserslist>caniuse-lite": true, "autoprefixer>normalize-range": true, "stylelint>autoprefixer>num2fraction": true, "stylelint>postcss>picocolors": true, @@ -1462,7 +1499,7 @@ }, "@babel/preset-env>babel-plugin-polyfill-corejs2": { "packages": { - "@babel/preset-env>@babel/compat-data": true, + "@babel/preset-env>babel-plugin-polyfill-corejs2>@babel/compat-data": true, "@babel/core": true, "@babel/preset-env>babel-plugin-polyfill-corejs2>@babel/helper-define-polyfill-provider": true, "@babel/preset-env>babel-plugin-polyfill-corejs2>semver": true @@ -1559,6 +1596,9 @@ } }, "chokidar>braces": { + "globals": { + "console.log": true + }, "packages": { "chokidar>braces>fill-range": true } @@ -1614,7 +1654,7 @@ "process.platform": true }, "packages": { - "depcheck>resolve": true + "browserify>resolve": true } }, "browserify": { @@ -1647,7 +1687,7 @@ "labeled-stream-splicer": true, "browserify>module-deps": true, "browserify>read-only-stream": true, - "depcheck>resolve": true, + "browserify>resolve": true, "browserify>shasum-object": true, "browserify>syntax-error": true, "browserify>through2": true, @@ -1671,7 +1711,7 @@ "process.versions.node": true }, "packages": { - "autoprefixer>caniuse-lite": true, + "browserslist>caniuse-lite": true, "browserslist>electron-to-chromium": true, "browserslist>node-releases": true } @@ -1717,15 +1757,44 @@ "process.cwd": true } }, - "string.prototype.matchall>call-bind": { + "string.prototype.matchall>call-bind>call-bind-apply-helpers": { + "packages": { + "string.prototype.matchall>es-abstract>es-errors": true, + "browserify>has>function-bind": true + } + }, + "string.prototype.matchall>regexp.prototype.flags>call-bind": { "packages": { "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>es-abstract>es-errors": true, "browserify>has>function-bind": true, - "string.prototype.matchall>get-intrinsic": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, "string.prototype.matchall>call-bind>set-function-length": true } }, + "string.prototype.matchall>es-abstract>safe-regex-test>call-bind": { + "packages": { + "string.prototype.matchall>call-bind>es-define-property": true, + "string.prototype.matchall>es-abstract>es-errors": true, + "browserify>has>function-bind": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>call-bind>set-function-length": true + } + }, + "string.prototype.matchall>call-bind": { + "packages": { + "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, + "string.prototype.matchall>call-bind>es-define-property": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>call-bind>set-function-length": true + } + }, + "gulp>vinyl-fs>object.assign>call-bound": { + "packages": { + "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, + "eslint-plugin-react>array-includes>get-intrinsic": true + } + }, "chalk": { "packages": { "chalk>ansi-styles": true, @@ -1795,6 +1864,7 @@ "chokidar>anymatch": true, "chokidar>braces": true, "chokidar>fsevents": true, + "tsx>fsevents": true, "eslint>glob-parent": true, "chokidar>is-binary-path": true, "del>is-glob": true, @@ -2196,7 +2266,7 @@ "process": true }, "packages": { - "nock>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -2245,7 +2315,7 @@ "string.prototype.matchall>define-properties>define-data-property": { "packages": { "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>es-abstract>es-errors": true, "string.prototype.matchall>es-abstract>gopd": true } }, @@ -2401,6 +2471,12 @@ "stylelint>postcss-html>htmlparser2>domelementtype": true } }, + "eslint-plugin-react>array-includes>get-intrinsic>get-proto>dunder-proto": { + "packages": { + "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, + "string.prototype.matchall>es-abstract>gopd": true + } + }, "browserify>duplexer2": { "packages": { "browserify>duplexer2>readable-stream": true @@ -2476,11 +2552,11 @@ "packages": { "string.prototype.matchall>call-bind": true, "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>es-abstract>es-errors": true, "string.prototype.matchall>es-abstract>es-object-atoms": true, "string.prototype.matchall>es-abstract>es-set-tostringtag": true, "string.prototype.matchall>es-abstract>es-to-primitive": true, - "string.prototype.matchall>get-intrinsic": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, "string.prototype.matchall>es-abstract>gopd": true, "string.prototype.matchall>es-abstract>has-property-descriptors": true, "string.prototype.matchall>es-abstract>has-proto": true, @@ -2495,19 +2571,15 @@ "string.prototype.matchall>es-abstract>string.prototype.trim": true } }, - "string.prototype.matchall>call-bind>es-define-property": { - "packages": { - "string.prototype.matchall>get-intrinsic": true - } - }, "string.prototype.matchall>es-abstract>es-object-atoms": { "packages": { - "string.prototype.matchall>call-bind>es-errors": true + "string.prototype.matchall>es-abstract>es-errors": true } }, "string.prototype.matchall>es-abstract>es-set-tostringtag": { "packages": { - "string.prototype.matchall>get-intrinsic": true, + "string.prototype.matchall>es-abstract>es-errors": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, "koa>is-generator-function>has-tostringtag": true, "depcheck>is-core-module>hasown": true } @@ -2520,7 +2592,7 @@ "string.prototype.matchall>es-abstract>es-to-primitive": { "packages": { "string.prototype.matchall>es-abstract>is-callable": true, - "@metamask/eth-token-tracker>deep-equal>is-date-object": true, + "string.prototype.matchall>es-abstract>es-to-primitive>is-date-object": true, "string.prototype.matchall>es-abstract>es-to-primitive>is-symbol": true } }, @@ -2638,7 +2710,8 @@ }, "packages": { "eslint-import-resolver-node>debug": true, - "depcheck>resolve": true + "depcheck>is-core-module": true, + "browserify>resolve": true } }, "eslint-import-resolver-typescript": { @@ -2653,8 +2726,8 @@ "nock>debug": true, "nyc>glob": true, "del>is-glob": true, - "depcheck>resolve": true, - "eslint-plugin-import>tsconfig-paths": true + "browserify>resolve": true, + "eslint-import-resolver-typescript>tsconfig-paths": true } }, "eslint-plugin-import>eslint-module-utils": { @@ -2764,7 +2837,7 @@ "eslint-plugin-node>eslint-utils": true, "eslint>ignore": true, "eslint>minimatch": true, - "depcheck>resolve": true, + "browserify>resolve": true, "eslint-plugin-node>semver": true } }, @@ -3364,6 +3437,13 @@ "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, + "tsx>fsevents": { + "globals": { + "console.assert": true, + "process.platform": true + }, + "native": true + }, "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge": { "builtin": { "util.format": true @@ -3390,18 +3470,34 @@ "assert.equal": true } }, - "string.prototype.matchall>get-intrinsic": { + "eslint-plugin-react>array-includes>get-intrinsic": { "globals": { "AggregateError": true, "FinalizationRegistry": true, "WeakRef": true }, "packages": { - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, + "string.prototype.matchall>call-bind>es-define-property": true, + "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>es-abstract>es-object-atoms": true, "browserify>has>function-bind": true, - "string.prototype.matchall>es-abstract>has-proto": true, + "eslint-plugin-react>array-includes>get-intrinsic>get-proto": true, + "string.prototype.matchall>es-abstract>gopd": true, "string.prototype.matchall>has-symbols": true, - "depcheck>is-core-module>hasown": true + "depcheck>is-core-module>hasown": true, + "eslint-plugin-react>array-includes>get-intrinsic>math-intrinsics": true + } + }, + "string.prototype.matchall>get-intrinsic": { + "packages": { + "string.prototype.matchall>has-symbols": true + } + }, + "eslint-plugin-react>array-includes>get-intrinsic>get-proto": { + "packages": { + "eslint-plugin-react>array-includes>get-intrinsic>get-proto>dunder-proto": true, + "string.prototype.matchall>es-abstract>es-object-atoms": true } }, "gulp-zip>get-stream": { @@ -3560,11 +3656,6 @@ "define": true } }, - "string.prototype.matchall>es-abstract>gopd": { - "packages": { - "string.prototype.matchall>get-intrinsic": true - } - }, "del>graceful-fs": { "builtin": { "assert.equal": true, @@ -3910,7 +4001,7 @@ }, "string.prototype.matchall>internal-slot": { "packages": { - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>es-abstract>es-errors": true, "depcheck>is-core-module>hasown": true, "string.prototype.matchall>side-channel": true } @@ -3963,6 +4054,14 @@ "depcheck>is-core-module>hasown": true } }, + "browserify>resolve>is-core-module": { + "globals": { + "process.versions": true + }, + "packages": { + "depcheck>is-core-module>hasown": true + } + }, "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { "packages": { "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true @@ -3978,7 +4077,7 @@ "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true } }, - "@metamask/eth-token-tracker>deep-equal>is-date-object": { + "string.prototype.matchall>es-abstract>es-to-primitive>is-date-object": { "packages": { "koa>is-generator-function>has-tostringtag": true } @@ -4082,8 +4181,10 @@ }, "string.prototype.matchall>es-abstract>is-regex": { "packages": { - "string.prototype.matchall>call-bind": true, - "koa>is-generator-function>has-tostringtag": true + "gulp>vinyl-fs>object.assign>call-bound": true, + "string.prototype.matchall>es-abstract>gopd": true, + "koa>is-generator-function>has-tostringtag": true, + "depcheck>is-core-module>hasown": true } }, "gulp>gulp-cli>replace-homedir>is-absolute>is-relative": { @@ -4093,12 +4194,13 @@ }, "eslint-plugin-react>array-includes>is-string": { "packages": { + "gulp>vinyl-fs>object.assign>call-bound": true, "koa>is-generator-function>has-tostringtag": true } }, "string.prototype.matchall>es-abstract>es-to-primitive>is-symbol": { "packages": { - "string.prototype.matchall>has-symbols": true + "string.prototype.matchall>es-abstract>es-to-primitive>is-symbol>has-symbols": true } }, "gulp>gulp-cli>replace-homedir>is-absolute>is-relative>is-unc-path": { @@ -4178,6 +4280,11 @@ "console.warn": true } }, + "eslint-import-resolver-typescript>tsconfig-paths>json5": { + "globals": { + "console.warn": true + } + }, "eslint-plugin-import>tsconfig-paths>json5": { "globals": { "console.warn": true @@ -4204,7 +4311,7 @@ "console.error": true }, "packages": { - "gulp>vinyl-fs>object.assign": true + "eslint-plugin-react>jsx-ast-utils>object.assign": true } }, "gulp>glob-watcher>just-debounce": { @@ -4348,8 +4455,8 @@ "console.log": true }, "packages": { - "@babel/core>@babel/parser": true, - "depcheck>@babel/traverse": true + "lavamoat>lavamoat-tofu>@babel/parser": true, + "lavamoat>lavamoat-tofu>@babel/traverse": true } }, "lavamoat>lavamoat-core>merge-deep>clone-deep>lazy-cache": { @@ -4651,7 +4758,7 @@ "loose-envify": true, "browserify>parents": true, "browserify>module-deps>readable-stream": true, - "depcheck>resolve": true, + "browserify>resolve": true, "browserify>module-deps>stream-combiner2": true, "browserify>module-deps>through2": true, "watchify>xtend": true @@ -4788,15 +4895,34 @@ "WeakRef": true } }, + "string.prototype.matchall>side-channel>object-inspect": { + "builtin": { + "util.inspect": true + }, + "globals": { + "HTMLElement": true, + "WeakRef": true + } + }, "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": { "packages": { "gulp>gulp-cli>isobject": true } }, + "eslint-plugin-react>jsx-ast-utils>object.assign": { + "packages": { + "string.prototype.matchall>call-bind": true, + "string.prototype.matchall>define-properties": true, + "string.prototype.matchall>has-symbols": true, + "@lavamoat/lavapack>json-stable-stringify>object-keys": true + } + }, "gulp>vinyl-fs>object.assign": { "packages": { "string.prototype.matchall>call-bind": true, + "gulp>vinyl-fs>object.assign>call-bound": true, "string.prototype.matchall>define-properties": true, + "string.prototype.matchall>es-abstract>es-object-atoms": true, "string.prototype.matchall>has-symbols": true, "@lavamoat/lavapack>json-stable-stringify>object-keys": true } @@ -7065,9 +7191,9 @@ }, "string.prototype.matchall>regexp.prototype.flags": { "packages": { - "string.prototype.matchall>call-bind": true, + "string.prototype.matchall>regexp.prototype.flags>call-bind": true, "string.prototype.matchall>define-properties": true, - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>es-abstract>es-errors": true, "string.prototype.matchall>regexp.prototype.flags>set-function-name": true } }, @@ -7222,7 +7348,37 @@ "gulp>vinyl-fs>value-or-function": true } }, - "depcheck>resolve": { + "lavamoat>@lavamoat/aa>resolve": { + "builtin": { + "fs.readFileSync": true, + "fs.realpathSync": true, + "fs.statSync": true, + "os.homedir": true, + "path.dirname": true, + "path.join": true, + "path.parse": true, + "path.relative": true, + "path.resolve": true + }, + "globals": { + "process.env.HOME": true, + "process.env.HOMEDRIVE": true, + "process.env.HOMEPATH": true, + "process.env.LNAME": true, + "process.env.LOGNAME": true, + "process.env.USER": true, + "process.env.USERNAME": true, + "process.env.USERPROFILE": true, + "process.getuid": true, + "process.platform": true, + "process.versions.pnp": true + }, + "packages": { + "depcheck>is-core-module": true, + "depcheck>resolve>path-parse": true + } + }, + "browserify>resolve": { "builtin": { "fs.readFile": true, "fs.readFileSync": true, @@ -7252,7 +7408,7 @@ "process.versions.pnp": true }, "packages": { - "depcheck>is-core-module": true, + "browserify>resolve>is-core-module": true, "depcheck>resolve>path-parse": true } }, @@ -7510,8 +7666,8 @@ }, "string.prototype.matchall>es-abstract>safe-regex-test": { "packages": { - "string.prototype.matchall>call-bind": true, - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>es-abstract>safe-regex-test>call-bind": true, + "string.prototype.matchall>es-abstract>es-errors": true, "string.prototype.matchall>es-abstract>is-regex": true } }, @@ -7651,8 +7807,8 @@ "string.prototype.matchall>call-bind>set-function-length": { "packages": { "string.prototype.matchall>define-properties>define-data-property": true, - "string.prototype.matchall>call-bind>es-errors": true, - "string.prototype.matchall>get-intrinsic": true, + "string.prototype.matchall>es-abstract>es-errors": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, "string.prototype.matchall>es-abstract>gopd": true, "string.prototype.matchall>es-abstract>has-property-descriptors": true } @@ -7660,7 +7816,7 @@ "string.prototype.matchall>regexp.prototype.flags>set-function-name": { "packages": { "string.prototype.matchall>define-properties>define-data-property": true, - "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>es-abstract>es-errors": true, "string.prototype.matchall>es-abstract>function.prototype.name>functions-have-names": true, "string.prototype.matchall>es-abstract>has-property-descriptors": true } @@ -7692,11 +7848,36 @@ "@metamask/rpc-errors>fast-safe-stringify": true } }, + "string.prototype.matchall>side-channel>side-channel-list": { + "packages": { + "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>side-channel>object-inspect": true + } + }, + "string.prototype.matchall>side-channel>side-channel-map": { + "packages": { + "gulp>vinyl-fs>object.assign>call-bound": true, + "string.prototype.matchall>es-abstract>es-errors": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>side-channel>object-inspect": true + } + }, + "string.prototype.matchall>side-channel>side-channel-weakmap": { + "packages": { + "gulp>vinyl-fs>object.assign>call-bound": true, + "string.prototype.matchall>es-abstract>es-errors": true, + "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>side-channel>object-inspect": true, + "string.prototype.matchall>side-channel>side-channel-map": true + } + }, "string.prototype.matchall>side-channel": { "packages": { - "string.prototype.matchall>call-bind": true, - "string.prototype.matchall>get-intrinsic": true, - "string.prototype.matchall>es-abstract>object-inspect": true + "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>side-channel>object-inspect": true, + "string.prototype.matchall>side-channel>side-channel-list": true, + "string.prototype.matchall>side-channel>side-channel-map": true, + "string.prototype.matchall>side-channel>side-channel-weakmap": true } }, "nyc>signal-exit": { @@ -8642,7 +8823,7 @@ "ts-node>yn": true } }, - "eslint-plugin-import>tsconfig-paths": { + "eslint-import-resolver-typescript>tsconfig-paths": { "builtin": { "fs.existsSync": true, "fs.lstatSync": true, @@ -8664,11 +8845,26 @@ "process.env": true }, "packages": { - "eslint-plugin-import>tsconfig-paths>json5": true, + "eslint-import-resolver-typescript>tsconfig-paths>json5": true, "wait-on>minimist": true, "eslint-plugin-import>tsconfig-paths>strip-bom": true } }, + "eslint-plugin-import>tsconfig-paths": { + "builtin": { + "fs.existsSync": true, + "fs.lstatSync": true, + "fs.readFileSync": true, + "fs.statSync": true, + "path.dirname": true, + "path.join": true, + "path.resolve": true + }, + "packages": { + "eslint-plugin-import>tsconfig-paths>json5": true, + "eslint-plugin-import>tsconfig-paths>strip-bom": true + } + }, "tsutils": { "packages": { "tslib": true, diff --git a/manifest-flags.json b/manifest-flags.json new file mode 100644 index 000000000000..f90de282f373 --- /dev/null +++ b/manifest-flags.json @@ -0,0 +1,8 @@ +{ + "remoteFeatureFlags": { + "testFlagForThreshold": { + "name": "test-flag", + "value": "121212" + } + } +} diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 2826200dc293..42a4e0fc3f12 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -87,3 +87,21 @@ export enum ACCOUNT_TYPE { /* Meta metricsId generated by generateMetaMetricsId */ export const MOCK_META_METRICS_ID = '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420'; + +/* Mock remote feature flags response */ +export const MOCK_REMOTE_FEATURE_FLAGS_RESPONSE = { + feature1: true, + feature2: false, + feature3: { + name: 'groupC', + value: 'valueC', + }, +}; + +/* Mock customized remote feature flags response */ +export const MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS = { + feature1: { + name: 'groupA', + value: 'valueA', + }, +}; diff --git a/test/e2e/page-objects/pages/developer-options-page.ts b/test/e2e/page-objects/pages/developer-options-page.ts index c15f6c767a82..8d37fc4091f3 100644 --- a/test/e2e/page-objects/pages/developer-options-page.ts +++ b/test/e2e/page-objects/pages/developer-options-page.ts @@ -1,4 +1,6 @@ +import { strict as assert } from 'assert'; import { Driver } from '../../webdriver/driver'; +import { MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS } from '../../constants'; class DevelopOptions { private readonly driver: Driver; @@ -12,6 +14,9 @@ class DevelopOptions { css: 'h4', }; + private readonly developerOptionsRemoteFeatureFlagsState: string = + '[data-testid="developer-options-remote-feature-flags"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -33,6 +38,18 @@ class DevelopOptions { console.log('Generate a page crash in Developer option page'); await this.driver.clickElement(this.generatePageCrashButton); } + + async validateRemoteFeatureFlagState(): Promise { + console.log('Validate remote feature flags state in Developer option page'); + const element = await this.driver.findElement( + this.developerOptionsRemoteFeatureFlagsState, + ); + const remoteFeatureFlagsState = await element.getText(); + assert.equal( + remoteFeatureFlagsState, + JSON.stringify(MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS), + ); + } } export default DevelopOptions; diff --git a/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts b/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts index 42ebe7dc762c..48c8dd7f5b28 100644 --- a/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts +++ b/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts @@ -4,8 +4,14 @@ import { getCleanAppState, withFixtures } from '../../helpers'; import FixtureBuilder from '../../fixture-builder'; import { TestSuiteArguments } from '../confirmations/transactions/shared'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; -import { MOCK_META_METRICS_ID } from '../../constants'; -import { MOCK_REMOTE_FEATURE_FLAGS_RESPONSE } from './mock-data'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import SettingsPage from '../../page-objects/pages/settings/settings-page'; +import DevelopOptions from '../../page-objects/pages/developer-options-page'; +import { + MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS, + MOCK_META_METRICS_ID, + MOCK_REMOTE_FEATURE_FLAGS_RESPONSE, +} from '../../constants'; describe('Remote feature flag', function (this: Suite) { it('should be fetched with threshold value when basic functionality toggle is on', async function () { @@ -45,4 +51,33 @@ describe('Remote feature flag', function (this: Suite) { }, ); }); + + it('offers the option to pass into manifest file for developers', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: MOCK_META_METRICS_ID, + participateInMetaMetrics: true, + }) + .build(), + manifestFlags: { + remoteFeatureFlags: MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS, + }, + title: this.test?.fullTitle(), + }, + async ({ driver }: TestSuiteArguments) => { + await loginWithBalanceValidation(driver); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptions(driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.validateRemoteFeatureFlagState(); + }, + ); + }); }); diff --git a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap index 6c1fae43d5e8..baba3f1ffc70 100644 --- a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap +++ b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap @@ -10,6 +10,45 @@ exports[`Develop options tab should match snapshot 1`] = ` > States

+

+ Current States +

+
+
+
+ + Remote feature flags + +
+ The remote feature flags here by + + getRemoteFeatureFlags() + + is retrieved from one of the following sources: +
+ 1) manifest-flags.json file 2) RemoteFeatureFlagsController +
+ Modify the manifest-flags.json file will change the state locally. +
+
+
+ {"feature1":"value1"} +
+
+

diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx index ff76752aed46..5f97bc987323 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx @@ -6,8 +6,11 @@ import { renderWithProvider } from '../../../../test/jest/rendering'; import mockState from '../../../../test/data/mock-state.json'; import DeveloperOptionsTab from '.'; -const mockSetServiceWorkerKeepAlivePreference = jest.fn(); - +const mockSetServiceWorkerKeepAlivePreference = jest.fn().mockReturnValue({ + type: 'SET_SERVICE_WORKER_KEEP_ALIVE', + value: true, +}); +const mockRemoteFeatureFlags = { feature1: 'value1' }; // eslint-disable-next-line /* @ts-expect-error: Avoids error from window property not existing */ window.metamaskFeatureFlags = {}; @@ -17,16 +20,24 @@ jest.mock('../../../store/actions.ts', () => ({ mockSetServiceWorkerKeepAlivePreference, })); +jest.mock('../../../selectors', () => ({ + ...jest.requireActual('../../../selectors'), + getRemoteFeatureFlags: jest.fn(() => mockRemoteFeatureFlags), +})); + describe('Develop options tab', () => { const mockStore = configureMockStore([thunk])(mockState); it('should match snapshot', () => { - const { container } = renderWithProvider( + const { getByTestId, container } = renderWithProvider( , mockStore, ); expect(container).toMatchSnapshot(); + expect( + getByTestId('developer-options-remote-feature-flags').textContent, + ).toEqual(JSON.stringify(mockRemoteFeatureFlags)); }); it('should toggle Service Worker Keep Alive', async () => { diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx index a604082586fc..866e711d821c 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx @@ -37,6 +37,7 @@ import { import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { getIsRedesignedConfirmationsDeveloperEnabled } from '../../confirmations/selectors/confirm'; +import { getRemoteFeatureFlags } from '../../../selectors'; import ToggleRow from './developer-options-toggle-row-component'; import SentryTest from './sentry-test'; import { ProfileSyncDevSettings } from './profile-sync'; @@ -241,11 +242,54 @@ const DeveloperOptionsTab = () => { ); }; + const renderRemoteFeatureFlags = () => { + const remoteFeatureFlags = useSelector(getRemoteFeatureFlags); + return ( + +

+ Remote feature flags +
+ The remote feature flags here by getRemoteFeatureFlags() is + retrieved from one of the following sources: +
+ 1) manifest-flags.json file 2) RemoteFeatureFlagsController +
+ Modify the manifest-flags.json file will change the state locally. +
+
+
+ {JSON.stringify(remoteFeatureFlags)} +
+ + ); + }; + return (
States + + } + > + Current States + +
+ {renderRemoteFeatureFlags()} +
{ > Reset States -
{renderAnnouncementReset()} {renderOnboardingReset()} diff --git a/ui/pages/settings/info-tab/info-tab.component.js b/ui/pages/settings/info-tab/info-tab.component.js index 6da93878c7ef..121f29e7c52c 100644 --- a/ui/pages/settings/info-tab/info-tab.component.js +++ b/ui/pages/settings/info-tab/info-tab.component.js @@ -28,10 +28,6 @@ import { } from '../../../../shared/lib/ui-utils'; export default class InfoTab extends PureComponent { - static propTypes = { - remoteFeatureFlags: PropTypes.object.isRequired, - }; - state = { version: process.env.METAMASK_VERSION, }; @@ -57,14 +53,6 @@ export default class InfoTab extends PureComponent { componentDidMount() { const { t } = this.context; handleSettingsRefs(t, t('about'), this.settingsRefs); - if (this.props.remoteFeatureFlags.testFlagForThreshold) { - // eslint-disable-next-line no-console - console.log( - `Fetch remote feature flag success, eg: testFlagForThreshold has value ${JSON.stringify( - this.props.remoteFeatureFlags.testFlagForThreshold, - )}`, - ); - } } renderInfoLinks() { diff --git a/ui/pages/settings/info-tab/info-tab.stories.js b/ui/pages/settings/info-tab/info-tab.stories.js index 020f60d2516e..34f855172d1c 100644 --- a/ui/pages/settings/info-tab/info-tab.stories.js +++ b/ui/pages/settings/info-tab/info-tab.stories.js @@ -5,6 +5,6 @@ export default { title: 'Pages/Settings/InfoTab', }; -export const DefaultStory = () => ; +export const DefaultStory = () => ; DefaultStory.storyName = 'Default'; diff --git a/ui/pages/settings/info-tab/info-tab.test.tsx b/ui/pages/settings/info-tab/info-tab.test.tsx index 7625caf09efb..b25be3f8ed5e 100644 --- a/ui/pages/settings/info-tab/info-tab.test.tsx +++ b/ui/pages/settings/info-tab/info-tab.test.tsx @@ -7,9 +7,7 @@ describe('InfoTab', () => { let getByText: (text: string) => HTMLElement; beforeEach(() => { - const renderResult = renderWithProvider( - , - ); + const renderResult = renderWithProvider(); getByText = renderResult.getByText; }); diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index 37257e2c8fcb..cd9307013a68 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -73,7 +73,6 @@ class SettingsPage extends PureComponent { isPopup: PropTypes.bool, mostRecentOverviewPage: PropTypes.string.isRequired, pathnameI18nKey: PropTypes.string, - remoteFeatureFlags: PropTypes.object.isRequired, settingsPageSnaps: PropTypes.array, snapSettingsTitle: PropTypes.string, toggleNetworkMenu: PropTypes.func.isRequired, @@ -410,13 +409,7 @@ class SettingsPage extends PureComponent { /> )} /> - ( - - )} - /> + } /> { const { metamask: { currencyRates }, } = state; - const remoteFeatureFlags = getRemoteFeatureFlags(state); - const settingsPageSnapsIds = getSettingsPageSnapsIds(state); const snapsMetadata = getSnapsMetadata(state); const conversionDate = currencyRates[ticker]?.conversionDate; @@ -133,7 +130,6 @@ const mapStateToProps = (state, ownProps) => { isPopup, mostRecentOverviewPage: getMostRecentOverviewPage(state), pathnameI18nKey, - remoteFeatureFlags, settingsPageSnaps, snapSettingsTitle, useExternalServices, diff --git a/ui/pages/settings/settings.stories.js b/ui/pages/settings/settings.stories.js index b6b695a89cb1..e56f679eda5b 100644 --- a/ui/pages/settings/settings.stories.js +++ b/ui/pages/settings/settings.stories.js @@ -61,7 +61,6 @@ const Settings = ({ history }) => { history={history} pathnameI18nKey={pathnameI18nKey} backRoute={SETTINGS_ROUTE} - remoteFeatureFlags={{}} settingsPageSnaps={[]} />
diff --git a/ui/pages/settings/settings.test.js b/ui/pages/settings/settings.test.js index 061175865fce..238b50f3ce5b 100644 --- a/ui/pages/settings/settings.test.js +++ b/ui/pages/settings/settings.test.js @@ -19,7 +19,6 @@ describe('SettingsPage', () => { isSnapViewPage: false, mostRecentOverviewPage: '/', pathnameI18nKey: '', - remoteFeatureFlags: {}, }; const mockStore = configureMockStore()(mockState); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 61f8c0ffa034..fa6eb97dd0b2 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -113,6 +113,7 @@ import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { isSnapIgnoredInProd } from '../helpers/utils/snaps'; +import { getManifestFlags } from '../../app/scripts/lib/manifestFlags'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -2977,7 +2978,23 @@ export function getMetaMetricsDataDeletionStatus(state) { return state.metamask.metaMetricsDataDeletionStatus; } +/** + * Gets the remote feature flags from either the manifest or state. + * First checks if remote feature flags exist in the manifest and returns those if present. + * Otherwise returns the remote feature flags from the MetaMask state that's retrieved from controller. + * + * @param {object} state - The MetaMask state object + * @returns {object} The remote feature flags object containing feature flag key-value pairs + */ export function getRemoteFeatureFlags(state) { + const remoteFeatureFlagsValueFromManifest = + getManifestFlags().remoteFeatureFlags; + if ( + remoteFeatureFlagsValueFromManifest && + Object.keys(remoteFeatureFlagsValueFromManifest).length > 0 + ) { + return remoteFeatureFlagsValueFromManifest; + } return state.metamask.remoteFeatureFlags; } diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 143f0b9e3d74..e1c131d08ca0 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -15,6 +15,7 @@ import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import { selectSwitchedNetworkNeverShowMessage } from '../components/app/toast-master/selectors'; import * as networkSelectors from '../../shared/modules/selectors/networks'; import * as selectors from './selectors'; +import * as manifestFlags from '../../app/scripts/lib/manifestFlags'; jest.mock('../../shared/modules/selectors/networks', () => ({ ...jest.requireActual('../../shared/modules/selectors/networks'), @@ -2155,16 +2156,71 @@ describe('#getConnectedSitesList', () => { }); describe('#getRemoteFeatureFlags', () => { - it('returns remoteFeatureFlags in state', () => { + let getManifestFlagsMock; + + beforeEach(() => { + // Mock the getManifestFlags function before each test + getManifestFlagsMock = jest.spyOn(manifestFlags, 'getManifestFlags').mockReturnValue({}); + }); + + afterEach(() => { + // Clean up mock after each test + getManifestFlagsMock.mockRestore(); + }); + + it('returns manifest flags when they are provided in manifest-flags.json', () => { + getManifestFlagsMock.mockReturnValue({ + remoteFeatureFlags: { + manifestFlag1: true, + manifestFlag2: false, + }, + }); + + const state = { + metamask: { + remoteFeatureFlags: { + stateFlag: true, + }, + }, + }; + + expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ + manifestFlag1: true, + manifestFlag2: false, + }); + }); + + it('returns state flags when manifest flags are empty', () => { + getManifestFlagsMock.mockReturnValue({ + remoteFeatureFlags: {}, + }); + + const state = { + metamask: { + remoteFeatureFlags: { + stateFlag: true, + }, + }, + }; + + expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ + stateFlag: true, + }); + }); + + it('returns state flags when manifest flags are undefined', () => { + getManifestFlagsMock.mockReturnValue({}); + const state = { metamask: { remoteFeatureFlags: { - existingFlag: true, + stateFlag: true, }, }, }; + expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ - existingFlag: true, + stateFlag: true, }); }); }); From 887ed35ab17653c3f11a42004d18a30687190c68 Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Tue, 14 Jan 2025 15:12:22 +0000 Subject: [PATCH 02/20] Update LavaMoat policies --- .../webpack/test/webpack.config.test.ts | 6 +- .../utils/plugins/ManifestPlugin/helpers.ts | 5 +- lavamoat/browserify/beta/policy.json | 2 +- lavamoat/browserify/flask/policy.json | 2 +- lavamoat/browserify/main/policy.json | 2 +- lavamoat/browserify/mmi/policy.json | 2 +- lavamoat/build-system/policy.json | 346 ++++-------------- ui/selectors/selectors.js | 1 + ui/selectors/selectors.test.js | 7 +- 9 files changed, 97 insertions(+), 276 deletions(-) diff --git a/development/webpack/test/webpack.config.test.ts b/development/webpack/test/webpack.config.test.ts index e1d11f953829..796a2233bdd3 100644 --- a/development/webpack/test/webpack.config.test.ts +++ b/development/webpack/test/webpack.config.test.ts @@ -78,7 +78,7 @@ ${Object.entries(env) } it('should have the correct defaults', () => { - const config: Configuration = getWebpackConfig(); + const config: Configuration = getWebpackConfig(['--test']); // check that options are valid const { options } = webpack(config); assert.strictEqual(options.name, 'MetaMask – development'); @@ -162,6 +162,7 @@ ${Object.entries(env) { manifest_version: 3, name: 'name', + permissions: ['tabs'], version: '1.2.3', content_scripts: [ { @@ -191,6 +192,7 @@ ${Object.entries(env) '--no-progress', '--no-cache', '--zip', + '--test', ...removeUnsupportedFeatures, ], { @@ -231,7 +233,7 @@ ${Object.entries(env) assert.deepStrictEqual(manifestPlugin.options.description, null); assert.deepStrictEqual(manifestPlugin.options.zip, true); assert(manifestPlugin.options.zipOptions, 'Zip options should be present'); - assert.strictEqual(manifestPlugin.options.transform, undefined); + assert.notEqual(manifestPlugin.options.transform, undefined); const progressPlugin = instance.options.plugins.find( (plugin) => plugin && plugin.constructor.name === 'ProgressPlugin', diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts index 1db54118996e..9f8104eb3c0a 100644 --- a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -35,7 +35,10 @@ export function transformManifest(args: { lockdown: boolean; test: boolean }) { browserManifest._flags = manifestFlags; } - transforms.push(addManifestFlags); + // Add manifest flags only for non-test builds so the test build is not affected by local feature flags + if (!args.test) { + transforms.push(addManifestFlags); + } if (!args.lockdown) { // remove lockdown scripts from content_scripts diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index dc35b61fdd86..c4985af54a1d 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -5775,4 +5775,4 @@ } } } -} +} \ No newline at end of file diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index dc35b61fdd86..c4985af54a1d 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -5775,4 +5775,4 @@ } } } -} +} \ No newline at end of file diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index dc35b61fdd86..c4985af54a1d 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -5775,4 +5775,4 @@ } } } -} +} \ No newline at end of file diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index c5d67dc34329..bc5be17e5a64 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -5867,4 +5867,4 @@ } } } -} +} \ No newline at end of file diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 7fc9728c7c90..5c9dbd68b17c 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -5,7 +5,7 @@ "define": true }, "packages": { - "@babel/core>@ampproject/remapping>@jridgewell/gen-mapping": true, + "terser>@jridgewell/source-map>@jridgewell/gen-mapping": true, "terser-webpack-plugin>@jridgewell/trace-mapping": true } }, @@ -60,7 +60,7 @@ "@babel/preset-react": true, "@babel/preset-typescript": true, "@babel/core>@babel/template": true, - "@babel/core>@babel/traverse": true, + "depcheck>@babel/traverse": true, "@babel/core>@babel/types": true, "@babel/core>convert-source-map": true, "nock>debug": true, @@ -147,7 +147,7 @@ "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": true, "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": true, - "@babel/core>@babel/traverse": true, + "depcheck>@babel/traverse": true, "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin>semver": true } }, @@ -175,7 +175,7 @@ "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>babel-plugin-polyfill-corejs2>@babel/helper-define-polyfill-provider>lodash.debounce": true, - "browserify>resolve": true + "depcheck>resolve": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": { @@ -200,8 +200,9 @@ "packages": { "@babel/core": true, "@babel/core>@babel/helper-module-transforms>@babel/helper-module-imports": true, + "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": true, "@babel/code-frame>@babel/helper-validator-identifier": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": { @@ -214,7 +215,7 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": { @@ -222,10 +223,10 @@ "@babel/core": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-member-expression-to-functions": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers>@babel/helper-optimise-call-expression": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, - "@babel/preset-env>@babel/plugin-transform-modules-commonjs>@babel/helper-simple-access": { + "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": { "packages": { "@babel/core>@babel/types": true } @@ -238,7 +239,7 @@ "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator>@babel/helper-wrap-function": { "packages": { "@babel/core>@babel/template": true, - "@babel/core>@babel/traverse": true, + "depcheck>@babel/traverse": true, "@babel/core>@babel/types": true } }, @@ -251,7 +252,7 @@ "@babel/preset-env>@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-bugfix-safari-class-field-initializer-scope": { @@ -277,7 +278,7 @@ "packages": { "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-syntax-import-assertions": { @@ -316,7 +317,7 @@ "@babel/core": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-async-to-generator>@babel/helper-remap-async-to-generator": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-async-to-generator": { @@ -358,8 +359,8 @@ "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-replace-supers": true, - "@babel/core>@babel/traverse": true, - "depcheck>@babel/traverse>globals": true + "depcheck>@babel/traverse": true, + "@babel/preset-env>@babel/plugin-transform-classes>globals": true } }, "@babel/preset-env>@babel/plugin-transform-computed-properties": { @@ -422,7 +423,7 @@ "packages": { "@babel/core>@babel/helper-compilation-targets": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-json-strings": { @@ -460,7 +461,7 @@ "@babel/core": true, "@babel/core>@babel/helper-module-transforms": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-env>@babel/plugin-transform-modules-commonjs>@babel/helper-simple-access": true + "@babel/core>@babel/helper-module-transforms>@babel/helper-simple-access": true } }, "@babel/preset-env>@babel/plugin-transform-modules-systemjs": { @@ -472,7 +473,7 @@ "@babel/core>@babel/helper-module-transforms": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/code-frame>@babel/helper-validator-identifier": true, - "@babel/core>@babel/traverse": true + "depcheck>@babel/traverse": true } }, "@babel/preset-env>@babel/plugin-transform-modules-umd": { @@ -779,7 +780,7 @@ "@babel/core>@babel/types": true } }, - "@babel/core>@babel/traverse": { + "depcheck>@babel/traverse": { "globals": { "console.log": true }, @@ -789,25 +790,7 @@ "@babel/core>@babel/parser": true, "@babel/core>@babel/template": true, "@babel/core>@babel/types": true, - "nock>debug": true, - "depcheck>@babel/traverse>globals": true - } - }, - "depcheck>@babel/traverse": { - "packages": { - "babel/preset-env>b@babel/types": true - } - }, - "lavamoat>lavamoat-tofu>@babel/traverse": { - "globals": { - "console.log": true - }, - "packages": { - "@babel/code-frame": true, - "@babel/core>@babel/generator": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/parser": true, - "@babel/core>@babel/template": true, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/types": true, + "babel/preset-env>b@babel/types": true, "nock>debug": true, "depcheck>@babel/traverse>globals": true } @@ -822,16 +805,6 @@ "@babel/code-frame>@babel/helper-validator-identifier": true } }, - "lavamoat>lavamoat-tofu>@babel/traverse>@babel/types": { - "globals": { - "console.warn": true, - "process.env": true - }, - "packages": { - "@babel/core>@babel/types>@babel/helper-string-parser": true, - "@babel/code-frame>@babel/helper-validator-identifier": true - } - }, "sass-embedded>@bufbuild/protobuf": { "globals": { "TextDecoder": true, @@ -908,7 +881,7 @@ "eslint-plugin-prettier": true, "eslint-plugin-react": true, "eslint-plugin-react-hooks": true, - "eslint>@eslint/eslintrc>globals": true, + "eslint>globals": true, "eslint>ignore": true, "eslint>minimatch": true, "mocha>strip-json-comments": true @@ -941,16 +914,6 @@ "eslint>minimatch": true } }, - "@babel/core>@ampproject/remapping>@jridgewell/gen-mapping": { - "globals": { - "define": true - }, - "packages": { - "terser>@jridgewell/source-map>@jridgewell/gen-mapping>@jridgewell/set-array": true, - "terser-webpack-plugin>@jridgewell/trace-mapping>@jridgewell/sourcemap-codec": true, - "terser-webpack-plugin>@jridgewell/trace-mapping": true - } - }, "terser>@jridgewell/source-map>@jridgewell/gen-mapping": { "globals": { "define": true @@ -1012,7 +975,7 @@ "node:path.relative": true }, "packages": { - "lavamoat>@lavamoat/aa>resolve": true + "depcheck>resolve": true } }, "@lavamoat/lavapack": { @@ -1413,7 +1376,7 @@ "string.prototype.matchall>call-bind": true, "string.prototype.matchall>define-properties": true, "string.prototype.matchall>es-abstract": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>get-intrinsic": true, "eslint-plugin-react>array-includes>is-string": true } }, @@ -1489,7 +1452,7 @@ }, "packages": { "browserslist": true, - "browserslist>caniuse-lite": true, + "autoprefixer>caniuse-lite": true, "autoprefixer>normalize-range": true, "stylelint>autoprefixer>num2fraction": true, "stylelint>postcss>picocolors": true, @@ -1499,7 +1462,7 @@ }, "@babel/preset-env>babel-plugin-polyfill-corejs2": { "packages": { - "@babel/preset-env>babel-plugin-polyfill-corejs2>@babel/compat-data": true, + "@babel/preset-env>@babel/compat-data": true, "@babel/core": true, "@babel/preset-env>babel-plugin-polyfill-corejs2>@babel/helper-define-polyfill-provider": true, "@babel/preset-env>babel-plugin-polyfill-corejs2>semver": true @@ -1596,9 +1559,6 @@ } }, "chokidar>braces": { - "globals": { - "console.log": true - }, "packages": { "chokidar>braces>fill-range": true } @@ -1654,7 +1614,7 @@ "process.platform": true }, "packages": { - "browserify>resolve": true + "depcheck>resolve": true } }, "browserify": { @@ -1687,7 +1647,7 @@ "labeled-stream-splicer": true, "browserify>module-deps": true, "browserify>read-only-stream": true, - "browserify>resolve": true, + "depcheck>resolve": true, "browserify>shasum-object": true, "browserify>syntax-error": true, "browserify>through2": true, @@ -1711,7 +1671,7 @@ "process.versions.node": true }, "packages": { - "browserslist>caniuse-lite": true, + "autoprefixer>caniuse-lite": true, "browserslist>electron-to-chromium": true, "browserslist>node-releases": true } @@ -1757,44 +1717,15 @@ "process.cwd": true } }, - "string.prototype.matchall>call-bind>call-bind-apply-helpers": { - "packages": { - "string.prototype.matchall>es-abstract>es-errors": true, - "browserify>has>function-bind": true - } - }, - "string.prototype.matchall>regexp.prototype.flags>call-bind": { - "packages": { - "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>es-abstract>es-errors": true, - "browserify>has>function-bind": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, - "string.prototype.matchall>call-bind>set-function-length": true - } - }, - "string.prototype.matchall>es-abstract>safe-regex-test>call-bind": { - "packages": { - "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>es-abstract>es-errors": true, - "browserify>has>function-bind": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, - "string.prototype.matchall>call-bind>set-function-length": true - } - }, "string.prototype.matchall>call-bind": { "packages": { - "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, "string.prototype.matchall>call-bind>es-define-property": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>call-bind>es-errors": true, + "browserify>has>function-bind": true, + "string.prototype.matchall>get-intrinsic": true, "string.prototype.matchall>call-bind>set-function-length": true } }, - "gulp>vinyl-fs>object.assign>call-bound": { - "packages": { - "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, - "eslint-plugin-react>array-includes>get-intrinsic": true - } - }, "chalk": { "packages": { "chalk>ansi-styles": true, @@ -2266,7 +2197,7 @@ "process": true }, "packages": { - "mocha>ms": true, + "nock>debug>ms": true, "mocha>supports-color": true } }, @@ -2315,7 +2246,7 @@ "string.prototype.matchall>define-properties>define-data-property": { "packages": { "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>call-bind>es-errors": true, "string.prototype.matchall>es-abstract>gopd": true } }, @@ -2471,12 +2402,6 @@ "stylelint>postcss-html>htmlparser2>domelementtype": true } }, - "eslint-plugin-react>array-includes>get-intrinsic>get-proto>dunder-proto": { - "packages": { - "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, - "string.prototype.matchall>es-abstract>gopd": true - } - }, "browserify>duplexer2": { "packages": { "browserify>duplexer2>readable-stream": true @@ -2552,11 +2477,11 @@ "packages": { "string.prototype.matchall>call-bind": true, "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>call-bind>es-errors": true, "string.prototype.matchall>es-abstract>es-object-atoms": true, "string.prototype.matchall>es-abstract>es-set-tostringtag": true, "string.prototype.matchall>es-abstract>es-to-primitive": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>get-intrinsic": true, "string.prototype.matchall>es-abstract>gopd": true, "string.prototype.matchall>es-abstract>has-property-descriptors": true, "string.prototype.matchall>es-abstract>has-proto": true, @@ -2571,15 +2496,19 @@ "string.prototype.matchall>es-abstract>string.prototype.trim": true } }, + "string.prototype.matchall>call-bind>es-define-property": { + "packages": { + "string.prototype.matchall>get-intrinsic": true + } + }, "string.prototype.matchall>es-abstract>es-object-atoms": { "packages": { - "string.prototype.matchall>es-abstract>es-errors": true + "string.prototype.matchall>call-bind>es-errors": true } }, "string.prototype.matchall>es-abstract>es-set-tostringtag": { "packages": { - "string.prototype.matchall>es-abstract>es-errors": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>get-intrinsic": true, "koa>is-generator-function>has-tostringtag": true, "depcheck>is-core-module>hasown": true } @@ -2592,7 +2521,7 @@ "string.prototype.matchall>es-abstract>es-to-primitive": { "packages": { "string.prototype.matchall>es-abstract>is-callable": true, - "string.prototype.matchall>es-abstract>es-to-primitive>is-date-object": true, + "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "string.prototype.matchall>es-abstract>es-to-primitive>is-symbol": true } }, @@ -2710,8 +2639,7 @@ }, "packages": { "eslint-import-resolver-node>debug": true, - "depcheck>is-core-module": true, - "browserify>resolve": true + "depcheck>resolve": true } }, "eslint-import-resolver-typescript": { @@ -2726,8 +2654,8 @@ "nock>debug": true, "nyc>glob": true, "del>is-glob": true, - "browserify>resolve": true, - "eslint-import-resolver-typescript>tsconfig-paths": true + "depcheck>resolve": true, + "eslint-plugin-import>tsconfig-paths": true } }, "eslint-plugin-import>eslint-module-utils": { @@ -2837,7 +2765,7 @@ "eslint-plugin-node>eslint-utils": true, "eslint>ignore": true, "eslint>minimatch": true, - "browserify>resolve": true, + "depcheck>resolve": true, "eslint-plugin-node>semver": true } }, @@ -3470,34 +3398,18 @@ "assert.equal": true } }, - "eslint-plugin-react>array-includes>get-intrinsic": { + "string.prototype.matchall>get-intrinsic": { "globals": { "AggregateError": true, "FinalizationRegistry": true, "WeakRef": true }, "packages": { - "string.prototype.matchall>call-bind>call-bind-apply-helpers": true, - "string.prototype.matchall>call-bind>es-define-property": true, - "string.prototype.matchall>es-abstract>es-errors": true, - "string.prototype.matchall>es-abstract>es-object-atoms": true, + "string.prototype.matchall>call-bind>es-errors": true, "browserify>has>function-bind": true, - "eslint-plugin-react>array-includes>get-intrinsic>get-proto": true, - "string.prototype.matchall>es-abstract>gopd": true, + "string.prototype.matchall>es-abstract>has-proto": true, "string.prototype.matchall>has-symbols": true, - "depcheck>is-core-module>hasown": true, - "eslint-plugin-react>array-includes>get-intrinsic>math-intrinsics": true - } - }, - "string.prototype.matchall>get-intrinsic": { - "packages": { - "string.prototype.matchall>has-symbols": true - } - }, - "eslint-plugin-react>array-includes>get-intrinsic>get-proto": { - "packages": { - "eslint-plugin-react>array-includes>get-intrinsic>get-proto>dunder-proto": true, - "string.prototype.matchall>es-abstract>es-object-atoms": true + "depcheck>is-core-module>hasown": true } }, "gulp-zip>get-stream": { @@ -3656,6 +3568,11 @@ "define": true } }, + "string.prototype.matchall>es-abstract>gopd": { + "packages": { + "string.prototype.matchall>get-intrinsic": true + } + }, "del>graceful-fs": { "builtin": { "assert.equal": true, @@ -4001,7 +3918,7 @@ }, "string.prototype.matchall>internal-slot": { "packages": { - "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>call-bind>es-errors": true, "depcheck>is-core-module>hasown": true, "string.prototype.matchall>side-channel": true } @@ -4054,14 +3971,6 @@ "depcheck>is-core-module>hasown": true } }, - "browserify>resolve>is-core-module": { - "globals": { - "process.versions": true - }, - "packages": { - "depcheck>is-core-module>hasown": true - } - }, "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor": { "packages": { "gulp>glob-watcher>anymatch>micromatch>extglob>expand-brackets>define-property>is-descriptor>is-data-descriptor>kind-of": true @@ -4077,7 +3986,7 @@ "gulp>gulp-cli>matchdep>micromatch>snapdragon>define-property>is-descriptor>is-data-descriptor>kind-of": true } }, - "string.prototype.matchall>es-abstract>es-to-primitive>is-date-object": { + "@metamask/eth-token-tracker>deep-equal>is-date-object": { "packages": { "koa>is-generator-function>has-tostringtag": true } @@ -4181,10 +4090,8 @@ }, "string.prototype.matchall>es-abstract>is-regex": { "packages": { - "gulp>vinyl-fs>object.assign>call-bound": true, - "string.prototype.matchall>es-abstract>gopd": true, - "koa>is-generator-function>has-tostringtag": true, - "depcheck>is-core-module>hasown": true + "string.prototype.matchall>call-bind": true, + "koa>is-generator-function>has-tostringtag": true } }, "gulp>gulp-cli>replace-homedir>is-absolute>is-relative": { @@ -4194,13 +4101,12 @@ }, "eslint-plugin-react>array-includes>is-string": { "packages": { - "gulp>vinyl-fs>object.assign>call-bound": true, "koa>is-generator-function>has-tostringtag": true } }, "string.prototype.matchall>es-abstract>es-to-primitive>is-symbol": { "packages": { - "string.prototype.matchall>es-abstract>es-to-primitive>is-symbol>has-symbols": true + "string.prototype.matchall>has-symbols": true } }, "gulp>gulp-cli>replace-homedir>is-absolute>is-relative>is-unc-path": { @@ -4280,11 +4186,6 @@ "console.warn": true } }, - "eslint-import-resolver-typescript>tsconfig-paths>json5": { - "globals": { - "console.warn": true - } - }, "eslint-plugin-import>tsconfig-paths>json5": { "globals": { "console.warn": true @@ -4311,7 +4212,7 @@ "console.error": true }, "packages": { - "eslint-plugin-react>jsx-ast-utils>object.assign": true + "gulp>vinyl-fs>object.assign": true } }, "gulp>glob-watcher>just-debounce": { @@ -4455,8 +4356,8 @@ "console.log": true }, "packages": { - "lavamoat>lavamoat-tofu>@babel/parser": true, - "lavamoat>lavamoat-tofu>@babel/traverse": true + "@babel/core>@babel/parser": true, + "depcheck>@babel/traverse": true } }, "lavamoat>lavamoat-core>merge-deep>clone-deep>lazy-cache": { @@ -4758,7 +4659,7 @@ "loose-envify": true, "browserify>parents": true, "browserify>module-deps>readable-stream": true, - "browserify>resolve": true, + "depcheck>resolve": true, "browserify>module-deps>stream-combiner2": true, "browserify>module-deps>through2": true, "watchify>xtend": true @@ -4895,34 +4796,15 @@ "WeakRef": true } }, - "string.prototype.matchall>side-channel>object-inspect": { - "builtin": { - "util.inspect": true - }, - "globals": { - "HTMLElement": true, - "WeakRef": true - } - }, "gulp>gulp-cli>matchdep>micromatch>snapdragon>base>cache-base>collection-visit>object-visit": { "packages": { "gulp>gulp-cli>isobject": true } }, - "eslint-plugin-react>jsx-ast-utils>object.assign": { - "packages": { - "string.prototype.matchall>call-bind": true, - "string.prototype.matchall>define-properties": true, - "string.prototype.matchall>has-symbols": true, - "@lavamoat/lavapack>json-stable-stringify>object-keys": true - } - }, "gulp>vinyl-fs>object.assign": { "packages": { "string.prototype.matchall>call-bind": true, - "gulp>vinyl-fs>object.assign>call-bound": true, "string.prototype.matchall>define-properties": true, - "string.prototype.matchall>es-abstract>es-object-atoms": true, "string.prototype.matchall>has-symbols": true, "@lavamoat/lavapack>json-stable-stringify>object-keys": true } @@ -7191,9 +7073,9 @@ }, "string.prototype.matchall>regexp.prototype.flags": { "packages": { - "string.prototype.matchall>regexp.prototype.flags>call-bind": true, + "string.prototype.matchall>call-bind": true, "string.prototype.matchall>define-properties": true, - "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>call-bind>es-errors": true, "string.prototype.matchall>regexp.prototype.flags>set-function-name": true } }, @@ -7348,37 +7230,7 @@ "gulp>vinyl-fs>value-or-function": true } }, - "lavamoat>@lavamoat/aa>resolve": { - "builtin": { - "fs.readFileSync": true, - "fs.realpathSync": true, - "fs.statSync": true, - "os.homedir": true, - "path.dirname": true, - "path.join": true, - "path.parse": true, - "path.relative": true, - "path.resolve": true - }, - "globals": { - "process.env.HOME": true, - "process.env.HOMEDRIVE": true, - "process.env.HOMEPATH": true, - "process.env.LNAME": true, - "process.env.LOGNAME": true, - "process.env.USER": true, - "process.env.USERNAME": true, - "process.env.USERPROFILE": true, - "process.getuid": true, - "process.platform": true, - "process.versions.pnp": true - }, - "packages": { - "depcheck>is-core-module": true, - "depcheck>resolve>path-parse": true - } - }, - "browserify>resolve": { + "depcheck>resolve": { "builtin": { "fs.readFile": true, "fs.readFileSync": true, @@ -7408,7 +7260,7 @@ "process.versions.pnp": true }, "packages": { - "browserify>resolve>is-core-module": true, + "depcheck>is-core-module": true, "depcheck>resolve>path-parse": true } }, @@ -7666,8 +7518,8 @@ }, "string.prototype.matchall>es-abstract>safe-regex-test": { "packages": { - "string.prototype.matchall>es-abstract>safe-regex-test>call-bind": true, - "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>call-bind": true, + "string.prototype.matchall>call-bind>es-errors": true, "string.prototype.matchall>es-abstract>is-regex": true } }, @@ -7807,8 +7659,8 @@ "string.prototype.matchall>call-bind>set-function-length": { "packages": { "string.prototype.matchall>define-properties>define-data-property": true, - "string.prototype.matchall>es-abstract>es-errors": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, + "string.prototype.matchall>call-bind>es-errors": true, + "string.prototype.matchall>get-intrinsic": true, "string.prototype.matchall>es-abstract>gopd": true, "string.prototype.matchall>es-abstract>has-property-descriptors": true } @@ -7816,7 +7668,7 @@ "string.prototype.matchall>regexp.prototype.flags>set-function-name": { "packages": { "string.prototype.matchall>define-properties>define-data-property": true, - "string.prototype.matchall>es-abstract>es-errors": true, + "string.prototype.matchall>call-bind>es-errors": true, "string.prototype.matchall>es-abstract>function.prototype.name>functions-have-names": true, "string.prototype.matchall>es-abstract>has-property-descriptors": true } @@ -7848,36 +7700,11 @@ "@metamask/rpc-errors>fast-safe-stringify": true } }, - "string.prototype.matchall>side-channel>side-channel-list": { - "packages": { - "string.prototype.matchall>es-abstract>es-errors": true, - "string.prototype.matchall>side-channel>object-inspect": true - } - }, - "string.prototype.matchall>side-channel>side-channel-map": { - "packages": { - "gulp>vinyl-fs>object.assign>call-bound": true, - "string.prototype.matchall>es-abstract>es-errors": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, - "string.prototype.matchall>side-channel>object-inspect": true - } - }, - "string.prototype.matchall>side-channel>side-channel-weakmap": { - "packages": { - "gulp>vinyl-fs>object.assign>call-bound": true, - "string.prototype.matchall>es-abstract>es-errors": true, - "eslint-plugin-react>array-includes>get-intrinsic": true, - "string.prototype.matchall>side-channel>object-inspect": true, - "string.prototype.matchall>side-channel>side-channel-map": true - } - }, "string.prototype.matchall>side-channel": { "packages": { - "string.prototype.matchall>es-abstract>es-errors": true, - "string.prototype.matchall>side-channel>object-inspect": true, - "string.prototype.matchall>side-channel>side-channel-list": true, - "string.prototype.matchall>side-channel>side-channel-map": true, - "string.prototype.matchall>side-channel>side-channel-weakmap": true + "string.prototype.matchall>call-bind": true, + "string.prototype.matchall>get-intrinsic": true, + "string.prototype.matchall>es-abstract>object-inspect": true } }, "nyc>signal-exit": { @@ -8823,7 +8650,7 @@ "ts-node>yn": true } }, - "eslint-import-resolver-typescript>tsconfig-paths": { + "eslint-plugin-import>tsconfig-paths": { "builtin": { "fs.existsSync": true, "fs.lstatSync": true, @@ -8844,24 +8671,9 @@ "process.cwd": true, "process.env": true }, - "packages": { - "eslint-import-resolver-typescript>tsconfig-paths>json5": true, - "wait-on>minimist": true, - "eslint-plugin-import>tsconfig-paths>strip-bom": true - } - }, - "eslint-plugin-import>tsconfig-paths": { - "builtin": { - "fs.existsSync": true, - "fs.lstatSync": true, - "fs.readFileSync": true, - "fs.statSync": true, - "path.dirname": true, - "path.join": true, - "path.resolve": true - }, "packages": { "eslint-plugin-import>tsconfig-paths>json5": true, + "wait-on>minimist": true, "eslint-plugin-import>tsconfig-paths>strip-bom": true } }, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index fa6eb97dd0b2..10878944bde9 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -113,6 +113,7 @@ import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { isSnapIgnoredInProd } from '../helpers/utils/snaps'; +// eslint-disable-next-line import/no-restricted-paths import { getManifestFlags } from '../../app/scripts/lib/manifestFlags'; import { getAllUnapprovedTransactions, diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index e1c131d08ca0..06ddacaee77c 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -6,6 +6,8 @@ import { } from '@metamask/keyring-api'; import { deepClone } from '@metamask/snaps-utils'; import { TransactionStatus } from '@metamask/transaction-controller'; +// eslint-disable-next-line import/no-restricted-paths +import * as manifestFlags from '../../app/scripts/lib/manifestFlags'; import { KeyringType } from '../../shared/constants/keyring'; import mockState from '../../test/data/mock-state.json'; import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; @@ -15,7 +17,6 @@ import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import { selectSwitchedNetworkNeverShowMessage } from '../components/app/toast-master/selectors'; import * as networkSelectors from '../../shared/modules/selectors/networks'; import * as selectors from './selectors'; -import * as manifestFlags from '../../app/scripts/lib/manifestFlags'; jest.mock('../../shared/modules/selectors/networks', () => ({ ...jest.requireActual('../../shared/modules/selectors/networks'), @@ -2160,7 +2161,9 @@ describe('#getConnectedSitesList', () => { beforeEach(() => { // Mock the getManifestFlags function before each test - getManifestFlagsMock = jest.spyOn(manifestFlags, 'getManifestFlags').mockReturnValue({}); + getManifestFlagsMock = jest + .spyOn(manifestFlags, 'getManifestFlags') + .mockReturnValue({}); }); afterEach(() => { From 4cef487d3dea7f3bd9076401713611fc810ae14b Mon Sep 17 00:00:00 2001 From: dddddanica Date: Thu, 16 Jan 2025 15:54:19 +0000 Subject: [PATCH 03/20] feat(27255): move the file to git ignore and add instructions to ReadMe for creating a remote feature flag --- .gitignore | 3 +++ .manifest-flags.json.dist | 5 ++++ README.md | 11 +++++++++ development/build/manifest.js | 24 ++++++++++++++++++- .../utils/plugins/ManifestPlugin/helpers.ts | 7 +++++- manifest-flags.json | 8 ------- 6 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 .manifest-flags.json.dist delete mode 100644 manifest-flags.json diff --git a/.gitignore b/.gitignore index 6ee150dd8653..ab47e255bbe9 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,9 @@ notes.txt .metamaskrc .metamaskprodrc +# Manifest customizated configuration +.manifest-flags.json + # Test results test-results/ diff --git a/.manifest-flags.json.dist b/.manifest-flags.json.dist new file mode 100644 index 000000000000..098779458f11 --- /dev/null +++ b/.manifest-flags.json.dist @@ -0,0 +1,5 @@ + // This configuration file is used to manage values passing to _flags that will be injected into manifest.json +{ + "remoteFeatureFlags": { + } +} diff --git a/README.md b/README.md index f76e913a6b6c..15c4daaa30aa 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,17 @@ If you are not a MetaMask Internal Developer, or are otherwise developing on a f - If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/), see [Developing on MetaMask - Segment](./development/README.md#segment). - If debugging unhandled exceptions, you'll need to add a value for `SENTRY_DSN` [Sentry Dsn](https://docs.sentry.io/product/sentry-basics/dsn-explainer/), see [Developing on MetaMask - Sentry](./development/README.md#sentry). - Optionally, replace the `PASSWORD` value with your development wallet password to avoid entering it each time you open the app. +- Duplicate `manifest-flags.json.dist` within the root and rename it to `manifest-flags.json` by running `cp .manifest-flags.json{.dist,}`. This file is used to add flags to `manifest.json` build files for the extension. You can add flags to the file to be used in the build process, for example: + ```json + { + "remoteFeatureFlags": { + "testFlagForThreshold": { + "name": "test-flag", + "value": "test-value" + } + } + } + ``` - Run `yarn install` to install the dependencies. - Build the project to the `./dist/` folder with `yarn dist` (for Chromium-based browsers) or `yarn dist:mv2` (for Firefox) diff --git a/development/build/manifest.js b/development/build/manifest.js index 17f6168df7dc..36e75352bf55 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -11,7 +11,6 @@ const baradDurManifest = isManifestV3 ? require('../../app/manifest/v3/_barad_dur.json') : require('../../app/manifest/v2/_barad_dur.json'); const { loadBuildTypesConfig } = require('../lib/build-type'); -const manifestFlags = require('../../manifest-flags.json'); const { TASKS, ENVIRONMENT } = require('./constants'); const { createTask, composeSeries } = require('./task'); @@ -19,6 +18,29 @@ const { getEnvironment, getBuildName } = require('./utils'); module.exports = createManifestTasks; +async function loadManifestFlags() { + try { + return JSON.parse( + await fs.readFile( + path.join(__dirname, '../../.manifest-flags.json'), + 'utf8', + ), + ); + } catch (error) { + return { remoteFeatureFlags: {} }; + } +} + +// Initialize with default value +let manifestFlags = { remoteFeatureFlags: {} }; + +// Load flags asynchronously +loadManifestFlags().then((flags) => { + manifestFlags = flags; +}); + +module.exports = createManifestTasks; + function createManifestTasks({ browserPlatforms, browserVersionMap, diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts index 9f8104eb3c0a..01ef6fdb725b 100644 --- a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -1,4 +1,9 @@ -import manifestFlags from '../../../../../manifest-flags.json'; +let manifestFlags: { remoteFeatureFlags: Record }; +try { + manifestFlags = require('../../../../../.manifest-flags.json'); +} catch (error) { + manifestFlags = { remoteFeatureFlags: {} }; +} /** * Returns a function that will transform a manifest JSON object based on the diff --git a/manifest-flags.json b/manifest-flags.json deleted file mode 100644 index f90de282f373..000000000000 --- a/manifest-flags.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "remoteFeatureFlags": { - "testFlagForThreshold": { - "name": "test-flag", - "value": "121212" - } - } -} From 5e3788b55a730488932bfe33107251729bed12b8 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Thu, 16 Jan 2025 21:13:06 +0000 Subject: [PATCH 04/20] feat(29629): Add _flags to webpack build only when in development; and restore default value for webpack config without passing in `--test` --- .../webpack/test/plugins.ManifestPlugin.test.ts | 2 +- development/webpack/test/webpack.config.test.ts | 6 ++++-- .../webpack/utils/plugins/ManifestPlugin/helpers.ts | 10 +++++++--- development/webpack/webpack.config.ts | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/development/webpack/test/plugins.ManifestPlugin.test.ts b/development/webpack/test/plugins.ManifestPlugin.test.ts index ff14904bb8d6..def2588dcb9c 100644 --- a/development/webpack/test/plugins.ManifestPlugin.test.ts +++ b/development/webpack/test/plugins.ManifestPlugin.test.ts @@ -232,7 +232,7 @@ describe('ManifestPlugin', () => { function runTest(baseManifest: Combination) { const manifest = baseManifest as unknown as chrome.runtime.Manifest; const hasTabsPermission = (manifest.permissions || []).includes('tabs'); - const transform = transformManifest(args); + const transform = transformManifest(args, false); if (args.test && hasTabsPermission) { it("throws in test mode when manifest already contains 'tabs' permission", () => { diff --git a/development/webpack/test/webpack.config.test.ts b/development/webpack/test/webpack.config.test.ts index 796a2233bdd3..7a872af6be2c 100644 --- a/development/webpack/test/webpack.config.test.ts +++ b/development/webpack/test/webpack.config.test.ts @@ -78,7 +78,7 @@ ${Object.entries(env) } it('should have the correct defaults', () => { - const config: Configuration = getWebpackConfig(['--test']); + const config: Configuration = getWebpackConfig([]); // check that options are valid const { options } = webpack(config); assert.strictEqual(options.name, 'MetaMask – development'); @@ -162,8 +162,10 @@ ${Object.entries(env) { manifest_version: 3, name: 'name', - permissions: ['tabs'], version: '1.2.3', + _flags: { + remoteFeatureFlags: {}, + }, content_scripts: [ { js: ['scripts/contentscript.js', 'scripts/inpage.js'], diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts index 01ef6fdb725b..4c496f56a6c7 100644 --- a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -16,11 +16,15 @@ try { * @param args * @param args.lockdown * @param args.test + * @param isDevelopment * @returns a function that will transform the manifest JSON object * @throws an error if the manifest already contains the "tabs" permission and * `test` is `true` */ -export function transformManifest(args: { lockdown: boolean; test: boolean }) { +export function transformManifest( + args: { lockdown: boolean; test: boolean }, + isDevelopment: boolean, +) { const transforms: ((manifest: chrome.runtime.Manifest) => void)[] = []; function removeLockdown(browserManifest: chrome.runtime.Manifest) { @@ -40,8 +44,8 @@ export function transformManifest(args: { lockdown: boolean; test: boolean }) { browserManifest._flags = manifestFlags; } - // Add manifest flags only for non-test builds so the test build is not affected by local feature flags - if (!args.test) { + // Add manifest flags only for development builds + if (isDevelopment) { transforms.push(addManifestFlags); } diff --git a/development/webpack/webpack.config.ts b/development/webpack/webpack.config.ts index 9ec97b5507e1..17d9d8a7bc45 100644 --- a/development/webpack/webpack.config.ts +++ b/development/webpack/webpack.config.ts @@ -131,7 +131,7 @@ const plugins: WebpackPluginInstance[] = [ version: version.version, versionName: version.versionName, browsers: args.browser, - transform: transformManifest(args), + transform: transformManifest(args, isDevelopment), zip: args.zip, ...(args.zip ? { From 5c703f2ed3c1c7ed98a57f80f4b97c1b4ecad08a Mon Sep 17 00:00:00 2001 From: dddddanica Date: Thu, 16 Jan 2025 21:23:54 +0000 Subject: [PATCH 05/20] feat(29629): adapted to right pattern in transform function --- .../webpack/utils/plugins/ManifestPlugin/helpers.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts index 4c496f56a6c7..b58d11c1eb58 100644 --- a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -35,6 +35,11 @@ export function transformManifest( } } + if (!args.lockdown) { + // remove lockdown scripts from content_scripts + transforms.push(removeLockdown); + } + /** * This function sets predefined flags in the manifest's _flags property. * @@ -44,16 +49,11 @@ export function transformManifest( browserManifest._flags = manifestFlags; } - // Add manifest flags only for development builds if (isDevelopment) { + // Add manifest flags only for development builds transforms.push(addManifestFlags); } - if (!args.lockdown) { - // remove lockdown scripts from content_scripts - transforms.push(removeLockdown); - } - function addTabsPermission(browserManifest: chrome.runtime.Manifest) { if (browserManifest.permissions) { if (browserManifest.permissions.includes('tabs')) { From 35fe1f03a900b79e7400f84dc48a7afe05f19ffd Mon Sep 17 00:00:00 2001 From: dddddanica Date: Thu, 16 Jan 2025 21:37:30 +0000 Subject: [PATCH 06/20] feat(29629): relocate `getManifestFlags` to `shared/lib/manifestFlags` --- app/scripts/lib/setupSentry.js | 2 +- development/lib/get-manifest-flag.ts | 2 +- {app/scripts => shared}/lib/manifestFlags.ts | 0 test/e2e/set-manifest-flags.ts | 2 +- ui/helpers/utils/mm-lazy.ts | 3 +-- ui/selectors/selectors.js | 3 +-- ui/selectors/selectors.test.js | 3 +-- 7 files changed, 6 insertions(+), 9 deletions(-) rename {app/scripts => shared}/lib/manifestFlags.ts (100%) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 76d66eba88d2..40327377c44e 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -3,8 +3,8 @@ import * as Sentry from '@sentry/browser'; import { logger } from '@sentry/utils'; import browser from 'webextension-polyfill'; import { isManifestV3 } from '../../../shared/modules/mv3.utils'; +import { getManifestFlags } from '../../../shared/lib/manifestFlags'; import extractEthjsErrorMessage from './extractEthjsErrorMessage'; -import { getManifestFlags } from './manifestFlags'; import { filterEvents } from './sentry-filter-events'; const projectLogger = createProjectLogger('sentry'); diff --git a/development/lib/get-manifest-flag.ts b/development/lib/get-manifest-flag.ts index 225b3a05121c..05a133ce0093 100644 --- a/development/lib/get-manifest-flag.ts +++ b/development/lib/get-manifest-flag.ts @@ -6,7 +6,7 @@ import { exec as callbackExec } from 'node:child_process'; import { hasProperty } from '@metamask/utils'; import { merge } from 'lodash'; -import type { ManifestFlags } from '../../app/scripts/lib/manifestFlags'; +import type { ManifestFlags } from '../../shared/lib/manifestFlags'; const exec = promisify(callbackExec); const PR_BODY_FILEPATH = path.resolve( diff --git a/app/scripts/lib/manifestFlags.ts b/shared/lib/manifestFlags.ts similarity index 100% rename from app/scripts/lib/manifestFlags.ts rename to shared/lib/manifestFlags.ts diff --git a/test/e2e/set-manifest-flags.ts b/test/e2e/set-manifest-flags.ts index 8a7e45050f14..1a824808c90e 100644 --- a/test/e2e/set-manifest-flags.ts +++ b/test/e2e/set-manifest-flags.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import { merge } from 'lodash'; -import { ManifestFlags } from '../../app/scripts/lib/manifestFlags'; +import { ManifestFlags } from '../../shared/lib/manifestFlags'; import { fetchManifestFlagsFromPRAndGit } from '../../development/lib/get-manifest-flag'; export const folder = `dist/${process.env.SELENIUM_BROWSER}`; diff --git a/ui/helpers/utils/mm-lazy.ts b/ui/helpers/utils/mm-lazy.ts index e31c22dfc99a..71a3d5d42e8a 100644 --- a/ui/helpers/utils/mm-lazy.ts +++ b/ui/helpers/utils/mm-lazy.ts @@ -1,6 +1,5 @@ import React from 'react'; -// eslint-disable-next-line import/no-restricted-paths -import { getManifestFlags } from '../../../app/scripts/lib/manifestFlags'; +import { getManifestFlags } from '../../../shared/lib/manifestFlags'; import { endTrace, trace, TraceName } from '../../../shared/lib/trace'; type DynamicImportType = () => Promise<{ default: React.ComponentType }>; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 10878944bde9..60d7892912b7 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -113,8 +113,7 @@ import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { isSnapIgnoredInProd } from '../helpers/utils/snaps'; -// eslint-disable-next-line import/no-restricted-paths -import { getManifestFlags } from '../../app/scripts/lib/manifestFlags'; +import { getManifestFlags } from '../../shared/lib/manifestFlags'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 06ddacaee77c..1cbd1a2a6e58 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -6,8 +6,7 @@ import { } from '@metamask/keyring-api'; import { deepClone } from '@metamask/snaps-utils'; import { TransactionStatus } from '@metamask/transaction-controller'; -// eslint-disable-next-line import/no-restricted-paths -import * as manifestFlags from '../../app/scripts/lib/manifestFlags'; +import * as manifestFlags from '../../shared/lib/manifestFlags'; import { KeyringType } from '../../shared/constants/keyring'; import mockState from '../../test/data/mock-state.json'; import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; From 473a3bd2b5ff63be8da0f6a452d76edd371af1d0 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Thu, 16 Jan 2025 21:47:21 +0000 Subject: [PATCH 07/20] feat(29629): enrich `getRemoteFeatureFlags` return type --- ui/selectors/selectors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 60d7892912b7..da0e2b78025c 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2984,7 +2984,7 @@ export function getMetaMetricsDataDeletionStatus(state) { * Otherwise returns the remote feature flags from the MetaMask state that's retrieved from controller. * * @param {object} state - The MetaMask state object - * @returns {object} The remote feature flags object containing feature flag key-value pairs + * @returns {ManifestFlags['remoteFeatureFlags']} The remote feature flags object containing feature flag key-value pairs */ export function getRemoteFeatureFlags(state) { const remoteFeatureFlagsValueFromManifest = From caa4b4f9780a94e51c494f08800c7ffc3d2ff981 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Thu, 16 Jan 2025 22:54:23 +0000 Subject: [PATCH 08/20] feat(29629): shallow merge states in `getRemoteFeatureFlags` --- ui/selectors/selectors.js | 24 +++++++++++------------- ui/selectors/selectors.test.js | 30 ++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index da0e2b78025c..c5b90310f8d7 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2979,23 +2979,21 @@ export function getMetaMetricsDataDeletionStatus(state) { } /** - * Gets the remote feature flags from either the manifest or state. - * First checks if remote feature flags exist in the manifest and returns those if present. - * Otherwise returns the remote feature flags from the MetaMask state that's retrieved from controller. + * Gets the remote feature flags by combining flags from both the manifest and state. + * Manifest flags take precedence and will override any duplicate flags from state. + * This allows for both static (manifest) and dynamic (state) feature flag configuration. * * @param {object} state - The MetaMask state object - * @returns {ManifestFlags['remoteFeatureFlags']} The remote feature flags object containing feature flag key-value pairs + * @returns {object} Combined feature flags object with manifest flags taking precedence over state flags */ export function getRemoteFeatureFlags(state) { - const remoteFeatureFlagsValueFromManifest = - getManifestFlags().remoteFeatureFlags; - if ( - remoteFeatureFlagsValueFromManifest && - Object.keys(remoteFeatureFlagsValueFromManifest).length > 0 - ) { - return remoteFeatureFlagsValueFromManifest; - } - return state.metamask.remoteFeatureFlags; + const manifestFlags = getManifestFlags().remoteFeatureFlags; + const stateFlags = state.metamask.remoteFeatureFlags; + + return { + ...stateFlags, + ...manifestFlags, + }; } /** diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 1cbd1a2a6e58..d6a940a4a676 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -2170,22 +2170,44 @@ describe('#getConnectedSitesList', () => { getManifestFlagsMock.mockRestore(); }); - it('returns manifest flags when they are provided in manifest-flags.json', () => { + it('performs shallow merge of manifest flags and state flags', () => { getManifestFlagsMock.mockReturnValue({ remoteFeatureFlags: { - manifestFlag1: true, - manifestFlag2: false, + flag1: true, + flag2: false, }, }); const state = { metamask: { remoteFeatureFlags: { - stateFlag: true, + flag1: false, + flag3: false, }, }, }; + expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ + flag1: true, + flag2: false, + flag3: false, + }); + }); + + it('returns manifest flags when they are only provided by manifest-flags.json', () => { + getManifestFlagsMock.mockReturnValue({ + remoteFeatureFlags: { + manifestFlag1: true, + manifestFlag2: false, + }, + }); + + const state = { + metamask: { + remoteFeatureFlags: {}, + }, + }; + expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ manifestFlag1: true, manifestFlag2: false, From 0bff314e795eac9730a1cc7d6be262e5ae48246d Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Thu, 16 Jan 2025 23:07:49 +0000 Subject: [PATCH 09/20] Update LavaMoat policies --- lavamoat/build-system/policy.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 5c9dbd68b17c..545922814d1f 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1795,7 +1795,6 @@ "chokidar>anymatch": true, "chokidar>braces": true, "chokidar>fsevents": true, - "tsx>fsevents": true, "eslint>glob-parent": true, "chokidar>is-binary-path": true, "del>is-glob": true, @@ -3365,13 +3364,6 @@ "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, - "tsx>fsevents": { - "globals": { - "console.assert": true, - "process.platform": true - }, - "native": true - }, "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge": { "builtin": { "util.format": true From d4e79a7d4b01987ffe4089d52afdc4f658c5868e Mon Sep 17 00:00:00 2001 From: dddddanica Date: Fri, 17 Jan 2025 11:10:37 +0000 Subject: [PATCH 10/20] feat(29629): fix e2e test due to shallow merging --- test/e2e/constants.ts | 4 ++-- test/e2e/page-objects/pages/developer-options-page.ts | 10 ++++++++-- .../remote-feature-flag/remote-feature-flag.spec.ts | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 42a4e0fc3f12..07565881c648 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -98,9 +98,9 @@ export const MOCK_REMOTE_FEATURE_FLAGS_RESPONSE = { }, }; -/* Mock customized remote feature flags response */ +/* Mock customized remote feature flags response*/ export const MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS = { - feature1: { + feature3: { name: 'groupA', value: 'valueA', }, diff --git a/test/e2e/page-objects/pages/developer-options-page.ts b/test/e2e/page-objects/pages/developer-options-page.ts index 8d37fc4091f3..61cd09e3383d 100644 --- a/test/e2e/page-objects/pages/developer-options-page.ts +++ b/test/e2e/page-objects/pages/developer-options-page.ts @@ -1,6 +1,9 @@ import { strict as assert } from 'assert'; import { Driver } from '../../webdriver/driver'; -import { MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS } from '../../constants'; +import { + MOCK_REMOTE_FEATURE_FLAGS_RESPONSE, + MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS, +} from '../../constants'; class DevelopOptions { private readonly driver: Driver; @@ -47,7 +50,10 @@ class DevelopOptions { const remoteFeatureFlagsState = await element.getText(); assert.equal( remoteFeatureFlagsState, - JSON.stringify(MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS), + JSON.stringify({ + ...MOCK_REMOTE_FEATURE_FLAGS_RESPONSE, + ...MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS, + }), ); } } diff --git a/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts b/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts index 48c8dd7f5b28..b65c827e5fe5 100644 --- a/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts +++ b/test/e2e/tests/remote-feature-flag/remote-feature-flag.spec.ts @@ -52,7 +52,7 @@ describe('Remote feature flag', function (this: Suite) { ); }); - it('offers the option to pass into manifest file for developers', async function () { + it('offers the option to pass into manifest file for developers along with original response', async function () { await withFixtures( { fixtures: new FixtureBuilder() From 8e88e7fb2a00d1e365d73fbae798e0cfabc17246 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Fri, 17 Jan 2025 13:54:12 +0000 Subject: [PATCH 11/20] feat(29629): Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15c4daaa30aa..ecdab37ca81e 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ If you are not a MetaMask Internal Developer, or are otherwise developing on a f - If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/), see [Developing on MetaMask - Segment](./development/README.md#segment). - If debugging unhandled exceptions, you'll need to add a value for `SENTRY_DSN` [Sentry Dsn](https://docs.sentry.io/product/sentry-basics/dsn-explainer/), see [Developing on MetaMask - Sentry](./development/README.md#sentry). - Optionally, replace the `PASSWORD` value with your development wallet password to avoid entering it each time you open the app. -- Duplicate `manifest-flags.json.dist` within the root and rename it to `manifest-flags.json` by running `cp .manifest-flags.json{.dist,}`. This file is used to add flags to `manifest.json` build files for the extension. You can add flags to the file to be used in the build process, for example: +- Duplicate `manifest-flags.json.dist` within the root and rename it to `manifest-flags.json` by running `cp .manifest-flags.json{.dist,}`. This file is used to add flags to `.manifest.json` build files for the extension. You can add flags to the file to be used in the build process, for example: ```json { "remoteFeatureFlags": { From b1a52ae542c9efcb6a57a70f3cb33f1c1207fde1 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Fri, 17 Jan 2025 17:14:10 +0000 Subject: [PATCH 12/20] feat(29629): Remove unused [] param in getWebpackConfig test --- development/webpack/test/webpack.config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/webpack/test/webpack.config.test.ts b/development/webpack/test/webpack.config.test.ts index 7a872af6be2c..94439084b052 100644 --- a/development/webpack/test/webpack.config.test.ts +++ b/development/webpack/test/webpack.config.test.ts @@ -78,7 +78,7 @@ ${Object.entries(env) } it('should have the correct defaults', () => { - const config: Configuration = getWebpackConfig([]); + const config: Configuration = getWebpackConfig(); // check that options are valid const { options } = webpack(config); assert.strictEqual(options.name, 'MetaMask – development'); From b2b58623abf962e3e283a60f82178490a5f4dae3 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Fri, 17 Jan 2025 17:16:40 +0000 Subject: [PATCH 13/20] feat(29629): Remove unnecessary --test flag in non-default options test and revert changes on this test --- development/webpack/test/webpack.config.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/development/webpack/test/webpack.config.test.ts b/development/webpack/test/webpack.config.test.ts index 94439084b052..7bda64031448 100644 --- a/development/webpack/test/webpack.config.test.ts +++ b/development/webpack/test/webpack.config.test.ts @@ -194,7 +194,6 @@ ${Object.entries(env) '--no-progress', '--no-cache', '--zip', - '--test', ...removeUnsupportedFeatures, ], { @@ -235,7 +234,7 @@ ${Object.entries(env) assert.deepStrictEqual(manifestPlugin.options.description, null); assert.deepStrictEqual(manifestPlugin.options.zip, true); assert(manifestPlugin.options.zipOptions, 'Zip options should be present'); - assert.notEqual(manifestPlugin.options.transform, undefined); + assert.deepStrictEqual(manifestPlugin.options.transform, undefined); const progressPlugin = instance.options.plugins.find( (plugin) => plugin && plugin.constructor.name === 'ProgressPlugin', From a5ea54f8dad5e227c2e9d83b31fa4e66314b2b6b Mon Sep 17 00:00:00 2001 From: dddddanica Date: Fri, 17 Jan 2025 22:45:08 +0000 Subject: [PATCH 14/20] feat(29629): Remove comment in json file to avoid copy error; read .manifest-flags.json file sync way in webpack build --- .manifest-flags.json.dist | 1 - .../test/plugins.ManifestPlugin.test.ts | 52 +++++++++++++++++++ .../utils/plugins/ManifestPlugin/helpers.ts | 26 +++++++--- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/.manifest-flags.json.dist b/.manifest-flags.json.dist index 098779458f11..d8a21d8df5fc 100644 --- a/.manifest-flags.json.dist +++ b/.manifest-flags.json.dist @@ -1,4 +1,3 @@ - // This configuration file is used to manage values passing to _flags that will be injected into manifest.json { "remoteFeatureFlags": { } diff --git a/development/webpack/test/plugins.ManifestPlugin.test.ts b/development/webpack/test/plugins.ManifestPlugin.test.ts index def2588dcb9c..2bc0b116e403 100644 --- a/development/webpack/test/plugins.ManifestPlugin.test.ts +++ b/development/webpack/test/plugins.ManifestPlugin.test.ts @@ -281,4 +281,56 @@ describe('ManifestPlugin', () => { } } }); + + describe('manifest flags in development mode', () => { + const testManifest = {} as chrome.runtime.Manifest; + const mockFlags = { remoteFeatureFlags: { testFlag: true } }; + + it('adds manifest flags in development mode', () => { + const transform = transformManifest({ lockdown: true, test: false }, true); + assert(transform, 'transform should be truthy'); + + // Mock fs.readFileSync + const fs = require('fs'); + const originalReadFileSync = fs.readFileSync; + fs.readFileSync = () => JSON.stringify(mockFlags); + + try { + const transformed = transform(testManifest, 'chrome'); + assert.deepStrictEqual( + transformed._flags, + mockFlags, + 'manifest should have flags in development mode' + ); + } finally { + // Restore original readFileSync + fs.readFileSync = originalReadFileSync; + } + }); + + it('handles missing manifest flags file', () => { + const transform = transformManifest({ lockdown: true, test: false }, true); + assert(transform, 'transform should be truthy'); + + // Mock fs.readFileSync to throw ENOENT + const fs = require('fs'); + const originalReadFileSync = fs.readFileSync; + fs.readFileSync = () => { + const error = new Error('ENOENT'); + throw error; + }; + + try { + const transformed = transform(testManifest, 'chrome'); + assert.deepStrictEqual( + transformed._flags, + { remoteFeatureFlags: {} }, + 'manifest should have default flags when file is missing' + ); + } finally { + // Restore original readFileSync + fs.readFileSync = originalReadFileSync; + } + }); + }); }); diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts index b58d11c1eb58..a59743c60b36 100644 --- a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -1,10 +1,3 @@ -let manifestFlags: { remoteFeatureFlags: Record }; -try { - manifestFlags = require('../../../../../.manifest-flags.json'); -} catch (error) { - manifestFlags = { remoteFeatureFlags: {} }; -} - /** * Returns a function that will transform a manifest JSON object based on the * given build args. @@ -41,11 +34,28 @@ export function transformManifest( } /** - * This function sets predefined flags in the manifest's _flags property. + * This function sets predefined flags in the manifest's _flags property + * that are stored in the .manifest-flags.json file. * * @param browserManifest - The Chrome extension manifest object to modify */ function addManifestFlags(browserManifest: chrome.runtime.Manifest) { + let manifestFlags = { remoteFeatureFlags: {} }; + + try { + const fs = require('fs'); + const manifestFlagsContent = fs.readFileSync( + '.manifest-flags.json', + 'utf8', + ); + manifestFlags = JSON.parse(manifestFlagsContent); + } catch (error: unknown) { + // Only ignore the error if the file doesn't exist + if (error instanceof Error && error.message !== 'ENOENT') { + throw error; + } + } + browserManifest._flags = manifestFlags; } From 5308bed8ec062195685cd4199d01f306ff422009 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Sun, 19 Jan 2025 11:33:42 +0000 Subject: [PATCH 15/20] feat(29629): Move getRemoteFeatureFlags selector to a separate ts file --- .../developer-options-tab.test.tsx | 3 +- .../developer-options-tab.tsx | 2 +- ui/selectors/remote-feature-flag.ts | 19 ++++ ui/selectors/remote-feature-flags.test.ts | 96 +++++++++++++++++++ ui/selectors/selectors.js | 19 ---- ui/selectors/selectors.test.js | 95 ------------------ 6 files changed, 117 insertions(+), 117 deletions(-) create mode 100644 ui/selectors/remote-feature-flag.ts create mode 100644 ui/selectors/remote-feature-flags.test.ts diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx index 5f97bc987323..e955e76ba7ef 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx @@ -20,8 +20,7 @@ jest.mock('../../../store/actions.ts', () => ({ mockSetServiceWorkerKeepAlivePreference, })); -jest.mock('../../../selectors', () => ({ - ...jest.requireActual('../../../selectors'), +jest.mock('../../../selectors/remote-feature-flag', () => ({ getRemoteFeatureFlags: jest.fn(() => mockRemoteFeatureFlags), })); diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx index 866e711d821c..f42199884af1 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx @@ -37,7 +37,7 @@ import { import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { getIsRedesignedConfirmationsDeveloperEnabled } from '../../confirmations/selectors/confirm'; -import { getRemoteFeatureFlags } from '../../../selectors'; +import { getRemoteFeatureFlags } from '../../../selectors/remote-feature-flag'; import ToggleRow from './developer-options-toggle-row-component'; import SentryTest from './sentry-test'; import { ProfileSyncDevSettings } from './profile-sync'; diff --git a/ui/selectors/remote-feature-flag.ts b/ui/selectors/remote-feature-flag.ts new file mode 100644 index 000000000000..caa28a2410d6 --- /dev/null +++ b/ui/selectors/remote-feature-flag.ts @@ -0,0 +1,19 @@ +import { getManifestFlags } from '../../shared/lib/manifestFlags'; + +/** + * Gets the remote feature flags by combining flags from both the manifest and state. + * Manifest flags take precedence and will override any duplicate flags from state. + * This allows for both static (manifest) and dynamic (state) feature flag configuration. + * + * @param state - The MetaMask state object + * @returns Combined feature flags object with manifest flags taking precedence over state flags + */ +export function getRemoteFeatureFlags(state) { + const manifestFlags = getManifestFlags().remoteFeatureFlags; + const stateFlags = state.metamask.remoteFeatureFlags; + + return { + ...stateFlags, + ...manifestFlags, + }; +} diff --git a/ui/selectors/remote-feature-flags.test.ts b/ui/selectors/remote-feature-flags.test.ts new file mode 100644 index 000000000000..8005d1326c9d --- /dev/null +++ b/ui/selectors/remote-feature-flags.test.ts @@ -0,0 +1,96 @@ +import * as manifestFlags from '../../shared/lib/manifestFlags'; +import { getRemoteFeatureFlags } from './remote-feature-flag'; + +describe('#getRemoteFeatureFlags', () => { + let getManifestFlagsMock: jest.SpyInstance; + + beforeEach(() => { + // Mock the getManifestFlags function before each test + getManifestFlagsMock = jest + .spyOn(manifestFlags, 'getManifestFlags') + .mockReturnValue({}); + }); + + afterEach(() => { + // Clean up mock after each test + getManifestFlagsMock.mockRestore(); + }); + + it('performs shallow merge of manifest flags and state flags', () => { + getManifestFlagsMock.mockReturnValue({ + remoteFeatureFlags: { + flag1: true, + flag2: false, + }, + }); + + const state = { + metamask: { + remoteFeatureFlags: { + flag1: false, + flag3: false, + }, + }, + }; + + expect(getRemoteFeatureFlags(state)).toStrictEqual({ + flag1: true, + flag2: false, + flag3: false, + }); + }); + + it('returns manifest flags when they are only provided by manifest-flags.json', () => { + getManifestFlagsMock.mockReturnValue({ + remoteFeatureFlags: { + manifestFlag1: true, + manifestFlag2: false, + }, + }); + + const state = { + metamask: { + remoteFeatureFlags: {}, + }, + }; + + expect(getRemoteFeatureFlags(state)).toStrictEqual({ + manifestFlag1: true, + manifestFlag2: false, + }); + }); + + it('returns state flags when manifest flags are empty', () => { + getManifestFlagsMock.mockReturnValue({ + remoteFeatureFlags: {}, + }); + + const state = { + metamask: { + remoteFeatureFlags: { + stateFlag: true, + }, + }, + }; + + expect(getRemoteFeatureFlags(state)).toStrictEqual({ + stateFlag: true, + }); + }); + + it('returns state flags when manifest flags are undefined', () => { + getManifestFlagsMock.mockReturnValue({}); + + const state = { + metamask: { + remoteFeatureFlags: { + stateFlag: true, + }, + }, + }; + + expect(getRemoteFeatureFlags(state)).toStrictEqual({ + stateFlag: true, + }); + }); +}); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index c5b90310f8d7..31c0ef436de0 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -113,7 +113,6 @@ import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; import { isSnapIgnoredInProd } from '../helpers/utils/snaps'; -import { getManifestFlags } from '../../shared/lib/manifestFlags'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -2978,24 +2977,6 @@ export function getMetaMetricsDataDeletionStatus(state) { return state.metamask.metaMetricsDataDeletionStatus; } -/** - * Gets the remote feature flags by combining flags from both the manifest and state. - * Manifest flags take precedence and will override any duplicate flags from state. - * This allows for both static (manifest) and dynamic (state) feature flag configuration. - * - * @param {object} state - The MetaMask state object - * @returns {object} Combined feature flags object with manifest flags taking precedence over state flags - */ -export function getRemoteFeatureFlags(state) { - const manifestFlags = getManifestFlags().remoteFeatureFlags; - const stateFlags = state.metamask.remoteFeatureFlags; - - return { - ...stateFlags, - ...manifestFlags, - }; -} - /** * To get all installed snaps with proper metadata * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index d6a940a4a676..7036f2575c15 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -6,7 +6,6 @@ import { } from '@metamask/keyring-api'; import { deepClone } from '@metamask/snaps-utils'; import { TransactionStatus } from '@metamask/transaction-controller'; -import * as manifestFlags from '../../shared/lib/manifestFlags'; import { KeyringType } from '../../shared/constants/keyring'; import mockState from '../../test/data/mock-state.json'; import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network'; @@ -2155,100 +2154,6 @@ describe('#getConnectedSitesList', () => { }); }); - describe('#getRemoteFeatureFlags', () => { - let getManifestFlagsMock; - - beforeEach(() => { - // Mock the getManifestFlags function before each test - getManifestFlagsMock = jest - .spyOn(manifestFlags, 'getManifestFlags') - .mockReturnValue({}); - }); - - afterEach(() => { - // Clean up mock after each test - getManifestFlagsMock.mockRestore(); - }); - - it('performs shallow merge of manifest flags and state flags', () => { - getManifestFlagsMock.mockReturnValue({ - remoteFeatureFlags: { - flag1: true, - flag2: false, - }, - }); - - const state = { - metamask: { - remoteFeatureFlags: { - flag1: false, - flag3: false, - }, - }, - }; - - expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ - flag1: true, - flag2: false, - flag3: false, - }); - }); - - it('returns manifest flags when they are only provided by manifest-flags.json', () => { - getManifestFlagsMock.mockReturnValue({ - remoteFeatureFlags: { - manifestFlag1: true, - manifestFlag2: false, - }, - }); - - const state = { - metamask: { - remoteFeatureFlags: {}, - }, - }; - - expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ - manifestFlag1: true, - manifestFlag2: false, - }); - }); - - it('returns state flags when manifest flags are empty', () => { - getManifestFlagsMock.mockReturnValue({ - remoteFeatureFlags: {}, - }); - - const state = { - metamask: { - remoteFeatureFlags: { - stateFlag: true, - }, - }, - }; - - expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ - stateFlag: true, - }); - }); - - it('returns state flags when manifest flags are undefined', () => { - getManifestFlagsMock.mockReturnValue({}); - - const state = { - metamask: { - remoteFeatureFlags: { - stateFlag: true, - }, - }, - }; - - expect(selectors.getRemoteFeatureFlags(state)).toStrictEqual({ - stateFlag: true, - }); - }); - }); - describe('getIsTokenNetworkFilterEqualCurrentNetwork', () => { beforeEach(() => { process.env.PORTFOLIO_VIEW = 'true'; From 2cc15607d22f8ff797ff7827b7bc9e02564aafa4 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Sun, 19 Jan 2025 11:37:20 +0000 Subject: [PATCH 16/20] feat(29629): Rename remote-feature-flags.ts file name --- .../developer-options-tab/developer-options-tab.test.tsx | 3 ++- .../settings/developer-options-tab/developer-options-tab.tsx | 2 +- ui/selectors/index.js | 1 + ui/selectors/remote-feature-flags.test.ts | 2 +- .../{remote-feature-flag.ts => remote-feature-flags.ts} | 0 5 files changed, 5 insertions(+), 3 deletions(-) rename ui/selectors/{remote-feature-flag.ts => remote-feature-flags.ts} (100%) diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx index e955e76ba7ef..5f97bc987323 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.test.tsx @@ -20,7 +20,8 @@ jest.mock('../../../store/actions.ts', () => ({ mockSetServiceWorkerKeepAlivePreference, })); -jest.mock('../../../selectors/remote-feature-flag', () => ({ +jest.mock('../../../selectors', () => ({ + ...jest.requireActual('../../../selectors'), getRemoteFeatureFlags: jest.fn(() => mockRemoteFeatureFlags), })); diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx index f42199884af1..866e711d821c 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx @@ -37,7 +37,7 @@ import { import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { getIsRedesignedConfirmationsDeveloperEnabled } from '../../confirmations/selectors/confirm'; -import { getRemoteFeatureFlags } from '../../../selectors/remote-feature-flag'; +import { getRemoteFeatureFlags } from '../../../selectors'; import ToggleRow from './developer-options-toggle-row-component'; import SentryTest from './sentry-test'; import { ProfileSyncDevSettings } from './profile-sync'; diff --git a/ui/selectors/index.js b/ui/selectors/index.js index 290c70fb2a31..e1dcd187ec40 100644 --- a/ui/selectors/index.js +++ b/ui/selectors/index.js @@ -8,3 +8,4 @@ export * from './selectors'; export * from './transactions'; export * from './approvals'; export * from './accounts'; +export * from './remote-feature-flags'; diff --git a/ui/selectors/remote-feature-flags.test.ts b/ui/selectors/remote-feature-flags.test.ts index 8005d1326c9d..9c62e5b34729 100644 --- a/ui/selectors/remote-feature-flags.test.ts +++ b/ui/selectors/remote-feature-flags.test.ts @@ -1,5 +1,5 @@ import * as manifestFlags from '../../shared/lib/manifestFlags'; -import { getRemoteFeatureFlags } from './remote-feature-flag'; +import { getRemoteFeatureFlags } from './remote-feature-flags'; describe('#getRemoteFeatureFlags', () => { let getManifestFlagsMock: jest.SpyInstance; diff --git a/ui/selectors/remote-feature-flag.ts b/ui/selectors/remote-feature-flags.ts similarity index 100% rename from ui/selectors/remote-feature-flag.ts rename to ui/selectors/remote-feature-flags.ts From 248ec01ff147253884c18464451992729007ac1f Mon Sep 17 00:00:00 2001 From: dddddanica Date: Sun, 19 Jan 2025 19:54:41 +0000 Subject: [PATCH 17/20] feat(29629): Refactor getRemoteFeatureFlags to use safe merge from lodash --- .../test/plugins.ManifestPlugin.test.ts | 14 ++- ui/selectors/remote-feature-flags.test.ts | 90 +++++++++++++------ ui/selectors/remote-feature-flags.ts | 15 ++-- 3 files changed, 83 insertions(+), 36 deletions(-) diff --git a/development/webpack/test/plugins.ManifestPlugin.test.ts b/development/webpack/test/plugins.ManifestPlugin.test.ts index 2bc0b116e403..b26fdad6d791 100644 --- a/development/webpack/test/plugins.ManifestPlugin.test.ts +++ b/development/webpack/test/plugins.ManifestPlugin.test.ts @@ -287,7 +287,10 @@ describe('ManifestPlugin', () => { const mockFlags = { remoteFeatureFlags: { testFlag: true } }; it('adds manifest flags in development mode', () => { - const transform = transformManifest({ lockdown: true, test: false }, true); + const transform = transformManifest( + { lockdown: true, test: false }, + true, + ); assert(transform, 'transform should be truthy'); // Mock fs.readFileSync @@ -300,7 +303,7 @@ describe('ManifestPlugin', () => { assert.deepStrictEqual( transformed._flags, mockFlags, - 'manifest should have flags in development mode' + 'manifest should have flags in development mode', ); } finally { // Restore original readFileSync @@ -309,7 +312,10 @@ describe('ManifestPlugin', () => { }); it('handles missing manifest flags file', () => { - const transform = transformManifest({ lockdown: true, test: false }, true); + const transform = transformManifest( + { lockdown: true, test: false }, + true, + ); assert(transform, 'transform should be truthy'); // Mock fs.readFileSync to throw ENOENT @@ -325,7 +331,7 @@ describe('ManifestPlugin', () => { assert.deepStrictEqual( transformed._flags, { remoteFeatureFlags: {} }, - 'manifest should have default flags when file is missing' + 'manifest should have default flags when file is missing', ); } finally { // Restore original readFileSync diff --git a/ui/selectors/remote-feature-flags.test.ts b/ui/selectors/remote-feature-flags.test.ts index 9c62e5b34729..f02eed536626 100644 --- a/ui/selectors/remote-feature-flags.test.ts +++ b/ui/selectors/remote-feature-flags.test.ts @@ -1,5 +1,36 @@ import * as manifestFlags from '../../shared/lib/manifestFlags'; -import { getRemoteFeatureFlags } from './remote-feature-flags'; +import { + getRemoteFeatureFlags, + RemoteFeatureFlagsState, +} from './remote-feature-flags'; + +const MOCK_DATA = { + manifestFlags: { + basic: { + flag1: true, + flag2: false, + }, + empty: {}, + nested: { + flag1: { b: 3 }, + flag2: false, + }, + }, + stateFlags: { + basic: { + flag1: false, + flag3: false, + }, + empty: {}, + withStateFlag: { + stateFlag: true, + }, + nested: { + flag1: { a: 1, b: 2 }, + flag3: false, + }, + }, +}; describe('#getRemoteFeatureFlags', () => { let getManifestFlagsMock: jest.SpyInstance; @@ -18,18 +49,12 @@ describe('#getRemoteFeatureFlags', () => { it('performs shallow merge of manifest flags and state flags', () => { getManifestFlagsMock.mockReturnValue({ - remoteFeatureFlags: { - flag1: true, - flag2: false, - }, + remoteFeatureFlags: MOCK_DATA.manifestFlags.basic, }); - const state = { + const state: RemoteFeatureFlagsState = { metamask: { - remoteFeatureFlags: { - flag1: false, - flag3: false, - }, + remoteFeatureFlags: MOCK_DATA.stateFlags.basic, }, }; @@ -42,34 +67,29 @@ describe('#getRemoteFeatureFlags', () => { it('returns manifest flags when they are only provided by manifest-flags.json', () => { getManifestFlagsMock.mockReturnValue({ - remoteFeatureFlags: { - manifestFlag1: true, - manifestFlag2: false, - }, + remoteFeatureFlags: MOCK_DATA.manifestFlags.basic, }); - const state = { + const state: RemoteFeatureFlagsState = { metamask: { - remoteFeatureFlags: {}, + remoteFeatureFlags: MOCK_DATA.stateFlags.empty, }, }; expect(getRemoteFeatureFlags(state)).toStrictEqual({ - manifestFlag1: true, - manifestFlag2: false, + flag1: true, + flag2: false, }); }); it('returns state flags when manifest flags are empty', () => { getManifestFlagsMock.mockReturnValue({ - remoteFeatureFlags: {}, + remoteFeatureFlags: MOCK_DATA.manifestFlags.empty, }); - const state = { + const state: RemoteFeatureFlagsState = { metamask: { - remoteFeatureFlags: { - stateFlag: true, - }, + remoteFeatureFlags: MOCK_DATA.stateFlags.withStateFlag, }, }; @@ -81,11 +101,9 @@ describe('#getRemoteFeatureFlags', () => { it('returns state flags when manifest flags are undefined', () => { getManifestFlagsMock.mockReturnValue({}); - const state = { + const state: RemoteFeatureFlagsState = { metamask: { - remoteFeatureFlags: { - stateFlag: true, - }, + remoteFeatureFlags: MOCK_DATA.stateFlags.withStateFlag, }, }; @@ -93,4 +111,22 @@ describe('#getRemoteFeatureFlags', () => { stateFlag: true, }); }); + + it('performs deep merge of manifest flags and state flags for nested objects', () => { + getManifestFlagsMock.mockReturnValue({ + remoteFeatureFlags: MOCK_DATA.manifestFlags.nested, + }); + + const state: RemoteFeatureFlagsState = { + metamask: { + remoteFeatureFlags: MOCK_DATA.stateFlags.nested, + }, + }; + + expect(getRemoteFeatureFlags(state)).toStrictEqual({ + flag1: { a: 1, b: 3 }, + flag2: false, + flag3: false, + }); + }); }); diff --git a/ui/selectors/remote-feature-flags.ts b/ui/selectors/remote-feature-flags.ts index caa28a2410d6..cb96e39a1d4b 100644 --- a/ui/selectors/remote-feature-flags.ts +++ b/ui/selectors/remote-feature-flags.ts @@ -1,5 +1,13 @@ +import merge from 'lodash/merge'; +import { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller'; import { getManifestFlags } from '../../shared/lib/manifestFlags'; +export type RemoteFeatureFlagsState = { + metamask: { + remoteFeatureFlags: RemoteFeatureFlagControllerState['remoteFeatureFlags']; + }; +}; + /** * Gets the remote feature flags by combining flags from both the manifest and state. * Manifest flags take precedence and will override any duplicate flags from state. @@ -8,12 +16,9 @@ import { getManifestFlags } from '../../shared/lib/manifestFlags'; * @param state - The MetaMask state object * @returns Combined feature flags object with manifest flags taking precedence over state flags */ -export function getRemoteFeatureFlags(state) { +export function getRemoteFeatureFlags(state: RemoteFeatureFlagsState) { const manifestFlags = getManifestFlags().remoteFeatureFlags; const stateFlags = state.metamask.remoteFeatureFlags; - return { - ...stateFlags, - ...manifestFlags, - }; + return merge({}, stateFlags, manifestFlags); } From 40f3c0795aa48d7f5a6e09de0c8ed328a3f93a15 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Sun, 19 Jan 2025 19:55:52 +0000 Subject: [PATCH 18/20] feat(29629): Rename gitnore comment --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ab47e255bbe9..5f01faba0a09 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ notes.txt .metamaskrc .metamaskprodrc -# Manifest customizated configuration +# Customized manifest configuration .manifest-flags.json # Test results From 3a1601950757bdc9be599eb78d7c9899a0592cf3 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Sun, 19 Jan 2025 20:06:22 +0000 Subject: [PATCH 19/20] feat(29629): Remove asyn reading for loadManifestFlags in normal build --- development/build/manifest.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/development/build/manifest.js b/development/build/manifest.js index 36e75352bf55..479275df9127 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -31,13 +31,7 @@ async function loadManifestFlags() { } } -// Initialize with default value -let manifestFlags = { remoteFeatureFlags: {} }; - -// Load flags asynchronously -loadManifestFlags().then((flags) => { - manifestFlags = flags; -}); +const manifestFlags = loadManifestFlags(); module.exports = createManifestTasks; From 77ad1e5adcc1083e80bf220e9cc18e3ed7652265 Mon Sep 17 00:00:00 2001 From: dddddanica Date: Sun, 19 Jan 2025 21:19:19 +0000 Subject: [PATCH 20/20] feat(29629): Fix unit test of file not exist --- development/webpack/utils/plugins/ManifestPlugin/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts index a59743c60b36..80cacfcb1233 100644 --- a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -51,7 +51,7 @@ export function transformManifest( manifestFlags = JSON.parse(manifestFlagsContent); } catch (error: unknown) { // Only ignore the error if the file doesn't exist - if (error instanceof Error && error.message !== 'ENOENT') { + if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { throw error; } }